283 lines
11 KiB
TypeScript
283 lines
11 KiB
TypeScript
"use client";
|
||
|
||
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";
|
||
|
||
// 配置管理页面内容组件
|
||
function AdminPageContent() {
|
||
const { configs, loading: loadingConfigs, error: errorConfigs, refetch: refetchConfigs } = useConfigs();
|
||
const { validation, loading: validationLoading, refetch: refetchValidation } = useConfigValidation();
|
||
const { updateConfigs, updating } = useConfigUpdater();
|
||
|
||
const [lastSaved, setLastSaved] = useState<Date | null>(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<string, any>) => {
|
||
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 (
|
||
<div className="flex items-center justify-center min-h-[400px]">
|
||
<div className="flex items-center gap-2">
|
||
<Loader2 className="h-6 w-6 animate-spin" />
|
||
<span>加载配置数据中...</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (errorConfigs) {
|
||
return (
|
||
<Card className="border-destructive">
|
||
<CardHeader>
|
||
<CardTitle className="flex items-center gap-2 text-destructive">
|
||
<AlertCircle className="h-5 w-5" />
|
||
配置加载失败
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-sm text-muted-foreground mb-4">
|
||
{(errorConfigs?.message) || '无法加载配置数据,请检查网络连接或联系管理员'}
|
||
</p>
|
||
<Button
|
||
onClick={() => { refetchConfigs(); }}
|
||
variant="outline"
|
||
size="sm"
|
||
className="gap-2"
|
||
>
|
||
<RefreshCw className="h-4 w-4" />
|
||
重试
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// 创建动态配置
|
||
const adminConfig = createDynamicAdminConfig(
|
||
undefined,
|
||
handleSave,
|
||
handleExport,
|
||
handleImport,
|
||
configs
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 状态信息栏 */}
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex items-center gap-4">
|
||
{/* 保存状态 */}
|
||
{lastSaved && (
|
||
<Badge variant="outline" className="gap-1">
|
||
<CheckCircle className="h-3 w-3" />
|
||
最后保存: {lastSaved.toLocaleTimeString()}
|
||
</Badge>
|
||
)}
|
||
|
||
{/* 更新状态 */}
|
||
{updating && (
|
||
<Badge variant="outline" className="gap-1">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
保存中...
|
||
</Badge>
|
||
)}
|
||
</div>
|
||
|
||
{/* 验证状态 */}
|
||
<div className="flex items-center gap-2">
|
||
{validationLoading ? (
|
||
<Badge variant="outline" className="gap-1">
|
||
<Loader2 className="h-3 w-3 animate-spin" />
|
||
验证中...
|
||
</Badge>
|
||
) : validation ? (
|
||
<Badge
|
||
variant={validation.valid ? "default" : "destructive"}
|
||
className="gap-1"
|
||
>
|
||
{validation.valid ? (
|
||
<CheckCircle className="h-3 w-3" />
|
||
) : (
|
||
<AlertCircle className="h-3 w-3" />
|
||
)}
|
||
{validation.valid ? '配置有效' : `${validation.errors.length} 个错误`}
|
||
</Badge>
|
||
) : null}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 验证警告和错误 */}
|
||
{validation && !validation.valid && (
|
||
<Card className="border-destructive bg-destructive/5">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-destructive text-sm">
|
||
<AlertCircle className="h-4 w-4" />
|
||
配置验证失败
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<ul className="list-disc list-inside text-sm space-y-1">
|
||
{validation.errors.map((error, index) => (
|
||
<li key={index}>{error}</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 验证警告 */}
|
||
{validation && validation.warnings.length > 0 && (
|
||
<Card className="border-yellow-500/50 bg-yellow-50/50">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-yellow-700 text-sm">
|
||
<AlertCircle className="h-4 w-4" />
|
||
配置警告
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="pt-0">
|
||
<ul className="list-disc list-inside text-sm space-y-1">
|
||
{validation.warnings.map((warning, index) => (
|
||
<li key={index}>{warning}</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* 管理面板 */}
|
||
<AdminPanel
|
||
config={adminConfig}
|
||
initialValues={initialValuesFromConfigs}
|
||
onSubmit={handleSave}
|
||
hasPermission={hasPermission}
|
||
className="min-h-screen"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 主页面组件(带Apollo Provider)
|
||
export default function AdminDemoPage() {
|
||
const apolloClient = createApolloClient();
|
||
|
||
return (
|
||
<ApolloProvider client={apolloClient}>
|
||
<AdminPageContent />
|
||
</ApolloProvider>
|
||
);
|
||
} |