-
-
Rolling Deployment
+ {config.maintenance.window.enabled && (
+ <>
+
+ 维护开始时间
+ updateConfig('maintenance.window.start_time', e.target.value + 'Z')}
+ />
-
-
-
Blue-Green Deployment
+
+
+ 维护结束时间
+ updateConfig('maintenance.window.end_time', e.target.value + 'Z')}
+ />
-
-
-
Canary Deployment
+
+
+ 中文维护消息
+
-
+
+
+ English Maintenance Message
+
+ >
+ )}
+
+
+
+ {/* Operations Configuration */}
+
+
+
+
+ 运营配置
+
+ 运营功能设置
+
+
+
+ 启用用户注册
+ updateConfig('ops.features.registration_enabled', checked)}
+ />
-
-
Enabled Features
-
- {[
- { id: "analytics", label: "Analytics" },
- { id: "caching", label: "Caching" },
- { id: "cdn", label: "CDN" },
- { id: "load-balancing", label: "Load Balancing" },
- { id: "auto-scaling", label: "Auto Scaling" },
- ].map((feature) => (
-
- {
- if (checked) {
- updateConfig('selectedFeatures', [...config.selectedFeatures, feature.id])
- } else {
- updateConfig('selectedFeatures', config.selectedFeatures.filter((f) => f !== feature.id))
- }
- }}
- />
- {feature.label}
-
- ))}
-
+
+ 需要邀请码
+ updateConfig('ops.features.invite_code_required', checked)}
+ />
+
+
+
+ 邮箱验证
+ updateConfig('ops.features.email_verification', checked)}
+ />
+
+
+
+ 最大用户数: {config.ops.limits.max_users}
+ updateConfig('ops.limits.max_users', value[0])}
+ min={100}
+ max={10000}
+ step={100}
+ />
+
+
+
+ 每用户最大邀请码数: {config.ops.limits.max_invite_codes_per_user}
+ updateConfig('ops.limits.max_invite_codes_per_user', value[0])}
+ min={1}
+ max={50}
+ step={1}
+ />
+
+
+
+ 会话超时: {config.ops.limits.session_timeout_hours}小时
+ updateConfig('ops.limits.session_timeout_hours', value[0])}
+ min={1}
+ max={168}
+ step={1}
+ />
- {/* Status Dashboard */}
+ {/* Action Buttons */}
-
- System Status Dashboard
+
+ 配置操作
- Real-time system metrics and performance indicators
+ 保存、重置和导出配置
-
-
-
-
{
- // 这里可以添加保存配置到后端的逻辑
console.log('Applying configuration:', config)
- alert('Configuration applied successfully!')
+ alert('配置应用成功!')
}}>
- Apply All Settings
+ 应用所有设置
+
+ setConfig(defaultConfig)}>
+ 重置为默认值
- setConfig(defaultConfig)}>Reset to Default
{
const dataStr = JSON.stringify(config, null, 2)
const dataBlob = new Blob([dataStr], { type: 'application/json' })
@@ -563,7 +577,9 @@ export default function Control() {
link.download = 'config.json'
link.click()
URL.revokeObjectURL(url)
- }}>Export Configuration
+ }}>
+ 导出配置
+
{
const input = document.createElement('input')
input.type = 'file'
@@ -578,21 +594,21 @@ export default function Control() {
setConfig(importedConfig)
} catch (error) {
console.error('Failed to parse config file:', error)
- alert('Invalid configuration file')
+ alert('无效的配置文件')
}
}
reader.readAsText(file)
}
}
input.click()
- }}>Import Configuration
- View Logs
- System Health Check
+ }}>
+ 导入配置
+
- );
-}
\ No newline at end of file
+ )
+}
\ No newline at end of file
diff --git a/app/admin/common/dynamic-admin-config.tsx b/app/admin/common/dynamic-admin-config.tsx
new file mode 100644
index 0000000..1b4c5cb
--- /dev/null
+++ b/app/admin/common/dynamic-admin-config.tsx
@@ -0,0 +1,559 @@
+"use client";
+
+import React from "react";
+import {
+ Settings,
+ Globe,
+ Palette,
+ Bell,
+ Shield,
+ Users,
+ Database,
+ Mail,
+ FileText,
+ MessageSquare,
+ Clock,
+ AlertTriangle,
+ Save,
+ RefreshCw,
+ Download,
+ Upload,
+ Server,
+ HardDrive
+} from "lucide-react";
+import { AdminPanelConfig } from "@/types/admin-panel";
+import { commonConfigSchema, zodErrorsToAdminErrors } from "@/lib/config-zod";
+// import { SiteOpsConfigType } from "@/types/site-config";
+import { SiteOpsConfigType } from "@/types/site-config";
+import { ConfigItemType } from "@/hooks/use-site-config";
+
+// 创建基于后端数据的动态管理面板配置
+export function createDynamicAdminConfig(
+ data?: SiteOpsConfigType,
+ onSave?: (values: Record
) => Promise,
+ onExport?: () => Promise,
+ onImport?: (file: File) => Promise,
+ configs?: ConfigItemType[]
+): AdminPanelConfig {
+
+ // 从后端数据获取初始值(安全访问嵌套属性)
+ const getInitialValues = () => {
+ if (!data) return {};
+
+ // helpers
+ const toDateTimeLocal = (value?: any) => {
+ if (!value) return "";
+ const d = new Date(value);
+ if (isNaN(d.getTime())) return "";
+ return d.toISOString().slice(0, 16);
+ };
+
+ const bannerText = data.notice_maintenance?.banner?.text || {
+ "zh-CN": "欢迎使用MMAP系统",
+ "en": "Welcome to MMAP System"
+ };
+ const maintenanceMsg = data.notice_maintenance?.maintenance_window?.message || {
+ "zh-CN": "系统维护中,请稍后再试",
+ "en": "System maintenance in progress"
+ };
+ const workingHours = data.docs_support?.channels?.working_hours || {
+ "zh-CN": "周一至周五 9:00-18:00",
+ "en": "Mon-Fri 9:00-18:00"
+ };
+
+ return {
+ // 站点信息
+ 'site.info.name': data.site?.info?.name || "MMAP System",
+ 'site.info.locale_default': data.site?.info?.locale_default || "zh-CN",
+ 'site.info.locales_supported': data.site?.info?.locales_supported || ["zh-CN", "en"],
+
+ // 品牌配置
+ 'site.brand.logo_url': data.site?.brand?.logo_url || "/images/logo.png",
+ 'site.brand.primary_color': data.site?.brand?.primary_color || "#3B82F6",
+ 'site.brand.dark_mode_default': data.site?.brand?.dark_mode_default || false,
+
+ // 页脚链接
+ 'site.footer_links': data.site?.footer_links || [],
+
+ // 横幅公告
+ 'notice.banner.enabled': data.notice_maintenance?.banner?.enabled || false,
+ 'notice.banner.text': JSON.stringify(bannerText, null, 2),
+
+ // 维护窗口
+ 'maintenance.window.enabled': data.notice_maintenance?.maintenance_window?.enabled || false,
+ 'maintenance.window.start_time': toDateTimeLocal(data.notice_maintenance?.maintenance_window?.start_time),
+ 'maintenance.window.end_time': toDateTimeLocal(data.notice_maintenance?.maintenance_window?.end_time),
+ 'maintenance.window.message': JSON.stringify(maintenanceMsg, null, 2),
+
+ // 弹窗公告
+ 'modal.announcements': data.notice_maintenance?.modal_announcements || [],
+
+ // 文档链接
+ 'docs.links': data.docs_support?.links || [],
+
+ // 支持渠道
+ 'support.channels.email': data.docs_support?.channels?.email || "support@mapp.com",
+ 'support.channels.ticket_system': data.docs_support?.channels?.ticket_system || "/support/tickets",
+ 'support.channels.chat_groups': data.docs_support?.channels?.chat_groups || [],
+ 'support.channels.working_hours': JSON.stringify(workingHours, null, 2),
+
+ // 运营功能开关
+ 'ops.features.registration_enabled': data.ops?.features?.registration_enabled ?? true,
+ 'ops.features.invite_code_required': data.ops?.features?.invite_code_required ?? false,
+ 'ops.features.email_verification': data.ops?.features?.email_verification ?? false,
+
+ // 运营限制
+ 'ops.limits.max_users': data.ops?.limits?.max_users || 1000,
+ 'ops.limits.max_invite_codes_per_user': data.ops?.limits?.max_invite_codes_per_user || 10,
+ 'ops.limits.session_timeout_hours': data.ops?.limits?.session_timeout_hours || 24,
+
+ // 通知配置
+ 'ops.notifications.welcome_email': data.ops?.notifications?.welcome_email ?? true,
+ 'ops.notifications.system_announcements': data.ops?.notifications?.system_announcements ?? true,
+ 'ops.notifications.maintenance_alerts': data.ops?.notifications?.maintenance_alerts ?? true,
+
+ // —— 通用配置(映射/默认)——
+ // Site
+ 'site.name': data.site?.info?.name || "MMAP System",
+ 'site.description': "",
+ 'site.keywords': "",
+ 'site.url': "/",
+ 'site.logo': data.site?.brand?.logo_url || "/images/logo.png",
+ 'site.copyright': "",
+ 'site.icp': "",
+ 'site.icp_url': "",
+ 'site.color_style': (data.site?.brand?.dark_mode_default ? 'dark' : 'light'),
+
+ // User
+ 'user.default_avatar': "/images/avatar.png",
+ 'user.default_role': 'user',
+ 'user.register_invite_code': data.ops?.features?.invite_code_required ?? false,
+ 'user.register_email_verification': data.ops?.features?.email_verification ?? false,
+ 'user.open_login': true,
+ 'user.open_reset_password': true,
+
+ // Email
+ 'email.smtp_host': "",
+ 'email.smtp_port': 465,
+ 'email.smtp_user': "",
+ 'email.smtp_password': "",
+ 'email.smtp_from': "",
+ 'email.smtp_from_name': "",
+ 'email.smtp_from_email': "",
+ 'email.system_template': "default",
+
+ // Blog
+ 'blog.default_author': "",
+ 'blog.default_category': "",
+ 'blog.default_tag': "",
+ 'blog.open_comment': true,
+
+ // Logging
+ 'logging.level': 'info',
+ 'logging.max_files': 10,
+ 'logging.max_file_size': 10,
+
+ // Cache
+ 'cache.ttl': 3600,
+ 'cache.max_size': 1024,
+
+ // Switches
+ 'switch.open_register': data.ops?.features?.registration_enabled ?? true,
+ 'switch.open_login': true,
+ 'switch.open_reset_password': true,
+ 'switch.open_comment': true,
+ 'switch.open_like': true,
+ 'switch.open_share': true,
+ 'switch.open_view': true
+ };
+ };
+
+ return {
+ header: {
+ title: "系统配置管理",
+ description: "管理站点配置、运营设置和系统参数",
+ breadcrumbs: [
+ { label: "首页", href: "/" },
+ { label: "管理中心", href: "/admin" },
+ { label: "系统配置" }
+ ],
+ actions: [
+ {
+ id: "refresh",
+ label: "刷新数据",
+ icon: ,
+ variant: "outline",
+ onClick: () => window.location.reload()
+ },
+ {
+ id: "export",
+ label: "导出配置",
+ icon: ,
+ variant: "outline",
+ onClick: onExport || (() => console.log("导出配置"))
+ },
+ {
+ id: "import",
+ label: "导入配置",
+ icon: ,
+ variant: "outline",
+ onClick: () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.json';
+ input.onchange = (e) => {
+ const file = (e.target as HTMLInputElement).files?.[0];
+ if (file && onImport) {
+ onImport(file);
+ }
+ };
+ input.click();
+ }
+ }
+ ]
+ },
+
+ tabs: [
+ // 通用配置(基于 configs 动态生成)
+ {
+ id: "common",
+ title: "通用配置",
+ icon: ,
+ sections: []
+ },
+ // 站点设置
+ {
+ id: "site",
+ title: "站点设置",
+ icon: ,
+ sections: [
+ {
+ id: "site-info",
+ title: "基本信息",
+ description: "网站基本信息和国际化配置",
+ icon: ,
+ fields: [
+ {
+ id: "site.info.name",
+ label: "网站名称",
+ description: "显示在浏览器标题栏的网站名称",
+ type: "input",
+ value: data?.site?.info?.name || "MMAP System",
+ placeholder: "请输入网站名称",
+ validation: {
+ required: true,
+ minLength: 2,
+ maxLength: 50
+ }
+ },
+ {
+ id: "site.info.locale_default",
+ label: "默认语言",
+ description: "网站的默认显示语言",
+ type: "select",
+ value: data?.site?.info?.locale_default || "zh-CN",
+ options: [
+ { label: "简体中文", value: "zh-CN" },
+ { label: "English", value: "en" }
+ ],
+ validation: {
+ required: true
+ }
+ },
+ {
+ id: "site.info.locales_supported",
+ label: "支持的语言",
+ description: "用户可以选择的所有语言选项",
+ type: "select",
+ value: data?.site?.info?.locales_supported || ["zh-CN", "en"],
+ multiple: true,
+ options: [
+ { label: "简体中文", value: "zh-CN" },
+ { label: "English", value: "en" },
+ { label: "繁體中文", value: "zh-TW" },
+ { label: "日本語", value: "ja" },
+ { label: "한국어", value: "ko" }
+ ]
+ }
+ ]
+ },
+ {
+ id: "site-brand",
+ title: "品牌设置",
+ description: "网站品牌形象和主题配置",
+ icon: ,
+ fields: [
+ {
+ id: "site.brand.logo_url",
+ label: "Logo地址",
+ description: "网站Logo图片的URL地址",
+ type: "input",
+ value: data?.site?.brand?.logo_url || "/images/logo.png",
+ placeholder: "请输入Logo URL"
+ },
+ {
+ id: "site.brand.primary_color",
+ label: "主题色",
+ description: "网站的主要色彩,用于按钮、链接等",
+ type: "color",
+ value: data?.site?.brand?.primary_color || "#3B82F6"
+ },
+ {
+ id: "site.brand.dark_mode_default",
+ label: "默认深色模式",
+ description: "新用户访问时是否默认使用深色模式",
+ type: "switch",
+ value: data?.site?.brand?.dark_mode_default || false
+ }
+ ]
+ }
+ ]
+ },
+
+ // 公告维护
+ {
+ id: "notice",
+ title: "公告维护",
+ icon: ,
+ sections: [
+ {
+ id: "banner-notice",
+ title: "横幅公告",
+ description: "网站顶部横幅公告设置",
+ icon: ,
+ fields: [
+ {
+ id: "notice.banner.enabled",
+ label: "启用横幅公告",
+ description: "是否在网站顶部显示横幅公告",
+ type: "switch",
+ value: data?.notice_maintenance?.banner?.enabled || false
+ },
+ {
+ id: "notice.banner.text",
+ label: "公告内容",
+ description: "多语言公告文本",
+ type: "textarea",
+ rows: 6,
+ value: JSON.stringify(data?.notice_maintenance?.banner?.text || {
+ "zh-CN": "欢迎使用MMAP系统",
+ "en": "Welcome to MMAP System"
+ }, null, 2),
+ showWhen: (values) => values["notice.banner.enabled"] === true
+ }
+ ]
+ },
+ {
+ id: "maintenance-window",
+ title: "维护窗口",
+ description: "系统维护时间配置",
+ icon: ,
+ fields: [
+ {
+ id: "maintenance.window.enabled",
+ label: "启用维护模式",
+ description: "启用后系统将显示维护页面",
+ type: "switch",
+ value: data?.notice_maintenance?.maintenance_window?.enabled || false
+ },
+ {
+ id: "maintenance.window.start_time",
+ label: "维护开始时间",
+ description: "维护窗口的开始时间",
+ type: "datetime-local",
+ value: (data?.notice_maintenance?.maintenance_window?.start_time ? new Date(data.notice_maintenance.maintenance_window.start_time).toISOString().slice(0, 16) : ""),
+ showWhen: (values) => values["maintenance.window.enabled"] === true
+ },
+ {
+ id: "maintenance.window.end_time",
+ label: "维护结束时间",
+ description: "维护窗口的结束时间",
+ type: "datetime-local",
+ value: (data?.notice_maintenance?.maintenance_window?.end_time ? new Date(data.notice_maintenance.maintenance_window.end_time).toISOString().slice(0, 16) : ""),
+ showWhen: (values) => values["maintenance.window.enabled"] === true
+ },
+ {
+ id: "maintenance.window.message",
+ label: "维护提示信息",
+ description: "维护期间显示给用户的多语言提示信息",
+ type: "textarea",
+ rows: 6,
+ value: JSON.stringify(data?.notice_maintenance?.maintenance_window?.message || {
+ "zh-CN": "系统维护中,请稍后再试",
+ "en": "System maintenance in progress"
+ }, null, 2),
+ showWhen: (values) => values["maintenance.window.enabled"] === true
+ }
+ ]
+ }
+ ]
+ },
+
+ // 运营配置
+ {
+ id: "operations",
+ title: "运营配置",
+ icon: ,
+ sections: [
+ {
+ id: "feature-switches",
+ title: "功能开关",
+ description: "控制各项功能的启用状态",
+ icon: ,
+ fields: [
+ {
+ id: "ops.features.registration_enabled",
+ label: "开放注册",
+ description: "是否允许新用户注册",
+ type: "switch",
+ value: data?.ops?.features?.registration_enabled ?? true
+ },
+ {
+ id: "ops.features.invite_code_required",
+ label: "需要邀请码",
+ description: "注册时是否需要邀请码",
+ type: "switch",
+ value: data?.ops?.features?.invite_code_required ?? false,
+ showWhen: (values) => values["ops.features.registration_enabled"] === true
+ },
+ {
+ id: "ops.features.email_verification",
+ label: "邮箱验证",
+ description: "注册后是否需要验证邮箱",
+ type: "switch",
+ value: data?.ops?.features?.email_verification ?? false
+ }
+ ]
+ },
+ {
+ id: "limits-config",
+ title: "限制配置",
+ description: "系统资源和使用限制",
+ icon: ,
+ fields: [
+ {
+ id: "ops.limits.max_users",
+ label: "最大用户数",
+ description: "系统允许的最大用户数量",
+ type: "number",
+ value: data?.ops?.limits?.max_users || 1000,
+ validation: {
+ required: true,
+ min: 1,
+ max: 100000
+ }
+ },
+ {
+ id: "ops.limits.max_invite_codes_per_user",
+ label: "用户最大邀请码数",
+ description: "每个用户最多可以生成的邀请码数量",
+ type: "number",
+ value: data?.ops?.limits?.max_invite_codes_per_user || 10,
+ validation: {
+ required: true,
+ min: 0,
+ max: 100
+ }
+ },
+ {
+ id: "ops.limits.session_timeout_hours",
+ label: "会话超时时间(小时)",
+ description: "用户会话的超时时间",
+ type: "number",
+ value: data?.ops?.limits?.session_timeout_hours || 24,
+ validation: {
+ required: true,
+ min: 1,
+ max: 720
+ }
+ }
+ ]
+ },
+ {
+ id: "notifications-config",
+ title: "通知配置",
+ description: "系统通知和提醒设置",
+ icon: ,
+ fields: [
+ {
+ id: "ops.notifications.welcome_email",
+ label: "发送欢迎邮件",
+ description: "新用户注册后是否发送欢迎邮件",
+ type: "switch",
+ value: data?.ops?.notifications?.welcome_email ?? true
+ },
+ {
+ id: "ops.notifications.system_announcements",
+ label: "系统公告通知",
+ description: "是否发送系统公告通知",
+ type: "switch",
+ value: data?.ops?.notifications?.system_announcements ?? true
+ },
+ {
+ id: "ops.notifications.maintenance_alerts",
+ label: "维护提醒",
+ description: "系统维护前是否发送提醒通知",
+ type: "switch",
+ value: data?.ops?.notifications?.maintenance_alerts ?? true
+ }
+ ]
+ }
+ ]
+ },
+
+ // 支持文档
+ {
+ id: "support",
+ title: "支持文档",
+ icon: ,
+ sections: [
+ {
+ id: "support-channels",
+ title: "支持渠道",
+ description: "用户支持和服务渠道配置",
+ icon: ,
+ fields: [
+ {
+ id: "support.channels.email",
+ label: "支持邮箱",
+ description: "用户联系支持的邮箱地址",
+ type: "email",
+ value: data?.docs_support?.channels?.email || "support@mapp.com",
+ validation: {
+ required: true,
+ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ }
+ },
+ {
+ id: "support.channels.ticket_system",
+ label: "工单系统地址",
+ description: "用户提交工单的系统地址",
+ type: "input",
+ value: data?.docs_support?.channels?.ticket_system || "/support/tickets"
+ },
+ {
+ id: "support.channels.working_hours",
+ label: "工作时间",
+ description: "多语言的工作时间说明",
+ type: "textarea",
+ rows: 6,
+ value: JSON.stringify(data?.docs_support?.channels?.working_hours || {
+ "zh-CN": "周一至周五 9:00-18:00",
+ "en": "Mon-Fri 9:00-18:00"
+ }, null, 2)
+ }
+ ]
+ }
+ ]
+ }
+ ],
+
+ // 配置选项
+ autoSave: false, // 禁用自动保存,使用手动保存
+ validateOnChange: true,
+ onValidate: (values) => {
+ const result = commonConfigSchema.safeParse(values);
+ return zodErrorsToAdminErrors(result);
+ }
+ };
+}
\ No newline at end of file
diff --git a/app/admin/common/page.tsx b/app/admin/common/page.tsx
index 050a0d4..56646ce 100644
--- a/app/admin/common/page.tsx
+++ b/app/admin/common/page.tsx
@@ -1,18 +1,283 @@
-"use client"
+"use client";
-import { useState } from "react"
+import React, { useMemo, useState } from "react";
+import { ApolloProvider } from '@apollo/client';
+import { AdminPanel } from "@/components/admin";
+import { createDynamicAdminConfig } from "./dynamic-admin-config";
+import {
+ useConfigs,
+ useConfigUpdater,
+ useConfigValidation,
+ flattenConfigObject,
+ unflattenConfigObject
+} from "@/hooks/use-site-config";
+import { createApolloClient } from "@/lib/apollo-client";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Loader2, AlertCircle, CheckCircle, RefreshCw } from "lucide-react";
+import { toast } from "sonner";
-import { SiteHeader } from "../site-header"
-import { ScrollArea } from "@/components/ui/scroll-area"
-import Control from "./control"
+// 配置管理页面内容组件
+function AdminPageContent() {
+ const { configs, loading: loadingConfigs, error: errorConfigs, refetch: refetchConfigs } = useConfigs();
+ const { validation, loading: validationLoading, refetch: refetchValidation } = useConfigValidation();
+ const { updateConfigs, updating } = useConfigUpdater();
-export default function ControlPanel() {
+ const [lastSaved, setLastSaved] = useState(null);
+ // 将 configs 列表转换为初始键值
+ const initialValuesFromConfigs = useMemo(() => {
+ const parseValue = (value: string | null | undefined, valueType: string) => {
+ if (value == null) return "";
+ const vt = (valueType || '').toLowerCase();
+ if (vt === 'number' || vt === 'int' || vt === 'integer') return Number(value);
+ if (vt === 'float' || vt === 'double') return parseFloat(value);
+ if (vt === 'bool' || vt === 'boolean') return value === 'true' || value === '1';
+ // 对于 json/object/array,保留原字符串,便于在 textarea 中编辑
+ return value;
+ };
+ const entries = configs.map((item) => ({ key: item.key, value: parseValue(item.value ?? undefined, item.valueType) }));
+ return unflattenConfigObject(entries);
+ }, [configs]);
+
+ // 处理配置保存
+ const handleSave = async (values: Record) => {
+ try {
+ console.log("保存数据:", values);
+
+ // 将表单值转换为配置更新格式
+ const configUpdates = flattenConfigObject(values);
+
+ const result = await updateConfigs(configUpdates);
+
+ if (result.success) {
+ setLastSaved(new Date());
+ toast.success(`配置保存成功${result.failedKeys?.length ? `,但有 ${result.failedKeys.length} 项失败` : ''}`);
+
+ // 刷新配置数据
+ refetchConfigs();
+ refetchValidation();
+ } else {
+ toast.error(result.message || '配置保存失败');
+ }
+ } catch (error) {
+ console.error('Save config error:', error);
+ toast.error('配置保存失败,请重试');
+ }
+ };
+
+ // 处理配置导出
+ const handleExport = async () => {
+ try {
+ const exportData = {
+ config: initialValuesFromConfigs,
+ timestamp: new Date().toISOString(),
+ version: '1.0'
+ };
+
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], {
+ type: 'application/json'
+ });
+
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `config-export-${new Date().toISOString().split('T')[0]}.json`;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ toast.success('配置导出成功');
+ } catch (error) {
+ console.error('Export error:', error);
+ toast.error('配置导出失败');
+ }
+ };
+
+ // 处理配置导入
+ const handleImport = async (file: File) => {
+ try {
+ const text = await file.text();
+ const importData = JSON.parse(text);
+
+ if (importData.config) {
+ const configUpdates = flattenConfigObject(importData.config);
+ const result = await updateConfigs(configUpdates);
+
+ if (result.success) {
+ toast.success('配置导入成功');
+ refetchConfigs();
+ refetchValidation();
+ } else {
+ toast.error(result.message || '配置导入失败');
+ }
+ } else {
+ toast.error('无效的配置文件格式');
+ }
+ } catch (error) {
+ console.error('Import error:', error);
+ toast.error('配置导入失败,请检查文件格式');
+ }
+ };
+
+ // 权限检查函数
+ const hasPermission = (permission: string) => {
+ // 这里应该实现实际的权限检查逻辑
+ const userPermissions = ["admin", "settings.read", "settings.write"];
+ return userPermissions.includes(permission);
+ };
+
+ if (loadingConfigs) {
+ return (
+
+ );
+ }
+
+ if (errorConfigs) {
+ return (
+
+
+
+
+ 配置加载失败
+
+
+
+
+ {(errorConfigs?.message) || '无法加载配置数据,请检查网络连接或联系管理员'}
+
+ { refetchConfigs(); }}
+ variant="outline"
+ size="sm"
+ className="gap-2"
+ >
+
+ 重试
+
+
+
+ );
+ }
+
+ // 创建动态配置
+ const adminConfig = createDynamicAdminConfig(
+ undefined,
+ handleSave,
+ handleExport,
+ handleImport,
+ configs
+ );
return (
-
-
-
+
+ {/* 状态信息栏 */}
+
+
+ {/* 保存状态 */}
+ {lastSaved && (
+
+
+ 最后保存: {lastSaved.toLocaleTimeString()}
+
+ )}
+
+ {/* 更新状态 */}
+ {updating && (
+
+
+ 保存中...
+
+ )}
+
+
+ {/* 验证状态 */}
+
+ {validationLoading ? (
+
+
+ 验证中...
+
+ ) : validation ? (
+
+ {validation.valid ? (
+
+ ) : (
+
+ )}
+ {validation.valid ? '配置有效' : `${validation.errors.length} 个错误`}
+
+ ) : null}
+
+
+
+ {/* 验证警告和错误 */}
+ {validation && !validation.valid && (
+
+
+
+
+ 配置验证失败
+
+
+
+
+ {validation.errors.map((error, index) => (
+ {error}
+ ))}
+
+
+
+ )}
+
+ {/* 验证警告 */}
+ {validation && validation.warnings.length > 0 && (
+
+
+
+
+ 配置警告
+
+
+
+
+ {validation.warnings.map((warning, index) => (
+ {warning}
+ ))}
+
+
+
+ )}
+
+ {/* 管理面板 */}
+
- )
+ );
}
+
+// 主页面组件(带Apollo Provider)
+export default function AdminDemoPage() {
+ const apolloClient = createApolloClient();
+
+ return (
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/admin/nav-documents.tsx b/app/admin/nav-documents.tsx
index 70e7c91..af0184a 100644
--- a/app/admin/nav-documents.tsx
+++ b/app/admin/nav-documents.tsx
@@ -5,7 +5,7 @@ import {
IconTrash,
} from "@tabler/icons-react"
import { cookies } from "next/headers"
-import { fetchCategories } from "@/lib/admin-fetchers"
+// import { fetchCategories } from "@/lib/admin-fetchers"
import {
DropdownMenu,
@@ -25,13 +25,13 @@ import {
export async function NavDocuments() {
const jwt = (await cookies()).get('jwt')?.value;
- const categoriesData = await fetchCategories(jwt);
+ // const categoriesData = await fetchCategories(jwt);
return (
Categories
- {categoriesData?.settingCategories?.filter((item) => item.page).map((item) => (
+ {/* {categoriesData?.settingCategories?.filter((item) => item.page).map((item) => (
@@ -70,7 +70,7 @@ export async function NavDocuments() {
- ))}
+ ))} */}
diff --git a/app/admin/page.tsx b/app/admin/page.tsx
index 7fc5425..17827ed 100644
--- a/app/admin/page.tsx
+++ b/app/admin/page.tsx
@@ -14,7 +14,16 @@ export default function Dashboard() {
if (!isLoading) {
if (!isAuthenticated) {
router.push('/login');
- console.log(user?.role)
+ return;
+ }
+ } else {
+ if (!isAuthenticated) {
+ router.push("/");
+ return;
+ }
+
+ if (!user?.permissionPairs?.some(pair => pair.resource === "admin" && pair.action === "write")) {
+ router.push("/");
return;
}
}
diff --git a/app/admin/users/user-table.tsx b/app/admin/users/user-table.tsx
index 00ce802..0b3f19c 100644
--- a/app/admin/users/user-table.tsx
+++ b/app/admin/users/user-table.tsx
@@ -120,12 +120,15 @@ import CreateUserForm from "./create-user-form";
import { useUser } from "@/app/user-context";
export const schema = z.object({
- id: z.string(),
- username: z.string(),
- email: z.string(),
- role: z.string(),
- createdAt: z.string(),
- updatedAt: z.string(),
+ user: z.object({
+ id: z.string(),
+ username: z.string(),
+ email: z.string(),
+ groups: z.array(z.string()),
+ createdAt: z.string(),
+ updatedAt: z.string(),
+ }),
+ groups: z.array(z.string()),
})
// Create a separate component for the drag handle
@@ -152,7 +155,7 @@ const columns: ColumnDef>[] = [
{
id: "drag",
header: () => null,
- cell: ({ row }) => ,
+ cell: ({ row }) => ,
},
{
id: "select",
@@ -194,18 +197,18 @@ const columns: ColumnDef>[] = [
cell: ({ row }) => (
- {row.original.email}
+ {row.original.user.email}
),
},
{
- accessorKey: "role",
- header: "Role",
+ accessorKey: "groups",
+ header: "Groups",
cell: ({ row }) => (
- {row.original.role}
+ {row.original.groups.join(", ")}
),
@@ -228,7 +231,7 @@ const columns: ColumnDef>[] = [
)
},
cell: ({ row }) => {
- const date = new Date(row.original.createdAt);
+ const date = new Date(row.original.user.createdAt);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
@@ -269,14 +272,14 @@ const columns: ColumnDef>[] = [
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
- const dateA = new Date(rowA.original.createdAt);
- const dateB = new Date(rowB.original.createdAt);
+ const dateA = new Date(rowA.original.user.createdAt);
+ const dateB = new Date(rowB.original.user.createdAt);
return dateA.getTime() - dateB.getTime();
},
},
{
id: "lastLogin",
- accessorFn: (row) => row.updatedAt,
+ accessorFn: (row) => row.user.updatedAt,
header: ({ column }) => {
return (
>[] = [
)
},
cell: ({ row }) => {
- const date = new Date(row.original.updatedAt);
+ const date = new Date(row.original.user.updatedAt);
const now = new Date();
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60));
@@ -330,8 +333,8 @@ const columns: ColumnDef>[] = [
},
enableSorting: true,
sortingFn: (rowA, rowB, columnId) => {
- const dateA = new Date(rowA.original.updatedAt);
- const dateB = new Date(rowB.original.updatedAt);
+ const dateA = new Date(rowA.original.user.updatedAt);
+ const dateB = new Date(rowB.original.user.updatedAt);
return dateA.getTime() - dateB.getTime();
},
},
@@ -363,7 +366,7 @@ const columns: ColumnDef>[] = [
function DraggableRow({ row }: { row: Row> }) {
const { transform, transition, setNodeRef, isDragging } = useSortable({
- id: row.original.id,
+ id: row.original.user.id,
})
return (
@@ -388,15 +391,17 @@ function DraggableRow({ row }: { row: Row> }) {
const GET_USERS = gql`
query GetUsers($offset: Int, $limit: Int, $sort_by: String, $sort_order: String, $filter: String) {
- users(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
- id
- username
- email
- role
- createdAt
- updatedAt
- }
+ userWithGroups(offset: $offset, limit: $limit, sortBy: $sort_by, sortOrder: $sort_order, filter: $filter) {
+ user{
+ id
+ username
+ email
+ createdAt
+ updatedAt
+ }
+ groups
}
+}
`
const USERS_INFO = gql`
@@ -407,12 +412,14 @@ const USERS_INFO = gql`
totalAdminUsers
totalUserUsers
users {
- id
- username
- email
- role
- createdAt
- updatedAt
+ user {
+ id
+ username
+ email
+ createdAt
+ updatedAt
+ }
+ groups
}
}
}
@@ -421,10 +428,8 @@ const USERS_INFO = gql`
export function UserTable() {
const { data, loading, error, refetch } = useQuery(USERS_INFO)
-
const [localData, setLocalData] = React.useState([])
- // 同步外部数据到本地状态
React.useEffect(() => {
if (data && Array.isArray(data.usersInfo.users)) {
setLocalData(data.usersInfo.users)
@@ -599,11 +604,12 @@ function UserDataTable({
fetchPolicy: 'cache-and-network'
})
- const data = useInitialData ? propData : queryData?.users
+ const data = useInitialData ? propData : queryData?.userWithGroups
const isLoading = useInitialData ? propIsLoading : queryLoading
// 同步数据到本地状态
React.useEffect(() => {
+ debugger
if (data && Array.isArray(data)) {
setLocalData(data)
}
@@ -662,7 +668,7 @@ function UserDataTable({
columnFilters,
pagination,
},
- getRowId: (row) => row.id.toString(),
+ getRowId: (row) => row.user.id.toString(),
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
@@ -886,10 +892,10 @@ function TableCellViewer({ item }: { item: z.infer }) {
- {item.username}
+ {item.user.username}
{
- item.id === user?.id ? (
+ item.user.id === user?.id ? (
Me
@@ -901,7 +907,7 @@ function TableCellViewer({ item }: { item: z.infer
}) {
- {item.username}
+ {item.user.username}
User profile and activity information
@@ -966,16 +972,16 @@ function TableCellViewer({ item }: { item: z.infer }) {