318 lines
9.4 KiB
TypeScript
318 lines
9.4 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState } from 'react';
|
||
import {
|
||
Building2,
|
||
Phone,
|
||
Mail,
|
||
MapPin,
|
||
ShieldCheck,
|
||
FileText,
|
||
Loader2,
|
||
} from 'lucide-react';
|
||
import { AdminHeader } from '@/components/admin/AdminHeader';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Input } from '@/components/ui/input';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Textarea } from '@/components/ui/textarea';
|
||
import { Card, CardContent } from '@/components/ui/card';
|
||
import { ImageUpload } from '@/components/admin/ImageUpload';
|
||
import { RichEditor } from '@/components/admin/RichEditor';
|
||
import { adminApi } from '@/lib/admin-services';
|
||
import type { SiteConfig } from '@/lib/types';
|
||
|
||
interface FormState {
|
||
siteName: string;
|
||
logo: string;
|
||
tel: string;
|
||
address: string;
|
||
email: string;
|
||
copyright: string;
|
||
icp: string;
|
||
aboutTitle: string;
|
||
aboutContent: string;
|
||
}
|
||
|
||
const DEFAULT: FormState = {
|
||
siteName: '',
|
||
logo: '',
|
||
tel: '',
|
||
address: '',
|
||
email: '',
|
||
copyright: '',
|
||
icp: '',
|
||
aboutTitle: '',
|
||
aboutContent: '',
|
||
};
|
||
|
||
function SectionHead({
|
||
icon,
|
||
title,
|
||
desc,
|
||
}: {
|
||
icon: React.ReactNode;
|
||
title: string;
|
||
desc: string;
|
||
}) {
|
||
return (
|
||
<div className="mb-5 flex items-center gap-3">
|
||
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-slate-800 to-slate-900 text-white shadow-md shadow-slate-900/20 ring-1 ring-inset ring-white/10">
|
||
{icon}
|
||
</span>
|
||
<div>
|
||
<div className="text-sm font-semibold text-slate-900">{title}</div>
|
||
<div className="text-xs text-slate-400">{desc}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function Field({
|
||
label,
|
||
required,
|
||
children,
|
||
}: {
|
||
label: string;
|
||
required?: boolean;
|
||
children: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="space-y-1.5">
|
||
<Label className="text-xs font-medium text-slate-600">
|
||
{label}
|
||
{required && <span className="ml-0.5 text-brand-600">*</span>}
|
||
</Label>
|
||
{children}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function SiteConfigAdminPage() {
|
||
const [hydrated, setHydrated] = useState(false);
|
||
const [form, setForm] = useState<FormState>(DEFAULT);
|
||
const [saving, setSaving] = useState(false);
|
||
const [loaded, setLoaded] = useState(false);
|
||
|
||
useEffect(() => setHydrated(true), []);
|
||
|
||
useEffect(() => {
|
||
if (!hydrated) return;
|
||
adminApi
|
||
.siteConfigGet()
|
||
.then((c: SiteConfig) => {
|
||
setForm({
|
||
siteName: c.siteName,
|
||
logo: c.logo,
|
||
tel: c.tel,
|
||
address: c.address,
|
||
email: c.email,
|
||
copyright: c.copyright,
|
||
icp: c.icp,
|
||
aboutTitle: c.aboutTitle,
|
||
aboutContent: c.aboutContent ?? '',
|
||
});
|
||
setLoaded(true);
|
||
})
|
||
.catch((e) => alert((e as Error).message));
|
||
}, [hydrated]);
|
||
|
||
const onSave = async () => {
|
||
if (!form.siteName) {
|
||
alert('请填写网站名称');
|
||
return;
|
||
}
|
||
setSaving(true);
|
||
try {
|
||
await adminApi.siteConfigUpdate(form);
|
||
alert('保存成功');
|
||
} catch (e) {
|
||
alert((e as Error).message);
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
};
|
||
|
||
if (!loaded) {
|
||
return (
|
||
<div className="flex h-40 items-center justify-center text-sm text-slate-400">
|
||
<Loader2 className="mr-2 h-5 w-5 animate-spin" /> 加载中…
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const cardCls =
|
||
'border-slate-200/70 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.04),0_8px_24px_-12px_rgba(79,70,229,0.12)]';
|
||
|
||
return (
|
||
<div>
|
||
<AdminHeader
|
||
title="网站配置"
|
||
description="管理全站基础信息、联系方式、底部版权与企业简介"
|
||
actions={
|
||
<Button onClick={onSave} disabled={saving}>
|
||
{saving ? (
|
||
<>
|
||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
保存中…
|
||
</>
|
||
) : (
|
||
'保存配置'
|
||
)}
|
||
</Button>
|
||
}
|
||
/>
|
||
|
||
<div className="grid gap-5 lg:grid-cols-2">
|
||
{/* ===== 品牌标识 ===== */}
|
||
<Card className={cardCls}>
|
||
<CardContent className="p-6">
|
||
<SectionHead
|
||
icon={<Building2 className="h-4 w-4" />}
|
||
title="品牌标识"
|
||
desc="站点名称与 Logo"
|
||
/>
|
||
<div className="space-y-4">
|
||
<Field label="网站名称" required>
|
||
<Input
|
||
className="h-10"
|
||
value={form.siteName}
|
||
onChange={(e) =>
|
||
setForm({ ...form, siteName: e.target.value })
|
||
}
|
||
placeholder="如:智管物业"
|
||
/>
|
||
</Field>
|
||
<Field label="Logo 图片">
|
||
<ImageUpload
|
||
value={form.logo}
|
||
onChange={(url) => setForm({ ...form, logo: url })}
|
||
hint="透明背景 PNG 最佳,建议 200x48"
|
||
/>
|
||
</Field>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ===== 联系方式 ===== */}
|
||
<Card className={cardCls}>
|
||
<CardContent className="p-6">
|
||
<SectionHead
|
||
icon={<Phone className="h-4 w-4" />}
|
||
title="联系方式"
|
||
desc="客户咨询与对外联络"
|
||
/>
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<Field label="联系电话">
|
||
<div className="relative">
|
||
<Phone className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||
<Input
|
||
className="h-10 pl-9"
|
||
value={form.tel}
|
||
onChange={(e) =>
|
||
setForm({ ...form, tel: e.target.value })
|
||
}
|
||
placeholder="400-000-0000"
|
||
/>
|
||
</div>
|
||
</Field>
|
||
<Field label="商务邮箱">
|
||
<div className="relative">
|
||
<Mail className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||
<Input
|
||
className="h-10 pl-9"
|
||
value={form.email}
|
||
onChange={(e) =>
|
||
setForm({ ...form, email: e.target.value })
|
||
}
|
||
placeholder="business@example.com"
|
||
/>
|
||
</div>
|
||
</Field>
|
||
</div>
|
||
<Field label="公司地址">
|
||
<div className="relative">
|
||
<MapPin className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||
<Input
|
||
className="h-10 pl-9"
|
||
value={form.address}
|
||
onChange={(e) =>
|
||
setForm({ ...form, address: e.target.value })
|
||
}
|
||
placeholder="省市区详细地址"
|
||
/>
|
||
</div>
|
||
</Field>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ===== 版权与备案 ===== */}
|
||
<Card className={cardCls}>
|
||
<CardContent className="p-6">
|
||
<SectionHead
|
||
icon={<ShieldCheck className="h-4 w-4" />}
|
||
title="版权与备案"
|
||
desc="底部声明与合规信息"
|
||
/>
|
||
<div className="space-y-4">
|
||
<Field label="底部版权">
|
||
<Textarea
|
||
rows={2}
|
||
className="resize-none"
|
||
value={form.copyright}
|
||
onChange={(e) =>
|
||
setForm({ ...form, copyright: e.target.value })
|
||
}
|
||
placeholder="© 2026 企业名称 版权所有"
|
||
/>
|
||
</Field>
|
||
<Field label="备案号">
|
||
<Input
|
||
className="h-10"
|
||
value={form.icp}
|
||
onChange={(e) => setForm({ ...form, icp: e.target.value })}
|
||
placeholder="如:京ICP备 00000000 号"
|
||
/>
|
||
</Field>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* ===== 企业简介 ===== */}
|
||
<Card className={`${cardCls} lg:col-span-2`}>
|
||
<CardContent className="p-6">
|
||
<SectionHead
|
||
icon={<FileText className="h-4 w-4" />}
|
||
title="企业简介"
|
||
desc="关于我们页面的标题与正文"
|
||
/>
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<Field label="简介标题">
|
||
<Input
|
||
className="h-10"
|
||
value={form.aboutTitle}
|
||
onChange={(e) =>
|
||
setForm({ ...form, aboutTitle: e.target.value })
|
||
}
|
||
placeholder="如:关于我们"
|
||
/>
|
||
</Field>
|
||
</div>
|
||
<Field label="简介正文(富文本)">
|
||
<RichEditor
|
||
value={form.aboutContent}
|
||
onChange={(html) =>
|
||
setForm({ ...form, aboutContent: html })
|
||
}
|
||
/>
|
||
</Field>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|