369 lines
11 KiB
TypeScript
369 lines
11 KiB
TypeScript
import type {
|
|
AdminAnalyticsResponse,
|
|
AdminAiImageProviderTestResponse,
|
|
AdminAiReindexResponse,
|
|
AdminAiProviderTestResponse,
|
|
AdminImageUploadResponse,
|
|
AdminMediaDeleteResponse,
|
|
AdminMediaListResponse,
|
|
AdminPostCoverImageRequest,
|
|
AdminPostCoverImageResponse,
|
|
AdminDashboardResponse,
|
|
AdminPostMetadataResponse,
|
|
AdminPostPolishResponse,
|
|
AdminReviewPolishRequest,
|
|
AdminReviewPolishResponse,
|
|
AdminR2ConnectivityResponse,
|
|
AdminSessionResponse,
|
|
AdminSiteSettingsResponse,
|
|
CommentListQuery,
|
|
CommentRecord,
|
|
CreatePostPayload,
|
|
CreateReviewPayload,
|
|
FriendLinkListQuery,
|
|
FriendLinkPayload,
|
|
FriendLinkRecord,
|
|
MarkdownDeleteResponse,
|
|
MarkdownDocumentResponse,
|
|
MarkdownImportResponse,
|
|
PostListQuery,
|
|
PostRecord,
|
|
ReviewRecord,
|
|
SiteSettingsPayload,
|
|
UpdateCommentPayload,
|
|
UpdatePostPayload,
|
|
UpdateReviewPayload,
|
|
} from '@/lib/types'
|
|
|
|
const API_BASE = import.meta.env.VITE_API_BASE?.trim() || ''
|
|
|
|
export class ApiError extends Error {
|
|
status: number
|
|
|
|
constructor(message: string, status: number) {
|
|
super(message)
|
|
this.name = 'ApiError'
|
|
this.status = status
|
|
}
|
|
}
|
|
|
|
async function readErrorMessage(response: Response) {
|
|
const text = await response.text().catch(() => '')
|
|
|
|
if (!text) {
|
|
return `请求失败,状态码 ${response.status}。`
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(text) as { description?: string; error?: string; message?: string }
|
|
return parsed.description || parsed.error || parsed.message || text
|
|
} catch {
|
|
return text
|
|
}
|
|
}
|
|
|
|
function appendQueryParams(path: string, params?: Record<string, unknown>) {
|
|
if (!params) {
|
|
return path
|
|
}
|
|
|
|
const searchParams = new URLSearchParams()
|
|
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value === undefined || value === null || value === '') {
|
|
return
|
|
}
|
|
|
|
if (typeof value === 'boolean') {
|
|
searchParams.set(key, String(value))
|
|
return
|
|
}
|
|
|
|
searchParams.set(key, String(value))
|
|
})
|
|
|
|
const queryString = searchParams.toString()
|
|
return queryString ? `${path}?${queryString}` : path
|
|
}
|
|
|
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
const headers = new Headers(init?.headers)
|
|
|
|
if (init?.body && !(init.body instanceof FormData) && !headers.has('Content-Type')) {
|
|
headers.set('Content-Type', 'application/json')
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}${path}`, {
|
|
...init,
|
|
headers,
|
|
})
|
|
|
|
if (!response.ok) {
|
|
throw new ApiError(await readErrorMessage(response), response.status)
|
|
}
|
|
|
|
if (response.status === 204) {
|
|
return undefined as T
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || ''
|
|
|
|
if (contentType.includes('application/json')) {
|
|
return (await response.json()) as T
|
|
}
|
|
|
|
return (await response.text()) as T
|
|
}
|
|
|
|
export const adminApi = {
|
|
sessionStatus: () => request<AdminSessionResponse>('/api/admin/session'),
|
|
login: (payload: { username: string; password: string }) =>
|
|
request<AdminSessionResponse>('/api/admin/session/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
logout: () =>
|
|
request<AdminSessionResponse>('/api/admin/session', {
|
|
method: 'DELETE',
|
|
}),
|
|
dashboard: () => request<AdminDashboardResponse>('/api/admin/dashboard'),
|
|
analytics: () => request<AdminAnalyticsResponse>('/api/admin/analytics'),
|
|
getSiteSettings: () => request<AdminSiteSettingsResponse>('/api/admin/site-settings'),
|
|
updateSiteSettings: (payload: SiteSettingsPayload) =>
|
|
request<AdminSiteSettingsResponse>('/api/admin/site-settings', {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
reindexAi: () =>
|
|
request<AdminAiReindexResponse>('/api/admin/ai/reindex', {
|
|
method: 'POST',
|
|
}),
|
|
testAiProvider: (provider: {
|
|
id: string
|
|
name: string
|
|
provider: string
|
|
api_base: string | null
|
|
api_key: string | null
|
|
chat_model: string | null
|
|
}) =>
|
|
request<AdminAiProviderTestResponse>('/api/admin/ai/test-provider', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ provider }),
|
|
}),
|
|
testAiImageProvider: (provider: {
|
|
provider: string
|
|
api_base: string | null
|
|
api_key: string | null
|
|
image_model: string | null
|
|
}) =>
|
|
request<AdminAiImageProviderTestResponse>('/api/admin/ai/test-image-provider', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
provider: provider.provider,
|
|
api_base: provider.api_base,
|
|
api_key: provider.api_key,
|
|
image_model: provider.image_model,
|
|
}),
|
|
}),
|
|
uploadReviewCoverImage: (file: File) => {
|
|
const formData = new FormData()
|
|
formData.append('file', file, file.name)
|
|
|
|
return request<AdminImageUploadResponse>('/api/admin/storage/review-cover', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
},
|
|
testR2Storage: () =>
|
|
request<AdminR2ConnectivityResponse>('/api/admin/storage/r2/test', {
|
|
method: 'POST',
|
|
}),
|
|
listMediaObjects: (query?: { prefix?: string; limit?: number }) =>
|
|
request<AdminMediaListResponse>(
|
|
appendQueryParams('/api/admin/storage/media', {
|
|
prefix: query?.prefix,
|
|
limit: query?.limit,
|
|
}),
|
|
),
|
|
deleteMediaObject: (key: string) =>
|
|
request<AdminMediaDeleteResponse>(
|
|
`/api/admin/storage/media?key=${encodeURIComponent(key)}`,
|
|
{
|
|
method: 'DELETE',
|
|
},
|
|
),
|
|
generatePostMetadata: (markdown: string) =>
|
|
request<AdminPostMetadataResponse>('/api/admin/ai/post-metadata', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ markdown }),
|
|
}),
|
|
polishPostMarkdown: (markdown: string) =>
|
|
request<AdminPostPolishResponse>('/api/admin/ai/polish-post', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ markdown }),
|
|
}),
|
|
polishReviewDescription: (payload: AdminReviewPolishRequest) =>
|
|
request<AdminReviewPolishResponse>('/api/admin/ai/polish-review', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
title: payload.title,
|
|
review_type: payload.reviewType,
|
|
rating: payload.rating,
|
|
review_date: payload.reviewDate,
|
|
status: payload.status,
|
|
tags: payload.tags,
|
|
description: payload.description,
|
|
}),
|
|
}),
|
|
generatePostCoverImage: (payload: AdminPostCoverImageRequest) =>
|
|
request<AdminPostCoverImageResponse>('/api/admin/ai/post-cover', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
title: payload.title,
|
|
description: payload.description,
|
|
category: payload.category,
|
|
tags: payload.tags,
|
|
post_type: payload.postType,
|
|
slug: payload.slug,
|
|
markdown: payload.markdown,
|
|
}),
|
|
}),
|
|
listPosts: (query?: PostListQuery) =>
|
|
request<PostRecord[]>(
|
|
appendQueryParams('/api/posts', {
|
|
slug: query?.slug,
|
|
category: query?.category,
|
|
tag: query?.tag,
|
|
search: query?.search,
|
|
type: query?.postType,
|
|
pinned: query?.pinned,
|
|
}),
|
|
),
|
|
getPostBySlug: (slug: string) => request<PostRecord>(`/api/posts/slug/${encodeURIComponent(slug)}`),
|
|
createPost: (payload: CreatePostPayload) =>
|
|
request<MarkdownDocumentResponse>('/api/posts/markdown', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
title: payload.title,
|
|
slug: payload.slug,
|
|
description: payload.description,
|
|
content: payload.content,
|
|
category: payload.category,
|
|
tags: payload.tags,
|
|
post_type: payload.postType,
|
|
image: payload.image,
|
|
images: payload.images,
|
|
pinned: payload.pinned,
|
|
published: payload.published,
|
|
}),
|
|
}),
|
|
updatePost: (id: number, payload: UpdatePostPayload) =>
|
|
request<PostRecord>(`/api/posts/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({
|
|
title: payload.title,
|
|
slug: payload.slug,
|
|
description: payload.description,
|
|
content: payload.content,
|
|
category: payload.category,
|
|
tags: payload.tags,
|
|
post_type: payload.postType,
|
|
image: payload.image,
|
|
images: payload.images,
|
|
pinned: payload.pinned,
|
|
}),
|
|
}),
|
|
getPostMarkdown: (slug: string) =>
|
|
request<MarkdownDocumentResponse>(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`),
|
|
importPosts: async (files: File[]) => {
|
|
const formData = new FormData()
|
|
|
|
files.forEach((file) => {
|
|
formData.append('files', file, file.webkitRelativePath || file.name)
|
|
})
|
|
|
|
return request<MarkdownImportResponse>('/api/posts/markdown/import', {
|
|
method: 'POST',
|
|
body: formData,
|
|
})
|
|
},
|
|
updatePostMarkdown: (slug: string, markdown: string) =>
|
|
request<MarkdownDocumentResponse>(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({ markdown }),
|
|
}),
|
|
deletePost: (slug: string) =>
|
|
request<MarkdownDeleteResponse>(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`, {
|
|
method: 'DELETE',
|
|
}),
|
|
listComments: (query?: CommentListQuery) =>
|
|
request<CommentRecord[]>(
|
|
appendQueryParams('/api/comments', {
|
|
post_id: query?.postId,
|
|
post_slug: query?.postSlug,
|
|
scope: query?.scope,
|
|
paragraph_key: query?.paragraphKey,
|
|
approved: query?.approved,
|
|
}),
|
|
),
|
|
updateComment: (id: number, payload: UpdateCommentPayload) =>
|
|
request<CommentRecord>(`/api/comments/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
deleteComment: (id: number) =>
|
|
request<void>(`/api/comments/${id}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
listFriendLinks: (query?: FriendLinkListQuery) =>
|
|
request<FriendLinkRecord[]>(
|
|
appendQueryParams('/api/friend_links', {
|
|
status: query?.status,
|
|
category: query?.category,
|
|
}),
|
|
),
|
|
createFriendLink: (payload: FriendLinkPayload) =>
|
|
request<FriendLinkRecord>('/api/friend_links', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
siteName: payload.siteName,
|
|
siteUrl: payload.siteUrl,
|
|
avatarUrl: payload.avatarUrl,
|
|
description: payload.description,
|
|
category: payload.category,
|
|
status: payload.status,
|
|
}),
|
|
}),
|
|
updateFriendLink: (id: number, payload: FriendLinkPayload) =>
|
|
request<FriendLinkRecord>(`/api/friend_links/${id}`, {
|
|
method: 'PATCH',
|
|
body: JSON.stringify({
|
|
site_name: payload.siteName,
|
|
site_url: payload.siteUrl,
|
|
avatar_url: payload.avatarUrl,
|
|
description: payload.description,
|
|
category: payload.category,
|
|
status: payload.status,
|
|
}),
|
|
}),
|
|
deleteFriendLink: (id: number) =>
|
|
request<void>(`/api/friend_links/${id}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
listReviews: () => request<ReviewRecord[]>('/api/reviews'),
|
|
createReview: (payload: CreateReviewPayload) =>
|
|
request<ReviewRecord>('/api/reviews', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
updateReview: (id: number, payload: UpdateReviewPayload) =>
|
|
request<ReviewRecord>(`/api/reviews/${id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
}),
|
|
deleteReview: (id: number) =>
|
|
request<void>(`/api/reviews/${id}`, {
|
|
method: 'DELETE',
|
|
}),
|
|
}
|