casualroom/apps/fenghuo/web/app/[locale]/dashboard/config/page.tsx

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>
);
}