refactor(frontend): colocate admin components and split service layer

Move route-specific admin components from components/common/admin into
app/(main)/admin/<feature>/components/. Split AdminService god object into
domain services, fix UploadService to use BaseService, add AdminUploadService,
consolidate DB export into DbManageService, and migrate consumers to the
unified services entry.
This commit is contained in:
ryan
2026-06-18 10:55:14 +08:00
parent de8a21a49f
commit beae8d2dd9
56 changed files with 594 additions and 888 deletions
@@ -20,7 +20,7 @@ import {
DialogTitle,
} from "@/components/ui/dialog"
import type {CacheStatus} from "@/lib/services/admin"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
/**
* 格式化字节大小
@@ -61,7 +61,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
const fetchCacheStatus = useCallback(async (isSilent = false) => {
if (!isSilent) setLoadingCache(true)
try {
const data = await AdminService.getCacheStatus()
const data = await services.adminCache.getCacheStatus()
setCacheStatus(data)
setMaxSizeMB(data.max_size_mb.toString())
setTtlMinutes(data.ttl_minutes.toString())
@@ -90,7 +90,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
}
setSavingConfig(true)
try {
await AdminService.updateCacheConfig({
await services.adminCache.updateCacheConfig({
max_size_mb: size,
ttl_minutes: ttl,
lru_enabled: lruEnabled,
@@ -110,7 +110,7 @@ export function CacheManager({ refreshTrigger }: CacheManagerProps) {
const handleClearCache = async () => {
setClearingCache(true)
try {
await AdminService.clearCache()
await services.adminCache.clearCache()
toast.success("清空成功", { description: "缓存数据已全部清除" })
setShowClearConfirm(false)
await fetchCacheStatus(true)
@@ -20,8 +20,7 @@ import {
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Button} from "@/components/ui/button"
import {Skeleton} from "@/components/ui/skeleton"
import {AdminService} from "@/lib/services/admin"
import {DbManageService} from "@/lib/services/db-manage"
import services from "@/lib/services"
import type {DBOverview} from "@/lib/services/db-manage"
const sectionFallback = (
@@ -29,17 +28,17 @@ const sectionFallback = (
)
const TableBrowser = dynamic(
() => import("@/components/common/admin/database-table-browser").then((mod) => mod.TableBrowser),
() => import("./components/table-browser").then((mod) => mod.TableBrowser),
{ loading: () => sectionFallback },
)
const CacheManager = dynamic(
() => import("@/components/common/admin/database-cache-manager").then((mod) => mod.CacheManager),
() => import("./components/cache-manager").then((mod) => mod.CacheManager),
{ loading: () => sectionFallback },
)
const SQLConsole = dynamic(
() => import("@/components/common/admin/database-sql-console").then((mod) => mod.SQLConsole),
() => import("./components/sql-console").then((mod) => mod.SQLConsole),
{ ssr: false },
)
@@ -67,7 +66,7 @@ export function DatabasePageClient() {
const fetchOverview = useCallback(async (isSilent = false) => {
if (!isSilent) setLoadingOverview(true)
try {
const data = await DbManageService.getOverview()
const data = await services.dbManage.getOverview()
setOverview(data)
} catch (err) {
toast.error("获取数据库概览失败", {
@@ -82,7 +81,7 @@ export function DatabasePageClient() {
const fetchTables = useCallback(async () => {
setLoadingTables(true)
try {
const data = await DbManageService.listTables()
const data = await services.dbManage.listTables()
setTables(data)
} catch (err) {
toast.error("获取数据库数据表列表失败", {
@@ -104,7 +103,7 @@ export function DatabasePageClient() {
const handleExport = async () => {
setExporting(true)
try {
const { blob, filename } = await AdminService.exportDatabase()
const { blob, filename } = await services.dbManage.exportDatabase()
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
+2 -2
View File
@@ -46,8 +46,8 @@ export default function HeaderDemoPage() {
<li><strong>容器与上边距对齐</strong>:使用 <code className="bg-muted px-1 py-0.5 rounded font-mono">flex items-center gap-2</code> 作为标题行基础容器(不含下边框),外层统一使用 <code className="bg-muted px-1 py-0.5 rounded font-mono">py-6 px-1</code> (或 <code className="bg-muted px-1 py-0.5 rounded font-mono">py-6</code>) 进行上边距的视觉对齐,确保切换菜单时顶部高度感一致。</li>
<li><strong>图标呈现</strong>:直接将 Lucide 图标组件嵌套于标题容器中,应用 <code className="bg-muted px-1 py-0.5 rounded font-mono">size-5 text-primary</code> 样式。禁止为图标包裹任何背景色小卡片、圆角或装饰性边框。</li>
<li><strong>文本样式标准</strong>:文本使用且仅使用 <code className="bg-muted px-1 py-0.5 rounded font-mono">{'h1 className="text-2xl font-semibold tracking-tight"'}</code>,禁用字重加粗(如 <code className="bg-muted px-1 py-0.5 rounded font-mono">font-bold</code>)或任何渐变色,保证全站字体一致性。</li>
<li><strong>Tabs 模块化拆分</strong>:凡是带有多个 Tab 页切换的页面,**禁止**将所有 Tab 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 Content 必须单独封装进全局 <code className="bg-muted px-1 py-0.5 rounded font-mono">components/</code> 目录下专属文件夹中,主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。这有利于模块复用、编译优化以及提升单文件可读性。</li>
<li><strong>复杂度驱动的区块拆分</strong>:不仅是 Tabs 切换,当一个页面为单页界面且代码行数超过 800 行时,必须将不同的物理块内容拆分为子组件。为保持路由树的清爽和逻辑纯粹,所有拆分出的非复用子组件也应统一存放在全局组件文件夹中(如 <code className="bg-muted px-1 py-0.5 rounded font-mono">components/common/admin/</code>),而路由入口 <code className="bg-muted px-1 py-0.5 rounded font-mono">page.tsx</code> 仅用于定义整个页面的布局排版与全局框架。典型标杆案例参考“数据管理” (<code className="bg-muted px-1 py-0.5 rounded font-mono">/admin/database</code>) 的重构。</li>
<li><strong>Tabs 模块化拆分</strong>:凡是带有多个 Tab 页切换的页面,**禁止**将所有 Tab 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 Content 必须单独拆分为独立组件文件(如 <code className="bg-muted px-1 py-0.5 rounded font-mono">tabs/events-tab.tsx</code> 或就近放在 <code className="bg-muted px-1 py-0.5 rounded font-mono">components/</code> 下),主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。</li>
<li><strong>复杂度驱动的区块拆分</strong>:不仅是 Tabs 切换,当一个页面代码行数超过 600 行时,必须将高密度区块拆分为子组件。路由专属、不复用的子组件应放在对应路由的特征目录中(如 <code className="bg-muted px-1 py-0.5 rounded font-mono">app/(main)/admin/database/components/</code>),只有真正跨页面复用的组件才进入全局 <code className="bg-muted px-1 py-0.5 rounded font-mono">components/common/</code>。典型标杆案例参考“数据管理” (<code className="bg-muted px-1 py-0.5 rounded font-mono">/admin/database</code>) 的 <code className="bg-muted px-1 py-0.5 rounded font-mono">table-browser.tsx</code>、<code className="bg-muted px-1 py-0.5 rounded font-mono">cache-manager.tsx</code> 与 <code className="bg-muted px-1 py-0.5 rounded font-mono">sql-console.tsx</code>。</li>
</ul>
</div>
</CardContent>
@@ -35,7 +35,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle} from "@/components/ui/sheet"
import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service"
import services, {formatFileSize, getFileUrl} from "@/lib/services"
import type {Upload as UploadRecord} from "@/lib/services/upload/types"
/* ─── 工具函数 ─────────────────────────────────────────── */
@@ -81,7 +81,7 @@ export function FileList() {
// 文件列表查询
const listQuery = useQuery({
queryKey: ["files", "all", page, pageSize, debouncedKeyword],
queryFn: () => UploadService.listUploads(page, pageSize, debouncedKeyword || undefined),
queryFn: () => services.adminUpload.listUploads(page, pageSize, debouncedKeyword || undefined),
})
const files = listQuery.data?.items ?? []
@@ -109,7 +109,7 @@ export function FileList() {
// 删除单文件
const deleteMutation = useMutation({
mutationFn: (id: string) => UploadService.deleteFile(id),
mutationFn: (id: string) => services.adminUpload.deleteFile(id),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: ["files", "my"] })
void queryClient.invalidateQueries({ queryKey: ["files", "stats"] })
@@ -121,7 +121,7 @@ export function FileList() {
// 批量 ZIP 下载
const batchDownloadMutation = useMutation({
mutationFn: (ids: string[]) => UploadService.batchDownload(ids),
mutationFn: (ids: string[]) => services.adminUpload.batchDownload(ids),
onSuccess: (blob) => {
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
@@ -151,7 +151,7 @@ export function FileList() {
const selectAll = () => setSelectedIds(new Set(files.map((f) => f.id)))
const handleDownload = (file: UploadRecord) => {
const url = UploadService.getDownloadUrl(file.id)
const url = services.adminUpload.getDownloadUrl(file.id)
const a = document.createElement("a")
a.href = url
a.download = file.file_name
@@ -32,7 +32,7 @@ import {
ChartLegend,
ChartLegendContent,
} from "@/components/ui/chart"
import {formatFileSize, UploadService} from "@/lib/services/upload/upload.service"
import services, {formatFileSize} from "@/lib/services"
const categoryMap: Record<string, string> = {
"图片": "images",
@@ -83,7 +83,7 @@ export function FileStats() {
const statsQuery = useQuery({
queryKey: ["files", "stats"],
queryFn: () => UploadService.getFileStats(),
queryFn: () => services.adminUpload.getFileStats(),
})
const stats = statsQuery.data
@@ -13,7 +13,7 @@ import {Input} from "@/components/ui/input"
import {Progress} from "@/components/ui/progress"
import {Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
import {Switch} from "@/components/ui/switch"
import {AdminService} from "@/lib/services/admin/admin.service"
import services from "@/lib/services"
import type {
ObjectStorageConfig,
StorageConfig,
@@ -116,8 +116,8 @@ export function StorageConfigTab() {
queryKey: ["admin", "storage-config"],
queryFn: async () => {
const [configRecord, executions] = await Promise.all([
AdminService.getSystemConfig(storageConfigKey),
AdminService.listTaskExecutions({task_type: storageMigrationTaskType, page: 1, page_size: 1}),
services.adminSystemConfig.getSystemConfig(storageConfigKey),
services.adminTask.listTaskExecutions({task_type: storageMigrationTaskType, page: 1, page_size: 1}),
])
const current = normalizeConfig(JSON.parse(configRecord.value) as StorageConfig)
const migration = latestMigration(executions.items, current)
@@ -154,7 +154,7 @@ export function StorageConfigTab() {
...value,
driver: activeDriver,
}
await AdminService.updateSystemConfig(storageConfigKey, {
await services.adminSystemConfig.updateSystemConfig(storageConfigKey, {
value: JSON.stringify(valueToSave),
})
},
@@ -171,7 +171,7 @@ export function StorageConfigTab() {
const migrateMutation = useMutation({
mutationFn: async (value: StorageConfig) => {
const payload: StorageMigrationPayload = {target: value}
return AdminService.dispatchTask({
return services.adminTask.dispatchTask({
task_type: storageMigrationTaskType,
payload: JSON.stringify(payload),
})
@@ -186,7 +186,7 @@ export function StorageConfigTab() {
},
})
const runMutation = useMutation({
mutationFn: (executionID: string) => AdminService.retryTaskExecution(executionID),
mutationFn: (executionID: string) => services.adminTask.retryTaskExecution(executionID),
onSuccess: () => {
toast.success("存储迁移任务已重新下发")
void queryClient.invalidateQueries({queryKey: ["admin", "storage-config"]})
+3 -3
View File
@@ -5,9 +5,9 @@ import {motion} from "motion/react"
import {FolderOpen} from "lucide-react"
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import {FileStats} from "@/components/common/admin/file-stats"
import {FileList} from "@/components/common/admin/file-list"
import {StorageConfigTab} from "@/components/common/admin/storage-config-tab"
import {FileStats} from "./components/file-stats"
import {FileList} from "./components/file-list"
import {StorageConfigTab} from "./components/storage-config-tab"
export default function FilesPage() {
const [activeTab, setActiveTab] = React.useState("stats")
@@ -20,7 +20,7 @@ import {useCallback, useEffect, useState} from "react"
import {BarChart3, Globe, RefreshCw, TrendingUp, Users, XCircle} from "lucide-react"
import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from "recharts"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {EmptyStateWithBorder} from "@/components/layout/empty"
@@ -77,7 +77,7 @@ export function AccessAnalytics() {
try {
setLoading(true)
setError(null)
const data = await AdminService.getLogsAnalytics()
const data = await services.adminLog.getLogsAnalytics()
// Formats the date labels for the X-axis representation
const formattedTrend = (data.trend || []).map(item => ({
@@ -22,7 +22,7 @@ import {toast} from "sonner"
import {format} from "date-fns"
import {Activity, ChevronLeft, ChevronRight, Copy, Eye, RotateCcw, Search, XCircle} from "lucide-react"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {EmptyStateWithBorder} from "@/components/layout/empty"
@@ -106,7 +106,7 @@ export function AccessLogs() {
endISO = new Date(searchParams.end_time).toISOString()
}
return AdminService.getAccessLogs({
return services.adminLog.getAccessLogs({
page,
page_size: PAGE_SIZE,
username: searchParams.username || undefined,
@@ -21,7 +21,7 @@ import {useVirtualizer} from "@tanstack/react-virtual"
import {toast} from "sonner"
import {ArrowDown, ChevronUp, Loader2} from "lucide-react"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {Button} from "@/components/ui/button"
@@ -156,7 +156,7 @@ export function AppLogs() {
// ---- Data fetching ---------------------------------------------------
const fetchLogs = useCallback(async (cursor: number = 0) => {
try {
return await AdminService.getLogs(cursor)
return await services.adminLog.getLogs(cursor)
} catch (err) {
throw err instanceof Error ? err : new Error("获取日志失败")
}
@@ -10,17 +10,17 @@ const tabFallback = (
)
const AccessAnalytics = dynamic(
() => import("@/components/common/admin/access-analytics").then((mod) => mod.AccessAnalytics),
() => import("./components/access-analytics").then((mod) => mod.AccessAnalytics),
{ loading: () => tabFallback },
)
const AccessLogs = dynamic(
() => import("@/components/common/admin/access-logs").then((mod) => mod.AccessLogs),
() => import("./components/access-logs").then((mod) => mod.AccessLogs),
{ loading: () => tabFallback },
)
const AppLogs = dynamic(
() => import("@/components/common/admin/app-logs").then((mod) => mod.AppLogs),
() => import("./components/app-logs").then((mod) => mod.AppLogs),
{ loading: () => tabFallback },
)
@@ -33,7 +33,7 @@ import {LoadingStateWithBorder} from "@/components/layout/loading"
import {EmptyStateWithBorder} from "@/components/layout/empty"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {PushService} from "@/lib/services/push"
import type {CreatePushEventRequest, PushEvent, UpdatePushEventRequest,} from "@/lib/services/push"
@@ -67,7 +67,7 @@ export function EventsTab() {
// --- 获取所有系统可调度任务类型 ---
const taskTypesQuery = useQuery({
queryKey: ["admin", "task-types"],
queryFn: () => AdminService.getTaskTypes(),
queryFn: () => services.adminTask.getTaskTypes(),
})
// --- 修改保存事件 Mutation ---
+3 -3
View File
@@ -8,9 +8,9 @@ import {motion} from "motion/react"
import {Bell, History, Layers, Settings} from "lucide-react"
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import {EventsTab} from "@/components/common/admin/push/events-tab"
import {HistoriesTab} from "@/components/common/admin/push/histories-tab"
import {SettingsTab} from "@/components/common/admin/push/settings-tab"
import {EventsTab} from "./components/events-tab"
import {HistoriesTab} from "./components/histories-tab"
import {SettingsTab} from "./components/settings-tab"
export default function PushAdminPage() {
const [activeTab, setActiveTab] = React.useState("events")
@@ -12,7 +12,7 @@ import {Badge} from "@/components/ui/badge"
import {Progress} from "@/components/ui/progress"
import {Skeleton} from "@/components/ui/skeleton"
import type {SystemStatus} from "@/lib/services/admin"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
/**
* 格式化数字,每3位加逗号
@@ -40,7 +40,7 @@ export function SystemStatusManager() {
const fetchStatus = useCallback(async (isSilent = false) => {
if (!isSilent) setWavelet(true)
try {
const data = await AdminService.getSystemStatus()
const data = await services.adminStatus.getSystemStatus()
// 检测变化的字段用于微动画效果
if (prevStatusRef.current) {
@@ -12,7 +12,7 @@ import {motion} from "motion/react"
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import {useAuth} from "@/components/providers/auth-provider"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {SystemConfig} from "@/lib/services/admin"
const tabFallback = (
@@ -45,7 +45,7 @@ const InfoTab = dynamic(
)
const SystemStatusManager = dynamic(
() => import("@/components/common/admin/status").then((mod) => mod.SystemStatusManager),
() => import("./components/system-status").then((mod) => mod.SystemStatusManager),
{ loading: () => tabFallback },
)
@@ -62,7 +62,7 @@ export function AdminSettingsPageClient() {
const systemConfigsQuery = useQuery({
queryKey: ["admin", "system-configs"],
queryFn: () => AdminService.listSystemConfigs("system"),
queryFn: () => services.adminSystemConfig.listSystemConfigs("system"),
enabled: !!user?.is_admin,
})
@@ -6,7 +6,7 @@ import {toast} from "sonner"
import {format} from "date-fns"
import {Activity, ChevronLeft, ChevronRight, RefreshCw, RotateCcw} from "lucide-react"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {TaskExecution, TaskExecutionStatus, TaskMeta} from "@/lib/services/admin"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
@@ -65,12 +65,12 @@ export function TaskExecutionsManager() {
const taskTypesQuery = useQuery({
queryKey: ["admin", "task-types"],
queryFn: () => AdminService.getTaskTypes(),
queryFn: () => services.adminTask.getTaskTypes(),
})
const executionsQuery = useQuery({
queryKey: ["admin", "task-executions", executionsPage, executionStatus, executionTaskType],
queryFn: () => AdminService.listTaskExecutions({
queryFn: () => services.adminTask.listTaskExecutions({
page: executionsPage,
page_size: PAGE_SIZE,
status: executionStatus === "all" ? undefined : executionStatus,
@@ -80,12 +80,12 @@ export function TaskExecutionsManager() {
const detailQuery = useQuery({
queryKey: ["admin", "task-execution", selectedExecutionId],
queryFn: () => AdminService.getTaskExecution(selectedExecutionId!),
queryFn: () => services.adminTask.getTaskExecution(selectedExecutionId!),
enabled: detailOpen && !!selectedExecutionId,
})
const retryMutation = useMutation({
mutationFn: (id: string) => AdminService.retryTaskExecution(id),
mutationFn: (id: string) => services.adminTask.retryTaskExecution(id),
onSuccess: (taskID) => {
toast.success("任务已重新下发", {
description: `新任务 ID:${ taskID }`,
@@ -11,7 +11,8 @@ import {Spinner} from "@/components/ui/spinner"
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {Calendar as CalendarIcon, Clock, Info, Layers, Play} from "lucide-react"
import {AdminService, DispatchTaskRequest, TaskMeta} from "@/lib/services/admin"
import type {DispatchTaskRequest, TaskMeta} from "@/lib/services/admin"
import services from "@/lib/services"
import {buildTaskPayload} from "@/lib/task-param-utils"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
@@ -148,7 +149,7 @@ export function TaskManager() {
try {
setLoading(true)
setError(null)
const data = await AdminService.getTaskTypes()
const data = await services.adminTask.getTaskTypes()
setTaskTypes(data)
} catch (err) {
setError(err instanceof Error ? err : new Error('加载任务类型失败'))
@@ -209,7 +210,7 @@ export function TaskManager() {
params.payload = payload ?? undefined
}
const taskID = await AdminService.dispatchTask(params)
const taskID = await services.adminTask.dispatchTask(params)
toast.success('任务下发成功', {
description: `已成功将任务 ${ targetTask?.name || selectedTaskType } 加入队列:${ taskID }`
@@ -12,7 +12,8 @@ import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, Di
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table"
import {Clock, Edit2, Info, Plus, RefreshCw, Trash2} from "lucide-react"
import {AdminService, CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin"
import type {CreateScheduleRequest, Schedule, TaskMeta, UpdateScheduleRequest} from "@/lib/services/admin"
import services from "@/lib/services"
import {buildTaskPayload} from "@/lib/task-param-utils"
import {ErrorInline} from "@/components/layout/error"
import {LoadingStateWithBorder} from "@/components/layout/loading"
@@ -47,8 +48,8 @@ export function TaskSchedulesManager() {
setLoading(true)
setError(null)
const [schedulesData, taskTypesData] = await Promise.all([
AdminService.listSchedules(),
AdminService.getTaskTypes()
services.adminTask.listSchedules(),
services.adminTask.getTaskTypes()
])
setSchedules(schedulesData || [])
setTaskTypes(taskTypesData || [])
@@ -97,7 +98,7 @@ export function TaskSchedulesManager() {
const handleToggleActive = async (schedule: Schedule) => {
try {
const updated = await AdminService.updateSchedule(schedule.id, {
const updated = await services.adminTask.updateSchedule(schedule.id, {
name: schedule.name,
task_type: schedule.task_type,
cron: schedule.cron,
@@ -167,7 +168,7 @@ export function TaskSchedulesManager() {
payload: payloadStr,
is_active: isActive
}
const updated = await AdminService.updateSchedule(editingSchedule.id, req)
const updated = await services.adminTask.updateSchedule(editingSchedule.id, req)
setSchedules(prev => prev.map(s => s.id === editingSchedule.id ? updated : s))
toast.success("定时任务更新成功")
} else {
@@ -178,7 +179,7 @@ export function TaskSchedulesManager() {
payload: payloadStr,
is_active: isActive
}
const created = await AdminService.createSchedule(req)
const created = await services.adminTask.createSchedule(req)
setSchedules(prev => [created, ...prev])
toast.success("定时任务创建成功")
}
@@ -202,7 +203,7 @@ export function TaskSchedulesManager() {
if (!deletingId) return
try {
setDeleteLoading(true)
await AdminService.deleteSchedule(deletingId)
await services.adminTask.deleteSchedule(deletingId)
setSchedules(prev => prev.filter(s => s.id !== deletingId))
toast.success("定时任务删除成功")
setDeleteOpen(false)
+3 -3
View File
@@ -3,9 +3,9 @@
import {Suspense} from "react"
import {useRouter, useSearchParams} from "next/navigation"
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
import {TaskManager} from "@/components/common/admin/task-manager"
import {TaskSchedulesManager} from "@/components/common/admin/task-schedules"
import {TaskExecutionsManager} from "@/components/common/admin/task-executions"
import {TaskManager} from "./components/task-manager"
import {TaskSchedulesManager} from "./components/task-schedules"
import {TaskExecutionsManager} from "./components/task-executions"
import {Spinner} from "@/components/ui/spinner"
import {Layers} from "lucide-react"
+3 -4
View File
@@ -25,10 +25,9 @@ import {EmptyStateWithBorder} from "@/components/layout/empty"
import {LoadingStateWithBorder} from "@/components/layout/loading"
import {ErrorInline} from "@/components/layout/error"
import {useAdminUsers} from "@/contexts/admin-users-context"
import {CreateUserModal} from "@/components/common/admin/create-user-modal"
import {UserFilterBar} from "@/components/common/admin/user-filter-bar"
import {UserDetailSheet} from "@/components/common/admin/user-detail-sheet"
import {CreateUserModal} from "./components/create-user-modal"
import {UserFilterBar} from "./components/user-filter-bar"
import {UserDetailSheet} from "./components/user-detail-sheet"
export default function UsersPage() {
const {
@@ -7,7 +7,7 @@ import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} fro
import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Switch} from "@/components/ui/switch"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {AuthSource, AuthSourceRequest} from "@/lib/services/admin"
const emptyForm: AuthSourceRequest = {
@@ -63,9 +63,9 @@ export function AuthSourceModal({
setSaving(true)
try {
if (source) {
await AdminService.updateAuthSource(source.id, form)
await services.adminAuthSource.updateAuthSource(source.id, form)
} else {
await AdminService.createAuthSource(form)
await services.adminAuthSource.createAuthSource(form)
}
await onChanged()
toast.success("认证源已保存")
@@ -4,7 +4,7 @@ import {useMutation, useQuery} from "@tanstack/react-query"
import {ExternalLink, RefreshCw, Sparkles} from "lucide-react"
import {toast} from "sonner"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {
AlertDialog,
AlertDialogAction,
@@ -33,13 +33,13 @@ function InfoRow({ label, value }: { label: string; value: React.ReactNode }) {
export function InfoTab() {
const updateQuery = useQuery({
queryKey: ["admin", "update"],
queryFn: () => AdminService.getUpdateStatus(),
queryFn: () => services.adminStatus.getUpdateStatus(),
refetchInterval: 30 * 60 * 1000,
staleTime: 5 * 60 * 1000,
})
const applyUpdateMutation = useMutation({
mutationFn: () => AdminService.applyUpdate(),
mutationFn: () => services.adminStatus.applyUpdate(),
onSuccess: () => {
toast.success("升级包已校验完成,服务正在重启")
},
@@ -6,7 +6,7 @@ import {KeyRound, ShieldAlert, X} from "lucide-react"
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Badge} from "@/components/ui/badge"
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {SystemConfig} from "@/lib/services/admin"
import {TemplatesManager} from "./templates"
import {toast} from "sonner"
@@ -21,7 +21,7 @@ export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps)
const uploadTypesQuery = useQuery({
queryKey: ["admin", "upload-types"],
queryFn: () => AdminService.listUploadTypes(),
queryFn: () => services.adminSystemConfig.listUploadTypes(),
})
const updateWhitelistMutation = useMutation({
@@ -30,7 +30,7 @@ export function OperationTab({ configs, systemConfigsQuery }: OperationTabProps)
if (!config) {
throw new Error("缺少配置项: file_access_whitelist")
}
await AdminService.updateSystemConfig("file_access_whitelist", {
await services.adminSystemConfig.updateSystemConfig("file_access_whitelist", {
value: newValue,
description: config.description,
})
@@ -21,7 +21,7 @@ import {
import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card"
import {Switch} from "@/components/ui/switch"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {SystemConfig} from "@/lib/services/admin"
import {toast} from "sonner"
@@ -90,7 +90,7 @@ export function OtherTab({ configs }: OtherTabProps) {
mutationFn: async ({ path, enabled }: { path: string; enabled: boolean }) => {
const newConfig = { ...menuDisplayConfig, [path]: enabled }
const currentCfg = configs["menu_display_config"]
await AdminService.updateSystemConfig("menu_display_config", {
await services.adminSystemConfig.updateSystemConfig("menu_display_config", {
value: JSON.stringify(newConfig),
description: currentCfg?.description || "目录显示配置(JSON 字符串,格式为 {url: enabled})",
})
@@ -47,7 +47,7 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from "@/
import {Label} from "@/components/ui/label"
import {AuthService} from "@/lib/services/auth"
import type {ChangePasswordRequest, UpdateProfileRequest} from "@/lib/services/auth"
import {UploadService} from "@/lib/services/upload/upload.service"
import services from "@/lib/services"
import {ImageCrop, ImageCropApply, ImageCropContent, ImageCropReset} from "@/components/ui/image-crop"
import {toast} from "sonner"
@@ -193,7 +193,7 @@ export function ProfileMain() {
const handleCroppedImage = async (croppedBase64: string) => {
try {
const res = await UploadService.uploadBase64Image(croppedBase64, "avatar", "avatar.png")
const res = await services.upload.uploadBase64Image(croppedBase64, "avatar", "avatar.png")
setAvatarUrl(`/f/${res.id}`)
setIsCropDialogOpen(false)
setCropFile(null)
@@ -24,7 +24,7 @@ import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select"
import {AuthSourceModal} from "@/components/common/settings/auth-source-modal"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {AuthSource, SystemConfig} from "@/lib/services/admin"
import {toast} from "sonner"
@@ -89,7 +89,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
const authSourcesQuery = useQuery({
queryKey: ["auth", "sources"],
queryFn: () => AdminService.listAuthSources(),
queryFn: () => services.adminAuthSource.listAuthSources(),
})
useEffect(() => {
@@ -120,7 +120,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
if (!config) {
throw new Error("缺少配置项: login_session_ttl_hours")
}
await AdminService.updateSystemConfig("login_session_ttl_hours", {
await services.adminSystemConfig.updateSystemConfig("login_session_ttl_hours", {
value: value,
description: config.description,
})
@@ -159,7 +159,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
if (!config) {
throw new Error(`缺少配置项: ${key}`)
}
await AdminService.updateSystemConfig(key, {
await services.adminSystemConfig.updateSystemConfig(key, {
value: value ? "true" : "false",
description: config.description,
})
@@ -176,7 +176,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
const toggleSourceMutation = useMutation({
mutationFn: async (source: AuthSource) => {
await AdminService.toggleAuthSource(source.id, { is_active: !source.is_active })
await services.adminAuthSource.toggleAuthSource(source.id, { is_active: !source.is_active })
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] })
@@ -190,7 +190,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
const deleteSourceMutation = useMutation({
mutationFn: async (sourceId: string) => {
await AdminService.deleteAuthSource(sourceId)
await services.adminAuthSource.deleteAuthSource(sourceId)
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["auth", "sources"] })
@@ -219,7 +219,7 @@ export function SecurityTab({ configs, systemConfigsQuery }: SecurityTabProps) {
for (const update of updates) {
const currentCfg = configs[update.key]
await AdminService.updateSystemConfig(update.key, {
await services.adminSystemConfig.updateSystemConfig(update.key, {
value: update.value,
description: currentCfg?.description || "",
})
@@ -11,7 +11,7 @@ import {Label} from "@/components/ui/label"
import {Switch} from "@/components/ui/switch"
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {Badge} from "@/components/ui/badge"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {SystemConfig} from "@/lib/services/admin"
import {toast} from "sonner"
@@ -58,7 +58,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
if (!config) {
throw new Error(`缺少配置项: ${key}`)
}
await AdminService.updateSystemConfig(key, {
await services.adminSystemConfig.updateSystemConfig(key, {
value: value ? "true" : "false",
description: config.description,
})
@@ -78,11 +78,11 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
const currentAddrCfg = configs["server_address"]
const currentSiteCfg = configs["site_name"]
await Promise.all([
AdminService.updateSystemConfig("server_address", {
services.adminSystemConfig.updateSystemConfig("server_address", {
value: serverAddress,
description: currentAddrCfg?.description || "服务器地址",
}),
AdminService.updateSystemConfig("site_name", {
services.adminSystemConfig.updateSystemConfig("site_name", {
value: siteName,
description: currentSiteCfg?.description || "系统平台的展示名称",
}),
@@ -122,7 +122,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
continue
}
}
await AdminService.updateSystemConfig(update.key, {
await services.adminSystemConfig.updateSystemConfig(update.key, {
value: update.value,
description: currentCfg?.description || "",
})
@@ -148,7 +148,7 @@ export function SystemTab({ configs, systemConfigsQuery }: SystemTabProps) {
setSmtpTestSuccess(null)
setSmtpTestError("")
const res = await AdminService.testSMTP({
const res = await services.adminSystemConfig.testSMTP({
smtp_host: smtpHost,
smtp_port: parseInt(smtpPort, 10) || 587,
smtp_username: smtpUsername,
@@ -10,7 +10,7 @@ import {Input} from "@/components/ui/input"
import {Label} from "@/components/ui/label"
import {Textarea} from "@/components/ui/textarea"
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import type {Template} from "@/lib/services/admin/types"
import {toast} from "sonner"
@@ -29,12 +29,12 @@ export function TemplatesManager() {
const templatesQuery = useQuery({
queryKey: ["admin", "templates"],
queryFn: () => AdminService.listTemplates(),
queryFn: () => services.adminTemplate.listTemplates(),
})
const createTemplateMutation = useMutation({
mutationFn: async () => {
await AdminService.createTemplate({
await services.adminTemplate.createTemplate({
key,
name,
type,
@@ -55,7 +55,7 @@ export function TemplatesManager() {
const updateTemplateMutation = useMutation({
mutationFn: async (key: string) => {
await AdminService.updateTemplate(key, {
await services.adminTemplate.updateTemplate(key, {
name,
type,
subject,
@@ -75,7 +75,7 @@ export function TemplatesManager() {
const deleteTemplateMutation = useMutation({
mutationFn: async (key: string) => {
await AdminService.deleteTemplate(key)
await services.adminTemplate.deleteTemplate(key)
},
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["admin", "templates"] })
@@ -36,7 +36,7 @@ import {
AlertDialogTitle,
} from "@/components/ui/alert-dialog"
import {formatFileSize, getFileUrl, UploadService} from "@/lib/services/upload/upload.service"
import services, {formatFileSize, getFileUrl} from "@/lib/services"
import type {Upload as UploadRecord} from "@/lib/services/upload/types"
/* ─── 工具函数 ─────────────────────────────────────────── */
@@ -83,7 +83,7 @@ export function UserFileManager() {
// 我的文件列表查询
const { data, isPending } = useQuery({
queryKey: ["user-files", page, pageSize, debouncedKeyword],
queryFn: () => UploadService.listMyUploads(page, pageSize, debouncedKeyword || undefined),
queryFn: () => services.upload.listMyUploads(page, pageSize, debouncedKeyword || undefined),
})
const files = data?.items ?? []
@@ -93,7 +93,7 @@ export function UserFileManager() {
// 上传文件 Mutation
const uploadMutation = useMutation({
mutationFn: async (file: File) => {
return UploadService.uploadFile(file, "generic", undefined, uploadAccessMode)
return services.upload.uploadFile(file, "generic", undefined, uploadAccessMode)
},
onMutate: () => {
setUploading(true)
@@ -115,7 +115,7 @@ export function UserFileManager() {
// 删除文件 Mutation
const deleteMutation = useMutation({
mutationFn: (id: string) => UploadService.deleteMyFile(id),
mutationFn: (id: string) => services.upload.deleteMyFile(id),
onSuccess: () => {
toast.success("文件已成功删除")
void queryClient.invalidateQueries({ queryKey: ["user-files"] })
@@ -138,7 +138,7 @@ export function UserFileManager() {
}
const handleDownload = (file: UploadRecord) => {
const url = UploadService.getDownloadUrl(file.id)
const url = services.upload.getDownloadUrl(file.id)
const a = document.createElement("a")
a.href = url
a.download = file.file_name
+3 -3
View File
@@ -5,7 +5,7 @@ import {createContext, useCallback, useContext, useRef, useState} from "react"
import {useQueryClient} from "@tanstack/react-query"
import type {SystemConfig, UpdateSystemConfigRequest} from "@/lib/services/admin"
import {AdminService} from "@/lib/services/admin"
import services from "@/lib/services"
import {handleContextError} from "@/lib/utils/error-handling"
@@ -49,7 +49,7 @@ export function AdminProvider({ children }: { children: React.ReactNode }) {
try {
setSystemConfigsLoading(true)
setSystemConfigsError(null)
const data = await AdminService.listSystemConfigs(type)
const data = await services.adminSystemConfig.listSystemConfigs(type)
if (requestId !== systemRequestIdRef.current) {
return
@@ -71,7 +71,7 @@ export function AdminProvider({ children }: { children: React.ReactNode }) {
/** 更新系统配置 */
const updateSystemConfig = useCallback(async (key: string, data: UpdateSystemConfigRequest) => {
try {
await AdminService.updateSystemConfig(key, data)
await services.adminSystemConfig.updateSystemConfig(key, data)
await queryClient.invalidateQueries({ queryKey: ['public-config'] })
await refetchSystemConfigs(lastConfigTypeRef.current)
} catch (error) {
+9 -8
View File
@@ -3,7 +3,8 @@
import * as React from "react"
import {createContext, useCallback, useContext, useEffect, useRef, useState} from "react"
import {toast} from "sonner"
import {AdminService, AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services/admin"
import type {AdminUser, CreateUserRequest, ListUsersRequest} from "@/lib/services/admin"
import services from "@/lib/services"
/** 用户列表查询参数 */
export interface UserQueryParams {
@@ -113,12 +114,12 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
// Current API doesn't support status filter in listUsers?
// The previous implementation did client-side filtering.
// Ideally backend supports it. If not, we fetch and filter?
// "AdminService.listUsers" in previous code only took page, page_size, username.
// "services.adminUser.listUsers" in previous code only took page, page_size, username.
// So status filter was client side.
// However, caching client-filtered result is tricky if we don't have all data.
// But previous implementation fetched *paged* data then filtered? No, that would be wrong (filtering 20 items might leave 0).
// Let's check previous implementation:
// "const data = await AdminService.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..."
// "const data = await services.adminUser.listUsers(...) ... let filteredUsers = data.users ... if (statusFilter...) filtered..."
// This means filtering happens ONLY on the current page of results! This is technically buggy if the user wants "all inactive users".
// But preserving that behavior for now.
@@ -129,7 +130,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
username: debouncedSearchUsername || undefined
}
const response = await AdminService.listUsers(requestParams)
const response = await services.adminUser.listUsers(requestParams)
if (requestId !== latestRequestIdRef.current) return
@@ -180,7 +181,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
cacheRef.current = {}
try {
await AdminService.updateUserStatus(user.id, { is_active: !user.is_active })
await services.adminUser.updateUserStatus(user.id, { is_active: !user.is_active })
toast.success(`已${ !user.is_active ? '启用' : '禁用' }用户 ${ user.username }`)
} catch {
// Revert on error
@@ -192,12 +193,12 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
}
const getUserDetail = async (id: string) => {
return AdminService.getUser(id)
return services.adminUser.getUser(id)
}
const createUser = async (req: CreateUserRequest) => {
try {
const newUser = await AdminService.createUser(req)
const newUser = await services.adminUser.createUser(req)
setUsers(prev => [newUser, ...prev])
setTotal(prev => prev + 1)
// Clear cache because data changed
@@ -212,7 +213,7 @@ export function AdminUsersProvider({ children }: { children: React.ReactNode })
const deleteUser = async (user: AdminUser) => {
try {
await AdminService.deleteUser(user.id)
await services.adminUser.deleteUser(user.id)
setUsers(prev => prev.filter(u => u.id !== user.id))
setTotal(prev => Math.max(0, prev - 1))
cacheRef.current = {}
+2 -2
View File
@@ -1,5 +1,5 @@
import {useQuery} from '@tanstack/react-query'
import {ConfigService} from '@/lib/services/config'
import services from '@/lib/services'
/**
* 获取公共配置(使用 React Query 进行统一缓存与自动同步)
@@ -12,7 +12,7 @@ import {ConfigService} from '@/lib/services/config'
export function usePublicConfig() {
const { data: config, isLoading: loading, error } = useQuery({
queryKey: ['public-config'],
queryFn: () => ConfigService.getPublicConfig(),
queryFn: () => services.config.getPublicConfig(),
staleTime: 5 * 60 * 1000, // 5 分钟缓存
})
@@ -1,543 +0,0 @@
import {BaseService} from '@/lib/services/core';
import type {
AdminUser,
AppUpdateStatus,
AuthSource,
AuthSourceRequest,
CacheConfig,
CacheStatus,
CreateScheduleRequest,
CreateSystemConfigRequest,
CreateTemplateRequest,
CreateUserRequest,
DatabaseInfo,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
ListUsersRequest,
ListUsersResponse,
Schedule,
SystemConfig,
SystemStatus,
TaskExecution,
TaskMeta,
TaskParamType,
TaskTypeResponse,
Template,
ToggleAuthSourceRequest,
UpdateScheduleRequest,
UpdateSystemConfigRequest,
UpdateTemplateRequest,
UpdateUserStatusRequest,
} from './types';
export type { AdminUser, CacheStatus, CacheConfig } from './types';
/**
* 管理员服务
*
* @remarks
* 所有接口都需要管理员权限
*/
export class AdminService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
// ==================== 系统配置管理 ====================
/**
* 创建系统配置
* @param request - 创建系统配置的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败或配置键已存在时
*
* @example
* ```typescript
* await AdminService.createSystemConfig({
* key: 'app.version',
* value: '1.0.0',
* description: '应用版本号'
* });
* ```
*/
static async createSystemConfig(
request: CreateSystemConfigRequest,
): Promise<void> {
return this.post<void>('/system-configs', request);
}
/**
* 获取系统配置列表
* @returns 系统配置列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*
* @example
* ```typescript
* const configs = await AdminService.listSystemConfigs();
* console.log('系统配置数量:', configs.length);
* ```
*/
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
const query = type ? `?type=${type}` : '';
return this.get<SystemConfig[]>(`/system-configs${query}`);
}
/**
* 获取单个系统配置
* @param key - 配置键
* @returns 系统配置信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
*
* @example
* ```typescript
* const config = await AdminService.getSystemConfig('app.version');
* console.log('应用版本:', config.value);
* ```
*/
static async getSystemConfig(key: string): Promise<SystemConfig> {
return this.get<SystemConfig>(`/system-configs/${ key }`);
}
/**
* 更新系统配置
* @param key - 配置键
* @param request - 更新系统配置的请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {NotFoundError} 当配置不存在时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* await AdminService.updateSystemConfig('app.version', {
* value: '1.1.0',
* description: '更新到新版本'
* });
* ```
*/
static async updateSystemConfig(
key: string,
request: UpdateSystemConfigRequest,
): Promise<void> {
return this.put<void>(`/system-configs/${ key }`, request);
}
static async testSMTP(request: {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
to: string;
}): Promise<{ success: boolean; log: string; error: string }> {
return this.post<{ success: boolean; log: string; error: string }>('/system-configs/smtp/test', request);
}
// ==================== 应用更新 ====================
static async getUpdateStatus(): Promise<AppUpdateStatus> {
return this.get<AppUpdateStatus>('/update');
}
static async applyUpdate(): Promise<void> {
return this.post<void>('/update/apply');
}
/**
* 获取所有已存在的文件业务类型及默认内置类型列表
* @returns 业务类型列表
*/
static async listUploadTypes(): Promise<string[]> {
return this.get<string[]>('/uploads/types');
}
// ==================== 认证源管理 ====================
static async listAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/auth-sources');
}
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
return this.post<AuthSource>('/auth-sources', request);
}
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
return this.put<AuthSource>(`/auth-sources/${ id }`, request);
}
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
return this.put<void>(`/auth-sources/${ id }/toggle`, request);
}
static async deleteAuthSource(id: string): Promise<void> {
return this.delete<void>(`/auth-sources/${ id }`);
}
// ==================== 任务管理 ====================
/**
* 获取支持的任务类型列表
* @returns 任务类型列表
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*
* @example
* ```typescript
* const taskTypes = await AdminService.getTaskTypes();
* console.log('可用任务类型:', taskTypes);
* ```
*/
static async getTaskTypes(): Promise<TaskMeta[]> {
const response = await this.get<TaskTypeResponse[]>('/tasks/types');
// Adapt backend PascalCase to frontend snake_case
return response.map(item => ({
type: item.Type || item.type || '',
asynq_task: item.AsynqTask || item.asynq_task || '',
name: item.Name || item.name || '',
description: item.Description || item.description || '',
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map(p => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
// Cast to TaskParamType: backend only emits 'string' | 'text' | 'number'.
// Unknown future values fall through safely to the string branch.
type: (p.Type || p.type || 'string') as TaskParamType,
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
/**
* 下发任务
* @param request - 下发任务请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败时
* @remarks
* - 不同任务类型需要不同的参数
* - order_sync 支持 start_time 和 end_time 参数
* - user_gamification 需要 user_id 参数
* - 其他任务无需额外参数
*/
static async dispatchTask(request: DispatchTaskRequest): Promise<string> {
return this.post<string>('/tasks/dispatch', request);
}
/**
* 查询任务执行记录列表
*/
static async listTaskExecutions(
request: ListTaskExecutionsRequest = {},
): Promise<ListTaskExecutionsResponse> {
return this.get<ListTaskExecutionsResponse>(
'/tasks/executions',
request as unknown as Record<string, unknown>,
);
}
/**
* 查询任务执行详情
*/
static async getTaskExecution(id: string): Promise<TaskExecution> {
return this.get<TaskExecution>(`/tasks/executions/${ id }`);
}
/**
* 重试失败任务
*/
static async retryTaskExecution(id: string): Promise<string> {
return this.post<string>(`/tasks/executions/${ id }/retry`);
}
// ==================== 定时任务管理 ====================
/**
* 获取定时任务列表
*/
static async listSchedules(): Promise<Schedule[]> {
return this.get<Schedule[]>('/tasks/schedules');
}
/**
* 创建定时任务
*/
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
return this.post<Schedule>('/tasks/schedules', request);
}
/**
* 更新定时任务
*/
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
return this.put<Schedule>(`/tasks/schedules/${ id }`, request);
}
/**
* 删除定时任务
*/
static async deleteSchedule(id: string): Promise<void> {
return this.delete<void>(`/tasks/schedules/${ id }`);
}
// ==================== 用户管理 ====================
/**
* 获取用户列表
* @param request - 查询参数
* @returns 用户列表及总数
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
* @throws {ValidationError} 当参数验证失败时
*
* @example
* ```typescript
* const result = await AdminService.listUsers({
* page: 1,
* page_size: 20,
* user_id: '10001',
* username: 'test'
* });
* console.log('用户总数:', result.total);
* console.log('用户列表:', result.users);
* ```
*
* @remarks
* - page 从 1 开始
* - page_size 范围 1-100
* - user_id 按用户 ID 精确搜索
* - username 按用户名做前缀搜索
*/
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
}
/**
* 获取用户详情
* @param id - 用户 ID
* @returns 用户完整资料
*/
static async getUser(id: string): Promise<AdminUser> {
return this.get<AdminUser>(`/users/${ id }`);
}
/**
* 更新用户状态
* @param id - 用户 ID
* @param request - 更新状态请求参数
* @returns void
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限或禁用管理员用户时
* @throws {NotFoundError} 当用户不存在时
*
* @example
* ```typescript
* // 禁用用户
* await AdminService.updateUserStatus(123, { is_active: false });
*
* // 启用用户
* await AdminService.updateUserStatus(123, { is_active: true });
* ```
*
* @remarks
* - 不能禁用管理员用户
*/
static async updateUserStatus(
id: string,
request: UpdateUserStatusRequest
): Promise<void> {
return this.put<void>(`/users/${ id }/status`, request);
}
/**
* 创建用户
* @param request - 创建用户请求参数
* @returns 创建成功的用户信息
*/
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
/**
* 删除用户
* @param id - 用户 ID
*/
static async deleteUser(id: string): Promise<void> {
return this.delete<void>(`/users/${ id }`);
}
/**
* 获取系统状态
* @returns 系统状态指标数据
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async getSystemStatus(): Promise<SystemStatus> {
return this.get<SystemStatus>('/status');
}
/**
* 获取数据库信息
* @returns 数据库类型、名称、版本信息
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async getDatabaseInfo(): Promise<DatabaseInfo> {
return this.get<DatabaseInfo>('/db-info');
}
/**
* 导出数据库
* SQLite 返回 .db 二进制,PostgreSQL 返回 pg_dump .sql 文本
* @returns 包含文件 Blob 和建议文件名的对象
* @throws {UnauthorizedError} 当未登录时
* @throws {ForbiddenError} 当无管理员权限时
*/
static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
const response = await import('axios').then(({ default: axios }) =>
axios.get<Blob>(this.getFullPath('/db-export'), {
withCredentials: true,
responseType: 'blob',
})
);
// 从 Content-Disposition 提取文件名
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'wavelet_export';
if (disposition) {
const match = disposition.match(/filename="?([^";]+)"?/);
if (match) filename = match[1];
}
return { blob: response.data, filename };
}
// ==================== 系统日志 ====================
/**
* 获取系统历史日志
* @param cursor - 日志游标,0=获取最新,>0=获取更早
* @param limit - 每页条数,默认 200
*/
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
lines: Array<{ index: number; data: string }>;
has_more: boolean;
next_cursor: number;
}> {
return this.get('/logs', { cursor, limit });
}
/**
* 获取 ClickHouse 访问日志列表
*/
static async getAccessLogs(params: {
page: number;
page_size: number;
username?: string;
path?: string;
start_time?: string;
end_time?: string;
}): Promise<{
total: number;
list: Array<{
id: string;
user_id: string;
username: string;
nickname: string;
path: string;
method: string;
ip: string;
user_agent: string;
headers: string;
status: number;
latency: number;
created_at: string;
}>;
}> {
return this.get('/logs/access', params as Record<string, unknown>);
}
/**
* 获取 ClickHouse 访问日志图表聚合指标
*/
static async getLogsAnalytics(): Promise<{
trend: Array<{ date: string; count: number }>;
browsers: Array<{ browser: string; count: number }>;
top_users: Array<{
user_id: string;
username: string;
nickname: string;
count: number;
}>;
}> {
return this.get('/logs/analytics');
}
// ==================== 模板管理 ====================
/**
* 获取模板列表
*/
static async listTemplates(): Promise<Template[]> {
return this.get<Template[]>('/templates');
}
/**
* 获取单个模板
*/
static async getTemplate(key: string): Promise<Template> {
return this.get<Template>(`/templates/${ key }`);
}
/**
* 创建模板
*/
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
return this.post<Template>('/templates', request);
}
/**
* 更新模板
*/
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
return this.put<Template>(`/templates/${ key }`, request);
}
/**
* 删除模板
*/
static async deleteTemplate(key: string): Promise<void> {
return this.delete<void>(`/templates/${ key }`);
}
// ==================== 磁盘缓存管理 ====================
/**
* 获取磁盘缓存状态与当前统计数据
*/
static async getCacheStatus(): Promise<CacheStatus> {
return this.get<CacheStatus>('/cache/status');
}
/**
* 更新磁盘缓存策略配置
*/
static async updateCacheConfig(config: CacheConfig): Promise<void> {
return this.post<void>('/cache/config', config);
}
/**
* 一键清空所有磁盘缓存数据
*/
static async clearCache(): Promise<void> {
return this.post<void>('/cache/clear');
}
}
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest} from './types';
export class AdminAuthSourceService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listAuthSources(): Promise<AuthSource[]> {
return this.get<AuthSource[]>('/auth-sources');
}
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
return this.post<AuthSource>('/auth-sources', request);
}
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
return this.put<AuthSource>(`/auth-sources/${id}`, request);
}
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
return this.put<void>(`/auth-sources/${id}/toggle`, request);
}
static async deleteAuthSource(id: string): Promise<void> {
return this.delete<void>(`/auth-sources/${id}`);
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {CacheConfig, CacheStatus} from './types';
export class AdminCacheService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getCacheStatus(): Promise<CacheStatus> {
return this.get<CacheStatus>('/cache/status');
}
static async updateCacheConfig(config: CacheConfig): Promise<void> {
return this.post<void>('/cache/config', config);
}
static async clearCache(): Promise<void> {
return this.post<void>('/cache/clear');
}
}
+15 -23
View File
@@ -1,25 +1,12 @@
/**
* 管理员服务模块
*
* @description
* 提供系统配置、用户与任务管理功能,包括:
* - 系统配置管理(创建、查询、更新、删除)
* - 异步任务配置及下发
* - 用户账号状态管理
*
* @remarks
* 所有接口都需要管理员权限
*
* @example
* ```typescript
* import { AdminService } from '@/lib/services';
*
* // 获取系统配置列表
* const configs = await AdminService.listSystemConfigs();
* ```
*/
export { AdminSystemConfigService } from './system-config.service';
export { AdminAuthSourceService } from './auth-source.service';
export { AdminTaskService } from './task.service';
export { AdminUserService } from './user.service';
export { AdminStatusService } from './status.service';
export { AdminLogService } from './log.service';
export { AdminTemplateService } from './template.service';
export { AdminCacheService } from './cache.service';
export { AdminService } from './admin.service';
export type {
SystemConfig,
CreateSystemConfigRequest,
@@ -41,11 +28,16 @@ export type {
ListUsersResponse,
UpdateUserStatusRequest,
SystemStatus,
DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
} from './types';
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './types';
@@ -0,0 +1,53 @@
import {BaseService} from '@/lib/services/core';
export class AdminLogService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
lines: Array<{ index: number; data: string }>;
has_more: boolean;
next_cursor: number;
}> {
return this.get('/logs', { cursor, limit });
}
static async getAccessLogs(params: {
page: number;
page_size: number;
username?: string;
path?: string;
start_time?: string;
end_time?: string;
}): Promise<{
total: number;
list: Array<{
id: string;
user_id: string;
username: string;
nickname: string;
path: string;
method: string;
ip: string;
user_agent: string;
headers: string;
status: number;
latency: number;
created_at: string;
}>;
}> {
return this.get('/logs/access', params as Record<string, unknown>);
}
static async getLogsAnalytics(): Promise<{
trend: Array<{ date: string; count: number }>;
browsers: Array<{ browser: string; count: number }>;
top_users: Array<{
user_id: string;
username: string;
nickname: string;
count: number;
}>;
}> {
return this.get('/logs/analytics');
}
}
@@ -0,0 +1,18 @@
import {BaseService} from '@/lib/services/core';
import type {AppUpdateStatus, SystemStatus} from './types';
export class AdminStatusService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getSystemStatus(): Promise<SystemStatus> {
return this.get<SystemStatus>('/status');
}
static async getUpdateStatus(): Promise<AppUpdateStatus> {
return this.get<AppUpdateStatus>('/update');
}
static async applyUpdate(): Promise<void> {
return this.post<void>('/update/apply');
}
}
@@ -0,0 +1,44 @@
import {BaseService} from '@/lib/services/core';
import type {
CreateSystemConfigRequest,
SystemConfig,
UpdateSystemConfigRequest,
} from './types';
export class AdminSystemConfigService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async createSystemConfig(request: CreateSystemConfigRequest): Promise<void> {
return this.post<void>('/system-configs', request);
}
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
const query = type ? `?type=${type}` : '';
return this.get<SystemConfig[]>(`/system-configs${query}`);
}
static async getSystemConfig(key: string): Promise<SystemConfig> {
return this.get<SystemConfig>(`/system-configs/${key}`);
}
static async updateSystemConfig(key: string, request: UpdateSystemConfigRequest): Promise<void> {
return this.put<void>(`/system-configs/${key}`, request);
}
static async testSMTP(request: {
smtp_host: string;
smtp_port: number;
smtp_username: string;
smtp_password: string;
to: string;
}): Promise<{ success: boolean; log: string; error: string }> {
return this.post<{ success: boolean; log: string; error: string }>(
'/system-configs/smtp/test',
request,
);
}
static async listUploadTypes(): Promise<string[]> {
return this.get<string[]>('/uploads/types');
}
}
@@ -0,0 +1,75 @@
import {BaseService} from '@/lib/services/core';
import type {
CreateScheduleRequest,
DispatchTaskRequest,
ListTaskExecutionsRequest,
ListTaskExecutionsResponse,
Schedule,
TaskExecution,
TaskMeta,
TaskParamType,
TaskTypeResponse,
UpdateScheduleRequest,
} from './types';
export class AdminTaskService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async getTaskTypes(): Promise<TaskMeta[]> {
const response = await this.get<TaskTypeResponse[]>('/tasks/types');
return response.map((item) => ({
type: item.Type || item.type || '',
asynq_task: item.AsynqTask || item.asynq_task || '',
name: item.Name || item.name || '',
description: item.Description || item.description || '',
supports_time: item.SupportsTime ?? item.supports_time ?? false,
max_retry: item.MaxRetry ?? item.max_retry ?? 0,
queue: item.Queue || item.queue || '',
params: (item.Params || item.params || []).map((p) => ({
name: p.Name || p.name || '',
label: p.Label || p.label || '',
type: (p.Type || p.type || 'string') as TaskParamType,
required: p.Required ?? p.required ?? false,
placeholder: p.Placeholder || p.placeholder || '',
description: p.Description || p.description || '',
})),
}));
}
static async dispatchTask(request: DispatchTaskRequest): Promise<string> {
return this.post<string>('/tasks/dispatch', request);
}
static async listTaskExecutions(
request: ListTaskExecutionsRequest = {},
): Promise<ListTaskExecutionsResponse> {
return this.get<ListTaskExecutionsResponse>(
'/tasks/executions',
request as unknown as Record<string, unknown>,
);
}
static async getTaskExecution(id: string): Promise<TaskExecution> {
return this.get<TaskExecution>(`/tasks/executions/${id}`);
}
static async retryTaskExecution(id: string): Promise<string> {
return this.post<string>(`/tasks/executions/${id}/retry`);
}
static async listSchedules(): Promise<Schedule[]> {
return this.get<Schedule[]>('/tasks/schedules');
}
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
return this.post<Schedule>('/tasks/schedules', request);
}
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
return this.put<Schedule>(`/tasks/schedules/${id}`, request);
}
static async deleteSchedule(id: string): Promise<void> {
return this.delete<void>(`/tasks/schedules/${id}`);
}
}
@@ -0,0 +1,26 @@
import {BaseService} from '@/lib/services/core';
import type {CreateTemplateRequest, Template, UpdateTemplateRequest} from './types';
export class AdminTemplateService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listTemplates(): Promise<Template[]> {
return this.get<Template[]>('/templates');
}
static async getTemplate(key: string): Promise<Template> {
return this.get<Template>(`/templates/${key}`);
}
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
return this.post<Template>('/templates', request);
}
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
return this.put<Template>(`/templates/${key}`, request);
}
static async deleteTemplate(key: string): Promise<void> {
return this.delete<void>(`/templates/${key}`);
}
}
-12
View File
@@ -456,18 +456,6 @@ export interface UpdateTemplateRequest {
description: string;
}
/**
* 数据库信息
*/
export interface DatabaseInfo {
/** 数据库类型:'sqlite' | 'postgres' */
type: 'sqlite' | 'postgres';
/** 数据库名称或文件路径 */
name: string;
/** 数据库版本字符串 */
version: string;
}
/**
* 应用更新状态
*/
@@ -0,0 +1,32 @@
import {BaseService} from '@/lib/services/core';
import type {
AdminUser,
CreateUserRequest,
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
} from './types';
export class AdminUserService extends BaseService {
protected static readonly basePath = '/api/v1/admin';
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
}
static async getUser(id: string): Promise<AdminUser> {
return this.get<AdminUser>(`/users/${id}`);
}
static async updateUserStatus(id: string, request: UpdateUserStatusRequest): Promise<void> {
return this.put<void>(`/users/${id}/status`, request);
}
static async createUser(request: CreateUserRequest): Promise<AdminUser> {
return this.post<AdminUser>('/users', request);
}
static async deleteUser(id: string): Promise<void> {
return this.delete<void>(`/users/${id}`);
}
}
@@ -1,3 +1,6 @@
import type {InternalAxiosRequestConfig} from 'axios';
import apiClient from '@/lib/services/core/api-client';
import {BaseService} from '@/lib/services/core';
import type {DBOverview, ExecuteSQLResponse, TableDataResponse} from './types';
@@ -38,4 +41,22 @@ export class DbManageService extends BaseService {
static async executeSQL(sql: string): Promise<ExecuteSQLResponse> {
return this.post<ExecuteSQLResponse>('/query', { sql });
}
/**
* 导出数据库备份(SQLite .db / PostgreSQL .sql)
*/
static async exportDatabase(): Promise<{ blob: Blob; filename: string }> {
const response = await apiClient.get<Blob>('/api/v1/admin/db-export', {
withCredentials: true,
responseType: 'blob',
} as InternalAxiosRequestConfig);
const disposition = response.headers['content-disposition'] as string | undefined;
let filename = 'wavelet_export';
if (disposition) {
const match = disposition.match(/filename="?([^";]+)"?/);
if (match) filename = match[1];
}
return { blob: response.data, filename };
}
}
+47 -42
View File
@@ -1,58 +1,52 @@
/**
* 服务层统一入口
* 提供所有业务服务的访问接口
*
* @example
* ```typescript
* // 推荐:使用统一的 services 对象
* import services from '@/lib/services';
*
* const user = await services.auth.getUserInfo();
* const systemConfig = await services.admin.listSystemConfigs();
* ```
*
* @example
* ```typescript
* // 按需导入:直接导入特定服务
* import { AuthService } from '@/lib/services';
*
* const user = await AuthService.getUserInfo();
* const configs = await services.adminSystemConfig.listSystemConfigs();
* ```
*/
import {
AdminAuthSourceService,
AdminCacheService,
AdminLogService,
AdminStatusService,
AdminSystemConfigService,
AdminTaskService,
AdminTemplateService,
AdminUserService,
} from './admin';
import {AuthService} from './auth';
import {AdminService} from './admin';
import {UserService} from './user';
import {ConfigService} from './config';
import {UploadService} from './upload';
import {DbManageService} from './db-manage';
import {PushService} from './push';
import {AdminUploadService, UploadService} from './upload';
import {UserService} from './user';
/**
* 服务对象
* 集中导出所有业务服务
*
* @description
* 推荐使用此对象访问所有服务,保持代码风格统一
*/
const services = {
/** 认证服务 */
auth: AuthService,
/** 管理员服务 */
admin: AdminService,
/** 用户服务 */
adminSystemConfig: AdminSystemConfigService,
adminAuthSource: AdminAuthSourceService,
adminTask: AdminTaskService,
adminUser: AdminUserService,
adminStatus: AdminStatusService,
adminLog: AdminLogService,
adminTemplate: AdminTemplateService,
adminCache: AdminCacheService,
user: UserService,
/** 配置服务 */
config: ConfigService,
/** 上传服务 */
upload: UploadService,
/** 数据库管理服务 */
adminUpload: AdminUploadService,
dbManage: DbManageService,
/** 通知推送服务 */
push: PushService,
} as const;
export default services;
export { services };
// ==================== 核心模块导出 ====================
@@ -86,20 +80,26 @@ export type {
// ==================== 业务服务导出 ====================
// 认证服务
export { AuthService } from './auth';
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest, UpdateProfileRequest } from './auth';
// 配置服务
export { ConfigService } from './config';
export type { PublicConfigResponse } from './config';
// 管理员服务
export { AdminService } from './admin';
export {
AdminSystemConfigService,
AdminAuthSourceService,
AdminTaskService,
AdminUserService,
AdminStatusService,
AdminLogService,
AdminTemplateService,
AdminCacheService,
} from './admin';
export type {
SystemConfig,
CreateSystemConfigRequest,
CreateUserRequest,
UpdateSystemConfigRequest,
TaskMeta,
TaskParam,
@@ -113,28 +113,33 @@ export type {
ListUsersRequest,
ListUsersResponse,
UpdateUserStatusRequest,
CreateUserRequest,
SystemStatus,
DatabaseInfo,
AppUpdateStatus,
Schedule,
CreateScheduleRequest,
UpdateScheduleRequest,
CacheStatus,
CacheConfig,
Template,
CreateTemplateRequest,
UpdateTemplateRequest,
AuthSource as AdminAuthSource,
AuthSourceRequest,
ToggleAuthSourceRequest,
StorageDriver,
StorageConfig,
ObjectStorageConfig,
} from './admin';
// 用户服务
export { UserService } from './user';
export type { AccessToken, CreateTokenResponse } from './user';
// 上传服务
export { UploadService } from './upload';
export type { UploadImageResponse } from './upload';
export { UploadService, AdminUploadService, formatFileSize, getFileUrl } from './upload';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse, ImageQuality } from './upload';
// 数据库管理服务
export { DbManageService } from './db-manage';
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './db-manage';
// 通知推送服务
export { PushService } from './push';
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
@@ -0,0 +1,40 @@
import type {InternalAxiosRequestConfig} from 'axios';
import {BaseService} from '../core/base.service';
import type {FileStatsResponse, ListUploadsResponse} from './types';
export class AdminUploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads';
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('', params);
}
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats');
}
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`);
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
static async batchDownload(ids: string[]): Promise<Blob> {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig);
}
}
+4 -1
View File
@@ -1,2 +1,5 @@
export { UploadService } from './upload.service';
export type { UploadImageResponse } from './types';
export { AdminUploadService } from './admin-upload.service';
export { formatFileSize, getFileUrl } from './utils';
export type { ImageQuality } from './utils';
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse } from './types';
+36 -147
View File
@@ -1,189 +1,78 @@
import {BaseService} from '../core/base.service'
import type {ListUploadsResponse, Upload, UploadImageResponse, FileStatsResponse} from './types'
import type {InternalAxiosRequestConfig} from 'axios'
import apiClient from '../core/api-client'
import type {InternalAxiosRequestConfig} from 'axios';
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin'
import {BaseService} from '../core/base.service';
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
/**
* 根据上传ID构造文件访问URL
* @param id - 上传记录ID
* @param quality - 图片质量
* @returns 文件访问URL
*/
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin'
): string | null {
if (!id) return null
if (quality === 'origin') return `/f/${id}`
return `/f/${id}?quality=${quality}`
}
/**
* 格式化文件大小
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
* 上传服务
* 处理文件上传相关的 API 请求
*/
export class UploadService extends BaseService {
protected static readonly basePath = '/api/v1/admin/uploads'
protected static readonly basePath = '/api/v1/upload';
/**
* 通用文件上传
* @param file - 文件对象
* @param type - 业务分类(如 avatar、attachment、generic)
* @param metadata - 可选额外 JSON 元数据
*/
static async uploadFile(
file: File,
type: string = 'generic',
metadata?: Record<string, unknown>,
accessMode?: number
accessMode?: number,
): Promise<Upload> {
const formData = new FormData()
formData.append('file', file)
formData.append('type', type)
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
if (metadata) {
formData.append('metadata', JSON.stringify(metadata))
formData.append('metadata', JSON.stringify(metadata));
}
if (accessMode !== undefined) {
formData.append('access_mode', String(accessMode))
formData.append('access_mode', String(accessMode));
}
const response = await apiClient.post<{ data: Upload }>('/api/v1/upload', formData, {
return this.post<Upload>('', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
} as InternalAxiosRequestConfig)
return response.data.data
} as InternalAxiosRequestConfig);
}
/**
* 获取文件列表
* @param page - 页码(1-based)
* @param pageSize - 每页数量
* @param keyword - 搜索关键词(文件名模糊)
* @param type - 业务分类过滤
* @param extension - 扩展名过滤
*/
static async listUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
return this.get<ListUploadsResponse>('', params)
}
/**
* 获取当前用户文件统计信息
*/
static async getFileStats(): Promise<FileStatsResponse> {
return this.get<FileStatsResponse>('/stats')
}
/**
* 删除文件
*/
static async deleteFile(id: string): Promise<void> {
return this.delete<void>(`/${id}`)
}
/**
* 获取单文件下载 URL(触发 attachment 下载)
*/
static getDownloadUrl(id: string): string {
return `/api/v1/admin/uploads/download/${id}`
}
/**
* 批量 ZIP 打包下载
* @param ids - 文件 ID 数组
*/
static async batchDownload(ids: string[]): Promise<Blob> {
const response = await this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response
}
/**
* 将 base64 图片转换为 Blob 并上传(兼容旧接口)
*/
static async uploadBase64Image(
base64: string,
type: string = 'generic',
filename: string = 'image.png',
accessMode?: number
accessMode?: number,
): Promise<UploadImageResponse> {
const response = await fetch(base64)
const blob = await response.blob()
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png'
const file = new File([blob], filename, { type: mimeType })
const result = await this.uploadFile(file, type, undefined, accessMode)
return { id: result.id }
const response = await fetch(base64);
const blob = await response.blob();
const mimeType = base64.match(/data:([^;]+);/)?.[1] || 'image/png';
const file = new File([blob], filename, { type: mimeType });
const result = await this.uploadFile(file, type, undefined, accessMode);
return { id: result.id };
}
/**
* 获取我的文件列表
*/
static async listMyUploads(
page = 1,
pageSize = 20,
keyword?: string,
type?: string,
extension?: string
extension?: string,
): Promise<ListUploadsResponse> {
const params: Record<string, string | number> = { page, page_size: pageSize }
if (keyword) params.keyword = keyword
if (type) params.type = type
if (extension) params.extension = extension
const response = await apiClient.get<{ data: ListUploadsResponse }>('/api/v1/upload/my', {
params
} as InternalAxiosRequestConfig)
return response.data.data
const params: Record<string, string | number> = { page, page_size: pageSize };
if (keyword) params.keyword = keyword;
if (type) params.type = type;
if (extension) params.extension = extension;
return this.get<ListUploadsResponse>('/my', params);
}
/**
* 删除我的文件
*/
static async deleteMyFile(id: string): Promise<void> {
const response = await apiClient.delete<{ data: void }>(`/api/v1/upload/${id}`)
return response.data.data
return this.delete<void>(`/${id}`);
}
/**
* 更新我的文件
*/
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
const response = await apiClient.put<{ data: Upload }>(`/api/v1/upload/${id}`, {
return this.put<Upload>(`/${id}`, {
file_name: fileName,
access_mode: accessMode
})
return response.data.data
access_mode: accessMode,
});
}
/**
* 我的文件批量 ZIP 打包下载
*/
static async batchDownloadMyFiles(ids: string[]): Promise<Blob> {
const response = await apiClient.post<Blob>('/api/v1/upload/download/batch', { ids }, {
return this.post<Blob>('/download/batch', { ids }, {
responseType: 'blob',
} as InternalAxiosRequestConfig)
return response.data
} as InternalAxiosRequestConfig);
}
}
static getDownloadUrl(id: string): string {
return `${this.basePath}/download/${id}`;
}
}
+18
View File
@@ -0,0 +1,18 @@
export type ImageQuality = 'low' | 'medium' | 'high' | 'origin';
export function getFileUrl(
id: string | number | null | undefined,
quality: ImageQuality = 'origin',
): string | null {
if (!id) return null;
if (quality === 'origin') return `/f/${id}`;
return `/f/${id}?quality=${quality}`;
}
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
}