634 lines
19 KiB
TypeScript
Executable File
634 lines
19 KiB
TypeScript
Executable File
'use client';
|
|
|
|
import { useState, useEffect, useMemo } from 'react';
|
|
import { SiteHeader, PageInfo } from '@/components/site-header';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nice/ui/components/card';
|
|
import { Button } from '@nice/ui/components/button';
|
|
import { Input } from '@nice/ui/components/input';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@nice/ui/components/select';
|
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@nice/ui/components/form';
|
|
import { useForm, useFieldArray } from 'react-hook-form';
|
|
import { IconPlus, IconTrash, IconHome, IconSettings, IconUsers, IconPhoto, IconHeart, IconSearch, IconMail, IconCalendar, IconBell } from '@tabler/icons-react';
|
|
import { SystemConfigGroup } from '@fenghuo/common/enum';
|
|
import { useSysConf } from '@fenghuo/client';
|
|
import { toast } from '@nice/ui/components/sonner';
|
|
import { useTRPC } from '@fenghuo/client';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import { useSetPageInfo } from '@/components/providers/dashboard-provider';
|
|
|
|
// 数据类型定义
|
|
interface NavItem {
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
icon: string;
|
|
}
|
|
|
|
interface NavigationConfig {
|
|
items: Array<{
|
|
id: string;
|
|
name: string;
|
|
url: string;
|
|
}>;
|
|
}
|
|
|
|
interface SystemConfigForm {
|
|
coverPoster: {
|
|
url: string;
|
|
};
|
|
navigation: NavigationConfig;
|
|
navItems: NavItem[];
|
|
bannerImages: Array<{
|
|
id: string;
|
|
url: string;
|
|
}>;
|
|
sidebarPosters: Array<{
|
|
id: string;
|
|
url: string;
|
|
}>;
|
|
}
|
|
|
|
// 可用图标选项
|
|
const iconOptions = [
|
|
{ value: 'Home', label: '首页', icon: IconHome },
|
|
{ value: 'Settings', label: '设置', icon: IconSettings },
|
|
{ value: 'Users', label: '用户', icon: IconUsers },
|
|
{ value: 'FileImage', label: '图片', icon: IconPhoto },
|
|
{ value: 'Heart', label: '收藏', icon: IconHeart },
|
|
{ value: 'Search', label: '搜索', icon: IconSearch },
|
|
{ value: 'Mail', label: '邮件', icon: IconMail },
|
|
{ value: 'Calendar', label: '日历', icon: IconCalendar },
|
|
{ value: 'Bell', label: '通知', icon: IconBell },
|
|
];
|
|
|
|
|
|
|
|
export default function ConfigPage() {
|
|
const trpc = useTRPC();
|
|
const { setValue } = useSysConf();
|
|
|
|
const [isLoading, setIsLoading] = useState({
|
|
[SystemConfigGroup.COVER_POSTER]: false,
|
|
[SystemConfigGroup.BANNER_IMAGES]: false,
|
|
[SystemConfigGroup.SIDEBAR_POSTERS]: false,
|
|
[SystemConfigGroup.NAVIGATION]: false,
|
|
[SystemConfigGroup.NAV_ITEMS]: false,
|
|
});
|
|
|
|
|
|
useSetPageInfo({
|
|
title: '系统配置',
|
|
subtitle: '配置系统参数',
|
|
})
|
|
// 使用 tRPC 获取各个配置
|
|
const { data: coverPosterData, isLoading: coverPosterLoading } = useQuery({
|
|
...trpc.system_config.getValue.queryOptions({
|
|
key: SystemConfigGroup.COVER_POSTER
|
|
}),
|
|
}) as { data: { url: string } | null | undefined; isLoading: boolean };
|
|
|
|
const { data: navigationData, isLoading: navigationLoading } = useQuery({
|
|
...trpc.system_config.getValue.queryOptions({
|
|
key: SystemConfigGroup.NAVIGATION
|
|
}),
|
|
}) as { data: NavigationConfig | null | undefined; isLoading: boolean };
|
|
|
|
const { data: navItemsData, isLoading: navItemsLoading } = useQuery({
|
|
...trpc.system_config.getValue.queryOptions({
|
|
key: SystemConfigGroup.NAV_ITEMS
|
|
}),
|
|
}) as { data: NavItem[] | null | undefined; isLoading: boolean };
|
|
|
|
const { data: bannerImagesData, isLoading: bannerImagesLoading } = useQuery({
|
|
...trpc.system_config.getValue.queryOptions({
|
|
key: SystemConfigGroup.BANNER_IMAGES
|
|
}),
|
|
}) as { data: Array<{ id: string; url: string }> | null | undefined; isLoading: boolean };
|
|
|
|
const { data: sidebarPostersData, isLoading: sidebarPostersLoading } = useQuery({
|
|
...trpc.system_config.getValue.queryOptions({
|
|
key: SystemConfigGroup.SIDEBAR_POSTERS
|
|
}),
|
|
}) as { data: Array<{ id: string; url: string }> | null | undefined; isLoading: boolean };
|
|
|
|
// 计算最终的表单数据
|
|
const finalFormData = useMemo(() => {
|
|
return {
|
|
coverPoster: coverPosterData || { url: '' },
|
|
navigation: navigationData || { items: [] },
|
|
navItems: navItemsData || [],
|
|
bannerImages: bannerImagesData || [],
|
|
sidebarPosters: sidebarPostersData || [],
|
|
};
|
|
}, [coverPosterData, navigationData, navItemsData, bannerImagesData, sidebarPostersData]);
|
|
|
|
// 检查所有数据是否已加载完成
|
|
const allDataLoaded = !coverPosterLoading && !navigationLoading && !navItemsLoading &&
|
|
!bannerImagesLoading && !sidebarPostersLoading;
|
|
|
|
const form = useForm<SystemConfigForm>({
|
|
defaultValues: finalFormData
|
|
});
|
|
|
|
// 当数据更新时重置表单
|
|
useEffect(() => {
|
|
if (allDataLoaded) {
|
|
form.reset(finalFormData);
|
|
}
|
|
}, [allDataLoaded, finalFormData, form]);
|
|
|
|
const handleSaveSection = async (section: SystemConfigGroup) => {
|
|
setIsLoading(prev => ({ ...prev, [section]: true }));
|
|
|
|
try {
|
|
const formData = form.getValues();
|
|
let sectionData;
|
|
|
|
switch (section) {
|
|
case SystemConfigGroup.COVER_POSTER:
|
|
sectionData = formData.coverPoster;
|
|
break;
|
|
case SystemConfigGroup.BANNER_IMAGES:
|
|
sectionData = formData.bannerImages;
|
|
break;
|
|
case SystemConfigGroup.SIDEBAR_POSTERS:
|
|
sectionData = formData.sidebarPosters;
|
|
break;
|
|
case SystemConfigGroup.NAVIGATION:
|
|
sectionData = formData.navigation;
|
|
break;
|
|
case SystemConfigGroup.NAV_ITEMS:
|
|
sectionData = formData.navItems;
|
|
break;
|
|
default:
|
|
throw new Error(`未知的配置分组: ${section}`);
|
|
}
|
|
|
|
// 使用 setValue 保存配置
|
|
await setValue.mutateAsync({
|
|
key: section, // 使用 SystemConfigGroup 枚举值作为唯一 key
|
|
value: sectionData, // 当前对象数据
|
|
type: 'json', // 类型设置为 json
|
|
description: undefined, // 可选:添加描述信息
|
|
group: section, // 分组设置为对应的 SystemConfigGroup
|
|
isPublic: true // 设置为公开配置
|
|
});
|
|
|
|
toast.success(`配置保存成功`);
|
|
console.log(`${section} 配置保存成功`);
|
|
|
|
} catch (error) {
|
|
console.error(`保存 ${section} 配置失败:`, error);
|
|
toast.error(`保存配置失败: ${error}`);
|
|
} finally {
|
|
setIsLoading(prev => ({ ...prev, [section]: false }));
|
|
}
|
|
};
|
|
|
|
// 检查是否仍在加载
|
|
const isDataLoading = !allDataLoaded;
|
|
|
|
if (isDataLoading) {
|
|
return (
|
|
<div className="flex flex-1 flex-col">
|
|
|
|
<div className="@container/main flex flex-1 p-6">
|
|
<div className="w-full mx-auto flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
|
|
<p className="text-muted-foreground">正在加载配置数据...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 只有在数据加载完成后才渲染表单内容
|
|
return (
|
|
<div className="flex flex-1 flex-col">
|
|
|
|
<div className="@container/main flex flex-1 p-6">
|
|
<ConfigFormContent
|
|
form={form}
|
|
handleSaveSection={handleSaveSection}
|
|
isLoading={isLoading}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 独立的表单内容组件,在数据加载完成后才会挂载
|
|
function ConfigFormContent({
|
|
form,
|
|
handleSaveSection,
|
|
isLoading
|
|
}: {
|
|
form: any;
|
|
handleSaveSection: (section: SystemConfigGroup) => Promise<void>;
|
|
isLoading: Record<string, boolean>;
|
|
}) {
|
|
// useFieldArray 在这里初始化,此时表单数据已经是最终数据
|
|
const { fields: navigationFields, append: appendNavigation, remove: removeNavigation } = useFieldArray({
|
|
control: form.control,
|
|
name: 'navigation.items',
|
|
});
|
|
|
|
const { fields: navItemFields, append: appendNavItem, remove: removeNavItem } = useFieldArray({
|
|
control: form.control,
|
|
name: 'navItems',
|
|
});
|
|
|
|
const { fields: sidebarFields, append: appendSidebar, remove: removeSidebar } = useFieldArray({
|
|
control: form.control,
|
|
name: 'sidebarPosters',
|
|
});
|
|
|
|
const { fields: bannerFields, append: appendBanner, remove: removeBanner } = useFieldArray({
|
|
control: form.control,
|
|
name: 'bannerImages',
|
|
});
|
|
|
|
return (
|
|
<Form {...form}>
|
|
<div className="w-full mx-auto">
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
{/* 左列 */}
|
|
<div className="space-y-8">
|
|
{/* 封面海报配置 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<IconPhoto className="w-5 h-5" />
|
|
封面海报配置
|
|
</CardTitle>
|
|
<CardDescription>
|
|
配置网站主页的封面海报图片
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="flex gap-6 items-start">
|
|
<div className="flex-1">
|
|
<FormField
|
|
control={form.control}
|
|
name="coverPoster.url"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>图片URL</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="请输入图片URL" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleSaveSection(SystemConfigGroup.COVER_POSTER)}
|
|
disabled={isLoading[SystemConfigGroup.COVER_POSTER]}
|
|
>
|
|
{isLoading[SystemConfigGroup.COVER_POSTER] ? '保存中...' : '保存封面配置'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 横幅图片配置 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<IconPhoto className="w-5 h-5" />
|
|
横幅图片配置
|
|
</CardTitle>
|
|
<CardDescription>
|
|
配置页面顶部的横幅广告图片
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
{bannerFields.map((field, index) => (
|
|
<div key={field.id} className="p-4 border rounded-lg">
|
|
<div className="flex gap-6 items-start">
|
|
<div className="flex-1">
|
|
<FormField
|
|
control={form.control}
|
|
name={`bannerImages.${index}.url`}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>图片URL</FormLabel>
|
|
<div className="flex gap-2">
|
|
<FormControl>
|
|
<Input placeholder="请输入图片URL" className="flex-1" {...field} />
|
|
</FormControl>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => removeBanner(index)}
|
|
>
|
|
<IconTrash className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => appendBanner({
|
|
id: Date.now().toString(),
|
|
url: ''
|
|
})}
|
|
>
|
|
<IconPlus className="w-4 h-4 mr-2" />
|
|
添加横幅图片
|
|
</Button>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleSaveSection(SystemConfigGroup.BANNER_IMAGES)}
|
|
disabled={isLoading[SystemConfigGroup.BANNER_IMAGES]}
|
|
>
|
|
{isLoading[SystemConfigGroup.BANNER_IMAGES] ? '保存中...' : '保存横幅配置'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 侧边海报配置 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<IconPhoto className="w-5 h-5" />
|
|
侧边海报配置
|
|
</CardTitle>
|
|
<CardDescription>
|
|
配置侧边栏的推广海报图片
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
{sidebarFields.map((field, index) => (
|
|
<div key={field.id} className="p-4 border rounded-lg">
|
|
<div className="flex gap-6 items-start">
|
|
<div className="flex-1">
|
|
<FormField
|
|
control={form.control}
|
|
name={`sidebarPosters.${index}.url`}
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>图片URL</FormLabel>
|
|
<div className="flex gap-2">
|
|
<FormControl>
|
|
<Input placeholder="请输入图片URL" className="flex-1" {...field} />
|
|
</FormControl>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => removeSidebar(index)}
|
|
>
|
|
<IconTrash className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => appendSidebar({
|
|
id: Date.now().toString(),
|
|
url: ''
|
|
})}
|
|
>
|
|
<IconPlus className="w-4 h-4 mr-2" />
|
|
添加侧边海报
|
|
</Button>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleSaveSection(SystemConfigGroup.SIDEBAR_POSTERS)}
|
|
disabled={isLoading[SystemConfigGroup.SIDEBAR_POSTERS]}
|
|
>
|
|
{isLoading[SystemConfigGroup.SIDEBAR_POSTERS] ? '保存中...' : '保存侧边海报配置'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* 右列 */}
|
|
<div className="space-y-8">
|
|
{/* 导航栏配置 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<IconSettings className="w-5 h-5" />
|
|
导航栏配置
|
|
</CardTitle>
|
|
<CardDescription>
|
|
配置顶部导航栏的菜单项
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
{navigationFields.map((field, index) => (
|
|
<div key={field.id} className="flex gap-4 items-end p-4 border rounded-lg">
|
|
<FormField
|
|
control={form.control}
|
|
name={`navigation.items.${index}.name`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>菜单名称</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="请输入菜单名称" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name={`navigation.items.${index}.url`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>跳转URL</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="请输入跳转URL" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => removeNavigation(index)}
|
|
>
|
|
<IconTrash className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => appendNavigation({ id: Date.now().toString(), name: '', url: '' })}
|
|
>
|
|
<IconPlus className="w-4 h-4 mr-2" />
|
|
添加导航项
|
|
</Button>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleSaveSection(SystemConfigGroup.NAVIGATION)}
|
|
disabled={isLoading[SystemConfigGroup.NAVIGATION]}
|
|
>
|
|
{isLoading[SystemConfigGroup.NAVIGATION] ? '保存中...' : '保存导航栏配置'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* 导航项配置 */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<IconHome className="w-5 h-5" />
|
|
导航项配置
|
|
</CardTitle>
|
|
<CardDescription>
|
|
配置侧边栏或应用内的导航项目
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
{navItemFields.map((field, index) => (
|
|
<div key={field.id} className="flex gap-4 items-end p-4 border rounded-lg">
|
|
<FormField
|
|
control={form.control}
|
|
name={`navItems.${index}.name`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>项目名称</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="请输入项目名称" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name={`navItems.${index}.url`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>跳转URL</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="请输入跳转URL" {...field} />
|
|
</FormControl>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name={`navItems.${index}.icon`}
|
|
render={({ field }) => (
|
|
<FormItem className="flex-1">
|
|
<FormLabel>图标</FormLabel>
|
|
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
|
<FormControl>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="选择图标" />
|
|
</SelectTrigger>
|
|
</FormControl>
|
|
<SelectContent>
|
|
{iconOptions.map((option) => {
|
|
const IconComponent = option.icon;
|
|
return (
|
|
<SelectItem key={option.value} value={option.value}>
|
|
<div className="flex items-center gap-2">
|
|
<IconComponent className="w-4 h-4" />
|
|
{option.label}
|
|
</div>
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => removeNavItem(index)}
|
|
>
|
|
<IconTrash className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
onClick={() => appendNavItem({
|
|
id: Date.now().toString(),
|
|
name: '',
|
|
url: '',
|
|
icon: 'Home'
|
|
})}
|
|
>
|
|
<IconPlus className="w-4 h-4 mr-2" />
|
|
添加导航项
|
|
</Button>
|
|
</div>
|
|
<div className="flex justify-end">
|
|
<Button
|
|
type="button"
|
|
onClick={() => handleSaveSection(SystemConfigGroup.NAV_ITEMS)}
|
|
disabled={isLoading[SystemConfigGroup.NAV_ITEMS]}
|
|
>
|
|
{isLoading[SystemConfigGroup.NAV_ITEMS] ? '保存中...' : '保存导航项配置'}
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Form>
|
|
);
|
|
}
|