feat: 前端
This commit is contained in:
34
frontend/src/api/account.ts
Normal file
34
frontend/src/api/account.ts
Normal 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 })
|
||||
}
|
||||
58
frontend/src/api/client.ts
Normal file
58
frontend/src/api/client.ts
Normal 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
|
||||
}
|
||||
14
frontend/src/api/comment.ts
Normal file
14
frontend/src/api/comment.ts
Normal 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
19
frontend/src/api/feed.ts
Normal 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
14
frontend/src/api/like.ts
Normal 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 })
|
||||
}
|
||||
26
frontend/src/api/social.ts
Normal file
26
frontend/src/api/social.ts
Normal 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
77
frontend/src/api/types.ts
Normal 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
14
frontend/src/api/video.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user