diff --git a/.agent/skills/new-api/references/logics_example.go b/.agent/skills/new-api/references/logics_example.go index d72bcaab..61310347 100644 --- a/.agent/skills/new-api/references/logics_example.go +++ b/.agent/skills/new-api/references/logics_example.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - "github.com/Rain-kl/Wavelet/internal/logger" + "github.com/Rain-kl/Wavelet/pkg/logger" "go.uber.org/zap" ) diff --git a/.agent/skills/new-api/references/service_example.go b/.agent/skills/new-api/references/service_example.go index 2e551314..5435a393 100644 --- a/.agent/skills/new-api/references/service_example.go +++ b/.agent/skills/new-api/references/service_example.go @@ -8,7 +8,7 @@ import ( "errors" "fmt" - "github.com/Rain-kl/Wavelet/internal/logger" + "github.com/Rain-kl/Wavelet/pkg/logger" "go.uber.org/zap" ) diff --git a/AGENTS.md b/AGENTS.md index d3b5fce2..c59b8638 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -205,6 +205,23 @@ Handler 规范: - 如果现有的变体不足以满足需求,请扩展 shadcn/ui 组件的变体,而不是硬编码一次性的颜色。 - 使用 Lucide 图标来满足常见的图标需求。将自定义图标作为命名导出放在 `frontend/components/icons/` 中。 +页面标题栏规范 (新人开发与重构必读): + +- **结构极简与高度专注**:标题栏应绝对干净,**禁止**在标题正下方放置任何描述性文本、小字副标题或段落(如 `

` 或 `CardDescription`)。这样能让用户迅速聚焦于业务主功能,避免低效的信息噪声。 +- **容器与对齐机制**: + - 标题容器统一使用 `flex items-center gap-2`。如果右侧有操作按钮(如“新增”、“刷新”),请使用 `justify-between` 布局让操作区与标题双向分布。 + - 为了确保所有页面在进入/切换时,顶部的呼吸感和视觉高度完全一致,页面最外层容器**必须**统一使用 `py-6 px-1` 或 `py-6` 进行上边距对齐。 +- **无下边框**:标题栏下方**严禁**带有横线(禁用 `border-b` / `border-border`),让页面在纵向上保持连贯性与开阔感。 +- **图标标准**:图标作为视觉辅助点缀,**必须**直接嵌套在标题容器中,直接使用 Lucide 图标组件,样式大小限制为 `size-5 text-primary`。**严禁**为图标包裹任何背景小卡片、圆角边框或额外的修饰容器。 +- **标题文字标准**:标题文字使用且仅使用 `h1 className="text-2xl font-semibold tracking-tight"`。不要自行定义字号、字量(如使用 `font-bold`)或添加任何渐变色,保持整个系统的字形规范化。 +- **Tabs 模块化与文件拆分规范**:凡是带有多个 Tab 页切换的复杂页面,**禁止**将所有 Tab 的渲染逻辑堆积在同一个主文件内。每个 Tab 的具体渲染内容必须单独拆分为独立的 React 组件文件(如 `tabs/events-tab.tsx`)。主页面文件应该仅用于导入子组件、注册 Tabs 触发器以及管理 Tab 的切换激活状态。这有利于防止单文件过大(避免单文件行数超过 600 行限制),并大幅度提高代码的可读性与编译维护效率。 +- **扁平化结构与避免冗余中间件**:为了消除无意义的“中间代理文件”,所有作为路由物理入口的 Tabs 状态维护、骨架及外层布局代码,**必须**直接定义在 Next.js 的 `app/` 页面文件(即 `page.tsx`)中。禁止在 `page.tsx` 中仅写一个单纯的 `` 转发,而在外部新建一个同名中转容器。 +- **复杂度驱动的组件拆分规范**:组件的拆分不应局限于“跨页面复用”。当一个路由页面的复杂度变高时(如渲染逻辑膨胀、存在大型嵌套弹窗或多层状态管理,如单文件代码行数超过 600 行),必须主动将其拆分为子组件以维持单文件的高可读性与低耦合度。拆分时遵循就近原则:特定于该路由且不复用的子组件应放置在最邻近该路由的特征目录(Feature Folder,如 `components/` 局部文件夹)中;只有真正具备跨页面复用价值的通用业务/基础 UI 组件才应存放在全局 `components/` 共享目录下。 + - **最佳实践标杆案例(数据管理 `/admin/database`)**: + 该页面由于整合了“运行状态概览”、“物理表网格浏览器”、“磁盘缓存管理”和“SQL 交互控台”多个复杂大区块,重构前单文件接近 1000 行。 + 重构后,主页面 `page.tsx` 仅做高级页面骨架与排版排布,维护全局刷新机制与终端视图切换;而“数据表浏览器 (`table-browser.tsx`)”、“缓存管理 (`cache-manager.tsx`)”与“SQL 终端 (`sql-console.tsx`)”等独立高状态密度区块均被抽离为局部子组件,存放在 `frontend/app/(main)/admin/database/components/`。这保证了代码结构层次清晰、单文件小巧好维护。所有复杂页面的新开发和重构必须遵循此模式。 + + 页面宽度: - 页面根容器必须支持全宽。使用 `w-full`。 diff --git a/frontend/app/(main)/admin/database/page.tsx b/frontend/app/(main)/admin/database/page.tsx index d1fde8ae..9d9e9b4c 100644 --- a/frontend/app/(main)/admin/database/page.tsx +++ b/frontend/app/(main)/admin/database/page.tsx @@ -1,8 +1,313 @@ "use client" -import {DatabaseManager} from "@/components/common/admin/database-manage" +import * as React from "react" +import {useCallback, useEffect, useState} from "react" +import {toast} from "sonner" +import { + Activity, + Cpu, + Database, + Download, + FileText, + HardDrive, + Layers, + RefreshCw, + Server, + Terminal, +} from "lucide-react" -/* 数据库管理页面 */ -export default function DatabasePage() { - return +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Button} from "@/components/ui/button" +import {Skeleton} from "@/components/ui/skeleton" +import type {DBOverview} from "@/lib/services" +import services, {AdminService} from "@/lib/services" + +import {TableBrowser} from "@/components/common/admin/database-table-browser" +import {CacheManager} from "@/components/common/admin/database-cache-manager" +import {SQLConsole} from "@/components/common/admin/database-sql-console" + +/** + * 格式化数字,每3位加逗号 + */ +const formatNumber = (num: number | string) => { + if (num === undefined || num === null) return "0" + return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") +} + +export default function DatabasePage() { + // 核心状态 + const [overview, setOverview] = useState(null) + const [tables, setTables] = useState([]) + const [loadingOverview, setLoadingOverview] = useState(true) + const [loadingTables, setLoadingTables] = useState(true) + const [exporting, setExporting] = useState(false) + + // 协调子组件刷新与视图切换状态 + const [showConsole, setShowConsole] = useState(false) + const [refreshTrigger, setRefreshTrigger] = useState(0) + + // 1. 获取运行概览 + const fetchOverview = useCallback(async (isSilent = false) => { + if (!isSilent) setLoadingOverview(true) + try { + const data = await services.dbManage.getOverview() + setOverview(data) + } catch (err) { + toast.error("获取数据库概览失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingOverview(false) + } + }, []) + + // 2. 获取表列表 + const fetchTables = useCallback(async () => { + setLoadingTables(true) + try { + const data = await services.dbManage.listTables() + setTables(data) + } catch (err) { + toast.error("获取数据库数据表列表失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingTables(false) + } + }, []) + + // 3. 协调刷新 + const handleRefreshAll = () => { + fetchOverview() + fetchTables() + setRefreshTrigger((prev) => prev + 1) + } + + // 4. 导出数据库备份 + const handleExport = async () => { + setExporting(true) + try { + const { blob, filename } = await AdminService.exportDatabase() + const url = URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + URL.revokeObjectURL(url) + toast.success("数据库导出成功", { description: `已下载归档文件: ${filename}` }) + } catch (err) { + toast.error("数据库导出失败", { + description: err instanceof Error ? err.message : "导出异常", + }) + } finally { + setExporting(false) + } + } + + // 初始化加载 + useEffect(() => { + fetchOverview() + fetchTables() + }, [fetchOverview, fetchTables]) + + // 渲染概览卡片骨架 + const renderOverviewSkeleton = () => ( +

+ {Array.from({ length: 6 }).map((_, i) => ( + + + + + + + + + ))} +
+ ) + + // 渲染 SQL 查询控台全屏视图 + if (showConsole) { + return ( + setShowConsole(false)} + /> + ) + } + + // 正常页面排版布局 + return ( +
+ {/* 顶部控制与标题 */} +
+
+ +
+

数据管理

+
+
+ +
+ + {/* 1. 概览指标卡片组 */} + {loadingOverview ? ( + renderOverviewSkeleton() + ) : ( + overview && ( +
+ {/* 卡片1: 数据库类型 */} + + + 数据库类型 + + + +
{overview.type}
+
+
+ + {/* 卡片2: 数据库版本 */} + + + 版本信息 + + + +
+ {overview.version.split(" ").slice(0, 2).join(" ")} +
+
+
+ + {/* 卡片3: 数据库名称 */} + + + 名称/路径 + + + +
+ {overview.name.substring(overview.name.lastIndexOf("/") + 1)} +
+
+
+ + {/* 卡片4: 数据库大小 */} + + + 数据库大小 + + + +
{overview.size}
+
+
+ + {/* 卡片5: 表数量 */} + + + 物理数据表 + + + +
{formatNumber(overview.table_count)}
+
+
+ + {/* 卡片6: 连接数 */} + + + 活跃连接数 + + + +
{formatNumber(overview.connections)}
+
+
+
+ ) + )} + + {/* 2. 数据表浏览器区块 */} + + + {/* 3. 缓存管理区块 */} + + + {/* 4. 底部功能卡片区 */} + + + 功能区 + 数据库导出及自定义高级 SQL 执行终端 + + +
+ {/* 功能一: 数据库导出 */} +
+
+

+ + 数据库备份导出 +

+

+ 直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL 导出为打包的 .sql 文本文件) +

+
+ +
+ + {/* 功能二: SQL 查询终端 */} +
+
+

+ + SQL 查询控台 +

+

+ 打开在线 SQL 控制终端,执行原生的 SQL 进行数据筛选、更新、调试或性能优化 +

+
+ +
+
+
+
+
+ ) } diff --git a/frontend/app/(main)/admin/demo/page.tsx b/frontend/app/(main)/admin/demo/page.tsx new file mode 100644 index 00000000..d9796459 --- /dev/null +++ b/frontend/app/(main)/admin/demo/page.tsx @@ -0,0 +1,82 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {Code} from "lucide-react" + +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" + +// 引入模块化拆分后的 Tab 子组件 +import {DashboardTab} from "./tabs/dashboard-tab" +import {TableTab} from "./tabs/table-tab" +import {ControlsTab} from "./tabs/controls-tab" + +export default function HeaderDemoPage() { + const [activeTab, setActiveTab] = React.useState("dashboard") + + return ( +
+ {/* 1. 标准页面标题 */} +
+ +
+

开发规范示例

+
+
+ + {/* 说明区域 */} + + +
+ + 规范说明 +
+ + 此页面展示了后台 Sidebar 关联页面的标准标题栏布局,供开发参考。 + +
+ +
+

核心规范细节与编写原则:

+
    +
  • 结构专注:页面标题区必须保持绝对干净。请不要在标题下方放置任何描述段落或小字副标题,让页面在视线进入时保持极简与信息纯粹。
  • +
  • 容器与上边距对齐:使用 flex items-center gap-2 作为标题行基础容器(不含下边框),外层统一使用 py-6 px-1 (或 py-6) 进行上边距的视觉对齐,确保切换菜单时顶部高度感一致。
  • +
  • 图标呈现:直接将 Lucide 图标组件嵌套于标题容器中,应用 size-5 text-primary 样式。禁止为图标包裹任何背景色小卡片、圆角或装饰性边框。
  • +
  • 文本样式标准:文本使用且仅使用 {'h1 className="text-2xl font-semibold tracking-tight"'},禁用字重加粗(如 font-bold)或任何渐变色,保证全站字体一致性。
  • +
  • Tabs 模块化拆分:凡是带有多个 Tab 页切换的页面,**禁止**将所有 Tab 的渲染代码堆积在同一个物理页面主文件内。每个 Tab 页对应的 Content 必须单独封装进全局 components/ 目录下专属文件夹中,主页面文件只负责维护当前 Tab 的激活状态及外层骨架布局。这有利于模块复用、编译优化以及提升单文件可读性。
  • +
  • 复杂度驱动的区块拆分:不仅是 Tabs 切换,当一个页面为单页界面且代码行数超过 800 行时,必须将不同的物理块内容拆分为子组件。为保持路由树的清爽和逻辑纯粹,所有拆分出的非复用子组件也应统一存放在全局组件文件夹中(如 components/common/admin/),而路由入口 page.tsx 仅用于定义整个页面的布局排版与全局框架。典型标杆案例参考“数据管理” (/admin/database) 的重构。
  • +
+
+
+
+ + {/* Tabs 切换 */} + + + + 仪表盘与指标卡片 + + + 数据表格规范 + + + 输入控件与表单 + + + + + + + + + + + + + +
+ ) +} diff --git a/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx b/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx new file mode 100644 index 00000000..5535365d --- /dev/null +++ b/frontend/app/(main)/admin/demo/tabs/controls-tab.tsx @@ -0,0 +1,150 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {Code} from "lucide-react" +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Label} from "@/components/ui/label" +import {Input} from "@/components/ui/input" +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" +import {Checkbox} from "@/components/ui/checkbox" +import {Button} from "@/components/ui/button" + +export function ControlsTab() { + const [formData, setFormData] = React.useState({ + name: "", + category: "database", + allowNotify: true, + channel: "all", + }) + + return ( +
+ {/* 控件展示 */} + + + 表单与输入框交互演示 + 包含按钮变体、下拉选择、多选框及基础输入框的标准样式 + + + {/* 输入框 */} +
+ + setFormData(prev => ({ ...prev, name: e.target.value }))} + placeholder="输入要创建的任务描述..." + className="h-8 text-xs shadow-none bg-background focus-visible:ring-1" + /> +

任务名称将用于后台执行记录的可读性显示

+
+ + {/* 下拉框 */} +
+ + +
+ + {/* 多选框 Checkbox */} +
+ setFormData(prev => ({ ...prev, allowNotify: checked === true }))} + className="mt-0.5" + /> +
+ +

+ 勾选后,在当前后台任务顺利执行结束时,会关联派发多渠道通知推送。 +

+
+
+ + {/* 按钮变体 */} +
+
按钮样式对齐:
+
+ + + + +
+
+
+
+ + {/* 控件设计规范说明 */} + + + + + 表单设计规范 + + + +
+

1. 统一控件高度与投影:

+

+ 输入框 (Input) 与下拉选择器 (SelectTrigger) 的高度应统一限制为小号高度 h-8,字体大小使用 text-xs,并使用 shadow-none 去除原生的重叠阴影。 +

+ +

2. Checkbox 与文本的对齐:

+

+ Checkbox 应在左侧垂直偏上对齐,使用容器 flex items-start gap-2.5 排列。Checkbox 组件本身添加 mt-0.5,从而在多行描述文本的情况下依然保持良好的顶对齐,避免出现居中对齐导致的凌乱感。 +

+ +

3. 按钮使用策略:

+

+ 普通主表单及新增/保存按钮,一律使用默认 Primary 按钮或次要 {'variant="secondary"'} 按钮,小号高度统一采用 h-8 text-xs。线框按钮必须带有 border-dashed shadow-none 类。 +

+
+ +
+
{`// 1. Label 与 Input 对齐`}
+
{`
+ + +
`}
+
{`// 2. Checkbox 顶对齐`}
+
{`
+ +
+ +

多行说明内容

+
+
`}
+
+
+
+
+ ) +} diff --git a/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx new file mode 100644 index 00000000..a02a68b2 --- /dev/null +++ b/frontend/app/(main)/admin/demo/tabs/dashboard-tab.tsx @@ -0,0 +1,98 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {Code, Cpu, Database, HardDrive} from "lucide-react" +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card" +import {Progress} from "@/components/ui/progress" + +export function DashboardTab() { + return ( +
+
+ {/* CPU */} + + + 容器 CPU 使用率 + + + +
42.8 %
+ +

+ + 容器运行正常,负载平稳 +

+
+
+ + {/* Storage */} + + + 存储已用容量 + + + +
72.4 GB / 100 GB
+ +

+ 数据已同步远端 S3 存储桶,支持自动过期 +

+
+
+ + {/* DB Overview */} + + + 物理数据库 + + + +
PostgreSQL (v16.2)
+
+
活跃连接: 12
+
总表数量: 34
+
+

+ 链接池自动维护中,最大连接限制为 100 +

+
+
+
+ + {/* 指标卡片规范与代码 */} + + + + + 指标卡片 & 仪表盘设计规范 + + + +
    +
  • 指标卡片统一采用 border-dashed shadow-none 作为边框和投影。
  • +
  • 头部使用 flex flex-row items-center justify-between pb-2 布局,左侧为标题小字,右侧为直接呈现的 Lucide 图标。
  • +
  • 数值强调使用且仅使用 text-2xl font-semibold tracking-tight 展示。
  • +
  • 进度条使用 h-1.5 细条高度,不喧宾夺主。
  • +
  • 底部的次要说明信息必须统一使用 text-[10px] text-muted-foreground 等辅助类。
  • +
+
+{`
+  
+    指标标题
+    
+  
+  
+    
42.8 %
+ +

状态说明小字

+
+
`} +
+
+
+
+ ) +} diff --git a/frontend/app/(main)/admin/demo/tabs/table-tab.tsx b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx new file mode 100644 index 00000000..2a65455c --- /dev/null +++ b/frontend/app/(main)/admin/demo/tabs/table-tab.tsx @@ -0,0 +1,142 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + +"use client" + +import * as React from "react" +import {Code, Edit, ListFilter, Plus, Search, Trash2} from "lucide-react" +import {Button} from "@/components/ui/button" +import {Input} from "@/components/ui/input" +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" +import {Badge} from "@/components/ui/badge" +import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card" + +export function TableTab() { + const mockTableData = [ + { id: "TX-1002", name: "用户信息批量同步导出", type: "数据同步", status: "success", duration: "1.2s", time: "2026-06-15 14:22" }, + { id: "TX-1003", name: "系统数据库每周自动备份归档", type: "数据库运维", status: "running", duration: "在执行中", time: "2026-06-15 15:00" }, + { id: "TX-1004", name: "远端S3过期缓存对象自动清理任务", type: "磁盘清理", status: "failed", duration: "0.4s", time: "2026-06-15 10:05" }, + ] + + return ( +
+ {/* 表格控制工具栏 */} +
+
+
+ + +
+ +
+ +
+ + {/* 数据表格本身 */} +
+ + + + 任务编号 + 执行任务名称 + 任务类型 + 状态 + 执行耗时 + 触发时间 + 操作 + + + + {mockTableData.map((row) => ( + + {row.id} + {row.name} + {row.type} + + {row.status === "success" && ( + + + 成功 + + )} + {row.status === "running" && ( + + + 运行中 + + )} + {row.status === "failed" && ( + + + 失败 + + )} + + {row.duration} + {row.time} + +
+ + +
+
+
+ ))} +
+
+
+ + {/* 表格规范指引 */} + + + + + 表格设计规范与代码模板 + + + +
    +
  • 表格容器必须统一使用带有虚线边框的容器,即类名:border border-dashed shadow-none rounded-lg overflow-hidden bg-background。
  • +
  • 表头背景设为 bg-muted/40。表头表项字号统一为小字粗体 text-xs font-semibold。
  • +
  • 单元格行与列之间使用虚线分割(border-dashed)。表格行悬浮交互采用温和过渡色 hover:bg-muted/10 transition-colors。
  • +
  • 编码、主键或唯一 ID 类字段一律采用等宽字体呈现:font-mono text-xs。
  • +
  • 状态标签(Badge)尽量圆角化(rounded-full py-0 px-2),并以浅背景配以各自对应的图标圆点,以实现现代、高对比度的视觉质感。
  • +
+
+{`
+ + + + 编号 + 状态 + + + + + ID-1001 + + + + 成功 + + + + +
+
`} +
+
+
+
+ ) +} diff --git a/frontend/app/(main)/admin/files/page.tsx b/frontend/app/(main)/admin/files/page.tsx index fbfcef22..a8e3d376 100644 --- a/frontend/app/(main)/admin/files/page.tsx +++ b/frontend/app/(main)/admin/files/page.tsx @@ -1,5 +1,59 @@ -import {FilesMain} from "@/components/common/admin/files" +"use client" + +import * as React from "react" +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" export default function FilesPage() { - return + const [activeTab, setActiveTab] = React.useState("stats") + + return ( + + {/* 顶部标题区 */} +
+ +
+

存储管理

+
+
+ + + + + 文件存储信息 + + + 文件列表 + + + 存储配置 + + + + {/* ──────── TAB 1: 统计看板 ──────── */} + + + + + {/* ──────── TAB 2: 文件列表 ──────── */} + + + + + + + + +
+ ) } diff --git a/frontend/app/(main)/admin/logs/page.tsx b/frontend/app/(main)/admin/logs/page.tsx index 317a750d..488485c3 100644 --- a/frontend/app/(main)/admin/logs/page.tsx +++ b/frontend/app/(main)/admin/logs/page.tsx @@ -1,8 +1,66 @@ +/* +Copyright 2026 Arctel.net + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + "use client" -import {SystemLogs} from "@/components/common/admin/system-logs" +import {Activity, BarChart3, Terminal} from "lucide-react" + +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" +import {AppLogs} from "@/components/common/admin/app-logs" +import {AccessLogs} from "@/components/common/admin/access-logs" +import {AccessAnalytics} from "@/components/common/admin/access-analytics" -/* 系统日志页面 */ export default function LogsPage() { - return + return ( +
+ {/* Header */} +
+ +
+

系统日志

+
+
+ {/* Tabs Layout */} + + + + + 访问分析 + + + + 用户访问日志 + + + + 应用运行日志 + + + + + + + + + + + + + +
+ ) } + diff --git a/frontend/app/(main)/admin/push/page.tsx b/frontend/app/(main)/admin/push/page.tsx index d8f2eea5..e12a1531 100644 --- a/frontend/app/(main)/admin/push/page.tsx +++ b/frontend/app/(main)/admin/push/page.tsx @@ -1,14 +1,65 @@ // Copyright 2026 Arctel.net // SPDX-License-Identifier: Apache-2.0 -import type {Metadata} from "next"; -import {PushNotificationManager} from "@/components/common/admin/push-notification"; +"use client" -export const metadata: Metadata = { - title: "通知推送 - Wavelet Admin", - description: "系统通知多渠道推送与事件管理控制台", -}; +import * as React from "react" +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" export default function PushAdminPage() { - return ; + const [activeTab, setActiveTab] = React.useState("events") + + return ( + +
+ +
+

通知推送管理

+
+
+ + + + + + 事件管理 + + + + 通知历史 + + + + 通道管理与设置 + + + + {/* ==================== 1. 事件管理 TAB ==================== */} + + + + + {/* ==================== 2. 通知历史 TAB ==================== */} + + + + + {/* ==================== 3. 通道管理与设置 TAB ==================== */} + + + + +
+ ) } diff --git a/frontend/app/(main)/admin/settings/page.tsx b/frontend/app/(main)/admin/settings/page.tsx index 4120557b..0f71c5bb 100644 --- a/frontend/app/(main)/admin/settings/page.tsx +++ b/frontend/app/(main)/admin/settings/page.tsx @@ -1,8 +1,117 @@ +// Copyright 2026 Arctel.net +// SPDX-License-Identifier: Apache-2.0 + "use client" -import {SystemSettingsMain} from "@/components/common/settings/system-settings" +import {useEffect, useMemo} from "react" +import {useQuery} from "@tanstack/react-query" +import {Loader2, Settings} from "lucide-react" +import {useRouter} from "next/navigation" +import {motion} from "motion/react" -/* 系统设置页面 */ -export default function AdminSettingsPage() { - return +import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" +import {useAuth} from "@/components/providers/auth-provider" +import {AdminService} from "@/lib/services" +import type {SystemConfig} from "@/lib/services/admin" +import {SystemStatusManager} from "@/components/common/admin/status" + +// 引入位于 components/common/settings/ 目录下的具体业务 Tab +import {SecurityTab} from "@/components/common/settings/security-tab" +import {OperationTab} from "@/components/common/settings/operation-tab" +import {SystemTab} from "@/components/common/settings/system-tab" +import {OtherTab} from "@/components/common/settings/other-tab" +import {InfoTab} from "@/components/common/settings/info-tab" + +function systemConfigMap(configs: SystemConfig[]) { + return configs.reduce>((accumulator, config) => { + accumulator[config.key] = config + return accumulator + }, {}) +} + +export default function AdminSettingsPage() { + const { user, loading } = useAuth() + const router = useRouter() + + const systemConfigsQuery = useQuery({ + queryKey: ["admin", "system-configs"], + queryFn: () => AdminService.listSystemConfigs("system"), + enabled: !!user?.is_admin, + }) + + const configs = useMemo( + () => systemConfigMap(systemConfigsQuery.data ?? []), + [systemConfigsQuery.data], + ) + + useEffect(() => { + if (!loading && (!user || !user.is_admin)) { + router.replace("/settings/profile") + } + }, [user, loading, router]) + + if (loading || !user || !user.is_admin) { + return ( +
+ +
+ ) + } + + return ( + +
+ +
+

系统设置

+
+
+ + + + 安全设置 + + + 业务设置 + + + 系统设置 + + + 其他设置 + + + 系统状态 + + + 系统信息 + + + + + + + + + + + + + + + + + + + + + + +
+ ) } diff --git a/frontend/app/(main)/admin/system/page.tsx b/frontend/app/(main)/admin/system/page.tsx index 15b705df..fb6f54ca 100644 --- a/frontend/app/(main)/admin/system/page.tsx +++ b/frontend/app/(main)/admin/system/page.tsx @@ -1,9 +1,243 @@ "use client" -import {SystemConfigs} from "@/components/common/admin/system" -import {AdminProvider} from "@/contexts/admin-context" +import * as React from "react" +import {Input} from "@/components/ui/input" +import {Switch} from "@/components/ui/switch" +import {ManageDetailPanel, ManagePage} from "@/components/common/general/manage-pannel" +import {Tabs, TabsList, TabsTrigger} from "@/components/ui/tabs" +import {ShieldCheck} from "lucide-react" + +import {formatDateTime} from "@/lib/utils" +import type {SystemConfig} from "@/lib/services" +import {AdminProvider, useAdmin} from "@/contexts/admin-context" + + +/** + * 系统配置 + * 显示系统配置的详细信息和编辑面板 + * + * @example + * ```tsx + * + * ``` + * @param {SystemConfig} config - 系统配置 + * @param {Partial} editData - 编辑数据 + * @param {function} onEditDataChange - 编辑数据改变回调 + * @param {function} onSave - 保存回调 + * @param {boolean} saving - 是否正在保存 + * @returns {React.ReactNode} 系统配置详情面板组件 + */ +function SystemConfigDetailPanel({ + config, + editData, + onEditDataChange, + onSave, + saving +}: { + config: SystemConfig | null + editData: Partial + onEditDataChange: (field: keyof SystemConfig, value: SystemConfig[keyof SystemConfig]) => void + onSave: () => void + saving: boolean +}) { + + return ( + +
+
+
+ +

{config?.key}

+
+ +
+ +
+ {config?.key.endsWith('_enabled') ? ( + { + onEditDataChange('value', checked ? 'true' : 'false') + }} + /> + ) : ( + { + const value = e.target.value + if (value === '') { + onEditDataChange('value', '') + return + } + onEditDataChange('value', value) + }} + className="!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]" + style={{ + MozAppearance: 'textfield' + }} + /> + )} +
+
+ +
+ +
+ { + const value = e.target.value + onEditDataChange('description', value) + }} + className="!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px]" + /> +
+
+ +
+ + { + onEditDataChange('visibility', checked ? 1 : 0) + }} + /> +
+ +
+ + + {config?.type === 'system' ? '系统配置' : '业务配置'} + +
+ +
+ +

{config ? formatDateTime(config.created_at) : ''}

+
+
+
+
+ ) +} + + + +/** + * 系统配置管理组件 + * + * @example + * ```tsx + * + * ``` + * @returns {React.ReactNode} 系统配置管理组件 + */ +export function SystemConfigs() { + const { + systemConfigs: configs, + systemConfigsLoading: loading, + systemConfigsError: error, + refetchSystemConfigs, + updateSystemConfig + } = useAdmin() + + const [activeTab, setActiveTab] = React.useState<'system' | 'business'>('business') + + React.useEffect(() => { + refetchSystemConfigs(activeTab) + }, [activeTab, refetchSystemConfigs]) + + const getInitialEditData = (config: SystemConfig) => ({ + value: config.value, + visibility: config.visibility, + description: config.description + }) + + const handleSave = async (config: SystemConfig, editData: Partial) => { + if (!config) return + + await updateSystemConfig(config.key, { + value: editData.value ?? config.value, + visibility: editData.visibility ?? config.visibility, + description: editData.description ?? config.description + }) + } + + return ( + + title="系统配置" + icon={ShieldCheck} + data={configs} + loading={loading} + error={error} + onReload={() => refetchSystemConfigs(activeTab)} + getInitialEditData={getInitialEditData} + onSave={handleSave} + getId={(config) => config.key} + emptyDescription="未发现系统配置" + loadingDescription="配置加载中" + headerExtra={ + setActiveTab(val as 'system' | 'business')} className="w-[180px]"> + + 业务配置 + 系统配置 + + + } + columns={[ + { header: "配置键", cell: (item) => {item.key}, width: "200px" }, + { header: "配置值", cell: (item) => {item.value}, width: "120px" }, + { header: "公共可见", cell: (item) => {item.visibility === 1 ? "可见" : "不可见"}, width: "80px" }, + { header: "描述", cell: (item) => {item.description}, width: "200px" }, + ]} + renderDetail={({ selected, hovered, editData, onEditDataChange, onSave, saving }) => ( + + )} + /> + ) +} -/* 系统配置页面 */ export default function SystemConfigPage() { return ( @@ -11,4 +245,3 @@ export default function SystemConfigPage() { ) } - diff --git a/frontend/app/(main)/admin/tasks/page.tsx b/frontend/app/(main)/admin/tasks/page.tsx index b7552f65..b18108b6 100644 --- a/frontend/app/(main)/admin/tasks/page.tsx +++ b/frontend/app/(main)/admin/tasks/page.tsx @@ -7,7 +7,7 @@ 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 {Spinner} from "@/components/ui/spinner" -import {Activity} from "lucide-react" +import {Layers} from "lucide-react" function TasksPageContent() { const searchParams = useSearchParams() @@ -19,23 +19,24 @@ function TasksPageContent() { } return (
-
-
- -
+
+
-

任务管理

-

- 下发异步镜像与同步导出任务,管理后台执行计划及调度日志。 -

+

任务管理

- - 任务管理 - 定时任务 - 任务日志 + + + 任务管理 + + + 定时任务 + + + 任务日志 + diff --git a/frontend/app/(main)/admin/users/page.tsx b/frontend/app/(main)/admin/users/page.tsx index 6e20d868..fb4ce452 100644 --- a/frontend/app/(main)/admin/users/page.tsx +++ b/frontend/app/(main)/admin/users/page.tsx @@ -1,8 +1,269 @@ "use client" -import { UsersManager } from "@/components/common/admin/users" +import {useEffect, useState} from "react" +import {Button} from "@/components/ui/button" +import {Switch} from "@/components/ui/switch" +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" +import {Badge} from "@/components/ui/badge" +import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar" +import {Eye, Layers, Loader2, Plus, Trash2, UserRound, UserX,} from "lucide-react" +import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle +} from "@/components/ui/alert-dialog" + +import {AdminUser} from "@/lib/services" +import {formatDateTime} from "@/lib/utils" +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" -/* 用户管理页面 */ export default function UsersPage() { - return + const { + users, + loading, + error, + fetchUsers, + getUserDetail, + updateUserStatus, + deleteUser + } = useAdminUsers() + + const [selectedUser, setSelectedUser] = useState(null) + const [detailOpen, setDetailOpen] = useState(false) + const [detailLoading, setDetailLoading] = useState(false) + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleteLoading, setDeleteLoading] = useState(false) + const [createModalOpen, setCreateModalOpen] = useState(false) + + useEffect(() => { + fetchUsers() + }, [fetchUsers]) + + const handleStatusToggle = async (user: AdminUser) => { + await updateUserStatus(user) + + if (selectedUser?.id === user.id) { + setSelectedUser(prev => prev ? { ...prev, is_active: !prev.is_active } : null) + } + } + + const handleShowDetail = async (user: AdminUser) => { + setSelectedUser(user) + setDetailOpen(true) + setDetailLoading(true) + + try { + const detail = await getUserDetail(user.id) + setSelectedUser(detail) + } catch { + setSelectedUser(user) + } finally { + setDetailLoading(false) + } + } + + const handleDeleteUser = async () => { + if (!deleteTarget) return + + setDeleteLoading(true) + try { + await deleteUser(deleteTarget) + if (selectedUser?.id === deleteTarget.id) { + setDetailOpen(false) + setSelectedUser(null) + } + setDeleteTarget(null) + } finally { + setDeleteLoading(false) + } + } + + return ( +
+ {/* 顶部标题栏 */} +
+
+ +
+

用户管理

+
+
+ +
+ + {/* 筛选与分页栏 (已拆分为独立自治组件) */} + + + {error ? ( +
+ fetchUsers(true)} className="justify-center" /> +
+ ) : loading && users.length === 0 ? ( + + ) : users.length === 0 ? ( + + ) : ( +
+ + + + ID + 用户 + 上次登陆 + 注册时间 + 上次更新 + 操作 + + + + {users.map((user) => ( + handleShowDetail(user)} + > + {user.id} + +
+ + + + {user.username.substring(0, 2).toUpperCase()} + + +
+
+ {user.nickname} + {user.is_admin && ( + + ADM + + )} +
+
+ @{user.username} +
+
+
+
+ + + {formatDateTime(user.last_login_at)} + + + {formatDateTime(user.created_at)} + + + {formatDateTime(user.updated_at)} + + e.stopPropagation()}> +
+ + + +
+ handleStatusToggle(user)} + disabled={user.is_admin} + className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7" + /> +
+
+ + {user.is_admin ? '管理员账户' : user.is_active ? '禁用账户' : '启用账户'} + +
+
+ + + + + + + + 查看详情 + + + + + {!user.is_admin && ( + + + + + + + 删除用户 + + + + )} +
+
+
+ ))} +
+
+
+ )} + + {/* 用户详情侧拉抽屉 (已拆分为独立子组件) */} + + + {/* 删除确认警告弹窗 */} + !open && !deleteLoading && setDeleteTarget(null)}> + + + 确认删除用户 + + 确定要删除用户 {deleteTarget?.nickname || deleteTarget?.username} 吗?该操作会移除用户账号,删除后无法撤销。 + + + + 取消 + + {deleteLoading && } + 确认删除 + + + + + + {/* 新建用户模态弹窗 */} + setCreateModalOpen(false)} /> +
+ ) } diff --git a/frontend/app/(main)/components/page.tsx b/frontend/app/(main)/components/page.tsx deleted file mode 100644 index a9a36b90..00000000 --- a/frontend/app/(main)/components/page.tsx +++ /dev/null @@ -1,723 +0,0 @@ -"use client" - -import * as React from "react" -import {useMemo, useState} from "react" -import {AnimatePresence, motion} from "motion/react" -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" -import {Badge} from "@/components/ui/badge" -import {Button} from "@/components/ui/button" -import {Progress} from "@/components/ui/progress" -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {Activity, ArrowRight, Check, Code2, Coins, Copy, Gift, Info, Layers, Shield, User} from "lucide-react" -import {Area, AreaChart, ResponsiveContainer, Tooltip as RechartsTooltip, XAxis} from "recharts" -import {toast} from "sonner" - -// ========================================== -// 1. 卡片类组件 (Cards) -// ========================================== - -function FeatureCard({ - title, - description, - linkText, - href -}: { - title: string - description: string - linkText: string - href?: string -}) { - return ( - - - {title} - - - - {description} - - - - - ) -} - -function ReceiveBanner() { - const [isVisible, setIsVisible] = useState(true) - - return ( - - {isVisible && ( - -
-
-

获得积分收益

-

- 通过无代码选项快速开始探索或使用与我们的 API 集成的可自定义积分服务。集成方式多样,满足各种场景需求。 -

-
- - -
-
-
-
- )} -
- ) -} - -// ========================================== -// 2. 仪表盘组件 (Dashboards) -// ========================================== - -const MOCK_CHART_DATA = [ - { date: "06-01", total: 12000, income: 400, expense: 200 }, - { date: "06-02", total: 13500, income: 1500, expense: 300 }, - { date: "06-03", total: 12800, income: 300, expense: 1000 }, - { date: "06-04", total: 15000, income: 2500, expense: 300 }, - { date: "06-05", total: 14200, income: 200, expense: 1000 }, - { date: "06-06", total: 16800, income: 3000, expense: 400 }, - { date: "06-07", total: 17500, income: 1200, expense: 500 }, -] - -function MockTrendChart() { - return ( - - - 积分变动趋势 (静态模拟) - -
- - - - - - - - - - - - - -
-
- ) -} - -function MockIncomeStats() { - const stats = [ - { date: "06-07", amount: 1200, percentage: 80 }, - { date: "06-06", amount: 3000, percentage: 100 }, - { date: "06-05", amount: 200, percentage: 15 }, - { date: "06-04", amount: 2500, percentage: 90 }, - { date: "06-03", amount: 300, percentage: 20 }, - { date: "06-02", amount: 1500, percentage: 65 }, - { date: "06-01", amount: 400, percentage: 25 }, - ] - - return ( - - -
- 7天收入统计 (静态模拟) -
LDC 9,100.00
-
- -
-
- {stats.map((s) => ( -
-
- {s.date} - +{s.amount.toFixed(2)} -
- -
- ))} -
-
- ) -} - -function MockTopCustomers() { - const customers = [ - { rank: 1, name: "dev_user", count: 42, amount: 28500 }, - { rank: 2, name: "linux_do_fan", count: 28, amount: 19800 }, - { rank: 3, name: "gorm_expert", count: 19, amount: 12400 }, - { rank: 4, name: "nextjs_coder", count: 12, amount: 8500 }, - { rank: 5, name: "redis_helper", count: 8, amount: 5000 }, - ] - - return ( - - - Top 客户统计 (静态模拟) - -
- {customers.map((c) => ( -
-
-
- #{c.rank} - {c.name} -
-
- {c.amount.toLocaleString()} LDC -
-
-
- - {c.count}单 -
-
- ))} -
-
- ) -} - -// ========================================== -// 3. 数据表格组件 (Tables) -// ========================================== - -interface MockOrder { - id: string - order_name: string - amount: number - type: 'payment' | 'transfer' | 'community' | 'red_envelope' - status: 'success' | 'pending' | 'expired' | 'disputing' | 'refund' - order_no: string - created_at: string -} - -const MOCK_ORDERS: MockOrder[] = [ - { id: "1", order_name: "API 接口充值积分", amount: 1000, type: "payment", status: "success", order_no: "ORD202606070001", created_at: "2026-06-07 14:20:15" }, - { id: "2", order_name: "退款原路退回", amount: 500, type: "payment", status: "refund", order_no: "ORD202606070002", created_at: "2026-06-07 12:05:44" }, - { id: "3", order_name: "拼手气红包分配", amount: 200, type: "red_envelope", status: "success", order_no: "ORD202606060012", created_at: "2026-06-06 18:30:00" }, - { id: "4", order_name: "在线流转纠纷处理", amount: 1500, type: "transfer", status: "disputing", order_no: "ORD202606050025", created_at: "2026-06-05 09:12:10" }, - { id: "5", order_name: "社区绑定自动转入", amount: 350, type: "community", status: "pending", order_no: "ORD202606040081", created_at: "2026-06-04 23:45:19" }, - { id: "6", order_name: "红包超时未领过期退回", amount: 100, type: "red_envelope", status: "expired", order_no: "ORD202606030009", created_at: "2026-06-03 15:00:00" }, -] - -function MockTransactionTable() { - const getStatusBadge = (status: MockOrder["status"]) => { - const map = { - success: { text: "交易成功", class: "bg-green-500/10 text-green-600 border-green-500/20" }, - pending: { text: "等待付款", class: "bg-amber-500/10 text-amber-600 border-amber-500/20" }, - expired: { text: "订单过期", class: "bg-slate-500/10 text-slate-500 border-slate-500/20" }, - disputing: { text: "争议中", class: "bg-rose-500/10 text-rose-600 border-rose-500/20" }, - refund: { text: "交易退款", class: "bg-indigo-500/10 text-indigo-600 border-indigo-500/20" }, - } - return ( - - {map[status].text} - - ) - } - - const getTypeBadge = (type: MockOrder["type"]) => { - const map = { - payment: { text: "消耗", class: "bg-orange-500/10 text-orange-600" }, - transfer: { text: "流转", class: "bg-blue-500/10 text-blue-600" }, - community: { text: "社区", class: "bg-purple-500/10 text-purple-600" }, - red_envelope: { text: "红包", class: "bg-red-500/10 text-red-600" }, - } - return ( - - {map[type].text} - - ) - } - - return ( - -
- - - - - - - - - - - - - - {MOCK_ORDERS.map((order) => ( - - - - - - - - - - ))} - -
名称类型额度状态流水编号交易时间操作
{order.order_name}{getTypeBadge(order.type)} - {order.amount.toFixed(2)} - {getStatusBadge(order.status)}{order.order_no}{order.created_at} - -
-
-
- ) -} - -// ========================================== -// 4. Tab 导航组件 (Tabs) -// ========================================== - -function MockTradeTabs() { - const [activeTab, setActiveTab] = useState("receive") - - const tabContent = { - receive: { - title: "积分收益 (Receive)", - description: "本模块汇集用户所有的进账记录。可通过 API、活动赚取等形式增加积分值。", - icon: Coins, - color: "text-emerald-500", - bgColor: "bg-emerald-500/10", - }, - payment: { - title: "积分消耗 (Payment)", - description: "本模块汇总用户的出账流水。支持兑换、系统扣减与在线付款场景的费用记账。", - icon: Gift, - color: "text-orange-500", - bgColor: "bg-orange-500/10", - }, - community: { - title: "社区划转 (Community)", - description: "对接 Discourse 外部论坛同步时自动调度的积分余额转换事件。", - icon: Shield, - color: "text-purple-500", - bgColor: "bg-purple-500/10", - }, - online: { - title: "在线流转 (Online)", - description: "指代用户通过付款链接或集市网关主动进行的收单支付流转活动记录。", - icon: Activity, - color: "text-blue-500", - bgColor: "bg-blue-500/10", - }, - all: { - title: "所有活动 (All Activities)", - description: "汇总上述所有分类的完整历史日志,支持按分类检索和导出报表。", - icon: Layers, - color: "text-slate-500", - bgColor: "bg-slate-500/10", - }, - } - - const activeInfo = tabContent[activeTab as keyof typeof tabContent] || tabContent.receive - - return ( - - - - - 积分收益 - - - 积分消耗 - - - 社区划转 - - - 在线流转 - - - 所有活动 - - - - -
-
- -
-
-

{activeInfo.title}

-

{activeInfo.description}

-
-
-
-
-
- ) -} - -// ========================================== -// 5. 代码片段复制辅助组件 -// ========================================== - -function CodeShowcase({ code, title }: { code: string; title: string }) { - const [copied, setCopied] = useState(false) - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(code) - setCopied(true) - toast.success("代码片段已复制到剪贴板") - setTimeout(() => setCopied(false), 2000) - } catch { - toast.error("复制失败") - } - } - - return ( - - -
- - {title} -
- -
-
-        {code}
-      
-
- ) -} - -// ========================================== -// 主组件 (Main Showcase) -// ========================================== - -export default function ComponentLibraryPage() { - const [activeCategory, setActiveCategory] = useState("cards") - - const categories = [ - { id: "cards", title: "卡片组件 (Cards)", desc: "页面核心功能聚合引导卡片", icon: User }, - { id: "dashboards", title: "大盘统计 (Dashboards)", desc: "交易波动与客户数据展示图表", icon: Activity }, - { id: "tables", title: "数据表格 (Tables)", desc: "支持多类型与状态的明细流水表", icon: Layers }, - { id: "tabs", title: "多标签页 (Tabs)", desc: "分类管理多页签内容切换", icon: Info }, - ] - - const codeSnippets = useMemo(() => { - return { - cards: `// FeatureCard: 三栏网格功能展示卡片 -interface FeatureCardProps { - title: string; - description: string; - linkText: string; - href?: string; -} - -export function FeatureCard({ title, description, linkText, href }: FeatureCardProps) { - return ( - - - {title} - - - - {description} - - - - - ); -}`, - dashboards: `// 7天收入统计: 进度条数据统计卡片 -interface IncomeStatItem { - date: string; - amount: number; - percentage: number; -} - -export function IncomeStatsCard({ stats }: { stats: IncomeStatItem[] }) { - return ( - - - 7天收入统计 - -
- {stats.map(s => ( -
-
- {s.date} - +{s.amount.toFixed(2)} -
- -
- ))} -
-
- ); -}`, - tables: `// 活动记录表格: 数据明细展现 -interface MockOrder { - id: string; - order_name: string; - amount: number; - type: 'payment' | 'transfer' | 'community' | 'red_envelope'; - status: 'success' | 'pending' | 'expired' | 'disputing' | 'refund'; - order_no: string; - created_at: string; -} - -export function TransactionTable({ orders }: { orders: MockOrder[] }) { - return ( - - - - - - - - - - {orders.map(order => ( - - - - - - ))} - -
名称额度状态
{order.order_name}{order.amount.toFixed(2)}{order.status}
- ); -}`, - tabs: `// Tab 触发器及多模块切换导航 -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; - -export function MockTradeTabs() { - return ( - - - - 积分收益 - - - 积分消耗 - - - 积分收益模块内容... - 积分消耗模块内容... - - ); -}`, - } - }, []) - - return ( -
-
-

静态组件库 (Component Library)

-

- 展示项目重构中提取的有价值静态组件资源,提供可视化的静态演示与代码片段,便于平台后续的二次开发集成。 -

-
- -
- {/* 左侧导航 */} -
- {categories.map((cat) => ( - - ))} -
- - {/* 右侧主展示区 */} -
- {activeCategory === "cards" && ( - -
-

卡片组件展示

- -
- - - -
-
- -
-

组件代码片段 (FeatureCard)

- -
-
- )} - - {activeCategory === "dashboards" && ( - -
-

大盘数据组件展示

-
-
- -
- - -
-
- -
-

组件代码片段 (IncomeStats)

- -
-
- )} - - {activeCategory === "tables" && ( - -
-

活动明细表格展示

- -
- -
-

组件代码片段 (TransactionTable)

- -
-
- )} - - {activeCategory === "tabs" && ( - -
-

多标签导航展示

- -
- -
-

组件代码片段 (MockTradeTabs)

- -
-
- )} -
-
-
- ) -} diff --git a/frontend/app/(main)/files/page.tsx b/frontend/app/(main)/files/page.tsx index 04a7f222..1f45e22c 100644 --- a/frontend/app/(main)/files/page.tsx +++ b/frontend/app/(main)/files/page.tsx @@ -1,5 +1,30 @@ -import {UserFilesMain} from "@/components/common/user/files" +"use client" + +import * as React from "react" +import {motion} from "motion/react" + +import {UserFileManager} from "@/components/common/user/file-manager" +import {FolderOpen} from "lucide-react" export default function UserFilesPage() { - return + return ( + + {/* 顶部标题区 */} +
+
+ +
+

我的文件

+
+
+
+ + +
+ ) } diff --git a/frontend/components/common/admin/database-cache-manager.tsx b/frontend/components/common/admin/database-cache-manager.tsx new file mode 100644 index 00000000..8547ddde --- /dev/null +++ b/frontend/components/common/admin/database-cache-manager.tsx @@ -0,0 +1,312 @@ +"use client" + +import * as React from "react" +import {useCallback, useEffect, useState} from "react" +import {toast} from "sonner" +import {HardDrive, RefreshCw, Trash2} from "lucide-react" + +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Button} from "@/components/ui/button" +import {Skeleton} from "@/components/ui/skeleton" +import {Label} from "@/components/ui/label" +import {Input} from "@/components/ui/input" +import {Switch} from "@/components/ui/switch" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import type {CacheStatus} from "@/lib/services" +import {AdminService} from "@/lib/services" + +/** + * 格式化字节大小 + */ +const formatBytes = (bytes: number) => { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB", "TB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] +} + +/** + * 格式化数字,每3位加逗号 + */ +const formatNumber = (num: number | string) => { + if (num === undefined || num === null) return "0" + return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") +} + +interface CacheManagerProps { + refreshTrigger: number +} + +export function CacheManager({ refreshTrigger }: CacheManagerProps) { + const [cacheStatus, setCacheStatus] = useState(null) + const [loadingCache, setLoadingCache] = useState(true) + const [savingConfig, setSavingConfig] = useState(false) + const [clearingCache, setClearingCache] = useState(false) + const [showClearConfirm, setShowClearConfirm] = useState(false) + + // 策略配置表单状态 + const [maxSizeMB, setMaxSizeMB] = useState("100") + const [ttlMinutes, setTtlMinutes] = useState("60") + const [lruEnabled, setLruEnabled] = useState(true) + + // 获取磁盘缓存状态 + const fetchCacheStatus = useCallback(async (isSilent = false) => { + if (!isSilent) setLoadingCache(true) + try { + const data = await AdminService.getCacheStatus() + setCacheStatus(data) + setMaxSizeMB(data.max_size_mb.toString()) + setTtlMinutes(data.ttl_minutes.toString()) + setLruEnabled(data.lru_enabled) + } catch (err) { + toast.error("获取磁盘缓存状态失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingCache(false) + } + }, []) + + // 保存配置 + const handleSaveConfig = async (e: React.FormEvent) => { + e.preventDefault() + const size = parseInt(maxSizeMB, 10) + const ttl = parseInt(ttlMinutes, 10) + if (isNaN(size) || size < 1) { + toast.error("保存失败", { description: "最大容量限制必须是大于等于 1 的整数" }) + return + } + if (isNaN(ttl) || ttl < 0) { + toast.error("保存失败", { description: "默认过期时间必须是大于等于 0 的整数" }) + return + } + setSavingConfig(true) + try { + await AdminService.updateCacheConfig({ + max_size_mb: size, + ttl_minutes: ttl, + lru_enabled: lruEnabled, + }) + toast.success("保存成功", { description: "磁盘缓存策略已热更新" }) + await fetchCacheStatus(true) + } catch (err) { + toast.error("保存配置失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setSavingConfig(false) + } + } + + // 清空磁盘缓存数据 + const handleClearCache = async () => { + setClearingCache(true) + try { + await AdminService.clearCache() + toast.success("清空成功", { description: "缓存数据已全部清除" }) + setShowClearConfirm(false) + await fetchCacheStatus(true) + } catch (err) { + toast.error("清空缓存失败", { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setClearingCache(false) + } + } + + // 初始化拉取 + useEffect(() => { + fetchCacheStatus() + }, [fetchCacheStatus]) + + // 当外部刷新触发器改变时自动刷新 + useEffect(() => { + fetchCacheStatus(true) + }, [refreshTrigger, fetchCacheStatus]) + + return ( + + +
+
+ + 缓存管理 +
+ 管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略 +
+ + +
+ + {loadingCache && !cacheStatus ? ( +
+ + + +
+ ) : cacheStatus ? ( +
+ {/* 左边:状态区 (2/5 cols) */} +
+

运行状态

+
+ {/* 已占空间 */} +
+

已用空间

+

+ {formatBytes(cacheStatus.total_size)} +

+
+ {/* Key数量 */} +
+

缓存键数量

+

+ {formatNumber(cacheStatus.keys_count)} 个文件 +

+
+
+ + {/* 存储路径 */} +
+

缓存基准目录

+ + {cacheStatus.base_path} + +
+ +
+ + {/* 右边:配置区 (3/5 cols) */} +
+

策略配置

+
+
+
+ + setMaxSizeMB(e.target.value)} + className="h-8 text-xs bg-background/50 border-border/40" + placeholder="例如 100" + required + /> +

当总大小超出该值时,自动触发淘汰算法

+
+ +
+ + setTtlMinutes(e.target.value)} + className="h-8 text-xs bg-background/50 border-border/40" + placeholder="例如 60,0 表示永不过期" + required + /> +

缓存项的最长存活时间,超期后将失效被清理

+
+
+ +
+
+ + + 在到达最大容量限制时,自动移除最久未被访问的缓存项。关闭该功能仅清理过期项。 + +
+ +
+ +
+ +
+
+
+
+ ) : ( +
+ + 未加载到缓存状态信息 +
+ )} +
+ + {/* 清除缓存确认对话框 */} + + + + + + 确认清空所有缓存? + + + 该操作将彻底清空磁盘目录下的全部缓存文件(包含临时解压、处理后的图片及各种块文件),重置键数量统计为 0。此操作不可撤销,且可能导致用户拉取资源时出现一过性的响应变慢。 + + + + + + + + +
+ ) +} diff --git a/frontend/components/common/admin/database-manage.tsx b/frontend/components/common/admin/database-manage.tsx deleted file mode 100644 index 66d42606..00000000 --- a/frontend/components/common/admin/database-manage.tsx +++ /dev/null @@ -1,1000 +0,0 @@ -"use client" - -import * as React from "react" -import {useCallback, useEffect, useRef, useState} from "react" -import {useTheme} from "next-themes" -import CodeMirror from "@uiw/react-codemirror" -import {sql} from "@codemirror/lang-sql" -import {toast} from "sonner" -import { - Activity, - ArrowLeft, - Cpu, - Database, - Download, - FileText, - HardDrive, - Layers, - Play, - RefreshCw, - Server, - Terminal, - Trash2, -} from "lucide-react" -import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" -import {Button} from "@/components/ui/button" -import {Skeleton} from "@/components/ui/skeleton" -import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" -import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" -import {Input} from "@/components/ui/input" -import {Switch} from "@/components/ui/switch" -import {Label} from "@/components/ui/label" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" -import type {CacheStatus, DBOverview, ExecuteSQLResponse, TableDataResponse} from "@/lib/services" -import services, {AdminService} from "@/lib/services" - -/** - * 格式化数字,每3位加逗号 - */ -const formatNumber = (num: number | string) => { - if (num === undefined || num === null) return "0" - return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") -} - -/** - * 格式化表格单元格内容 - */ -const formatCellValue = (val: unknown): string => { - if (val === null || val === undefined) return "-" - if (typeof val === "boolean") return val ? "true" : "false" - if (typeof val === "object") { - try { - return JSON.stringify(val) - } catch { - return "[Object]" - } - } - return String(val) -} - -export function DatabaseManager() { - // 基础状态 - const [overview, setOverview] = useState(null) - const [tables, setTables] = useState([]) - const [selectedTable, setSelectedTable] = useState("") - const [tableData, setTableData] = useState(null) - - // 分页状态 - const [page, setPage] = useState(1) - const pageSize = 10 - - // 加载状态 - const [loadingOverview, setLoadingOverview] = useState(true) - const [loadingTables, setLoadingTables] = useState(true) - const [loadingData, setLoadingData] = useState(false) - const [exporting, setExporting] = useState(false) - - // SQL 控制台状态 - const [showConsole, setShowConsole] = useState(false) - const [sqlQuery, setSqlQuery] = useState("") - const [executingSQL, setExecutingSQL] = useState(false) - const [sqlResult, setSqlResult] = useState(null) - const [sqlError, setSqlError] = useState(null) - - // 缓存管理状态 - const [cacheStatus, setCacheStatus] = useState(null) - const [loadingCache, setLoadingCache] = useState(false) - const [savingConfig, setSavingConfig] = useState(false) - const [clearingCache, setClearingCache] = useState(false) - const [maxSizeMB, setMaxSizeMB] = useState("100") - const [ttlMinutes, setTtlMinutes] = useState("60") - const [lruEnabled, setLruEnabled] = useState(true) - const [showClearConfirm, setShowClearConfirm] = useState(false) - - // 拖拽与主题状态 - const containerRef = useRef(null) - const [editorHeight, setEditorHeight] = useState(240) - const { resolvedTheme } = useTheme() - const cmTheme = resolvedTheme === "dark" ? "dark" : "light" - - const handleMouseDown = (e: React.MouseEvent) => { - e.preventDefault() - const startY = e.clientY - const startHeight = editorHeight - - const handleMouseMove = (moveEvent: MouseEvent) => { - if (!containerRef.current) return - const deltaY = moveEvent.clientY - startY - const containerHeight = containerRef.current.getBoundingClientRect().height - const newHeight = startHeight + deltaY - - // Limit editor height between 80px and containerHeight - 80px - if (newHeight > 80 && newHeight < containerHeight - 80) { - setEditorHeight(newHeight) - } - } - - const handleMouseUp = () => { - document.removeEventListener("mousemove", handleMouseMove) - document.removeEventListener("mouseup", handleMouseUp) - } - - document.addEventListener("mousemove", handleMouseMove) - document.addEventListener("mouseup", handleMouseUp) - } - - // 1. 获取运行概览 - const fetchOverview = useCallback(async (isSilent = false) => { - if (!isSilent) setLoadingOverview(true) - try { - const data = await services.dbManage.getOverview() - setOverview(data) - } catch (err) { - toast.error("获取数据库概览失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingOverview(false) - } - }, []) - - // 2. 获取表列表 - const fetchTables = useCallback(async () => { - setLoadingTables(true) - try { - const data = await services.dbManage.listTables() - setTables(data) - // 默认选择第一张表 - if (data.length > 0 && !selectedTable) { - setSelectedTable(data[0]) - } - } catch (err) { - toast.error("获取数据库数据表列表失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingTables(false) - } - }, [selectedTable]) - - // 3. 获取具体表的数据 - const fetchTableData = useCallback(async (tableName: string, targetPage: number, size: number) => { - if (!tableName) return - setLoadingData(true) - try { - const data = await services.dbManage.getTableData({ - table: tableName, - page: targetPage, - pageSize: size, - }) - setTableData(data) - } catch (err) { - toast.error(`获取数据表 ${tableName} 数据失败`, { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingData(false) - } - }, []) - - // 4. 导出数据库 - const handleExport = async () => { - setExporting(true) - try { - const { blob, filename } = await AdminService.exportDatabase() - const url = URL.createObjectURL(blob) - const a = document.createElement("a") - a.href = url - a.download = filename - document.body.appendChild(a) - a.click() - a.remove() - URL.revokeObjectURL(url) - toast.success("数据库导出成功", { description: `已下载归档文件: ${filename}` }) - } catch (err) { - toast.error("数据库导出失败", { - description: err instanceof Error ? err.message : "导出异常", - }) - } finally { - setExporting(false) - } - } - - // 5. 执行 SQL 查询 - const handleExecuteSQL = async () => { - if (!sqlQuery.trim()) return - setExecutingSQL(true) - setSqlResult(null) - setSqlError(null) - try { - const result = await services.dbManage.executeSQL(sqlQuery) - setSqlResult(result) - toast.success("SQL 执行成功") - } catch (err) { - setSqlError(err instanceof Error ? err.message : "未知执行错误") - toast.error("SQL 执行失败") - } finally { - setExecutingSQL(false) - } - } - - // 6. 格式化字节大小 - const formatBytes = (bytes: number) => { - if (bytes === 0) return "0 Bytes" - const k = 1024 - const sizes = ["Bytes", "KB", "MB", "GB", "TB"] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] - } - - // 7. 获取磁盘缓存状态 - const fetchCacheStatus = useCallback(async (isSilent = false) => { - if (!isSilent) setLoadingCache(true) - try { - const data = await AdminService.getCacheStatus() - setCacheStatus(data) - setMaxSizeMB(data.max_size_mb.toString()) - setTtlMinutes(data.ttl_minutes.toString()) - setLruEnabled(data.lru_enabled) - } catch (err) { - toast.error("获取磁盘缓存状态失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setLoadingCache(false) - } - }, []) - - // 8. 保存磁盘缓存配置 - const handleSaveConfig = async (e: React.FormEvent) => { - e.preventDefault() - const size = parseInt(maxSizeMB, 10) - const ttl = parseInt(ttlMinutes, 10) - if (isNaN(size) || size < 1) { - toast.error("保存失败", { description: "最大容量限制必须是大于等于 1 的整数" }) - return - } - if (isNaN(ttl) || ttl < 0) { - toast.error("保存失败", { description: "默认过期时间必须是大于等于 0 的整数" }) - return - } - setSavingConfig(true) - try { - await AdminService.updateCacheConfig({ - max_size_mb: size, - ttl_minutes: ttl, - lru_enabled: lruEnabled, - }) - toast.success("保存成功", { description: "磁盘缓存策略已热更新" }) - await fetchCacheStatus(true) - } catch (err) { - toast.error("保存配置失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setSavingConfig(false) - } - } - - // 9. 清空磁盘缓存数据 - const handleClearCache = async () => { - setClearingCache(true) - try { - await AdminService.clearCache() - toast.success("清空成功", { description: "缓存数据已全部清除" }) - setShowClearConfirm(false) - await fetchCacheStatus(true) - } catch (err) { - toast.error("清空缓存失败", { - description: err instanceof Error ? err.message : "未知错误", - }) - } finally { - setClearingCache(false) - } - } - - // 页面初始化加载 - useEffect(() => { - fetchOverview() - fetchTables() - fetchCacheStatus() - }, [fetchOverview, fetchTables, fetchCacheStatus]) - - // 选择数据表或页码变化时,拉取对应的数据 - useEffect(() => { - if (selectedTable) { - fetchTableData(selectedTable, page, pageSize) - } - }, [selectedTable, page, pageSize, fetchTableData]) - - // 处理页码改变 - const handlePageChange = (newPage: number) => { - setPage(newPage) - } - - // 处理表切换 - const handleTableChange = (value: string) => { - setSelectedTable(value) - setPage(1) // 切换表重置为第一页 - } - - // Preset SQL 选择器 - const handlePresetSQLChange = (query: string) => { - setSqlQuery(query) - } - - // 渲染概览骨架 - const renderOverviewSkeleton = () => ( -
- {Array.from({ length: 6 }).map((_, i) => ( - - - - - - - - - ))} -
- ) - - // SQL 查询控台渲染 - if (showConsole) { - return ( -
- {/* 顶部控制与标题 */} -
-
- -
-

SQL 查询终端

-

执行自定义 SQL 查询,支持数据读取与写入操作

-
-
-
- {overview?.type === "postgres" ? "PostgreSQL Connected" : "SQLite Connected"} -
-
- - {/* 类似于 VS Code 的单个整体编辑器+结果区域,固定高度,有分水岭拖拽调整大小 */} -
- {/* 顶部编辑工具栏 */} -
-
- - SQL 编辑器 -
- -
- {/* 快速模板 */} -
- 快速模板: - -
- - {/* 控制按钮 */} -
- - -
-
-
- - {/* 上半区:编辑器输入框 */} -
- setSqlQuery(value)} - className="h-full text-xs font-mono" - basicSetup={{ - lineNumbers: true, - foldGutter: true, - dropCursor: true, - allowMultipleSelections: false, - indentOnInput: true, - }} - /> -
- - {/* 拖动分割线 */} -
-
-
- - {/* 下半区:查看结果 (可滑动查看) */} -
- {/* 结果栏工具提示 */} -
- 执行输出 - {sqlResult && ( - - 类型: {sqlResult.type.toUpperCase()} | 耗时: {sqlResult.execution_time_ms} ms - - )} -
- - {/* 滚动结果内容 */} -
- {executingSQL && ( -
- - 正在在数据库执行查询,请稍候... -
- )} - - {sqlError && ( -
-

SQL 执行报错 (Error):

-
{sqlError}
-
- )} - - {sqlResult && ( -
- {sqlResult.type === "select" && sqlResult.columns && sqlResult.columns.length > 0 && ( -
- - - - {sqlResult.columns.map((col) => ( - {col} - ))} - - - - {sqlResult.results && sqlResult.results.length > 0 ? ( - sqlResult.results.map((row, rIndex) => ( - - {sqlResult.columns!.map((col) => ( - - - {formatCellValue(row[col])} - - - ))} - - )) - ) : ( - - - 查询结果为空 - - - )} - -
-
- )} - - {sqlResult.type === "exec" && ( -
-

SQL 执行成功

-

- 受影响行数: {sqlResult.affected_rows} 行,耗时 {sqlResult.execution_time_ms} 毫秒。 -

-
- )} -
- )} - - {!executingSQL && !sqlResult && !sqlError && ( -
- - 编辑器就绪,请在上方编写 SQL 并运行查看结果 -
- )} -
-
-
-
- ) - } - - // 正常数据概览及表分页浏览器渲染 - return ( -
- {/* 顶部控制与标题 */} -
-
-

数据管理

-

查看物理数据库运行运维数据,进行表级数据浏览与 SQL 交互查询

-
- -
- - {/* 1. 顶部概览卡片 */} - {loadingOverview ? ( - renderOverviewSkeleton() - ) : ( - overview && ( -
- {/* 卡片1: 数据库类型 */} - - - 数据库类型 - - - -
{overview.type}
-
-
- - {/* 卡片2: 数据库版本 */} - - - 版本信息 - - - -
- {overview.version.split(" ").slice(0, 2).join(" ")} -
-
-
- - {/* 卡片3: 数据库名称 */} - - - 名称/路径 - - - -
- {overview.name.substring(overview.name.lastIndexOf("/") + 1)} -
-
-
- - {/* 卡片4: 数据库大小 */} - - - 数据库大小 - - - -
{overview.size}
-
-
- - {/* 卡片5: 表数量 */} - - - 物理数据表 - - - -
{formatNumber(overview.table_count)}
-
-
- - {/* 卡片6: 连接数 */} - - - 活跃连接数 - - - -
{formatNumber(overview.connections)}
-
-
-
- ) - )} - - {/* 2. 中部数据表浏览器 */} - - -
- 数据表浏览器 - 浏览数据库中的物理数据表详情及内容 -
- -
- {loadingTables ? ( - - ) : ( - - )} -
-
- - {loadingData && !tableData ? ( -
- - - - -
- ) : tableData && tableData.columns.length > 0 ? ( -
- {/* 数据表格区域 */} -
- {loadingData && ( -
- -
- )} - - - - {tableData.columns.map((col) => ( - {col} - ))} - - - - {tableData.results && tableData.results.length > 0 ? ( - tableData.results.map((row, rIndex) => ( - - {tableData.columns.map((col) => ( - - - {formatCellValue(row[col])} - - - ))} - - )) - ) : ( - - - 表中无记录数据 - - - )} - -
-
- - {/* 分页控制 */} -
-
- 总行数: {formatNumber(tableData.total)} 条记录 -
-
- - - 第 {page} / {Math.max(1, Math.ceil(tableData.total / pageSize))} 页 - - -
-
-
- ) : ( -
- - 未选择数据表或表结构无法加载 -
- )} -
-
- - {/* 2.5 缓存管理 */} - - -
-
- - 缓存管理 -
- 管理和监控系统级磁盘缓存的资源占用、生命周期及淘汰策略 -
- - -
- - {loadingCache && !cacheStatus ? ( -
- - - -
- ) : cacheStatus ? ( -
- {/* 左边:状态区 (2/5 cols) */} -
-

运行状态

-
- {/* 已占空间 */} -
-

已用空间

-

- {formatBytes(cacheStatus.total_size)} -

-
- {/* Key数量 */} -
-

缓存键数量

-

- {formatNumber(cacheStatus.keys_count)} 个文件 -

-
-
- - {/* 存储路径 */} -
-

缓存基准目录

- - {cacheStatus.base_path} - -
- -
- - {/* 右边:配置区 (3/5 cols) */} -
-

策略配置

-
-
-
- - setMaxSizeMB(e.target.value)} - className="h-8 text-xs bg-background/50 border-border/40" - placeholder="例如 100" - required - /> -

当总大小超出该值时,自动触发淘汰算法

-
- -
- - setTtlMinutes(e.target.value)} - className="h-8 text-xs bg-background/50 border-border/40" - placeholder="例如 60,0 表示永不过期" - required - /> -

缓存项的最长存活时间,超期后将失效被清理

-
-
- -
-
- - - 在到达最大容量限制时,自动移除最久未被访问的缓存项。关闭该功能仅清理过期项。 - -
- -
- -
- -
-
-
-
- ) : ( -
- - 未加载到缓存状态信息 -
- )} -
-
- - {/* 2.6 清除缓存确认对话框 */} - - - - - - 确认清空所有缓存? - - - 该操作将彻底清空磁盘目录下的全部缓存文件(包含临时解压、处理后的图片及各种块文件),重置键数量统计为 0。此操作不可撤销,且可能导致用户拉取资源时出现一过性的响应变慢。 - - - - - - - - - - {/* 3. 底部功能区 */} - - - 功能区 - 数据库导出及自定义高级 SQL 执行终端 - - -
- {/* 功能一: 数据库导出 */} -
-
-

- - 数据库备份导出 -

-

- 直接导出并下载物理数据库镜像文件(SQLite 导出 .db,PostgreSQL 导出为打包的 .sql 文本文件) -

-
- -
- - {/* 功能二: SQL 查询终端 */} -
-
-

- - SQL 查询控台 -

-

- 打开在线 SQL 控制终端,执行原生的 SQL 进行数据筛选、更新、调试或性能优化 -

-
- -
-
-
-
-
- ) -} diff --git a/frontend/components/common/admin/database-sql-console.tsx b/frontend/components/common/admin/database-sql-console.tsx new file mode 100644 index 00000000..99a2767d --- /dev/null +++ b/frontend/components/common/admin/database-sql-console.tsx @@ -0,0 +1,303 @@ +"use client" + +import * as React from "react" +import {useRef, useState} from "react" +import {useTheme} from "next-themes" +import CodeMirror from "@uiw/react-codemirror" +import {sql} from "@codemirror/lang-sql" +import {toast} from "sonner" +import {ArrowLeft, Play, RefreshCw, Terminal, Trash2,} from "lucide-react" + +import {Button} from "@/components/ui/button" +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" +import type {ExecuteSQLResponse} from "@/lib/services" +import services from "@/lib/services" + +/** + * 格式化表格单元格内容 + */ +const formatCellValue = (val: unknown): string => { + if (val === null || val === undefined) return "-" + if (typeof val === "boolean") return val ? "true" : "false" + if (typeof val === "object") { + try { + return JSON.stringify(val) + } catch { + return "[Object]" + } + } + return String(val) +} + +interface SQLConsoleProps { + dbType?: string // "postgres" | "sqlite" + onClose: () => void +} + +export function SQLConsole({ dbType, onClose }: SQLConsoleProps) { + // SQL 控制台状态 + const [sqlQuery, setSqlQuery] = useState("") + const [executingSQL, setExecutingSQL] = useState(false) + const [sqlResult, setSqlResult] = useState(null) + const [sqlError, setSqlError] = useState(null) + + // 拖拽与主题状态 + const containerRef = useRef(null) + const [editorHeight, setEditorHeight] = useState(240) + const { resolvedTheme } = useTheme() + const cmTheme = resolvedTheme === "dark" ? "dark" : "light" + + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault() + const startY = e.clientY + const startHeight = editorHeight + + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!containerRef.current) return + const deltaY = moveEvent.clientY - startY + const containerHeight = containerRef.current.getBoundingClientRect().height + const newHeight = startHeight + deltaY + + // Limit editor height between 80px and containerHeight - 80px + if (newHeight > 80 && newHeight < containerHeight - 80) { + setEditorHeight(newHeight) + } + } + + const handleMouseUp = () => { + document.removeEventListener("mousemove", handleMouseMove) + document.removeEventListener("mouseup", handleMouseUp) + } + + document.addEventListener("mousemove", handleMouseMove) + document.addEventListener("mouseup", handleMouseUp) + } + + // 执行 SQL 查询 + const handleExecuteSQL = async () => { + if (!sqlQuery.trim()) return + setExecutingSQL(true) + setSqlResult(null) + setSqlError(null) + try { + const result = await services.dbManage.executeSQL(sqlQuery) + setSqlResult(result) + toast.success("SQL 执行成功") + } catch (err) { + setSqlError(err instanceof Error ? err.message : "未知执行错误") + toast.error("SQL 执行失败") + } finally { + setExecutingSQL(false) + } + } + + // Preset SQL 选择器 + const handlePresetSQLChange = (query: string) => { + setSqlQuery(query) + } + + const handleClose = () => { + setSqlResult(null) + setSqlError(null) + onClose() + } + + return ( +
+ {/* 顶部控制与标题 */} +
+
+ +
+ +
+

SQL 查询终端

+
+
+
+
+ {dbType === "postgres" ? "PostgreSQL Connected" : "SQLite Connected"} +
+
+ + {/* 类似于 VS Code 的单个整体编辑器+结果区域,固定高度,有分水岭拖拽调整大小 */} +
+ {/* 顶部编辑工具栏 */} +
+
+ + SQL 编辑器 +
+ +
+ {/* 快速模板 */} +
+ 快速模板: + +
+ + {/* 控制按钮 */} +
+ + +
+
+
+ + {/* 上半区:编辑器输入框 */} +
+ setSqlQuery(value)} + className="h-full text-xs font-mono" + basicSetup={{ + lineNumbers: true, + foldGutter: true, + dropCursor: true, + allowMultipleSelections: false, + indentOnInput: true, + }} + /> +
+ + {/* 拖动分割线 */} +
+
+
+ + {/* 下半区:查看结果 (可滑动查看) */} +
+ {/* 结果栏工具提示 */} +
+ 执行输出 + {sqlResult && ( + + 类型: {sqlResult.type.toUpperCase()} | 耗时: {sqlResult.execution_time_ms} ms + + )} +
+ + {/* 滚动结果内容 */} +
+ {executingSQL && ( +
+ + 正在在数据库执行查询,请稍候... +
+ )} + + {sqlError && ( +
+

SQL 执行报错 (Error):

+
{sqlError}
+
+ )} + + {sqlResult && ( +
+ {sqlResult.type === "select" && sqlResult.columns && sqlResult.columns.length > 0 && ( +
+ + + + {sqlResult.columns.map((col) => ( + {col} + ))} + + + + {sqlResult.results && sqlResult.results.length > 0 ? ( + sqlResult.results.map((row, rIndex) => ( + + {sqlResult.columns!.map((col) => ( + + + {formatCellValue(row[col])} + + + ))} + + )) + ) : ( + + + 查询结果为空 + + + )} + +
+
+ )} + + {sqlResult.type === "exec" && ( +
+

SQL 执行成功

+

+ 受影响行数: {sqlResult.affected_rows} 行,耗时 {sqlResult.execution_time_ms} 毫秒。 +

+
+ )} +
+ )} + + {!executingSQL && !sqlResult && !sqlError && ( +
+ + 编辑器就绪,请在上方编写 SQL 并运行查看结果 +
+ )} +
+
+
+
+ ) +} diff --git a/frontend/components/common/admin/database-table-browser.tsx b/frontend/components/common/admin/database-table-browser.tsx new file mode 100644 index 00000000..0ba1a52b --- /dev/null +++ b/frontend/components/common/admin/database-table-browser.tsx @@ -0,0 +1,211 @@ +"use client" + +import * as React from "react" +import {useCallback, useEffect, useState} from "react" +import {toast} from "sonner" +import {Layers, RefreshCw} from "lucide-react" + +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from "@/components/ui/card" +import {Skeleton} from "@/components/ui/skeleton" +import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from "@/components/ui/select" +import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" +import {Button} from "@/components/ui/button" +import type {TableDataResponse} from "@/lib/services" +import services from "@/lib/services" + +/** + * 格式化数字,每3位加逗号 + */ +const formatNumber = (num: number | string) => { + if (num === undefined || num === null) return "0" + return num.toString().replace(/\B(?=(\d{3})+(?!\B))/g, ",") +} + +/** + * 格式化表格单元格内容 + */ +const formatCellValue = (val: unknown): string => { + if (val === null || val === undefined) return "-" + if (typeof val === "boolean") return val ? "true" : "false" + if (typeof val === "object") { + try { + return JSON.stringify(val) + } catch { + return "[Object]" + } + } + return String(val) +} + +interface TableBrowserProps { + tables: string[] + loadingTables: boolean + refreshTrigger: number +} + +export function TableBrowser({ tables, loadingTables, refreshTrigger }: TableBrowserProps) { + const [selectedTable, setSelectedTable] = useState("") + const [tableData, setTableData] = useState(null) + const [page, setPage] = useState(1) + const [loadingData, setLoadingData] = useState(false) + const pageSize = 10 + + // 获取具体表数据 + const fetchTableData = useCallback(async (tableName: string, targetPage: number, size: number) => { + if (!tableName) return + setLoadingData(true) + try { + const data = await services.dbManage.getTableData({ + table: tableName, + page: targetPage, + pageSize: size, + }) + setTableData(data) + } catch (err) { + toast.error(`获取数据表 ${tableName} 数据失败`, { + description: err instanceof Error ? err.message : "未知错误", + }) + } finally { + setLoadingData(false) + } + }, []) + + // 默认选择第一张表 + useEffect(() => { + if (tables.length > 0 && !selectedTable) { + setSelectedTable(tables[0]) + } + }, [tables, selectedTable]) + + // 当选中的表、页码或外部刷新触发器改变时获取数据 + useEffect(() => { + if (selectedTable) { + fetchTableData(selectedTable, page, pageSize) + } + }, [selectedTable, page, pageSize, refreshTrigger, fetchTableData]) + + // 处理页码改变 + const handlePageChange = (newPage: number) => { + setPage(newPage) + } + + // 处理表切换 + const handleTableChange = (value: string) => { + setSelectedTable(value) + setPage(1) // 切换表重置为第一页 + } + + return ( + + +
+ 数据表浏览器 + 浏览数据库中的物理数据表详情及内容 +
+ +
+ {loadingTables ? ( + + ) : ( + + )} +
+
+ + {loadingData && !tableData ? ( +
+ + + + +
+ ) : tableData && tableData.columns.length > 0 ? ( +
+ {/* 数据表格区域 */} +
+ {loadingData && ( +
+ +
+ )} + + + + {tableData.columns.map((col) => ( + {col} + ))} + + + + {tableData.results && tableData.results.length > 0 ? ( + tableData.results.map((row, rIndex) => ( + + {tableData.columns.map((col) => ( + + + {formatCellValue(row[col])} + + + ))} + + )) + ) : ( + + + 表中无记录数据 + + + )} + +
+
+ + {/* 分页控制 */} +
+
+ 总行数: {formatNumber(tableData.total)} 条记录 +
+
+ + + 第 {page} / {Math.max(1, Math.ceil(tableData.total / pageSize))} 页 + + +
+
+
+ ) : ( +
+ + 未选择数据表或表结构无法加载 +
+ )} +
+
+ ) +} diff --git a/frontend/components/common/admin/files.tsx b/frontend/components/common/admin/files.tsx deleted file mode 100644 index cd958957..00000000 --- a/frontend/components/common/admin/files.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client" - -import * as React from "react" -import {motion} from "motion/react" - -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {FileStats} from "./file-stats" -import {FileList} from "./file-list" -import {StorageConfigTab} from "./storage-config-tab" - -export function FilesMain() { - const [activeTab, setActiveTab] = React.useState("stats") - - return ( - - {/* 顶部标题区 */} -
-
-

- 存储管理 -

-

- 管理系统存储的所有文件,支持下载、数据统计与批量操作 -

-
-
- - - - 文件存储信息 - 文件列表 - 存储配置 - - - {/* ──────── TAB 1: 统计看板 ──────── */} - - - - - {/* ──────── TAB 2: 文件列表 ──────── */} - - - - - - - - -
- ) -} diff --git a/frontend/components/common/admin/push-notification.tsx b/frontend/components/common/admin/push-notification.tsx deleted file mode 100644 index e72a5eea..00000000 --- a/frontend/components/common/admin/push-notification.tsx +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2026 Arctel.net -// SPDX-License-Identifier: Apache-2.0 - -"use client" - -import * as React from "react" -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 "./push/events-tab" -import {HistoriesTab} from "./push/histories-tab" -import {SettingsTab} from "./push/settings-tab" - -export function PushNotificationManager() { - const [activeTab, setActiveTab] = React.useState("events") - - return ( - -
- -
-

通知推送管理

-
-
- - - - - - 事件管理 - - - - 通知历史 - - - - 通道管理与设置 - - - - {/* ==================== 1. 事件管理 TAB ==================== */} - - - - - {/* ==================== 2. 通知历史 TAB ==================== */} - - - - - {/* ==================== 3. 通道管理与设置 TAB ==================== */} - - - - -
- ) -} diff --git a/frontend/components/common/admin/status.tsx b/frontend/components/common/admin/status.tsx index af8697cc..daf2d4a6 100644 --- a/frontend/components/common/admin/status.tsx +++ b/frontend/components/common/admin/status.tsx @@ -157,12 +157,14 @@ export function SystemStatusManager() { } return ( -
+
{/* 顶部控制栏 */} -
-
-

系统状态

-

监控后端服务的核心运行时状态与内存指标

+
+
+ +
+

系统状态

+
diff --git a/frontend/components/common/admin/system-logs.tsx b/frontend/components/common/admin/system-logs.tsx deleted file mode 100644 index 15fb69b3..00000000 --- a/frontend/components/common/admin/system-logs.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* -Copyright 2026 Arctel.net - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -"use client" - -import {Activity, BarChart3, Terminal} from "lucide-react" - -import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs" -import {AppLogs} from "./app-logs" -import {AccessLogs} from "./access-logs" -import {AccessAnalytics} from "./access-analytics" - -export function SystemLogs() { - return ( -
- {/* Header */} -
- -
-

系统日志

-
-
- {/* Tabs Layout */} - - - - - 访问分析 - - - - 用户访问日志 - - - - 应用运行日志 - - - - - - - - - - - - - -
- ) -} - diff --git a/frontend/components/common/admin/system.tsx b/frontend/components/common/admin/system.tsx deleted file mode 100644 index 11168881..00000000 --- a/frontend/components/common/admin/system.tsx +++ /dev/null @@ -1,237 +0,0 @@ -"use client" - -import * as React from "react" -import {Input} from "@/components/ui/input" -import {Switch} from "@/components/ui/switch" -import {ManageDetailPanel, ManagePage} from "@/components/common/general/manage-pannel" -import {Tabs, TabsList, TabsTrigger} from "@/components/ui/tabs" - -import {formatDateTime} from "@/lib/utils" -import type {SystemConfig} from "@/lib/services" -import {useAdmin} from "@/contexts/admin-context" - - -/** - * 系统配置 - * 显示系统配置的详细信息和编辑面板 - * - * @example - * ```tsx - * - * ``` - * @param {SystemConfig} config - 系统配置 - * @param {Partial} editData - 编辑数据 - * @param {function} onEditDataChange - 编辑数据改变回调 - * @param {function} onSave - 保存回调 - * @param {boolean} saving - 是否正在保存 - * @returns {React.ReactNode} 系统配置详情面板组件 - */ -function SystemConfigDetailPanel({ - config, - editData, - onEditDataChange, - onSave, - saving -}: { - config: SystemConfig | null - editData: Partial - onEditDataChange: (field: keyof SystemConfig, value: SystemConfig[keyof SystemConfig]) => void - onSave: () => void - saving: boolean -}) { - - return ( - -
-
-
- -

{config?.key}

-
- -
- -
- {config?.key.endsWith('_enabled') ? ( - { - onEditDataChange('value', checked ? 'true' : 'false') - }} - /> - ) : ( - { - const value = e.target.value - if (value === '') { - onEditDataChange('value', '') - return - } - onEditDataChange('value', value) - }} - className="!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]" - style={{ - MozAppearance: 'textfield' - }} - /> - )} -
-
- -
- -
- { - const value = e.target.value - onEditDataChange('description', value) - }} - className="!text-[12px] text-right h-4 rounded-none border-none shadow-none focus-visible:ring-0 focus-visible:ring-offset-0 placeholder:!text-[12px]" - /> -
-
- -
- - { - onEditDataChange('visibility', checked ? 1 : 0) - }} - /> -
- -
- - - {config?.type === 'system' ? '系统配置' : '业务配置'} - -
- -
- -

{config ? formatDateTime(config.created_at) : ''}

-
-
-
-
- ) -} - - - -/** - * 系统配置管理组件 - * - * @example - * ```tsx - * - * ``` - * @returns {React.ReactNode} 系统配置管理组件 - */ -export function SystemConfigs() { - const { - systemConfigs: configs, - systemConfigsLoading: loading, - systemConfigsError: error, - refetchSystemConfigs, - updateSystemConfig - } = useAdmin() - - const [activeTab, setActiveTab] = React.useState<'system' | 'business'>('business') - - React.useEffect(() => { - refetchSystemConfigs(activeTab) - }, [activeTab, refetchSystemConfigs]) - - const getInitialEditData = (config: SystemConfig) => ({ - value: config.value, - visibility: config.visibility, - description: config.description - }) - - const handleSave = async (config: SystemConfig, editData: Partial) => { - if (!config) return - - await updateSystemConfig(config.key, { - value: editData.value ?? config.value, - visibility: editData.visibility ?? config.visibility, - description: editData.description ?? config.description - }) - } - - return ( - - title="系统配置" - data={configs} - loading={loading} - error={error} - onReload={() => refetchSystemConfigs(activeTab)} - getInitialEditData={getInitialEditData} - onSave={handleSave} - getId={(config) => config.key} - emptyDescription="未发现系统配置" - loadingDescription="配置加载中" - headerExtra={ - setActiveTab(val as 'system' | 'business')} className="w-[180px]"> - - 业务配置 - 系统配置 - - - } - columns={[ - { header: "配置键", cell: (item) => {item.key}, width: "200px" }, - { header: "配置值", cell: (item) => {item.value}, width: "120px" }, - { header: "公共可见", cell: (item) => {item.visibility === 1 ? "可见" : "不可见"}, width: "80px" }, - { header: "描述", cell: (item) => {item.description}, width: "200px" }, - ]} - renderDetail={({ selected, hovered, editData, onEditDataChange, onSave, saving }) => ( - - )} - /> - ) -} diff --git a/frontend/components/common/admin/user-detail-sheet.tsx b/frontend/components/common/admin/user-detail-sheet.tsx new file mode 100644 index 00000000..80024bc2 --- /dev/null +++ b/frontend/components/common/admin/user-detail-sheet.tsx @@ -0,0 +1,198 @@ +"use client" + +import * as React from "react" +import {Globe, Loader2, Mail, MapPin, ShieldCheck, Smartphone, Trash2, UserCheck,} from "lucide-react" + +import {Button} from "@/components/ui/button" +import {Badge} from "@/components/ui/badge" +import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar" +import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet" +import type {AdminUser} from "@/lib/services" +import {cn, formatDateTime} from "@/lib/utils" + +interface UserDetailSheetProps { + selectedUser: AdminUser | null + isOpen: boolean + onOpenChange: (open: boolean) => void + detailLoading: boolean + onStatusToggle: (user: AdminUser) => Promise + onDeleteTarget: (user: AdminUser) => void +} + +export function UserDetailSheet({ + selectedUser, + isOpen, + onOpenChange, + detailLoading, + onStatusToggle, + onDeleteTarget, +}: UserDetailSheetProps) { + + const displayValue = (value?: string) => value && value.trim() ? value : "-" + + return ( + + + 用户档案 + + {selectedUser && ( + <> +
+
+
+
+ + + + {selectedUser.username.substring(0, 2).toUpperCase()} + + + +
+

{selectedUser.nickname}

+
+ @{selectedUser.username} + + UID: {selectedUser.id} + + {selectedUser.is_admin && ( + + Admin + + )} +
+
+ + {detailLoading && ( +
+ + 正在刷新详情 +
+ )} + +
+
+ 注册时间 + {formatDateTime(selectedUser.created_at).split(' ')[0]} +
+
+
+
+ +
+
+

个人资料

+
+
+ + + 邮箱 + + {displayValue(selectedUser.email)} +
+
+ + + 手机 + + {displayValue(selectedUser.phone)} +
+
+ + ⚧ + 性别 + + {displayValue(selectedUser.gender)} +
+
+ + + 所在地 + + {displayValue(selectedUser.location)} +
+
+ + + 网站 + + {displayValue(selectedUser.website)} +
+
+ 简介 + {displayValue(selectedUser.bio)} +
+
+
+ +
+

系统记录

+
+
+ 账户状态 + + {selectedUser.is_active ? "正常" : "禁用"} + +
+
+ 管理员 + {selectedUser.is_admin ? "是" : "否"} +
+
+ 最后登录 + {formatDateTime(selectedUser.last_login_at)} +
+
+ 注册时间 + {formatDateTime(selectedUser.created_at)} +
+
+ 最后更新 + {formatDateTime(selectedUser.updated_at)} +
+
+
+
+
+ + {!selectedUser.is_admin && ( +
+ + +
+ )} +
+ + )} +
+
+ ) +} diff --git a/frontend/components/common/admin/user-filter-bar.tsx b/frontend/components/common/admin/user-filter-bar.tsx new file mode 100644 index 00000000..0835ba12 --- /dev/null +++ b/frontend/components/common/admin/user-filter-bar.tsx @@ -0,0 +1,237 @@ +"use client" + +import * as React from "react" +import {ChevronDown, ChevronLeft, ChevronRight, Filter, Loader2, Search, X} from "lucide-react" + +import {Button} from "@/components/ui/button" +import {Badge} from "@/components/ui/badge" +import {Separator} from "@/components/ui/separator" +import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu" +import {useAdminUsers} from "@/contexts/admin-users-context" +import {cn} from "@/lib/utils" + +export function UserFilterBar() { + const { + total, + loading, + page, + pageSize, + searchUserId, + searchUsername, + statusFilter, + setPage, + setPageSize, + setSearchUserId, + setSearchUsername, + setStatusFilter, + fetchUsers + } = useAdminUsers() + + const totalPages = Math.ceil(total / pageSize) + const hasSearchFilter = Boolean(searchUserId || searchUsername) + + return ( +
+
+ + + + + +
+ setSearchUserId(e.target.value)} + /> + setSearchUsername(e.target.value)} + /> + {hasSearchFilter && ( + + )} +
+
+
+ + + + + + + { e.preventDefault(); setStatusFilter('all') }} + > +
+ 全部状态 + + { e.preventDefault(); setStatusFilter('active') }} + > +
+ 正常 + + { e.preventDefault(); setStatusFilter('inactive') }} + > +
+ 禁用 + + + + + {(hasSearchFilter || statusFilter !== 'all') && ( + <> + + + + )} +
+ + + +
+ + {total} 条记录 + +
+ + + {page}/{totalPages} + + +
+ + + + + + + {[20, 50, 100].map(size => ( + setPageSize(size)} + className={cn("font-mono text-xs", pageSize === size && "bg-accent")} + > + {size}条/页 + + ))} + + + + +
+
+ ) +} diff --git a/frontend/components/common/admin/users.tsx b/frontend/components/common/admin/users.tsx deleted file mode 100644 index 0b015516..00000000 --- a/frontend/components/common/admin/users.tsx +++ /dev/null @@ -1,659 +0,0 @@ -"use client" - -import {useEffect, useState} from "react" -import {Button} from "@/components/ui/button" -import {Switch} from "@/components/ui/switch" -import {Separator} from "@/components/ui/separator" -import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table" -import {Sheet, SheetContent, SheetTitle} from "@/components/ui/sheet" -import {Badge} from "@/components/ui/badge" -import {Avatar, AvatarFallback, AvatarImage} from "@/components/ui/avatar" -import { - ChevronDown, - ChevronLeft, - ChevronRight, - Eye, - Filter, - Globe, - Layers, - Loader2, - Mail, - MapPin, - Plus, - Search, - ShieldCheck, - Smartphone, - Trash2, - UserCheck, - UserX, - VenusAndMars, - X -} from "lucide-react" -import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip" -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle -} from "@/components/ui/alert-dialog" - -import {AdminUser} from "@/lib/services" -import {cn, formatDateTime} from "@/lib/utils" -import {EmptyStateWithBorder} from "@/components/layout/empty" -import {LoadingStateWithBorder} from "@/components/layout/loading" -import {ErrorInline} from "@/components/layout/error" -import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu" -import {useAdminUsers} from "@/contexts/admin-users-context" -import {CreateUserModal} from "./create-user-modal" - -export function UsersManager() { - const { - users, - total, - loading, - error, - page, - pageSize, - searchUserId, - searchUsername, - statusFilter, - setPage, - setPageSize, - setSearchUserId, - setSearchUsername, - setStatusFilter, - fetchUsers, - getUserDetail, - updateUserStatus, - deleteUser - } = useAdminUsers() - - const [selectedUser, setSelectedUser] = useState(null) - const [detailOpen, setDetailOpen] = useState(false) - const [detailLoading, setDetailLoading] = useState(false) - const [deleteTarget, setDeleteTarget] = useState(null) - const [deleteLoading, setDeleteLoading] = useState(false) - const [createModalOpen, setCreateModalOpen] = useState(false) - - useEffect(() => { - fetchUsers() - }, [fetchUsers]) - - const handleStatusToggle = async (user: AdminUser) => { - await updateUserStatus(user) - - if (selectedUser?.id === user.id) { - setSelectedUser(prev => prev ? { ...prev, is_active: !prev.is_active } : null) - } - } - - const handleShowDetail = async (user: AdminUser) => { - setSelectedUser(user) - setDetailOpen(true) - setDetailLoading(true) - - try { - const detail = await getUserDetail(user.id) - setSelectedUser(detail) - } catch { - setSelectedUser(user) - } finally { - setDetailLoading(false) - } - } - - const handleDeleteUser = async () => { - if (!deleteTarget) return - - setDeleteLoading(true) - try { - await deleteUser(deleteTarget) - if (selectedUser?.id === deleteTarget.id) { - setDetailOpen(false) - setSelectedUser(null) - } - setDeleteTarget(null) - } finally { - setDeleteLoading(false) - } - } - - const displayValue = (value?: string) => value && value.trim() ? value : "-" - - const totalPages = Math.ceil(total / pageSize) - const hasSearchFilter = Boolean(searchUserId || searchUsername) - - const renderFilterBar = () => ( -
-
- - - - - -
- setSearchUserId(e.target.value)} - /> - setSearchUsername(e.target.value)} - /> - {hasSearchFilter && ( - - )} -
-
-
- - - - - - - { e.preventDefault(); setStatusFilter('all') }} - > -
- 全部状态 - - { e.preventDefault(); setStatusFilter('active') }} - > -
- 正常 - - { e.preventDefault(); setStatusFilter('inactive') }} - > -
- 禁用 - - - - - {(hasSearchFilter || statusFilter !== 'all') && ( - <> - - - - )} -
- - - -
- - {total} 条记录 - -
- - - {page}/{totalPages} - - -
- - - - - - - {[20, 50, 100].map(size => ( - setPageSize(size)} - className={cn("font-mono text-xs", pageSize === size && "bg-accent")} - > - {size}条/页 - - ))} - - - - -
-
- ) - - return ( -
-
-
-
用户管理
-
- -
- - {renderFilterBar()} - - {error ? ( -
- fetchUsers(true)} className="justify-center" /> -
- ) : loading && users.length === 0 ? ( - - ) : users.length === 0 ? ( - - ) : ( -
- - - - ID - 用户 - - 上次登陆 - 注册时间 - 上次更新 - 操作 - - - - {users.map((user) => ( - handleShowDetail(user)} - > - {user.id} - -
- - - - {user.username.substring(0, 2).toUpperCase()} - - -
-
- {user.nickname} - {user.is_admin && ( - - ADM - - )} -
-
- @{user.username} -
-
-
-
- - - {formatDateTime(user.last_login_at)} - - - {formatDateTime(user.created_at)} - - - {formatDateTime(user.updated_at)} - - e.stopPropagation()}> -
- - - -
- handleStatusToggle(user)} - disabled={user.is_admin} - className="scale-75 data-[state=checked]:bg-green-600 h-4 w-7" - /> -
-
- - {user.is_admin ? '管理员账户' : user.is_active ? '禁用账户' : '启用账户'} - -
-
- - - - - - - - 查看详情 - - - - - {!user.is_admin && ( - - - - - - - 删除用户 - - - - )} -
-
-
- ))} -
-
-
- )} - - - - 用户档案 - - {selectedUser && ( - <> -
-
-
-
- - - - {selectedUser.username.substring(0, 2).toUpperCase()} - - - -
-

{selectedUser.nickname}

-
- @{selectedUser.username} - - UID: {selectedUser.id} - - {selectedUser.is_admin && ( - - Admin - - )} -
-
- - {detailLoading && ( -
- - 正在刷新详情 -
- )} - -
-
- 注册时间 - {formatDateTime(selectedUser.created_at).split(' ')[0]} -
-
-
-
- -
-
-

个人资料

-
-
- - - 邮箱 - - {displayValue(selectedUser.email)} -
-
- - - 手机 - - {displayValue(selectedUser.phone)} -
-
- - - 性别 - - {displayValue(selectedUser.gender)} -
-
- - - 所在地 - - {displayValue(selectedUser.location)} -
-
- - - 网站 - - {displayValue(selectedUser.website)} -
-
- 简介 - {displayValue(selectedUser.bio)} -
-
-
- -
-

系统记录

-
-
- 账户状态 - - {selectedUser.is_active ? "正常" : "禁用"} - -
-
- 管理员 - {selectedUser.is_admin ? "是" : "否"} -
-
- 最后登录 - {formatDateTime(selectedUser.last_login_at)} -
-
- 注册时间 - {formatDateTime(selectedUser.created_at)} -
-
- 最后更新 - {formatDateTime(selectedUser.updated_at)} -
-
-
-
- -
-
- - {!selectedUser.is_admin && ( -
- - -
- )} - - )} -
-
- - !open && !deleteLoading && setDeleteTarget(null)}> - - - 确认删除用户 - - 确定要删除用户 {deleteTarget?.nickname || deleteTarget?.username} 吗?该操作会移除用户账号,删除后无法撤销。 - - - - 取消 - - {deleteLoading && } - 确认删除 - - - - - - setCreateModalOpen(false)} /> -
- ) -} diff --git a/frontend/components/common/general/manage-pannel.tsx b/frontend/components/common/general/manage-pannel.tsx index c05ed989..08317b33 100644 --- a/frontend/components/common/general/manage-pannel.tsx +++ b/frontend/components/common/general/manage-pannel.tsx @@ -22,6 +22,7 @@ import {ScrollArea, ScrollBar} from "@/components/ui/scroll-area" interface ManagePageProps { title: string + icon?: LucideIcon data: T[] loading: boolean error: Error | null @@ -61,6 +62,7 @@ interface ManagePageProps { export function ManagePage({ title, + icon: Icon, data, loading, error, @@ -201,8 +203,13 @@ export function ManagePage({ return (
-
-
{title}
+
+
+ {Icon && } +
+

{title}

+
+
{headerExtra}
diff --git a/frontend/components/common/home/home-main.tsx b/frontend/components/common/home/home-main.tsx index b1755a0a..7f95aebb 100644 --- a/frontend/components/common/home/home-main.tsx +++ b/frontend/components/common/home/home-main.tsx @@ -1,7 +1,7 @@ "use client" import * as React from "react" -import {motion} from "motion/react" +import {AnimatePresence, motion} from "motion/react" import {useUser} from "@/contexts/user-context" import {Card, CardHeader, CardTitle} from "@/components/ui/card" import {Button} from "@/components/ui/button" @@ -10,6 +10,7 @@ import Link from "next/link" export function HomeMain() { const { user } = useUser() + const [isBannerVisible, setIsBannerVisible] = React.useState(true) const quickLinks = [ { @@ -93,10 +94,49 @@ export function HomeMain() { return (
+ {/* 积分收益提示 banner */} + + {isBannerVisible && ( + +
+
+

Modern Platform

+

+ 通用的、现代化的后台管理系统脚手架, 开箱即用、完整基建、极易扩展,助您快速构建企业级应用。 +

+
+ + +
+
+
+
+ )} +
+ {/* 快捷导航 */}

常用功能导航

-
+
{quickLinks.map((link, idx) => ( >((accumulator, config) => { - accumulator[config.key] = config - return accumulator - }, {}) -} - -export function SystemSettingsMain() { - const { user, loading } = useAuth() - const router = useRouter() - - const systemConfigsQuery = useQuery({ - queryKey: ["admin", "system-configs"], - queryFn: () => AdminService.listSystemConfigs("system"), - enabled: !!user?.is_admin, - }) - - const configs = useMemo( - () => systemConfigMap(systemConfigsQuery.data ?? []), - [systemConfigsQuery.data], - ) - - useEffect(() => { - if (!loading && (!user || !user.is_admin)) { - router.replace("/settings/profile") - } - }, [user, loading, router]) - - if (loading || !user || !user.is_admin) { - return ( -
- -
- ) - } - - return ( - - -
- -
-

系统设置

-
-
- - - - 安全设置 - - - 业务设置 - - - 系统设置 - - - 其他设置 - - - 系统状态 - - - 系统信息 - - - - - - - - - - - - - - - - - - - - - - -
- ) -} diff --git a/frontend/components/common/user/files.tsx b/frontend/components/common/user/files.tsx deleted file mode 100644 index 13bd52f0..00000000 --- a/frontend/components/common/user/files.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import * as React from "react" -import {motion} from "motion/react" - -import {UserFileManager} from "./file-manager" - -export function UserFilesMain() { - return ( - - {/* 顶部标题区 */} -
-
-

- 我的文件 -

-

- 管理您上传的个人文件,支持上传、下载、搜索与删除操作 -

-
-
- - -
- ) -} diff --git a/frontend/components/layout/sidebar.tsx b/frontend/components/layout/sidebar.tsx index bcd377bd..64cb97ee 100644 --- a/frontend/components/layout/sidebar.tsx +++ b/frontend/components/layout/sidebar.tsx @@ -45,7 +45,9 @@ import { } from "@/components/ui/alert-dialog" import { ArrowUpRight, + Bell, ChevronDown, + Code, CreditCard, Database, FileQuestionMark, @@ -54,12 +56,10 @@ import { Home, Layers, LogOut, - Palette, Settings, ShieldCheck, Terminal, UserRound, - Bell, } from "lucide-react" import {useUser} from "@/contexts/user-context" @@ -82,7 +82,7 @@ const data = { { title: "系统设置", url: "/admin/settings", icon: Settings }, ], document: [ - { title: "组件库", url: "/components", icon: Palette }, + { title: "规范示例", url: "/admin/demo", icon: Code }, { title: "接口文档", url: "/docs/api", icon: CreditCard, external: true }, { title: "使用文档", url: "/docs/how-to-use", icon: FileText, external: true }, ],