增加网站配置

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>
);
}
+2 -2
View File
@@ -108,8 +108,8 @@ export default function IndexPage() {
return (
<DefaultLayout>
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10 min-h-[calc(100vh-200px)]">
<div className="w-full max-w-md">
<section className="flex flex-col items-center justify-center gap-4 py-4 sm:py-8 md:py-10 min-h-[calc(100vh-120px)] sm:min-h-[calc(100vh-200px)]">
<div className="w-full max-w-md px-4 sm:px-0">
<Card className="w-full">
<CardHeader className="pb-0 pt-6 px-6 flex-col items-center">
<h1 className={title({ size: "sm" })}>登陆</h1>
+51 -21
View File
@@ -698,14 +698,24 @@ export default function UserPage() {
label="流量限制(GB)"
type="number"
value={userForm.flow.toString()}
onChange={(e) => setUserForm(prev => ({ ...prev, flow: Number(e.target.value) || 0 }))}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setUserForm(prev => ({ ...prev, flow: value }));
}}
min="1"
max="99999"
isRequired
/>
<Input
label="转发数量"
type="number"
value={userForm.num.toString()}
onChange={(e) => setUserForm(prev => ({ ...prev, num: Number(e.target.value) || 0 }))}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setUserForm(prev => ({ ...prev, num: value }));
}}
min="1"
max="99999"
isRequired
/>
<Select
@@ -720,11 +730,11 @@ export default function UserPage() {
<SelectItem key="0" textValue="不重置">
不重置
</SelectItem>
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
</>
</Select>
<DatePicker
@@ -823,14 +833,24 @@ export default function UserPage() {
label="流量限制(GB)"
type="number"
value={tunnelForm.flow.toString()}
onChange={(e) => setTunnelForm(prev => ({ ...prev, flow: Number(e.target.value) || 0 }))}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setTunnelForm(prev => ({ ...prev, flow: value }));
}}
min="1"
max="99999"
/>
<Input
label="转发数量"
type="number"
value={tunnelForm.num.toString()}
onChange={(e) => setTunnelForm(prev => ({ ...prev, num: Number(e.target.value) || 0 }))}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setTunnelForm(prev => ({ ...prev, num: value }));
}}
min="1"
max="99999"
/>
<Select
@@ -845,11 +865,11 @@ export default function UserPage() {
<SelectItem key="0" textValue="不重置">
不重置
</SelectItem>
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
</>
</Select>
@@ -986,14 +1006,24 @@ export default function UserPage() {
label="流量限制(GB)"
type="number"
value={editTunnelForm.flow.toString()}
onChange={(e) => setEditTunnelForm(prev => prev ? { ...prev, flow: Number(e.target.value) || 0 } : null)}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setEditTunnelForm(prev => prev ? { ...prev, flow: value } : null);
}}
min="1"
max="99999"
/>
<Input
label="转发数量"
type="number"
value={editTunnelForm.num.toString()}
onChange={(e) => setEditTunnelForm(prev => prev ? { ...prev, num: Number(e.target.value) || 0 } : null)}
onChange={(e) => {
const value = Math.min(Math.max(Number(e.target.value) || 0, 1), 99999);
setEditTunnelForm(prev => prev ? { ...prev, num: value } : null);
}}
min="1"
max="99999"
/>
<Select
@@ -1026,11 +1056,11 @@ export default function UserPage() {
<SelectItem key="0" textValue="不重置">
不重置
</SelectItem>
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
{Array.from({ length: 31 }, (_, i) => i + 1).map(day => (
<SelectItem key={day.toString()} textValue={`每月${day}号(0点重置)`}>
每月{day}号(0点重置)
</SelectItem>
))}
</>
</Select>