feat: 前端

This commit is contained in:
Leon
2025-12-25 22:40:56 +08:00
parent 027df9d32b
commit ce0aec6110
42 changed files with 6170 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
import { postJson } from './client'
import type { Account, MessageResponse, TokenResponse } from './types'
export function register(username: string, password: string) {
return postJson<MessageResponse>('/account/register', { username, password })
}
export function login(username: string, password: string) {
return postJson<TokenResponse>('/account/login', { username, password })
}
export function logout() {
return postJson<MessageResponse>('/account/logout', {}, { authRequired: true })
}
export function rename(newUsername: string) {
return postJson<TokenResponse>('/account/rename', { new_username: newUsername }, { authRequired: true })
}
export function changePassword(username: string, oldPassword: string, newPassword: string) {
return postJson<MessageResponse>('/account/changePassword', {
username,
old_password: oldPassword,
new_password: newPassword,
})
}
export function findById(id: number) {
return postJson<Account>('/account/findByID', { id })
}
export function findByUsername(username: string) {
return postJson<Account>('/account/findByUsername', { username })
}

View File

@@ -0,0 +1,58 @@
import { useAuthStore } from '../stores/auth'
export class ApiError extends Error {
status: number
payload?: unknown
constructor(message: string, status: number, payload?: unknown) {
super(message)
this.name = 'ApiError'
this.status = status
this.payload = payload
}
}
type ApiErrorBody = { error?: string }
const API_BASE = (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api'
export async function postJson<T>(path: string, body: unknown, options?: { authRequired?: boolean }): Promise<T> {
const auth = useAuthStore()
const token = auth.token
if (options?.authRequired && !token) {
throw new ApiError('需要先登录(缺少 token', 401)
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (token) headers.Authorization = `Bearer ${token}`
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers,
body: JSON.stringify(body ?? {}),
})
const text = await res.text()
let data: unknown = null
if (text) {
try {
data = JSON.parse(text)
} catch {
data = text
}
}
if (!res.ok) {
if (res.status === 401) {
auth.clearToken()
}
const msg =
data && typeof data === 'object' && (data as ApiErrorBody).error
? String((data as ApiErrorBody).error)
: `请求失败 (${res.status})`
throw new ApiError(msg, res.status, data)
}
return data as T
}

View File

@@ -0,0 +1,14 @@
import { postJson } from './client'
import type { Comment, MessageResponse } from './types'
export function listAll(videoId: number) {
return postJson<Comment[]>('/comment/listAll', { video_id: videoId })
}
export function publish(videoId: number, content: string) {
return postJson<MessageResponse>('/comment/publish', { video_id: videoId, content }, { authRequired: true })
}
export function remove(commentId: number) {
return postJson<MessageResponse>('/comment/delete', { comment_id: commentId }, { authRequired: true })
}

19
frontend/src/api/feed.ts Normal file
View File

@@ -0,0 +1,19 @@
import { postJson } from './client'
import type { ListByFollowingResponse, ListLatestResponse, ListLikesCountResponse } from './types'
export function listLatest(input: { limit: number; latest_time: number }) {
return postJson<ListLatestResponse>('/feed/listLatest', input)
}
export function listLikesCount(input: { limit: number; likes_count_before?: number; id_before?: number }) {
const body: Record<string, unknown> = { limit: input.limit }
if (typeof input.likes_count_before === 'number' || typeof input.id_before === 'number') {
body.likes_count_before = input.likes_count_before ?? 0
body.id_before = input.id_before ?? 0
}
return postJson<ListLikesCountResponse>('/feed/listLikesCount', body)
}
export function listByFollowing(input: { limit: number; latest_time: number }) {
return postJson<ListByFollowingResponse>('/feed/listByFollowing', input, { authRequired: true })
}

14
frontend/src/api/like.ts Normal file
View File

@@ -0,0 +1,14 @@
import { postJson } from './client'
import type { IsLikedResponse, MessageResponse } from './types'
export function like(videoId: number) {
return postJson<MessageResponse>('/like/like', { video_id: videoId }, { authRequired: true })
}
export function unlike(videoId: number) {
return postJson<MessageResponse>('/like/unlike', { video_id: videoId }, { authRequired: true })
}
export function isLiked(videoId: number) {
return postJson<IsLikedResponse>('/like/isLiked', { video_id: videoId }, { authRequired: true })
}

View File

@@ -0,0 +1,26 @@
import { postJson } from './client'
import type { GetAllFollowersResponse, GetAllVloggersResponse, MessageResponse } from './types'
export function follow(vloggerId: number) {
return postJson<MessageResponse>('/social/follow', { vlogger_id: vloggerId }, { authRequired: true })
}
export function unfollow(vloggerId: number) {
return postJson<MessageResponse>('/social/unfollow', { vlogger_id: vloggerId }, { authRequired: true })
}
export function getAllFollowers(vloggerId?: number) {
return postJson<GetAllFollowersResponse>(
'/social/getAllFollowers',
vloggerId ? { vlogger_id: vloggerId } : {},
{ authRequired: true },
)
}
export function getAllVloggers(followerId?: number) {
return postJson<GetAllVloggersResponse>(
'/social/getAllVloggers',
followerId ? { follower_id: followerId } : {},
{ authRequired: true },
)
}

77
frontend/src/api/types.ts Normal file
View File

@@ -0,0 +1,77 @@
export type MessageResponse = { message: string }
export type TokenResponse = { token: string }
export type Account = {
id: number
username: string
}
export type Video = {
id: number
author_id: number
username: string
title: string
description?: string
play_url: string
cover_url: string
create_time: string
likes_count: number
}
export type Comment = {
id: number
username: string
video_id: number
author_id: number
content: string
created_at: string
}
export type FeedAuthor = {
id: number
username: string
}
export type FeedVideoItem = {
id: number
author: FeedAuthor
title: string
description?: string
play_url: string
cover_url: string
create_time: number
likes_count: number
is_liked: boolean
}
export type ListLatestResponse = {
video_list: FeedVideoItem[]
next_time: number
has_more: boolean
}
export type ListLikesCountResponse = {
video_list: FeedVideoItem[]
next_likes_count_before?: number
next_id_before?: number
has_more: boolean
}
export type ListByFollowingResponse = {
video_list: FeedVideoItem[]
next_time: number
has_more: boolean
}
export type IsLikedResponse = {
is_liked: boolean
}
export type GetAllFollowersResponse = {
followers: Account[]
}
export type GetAllVloggersResponse = {
vloggers: Account[]
}

14
frontend/src/api/video.ts Normal file
View File

@@ -0,0 +1,14 @@
import { postJson } from './client'
import type { Video } from './types'
export function publishVideo(input: { title: string; description: string; play_url: string; cover_url: string }) {
return postJson<Video>('/video/publish', input, { authRequired: true })
}
export function listByAuthorId(authorId: number) {
return postJson<Video[]>('/video/listByAuthorID', { author_id: authorId })
}
export function getDetail(id: number) {
return postJson<Video>('/video/getDetail', { id })
}