增加网站配置

This commit is contained in:
qaq
2025-07-24 16:48:47 +08:00
parent b3135d3b0f
commit d28c04d7ba
21 changed files with 983 additions and 61 deletions
+294
View File
@@ -0,0 +1,294 @@
import { useState, useEffect } from 'react';
import { Button } from "@heroui/button";
import { Card, CardBody, CardHeader } from "@heroui/card";
import { Input } from "@heroui/input";
import { Spinner } from "@heroui/spinner";
import { Divider } from "@heroui/divider";
import toast from 'react-hot-toast';
import { updateConfigs } from '@/api';
import { SettingsIcon } from '@/components/icons';
import AdminLayout from "@/layouts/admin";
import { isAdmin } from '@/utils/auth';
import { getCachedConfigs, clearConfigCache, updateSiteConfig } from '@/config/site';
// 简单的保存图标组件
const SaveIcon = ({ className }: { className?: string }) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17,21 17,13 7,13 7,21" />
<polyline points="7,3 7,8 15,8" />
</svg>
);
interface ConfigItem {
key: string;
label: string;
placeholder: string;
description?: string;
}
// 网站配置项定义
const CONFIG_ITEMS: ConfigItem[] = [
{
key: 'app_name',
label: '应用名称',
placeholder: '请输入应用名称',
description: '在浏览器标签页和导航栏显示的应用名称'
}
];
// 初始化时从缓存读取配置,避免闪烁
const getInitialConfigs = (): Record<string, string> => {
if (typeof window === 'undefined') return {};
const configKeys = ['app_name', 'site_title', 'site_description', 'contact_email', 'footer_text', 'logo_url'];
const initialConfigs: Record<string, string> = {};
try {
configKeys.forEach(key => {
const cachedValue = localStorage.getItem('vite_config_' + key);
if (cachedValue) {
initialConfigs[key] = cachedValue;
}
});
} catch (error) {
}
return initialConfigs;
};
export default function ConfigPage() {
const initialConfigs = getInitialConfigs();
const [configs, setConfigs] = useState<Record<string, string>>(initialConfigs);
const [loading, setLoading] = useState(Object.keys(initialConfigs).length === 0); // 如果有缓存数据,不显示loading
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
const [originalConfigs, setOriginalConfigs] = useState<Record<string, string>>(initialConfigs);
// 权限检查
useEffect(() => {
if (!isAdmin()) {
toast.error('权限不足,只有管理员可以访问此页面');
window.location.href = '/dashboard';
return;
}
}, []);
// 加载配置数据(优先从缓存)
const loadConfigs = async (currentConfigs?: Record<string, string>) => {
const configsToCompare = currentConfigs || configs;
const hasInitialData = Object.keys(configsToCompare).length > 0;
// 如果已有缓存数据,不显示loading,静默更新
if (!hasInitialData) {
setLoading(true);
}
try {
const configData = await getCachedConfigs();
// 只有在数据有变化时才更新
const hasDataChanged = JSON.stringify(configData) !== JSON.stringify(configsToCompare);
if (hasDataChanged) {
setConfigs(configData);
setOriginalConfigs({ ...configData });
setHasChanges(false);
} else {
}
} catch (error) {
// 只有在没有缓存数据时才显示错误
if (!hasInitialData) {
toast.error('加载配置出错,请重试');
}
} finally {
setLoading(false);
}
};
useEffect(() => {
// 延迟加载,避免阻塞初始渲染
const timer = setTimeout(() => {
loadConfigs(initialConfigs);
}, 100);
return () => clearTimeout(timer);
}, []); // 只在组件挂载时执行一次
// 处理配置项变更
const handleConfigChange = (key: string, value: string) => {
const newConfigs = { ...configs, [key]: value };
setConfigs(newConfigs);
// 检查是否有变更
const hasChangesNow = Object.keys(newConfigs).some(
k => newConfigs[k] !== originalConfigs[k]
) || Object.keys(originalConfigs).some(
k => originalConfigs[k] !== newConfigs[k]
);
setHasChanges(hasChangesNow);
};
// 保存配置
const handleSave = async () => {
setSaving(true);
try {
const response = await updateConfigs(configs);
if (response.code === 0) {
toast.success('配置保存成功');
// 清除所有配置缓存,强制下次重新获取
clearConfigCache();
// 获取变更的配置项
const changedKeys = Object.keys(configs).filter(
key => configs[key] !== originalConfigs[key]
);
setOriginalConfigs({ ...configs });
setHasChanges(false);
// 如果应用名称发生变化,立即更新网站配置
if (changedKeys.includes('app_name')) {
await updateSiteConfig();
}
// 触发配置更新事件,通知其他组件
window.dispatchEvent(new CustomEvent('configUpdated', {
detail: { changedKeys }
}));
} else {
toast.error('保存配置失败: ' + response.msg);
}
} catch (error) {
toast.error('保存配置出错,请重试');
} finally {
setSaving(false);
}
};
// 重置配置
const handleReset = () => {
setConfigs({ ...originalConfigs });
setHasChanges(false);
toast.success('已重置为保存前的状态');
};
if (loading) {
return (
<AdminLayout>
<div className="flex items-center justify-center min-h-[400px]">
<Spinner size="lg" label="加载配置中..." />
</div>
</AdminLayout>
);
}
return (
<AdminLayout>
<div className="p-6 max-w-4xl mx-auto">
{/* 页面标题 */}
<div className="flex items-center gap-3 mb-6">
<SettingsIcon className="w-8 h-8 text-primary" />
<div>
<h1 className="text-2xl font-bold">网站配置</h1>
<p className="text-gray-600 dark:text-gray-400">
管理网站的基本信息和显示设置
</p>
</div>
</div>
<Card className="shadow-md">
<CardHeader className="pb-4">
<div className="flex justify-between items-center w-full">
<div>
<h2 className="text-xl font-semibold">基本设置</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">
配置网站的基本信息,这些设置会影响网站的显示效果
</p>
</div>
<div className="flex gap-2">
{hasChanges && (
<Button
color="default"
variant="bordered"
size="sm"
onClick={handleReset}
disabled={saving}
>
重置
</Button>
)}
<Button
color="primary"
startContent={<SaveIcon className="w-4 h-4" />}
onClick={handleSave}
isLoading={saving}
disabled={!hasChanges}
>
{saving ? '保存中...' : '保存配置'}
</Button>
</div>
</div>
</CardHeader>
<Divider />
<CardBody className="space-y-6 pt-6">
{CONFIG_ITEMS.map((item, index) => (
<div key={item.key} className="space-y-2">
<div className="flex flex-col gap-1">
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
{item.label}
</label>
{item.description && (
<p className="text-xs text-gray-500 dark:text-gray-400">
{item.description}
</p>
)}
</div>
<Input
value={configs[item.key] || ''}
onChange={(e) => handleConfigChange(item.key, e.target.value)}
placeholder={item.placeholder}
variant="bordered"
size="md"
classNames={{
input: "text-sm",
inputWrapper: hasChanges && configs[item.key] !== originalConfigs[item.key]
? "border-warning-300 data-[hover=true]:border-warning-400"
: ""
}}
/>
{index < CONFIG_ITEMS.length - 1 && (
<Divider className="mt-4" />
)}
</div>
))}
</CardBody>
</Card>
{/* 操作提示 */}
{hasChanges && (
<Card className="mt-4 bg-warning-50 dark:bg-warning-900/20 border-warning-200 dark:border-warning-800">
<CardBody className="py-3">
<div className="flex items-center gap-2 text-warning-700 dark:text-warning-300">
<div className="w-2 h-2 bg-warning-500 rounded-full animate-pulse" />
<span className="text-sm">
检测到配置变更,请记得保存您的修改
</span>
</div>
</CardBody>
</Card>
)}
</div>
</AdminLayout>
);
}