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,
@@ -37,6 +38,8 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { TableSkeleton } from '@/components/ui/skeleton'
import { ErrorBanner, EmptyState } from '@/components/ui/state'
import { api } from '@/lib/api-client'
import { ApiRequestError } from '@/lib/api-client'
import { formatNumber } from '@/lib/utils'
@@ -71,14 +74,29 @@ const emptyForm: ModelForm = {
}
export default function ModelsPage() {
const [models, setModels] = useState<Model[]>([])
const [providers, setProviders] = useState<Provider[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [providerFilter, setProviderFilter] = useState<string>('all')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
// SWR for models list
const { data, isLoading, mutate } = useSWR(
['models', page, providerFilter],
() => {
const params: Record<string, unknown> = { page, page_size: PAGE_SIZE }
if (providerFilter !== 'all') params.provider_id = providerFilter
return api.models.list(params)
}
)
const models = data?.items ?? []
const total = data?.total ?? 0
// SWR for providers list (dropdown)
const { data: providersData } = useSWR(
['providers.all'],
() => api.providers.list({ page: 1, page_size: 100 })
)
const providers = providersData?.items ?? []
// Dialog
const [dialogOpen, setDialogOpen] = useState(false)
const [editTarget, setEditTarget] = useState<Model | null>(null)
@@ -89,37 +107,6 @@ export default function ModelsPage() {
const [deleteTarget, setDeleteTarget] = useState<Model | null>(null)
const [deleting, setDeleting] = useState(false)
const fetchModels = useCallback(async () => {
setLoading(true)
setError('')
try {
const params: Record<string, unknown> = { page, page_size: PAGE_SIZE }
if (providerFilter !== 'all') params.provider_id = providerFilter
const res = await api.models.list(params)
setModels(res.items)
setTotal(res.total)
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
else setError('加载失败')
} finally {
setLoading(false)
}
}, [page, providerFilter])
const fetchProviders = useCallback(async () => {
try {
const res = await api.providers.list({ page: 1, page_size: 100 })
setProviders(res.items)
} catch {
// ignore
}
}, [])
useEffect(() => {
fetchModels()
fetchProviders()
}, [fetchModels, fetchProviders])
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
const providerMap = new Map(providers.map((p) => [p.id, p.display_name || p.name]))
@@ -169,7 +156,7 @@ export default function ModelsPage() {
await api.models.create(payload)
}
setDialogOpen(false)
fetchModels()
mutate()
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
} finally {
@@ -183,7 +170,7 @@ export default function ModelsPage() {
try {
await api.models.delete(deleteTarget.id)
setDeleteTarget(null)
fetchModels()
mutate()
} catch (err) {
if (err instanceof ApiRequestError) setError(err.body.message)
} finally {
@@ -213,21 +200,12 @@ export default function ModelsPage() {
</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={() => setError('')} />}
{loading ? (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : models.length === 0 ? (
<div className="flex h-64 items-center justify-center text-muted-foreground text-sm">
</div>
{isLoading ? (
<TableSkeleton rows={8} cols={9} hasToolbar={false} />
) : error ? null : models.length === 0 ? (
<EmptyState />
) : (
<>
<Table>