chore: 提交所有工作进度 — SaaS 后端增强、Admin UI、桌面端集成

包含大量 SaaS 平台改进、Admin 管理后台更新、桌面端集成完善、
文档同步、测试文件重构等内容。为 QA 测试准备干净工作树。
This commit is contained in:
iven
2026-03-29 10:46:26 +08:00
parent 9a5fad2b59
commit 5fdf96c3f5
268 changed files with 22011 additions and 3886 deletions

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState, useCallback } from 'react'
import { useState } from 'react'
import useSWR from 'swr'
import {
Plus,
Loader2,
@@ -32,8 +33,10 @@ import {
DialogDescription,
} from '@/components/ui/dialog'
import { api } from '@/lib/api-client'
import { ErrorBanner, EmptyState } from '@/components/ui/state'
import { ApiRequestError } from '@/lib/api-client'
import { formatDate } from '@/lib/utils'
import { TableSkeleton } from '@/components/ui/skeleton'
import type { TokenInfo } from '@/lib/types'
const PAGE_SIZE = 20
@@ -45,11 +48,17 @@ const allPermissions = [
]
export default function ApiKeysPage() {
const [tokens, setTokens] = useState<TokenInfo[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [mutationError, setMutationError] = useState('')
const { data, error: swrError, isLoading, mutate } = useSWR(
['tokens', page],
() => api.tokens.list({ page, page_size: PAGE_SIZE }),
)
const tokens = data?.items ?? []
const total = data?.total ?? 0
const error = swrError?.message || mutationError
// 创建 Dialog
const [createOpen, setCreateOpen] = useState(false)
@@ -64,25 +73,6 @@ export default function ApiKeysPage() {
const [revokeTarget, setRevokeTarget] = useState<TokenInfo | null>(null)
const [revoking, setRevoking] = useState(false)
const fetchTokens = useCallback(async () => {
setLoading(true)
setError('')
try {
const res = await api.tokens.list({ page, page_size: PAGE_SIZE })
setTokens(res.items)
setTotal(res.total)
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
else setError('加载失败')
} finally {
setLoading(false)
}
}, [page])
useEffect(() => {
fetchTokens()
}, [fetchTokens])
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
function togglePermission(perm: string) {
@@ -107,9 +97,9 @@ export default function ApiKeysPage() {
setCreateOpen(false)
setCreatedToken(res)
setCreateForm({ name: '', expires_days: '', permissions: ['chat'] })
fetchTokens()
mutate()
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
if (err instanceof ApiRequestError) setMutationError(err.body.message)
} finally {
setCreating(false)
}
@@ -121,9 +111,9 @@ export default function ApiKeysPage() {
try {
await api.tokens.revoke(revokeTarget.id)
setRevokeTarget(null)
fetchTokens()
mutate()
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
if (err instanceof ApiRequestError) setMutationError(err.body.message)
} finally {
setRevoking(false)
}
@@ -158,21 +148,12 @@ export default function ApiKeysPage() {
</Button>
</div>
{error && (
<div className="rounded-md bg-destructive/10 border border-destructive/20 px-4 py-3 text-sm text-destructive">
{error}
<button onClick={() => setError('')} className="ml-2 underline cursor-pointer"></button>
</div>
)}
{error && <ErrorBanner message={error} onDismiss={() => setMutationError('')} />}
{loading ? (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : tokens.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground text-sm">
</div>
{isLoading ? (
<TableSkeleton rows={6} cols={7} />
) : error ? null : tokens.length === 0 ? (
<EmptyState />
) : (
<>
<Table>