|
|
@@ -0,0 +1,378 @@
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
+import { Table, Card, Tabs, Button, Space, Tag, Switch, Popconfirm, message, Modal, Form, Input, Select, Upload } from 'antd';
|
|
|
+import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, LinkOutlined } from '@ant-design/icons';
|
|
|
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
|
+import { homeIconClient } from '@/client/api';
|
|
|
+import type { HomeIcon } from '@/server/modules/home/home-icon.entity';
|
|
|
+import type { File } from '@/server/modules/files/file.entity';
|
|
|
+import type { InferResponseType, InferRequestType } from 'hono/client';
|
|
|
+import MinioUploader from '@/client/admin/components/MinioUploader';
|
|
|
+
|
|
|
+const { TabPane } = Tabs;
|
|
|
+const { TextArea } = Input;
|
|
|
+
|
|
|
+type HomeIconResponse = InferResponseType<typeof homeIconClient.$get, 200>;
|
|
|
+type CreateHomeIconRequest = InferRequestType<typeof homeIconClient.$post>['json'];
|
|
|
+type UpdateHomeIconRequest = InferRequestType<typeof homeIconClient[":id"]["$put"]>['json'];
|
|
|
+
|
|
|
+const HomeIconsPage: React.FC = () => {
|
|
|
+ const [activeTab, setActiveTab] = useState<'banner' | 'category'>('banner');
|
|
|
+ const [modalOpen, setModalOpen] = useState(false);
|
|
|
+ const [editingRecord, setEditingRecord] = useState<HomeIcon | null>(null);
|
|
|
+ const [form] = Form.useForm();
|
|
|
+ const queryClient = useQueryClient();
|
|
|
+ const client = useClient();
|
|
|
+
|
|
|
+ // 获取图标列表
|
|
|
+ const { data: iconsData, isLoading } = useQuery({
|
|
|
+ queryKey: ['home-icons', activeTab],
|
|
|
+ queryFn: async () => {
|
|
|
+ const response = await client.homeIcons.$get({
|
|
|
+ query: {
|
|
|
+ type: activeTab,
|
|
|
+ page: 1,
|
|
|
+ pageSize: 100
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('获取图标失败');
|
|
|
+ return response.json();
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 创建图标
|
|
|
+ const createMutation = useMutation({
|
|
|
+ mutationFn: async (data: CreateHomeIconRequest) => {
|
|
|
+ const response = await client.homeIcons.$post({ json: data });
|
|
|
+ if (response.status !== 200) throw new Error('创建图标失败');
|
|
|
+ return response.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ message.success('创建成功');
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['home-icons'] });
|
|
|
+ setModalOpen(false);
|
|
|
+ form.resetFields();
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ message.error(error.message || '创建失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新图标
|
|
|
+ const updateMutation = useMutation({
|
|
|
+ mutationFn: async ({ id, data }: { id: number; data: UpdateHomeIconRequest }) => {
|
|
|
+ const response = await client.homeIcons[":id"].$put({
|
|
|
+ param: { id: id.toString() },
|
|
|
+ json: data
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('更新图标失败');
|
|
|
+ return response.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ message.success('更新成功');
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['home-icons'] });
|
|
|
+ setModalOpen(false);
|
|
|
+ form.resetFields();
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ message.error(error.message || '更新失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 删除图标
|
|
|
+ const deleteMutation = useMutation({
|
|
|
+ mutationFn: async (id: number) => {
|
|
|
+ const response = await client.homeIcons[":id"].$delete({
|
|
|
+ param: { id: id.toString() }
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('删除图标失败');
|
|
|
+ return response.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ message.success('删除成功');
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['home-icons'] });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ message.error(error.message || '删除失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 切换启用状态
|
|
|
+ const toggleEnabledMutation = useMutation({
|
|
|
+ mutationFn: async ({ id, isEnabled }: { id: number; isEnabled: number }) => {
|
|
|
+ const response = await client.homeIcons[":id"].$put({
|
|
|
+ param: { id: id.toString() },
|
|
|
+ json: { isEnabled }
|
|
|
+ });
|
|
|
+ if (response.status !== 200) throw new Error('更新状态失败');
|
|
|
+ return response.json();
|
|
|
+ },
|
|
|
+ onSuccess: () => {
|
|
|
+ message.success('状态更新成功');
|
|
|
+ queryClient.invalidateQueries({ queryKey: ['home-icons'] });
|
|
|
+ },
|
|
|
+ onError: (error) => {
|
|
|
+ message.error(error.message || '状态更新失败');
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleAdd = () => {
|
|
|
+ setEditingRecord(null);
|
|
|
+ form.setFieldsValue({
|
|
|
+ type: activeTab,
|
|
|
+ sortOrder: 0,
|
|
|
+ isEnabled: 1
|
|
|
+ });
|
|
|
+ setModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleEdit = (record: HomeIcon) => {
|
|
|
+ setEditingRecord(record);
|
|
|
+ form.setFieldsValue({
|
|
|
+ ...record,
|
|
|
+ fileId: record.fileId
|
|
|
+ });
|
|
|
+ setModalOpen(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleDelete = (id: number) => {
|
|
|
+ deleteMutation.mutate(id);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleToggleEnabled = (record: HomeIcon) => {
|
|
|
+ toggleEnabledMutation.mutate({
|
|
|
+ id: record.id,
|
|
|
+ isEnabled: record.isEnabled === 1 ? 0 : 1
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleSubmit = async (values: any) => {
|
|
|
+ const data = {
|
|
|
+ ...values,
|
|
|
+ fileId: values.fileId
|
|
|
+ };
|
|
|
+
|
|
|
+ if (editingRecord) {
|
|
|
+ updateMutation.mutate({ id: editingRecord.id, data });
|
|
|
+ } else {
|
|
|
+ createMutation.mutate(data);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleUploadSuccess = (fileKey: string, fileUrl: string, file: File) => {
|
|
|
+ // 这里需要获取刚上传的文件ID
|
|
|
+ // 实际项目中,上传成功后应该返回文件ID
|
|
|
+ message.success('文件上传成功,请填写其他信息后提交');
|
|
|
+ };
|
|
|
+
|
|
|
+ const columns = [
|
|
|
+ {
|
|
|
+ title: '预览',
|
|
|
+ dataIndex: 'file',
|
|
|
+ key: 'file',
|
|
|
+ width: 80,
|
|
|
+ render: (file: File) => (
|
|
|
+ <img
|
|
|
+ src={file.path}
|
|
|
+ alt={file.name}
|
|
|
+ style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '标题',
|
|
|
+ dataIndex: 'title',
|
|
|
+ key: 'title',
|
|
|
+ width: 200,
|
|
|
+ ellipsis: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '描述',
|
|
|
+ dataIndex: 'description',
|
|
|
+ key: 'description',
|
|
|
+ width: 200,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string) => text || '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '链接地址',
|
|
|
+ dataIndex: 'linkUrl',
|
|
|
+ key: 'linkUrl',
|
|
|
+ width: 150,
|
|
|
+ ellipsis: true,
|
|
|
+ render: (text: string) => text ? <a href={text} target="_blank" rel="noopener noreferrer"><LinkOutlined /></a> : '-'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '排序',
|
|
|
+ dataIndex: 'sortOrder',
|
|
|
+ key: 'sortOrder',
|
|
|
+ width: 80,
|
|
|
+ align: 'center' as const
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '状态',
|
|
|
+ dataIndex: 'isEnabled',
|
|
|
+ key: 'isEnabled',
|
|
|
+ width: 100,
|
|
|
+ render: (isEnabled: number) => (
|
|
|
+ <Tag color={isEnabled === 1 ? 'green' : 'red'}>
|
|
|
+ {isEnabled === 1 ? '启用' : '禁用'}
|
|
|
+ </Tag>
|
|
|
+ )
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '创建时间',
|
|
|
+ dataIndex: 'createdAt',
|
|
|
+ key: 'createdAt',
|
|
|
+ width: 180,
|
|
|
+ render: (date: string) => new Date(date).toLocaleString()
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: '操作',
|
|
|
+ key: 'actions',
|
|
|
+ width: 150,
|
|
|
+ fixed: 'right' as const,
|
|
|
+ render: (_, record: HomeIcon) => (
|
|
|
+ <Space>
|
|
|
+ <Button
|
|
|
+ type="text"
|
|
|
+ icon={<EditOutlined />}
|
|
|
+ onClick={() => handleEdit(record)}
|
|
|
+ />
|
|
|
+ <Popconfirm
|
|
|
+ title="确定要删除吗?"
|
|
|
+ onConfirm={() => handleDelete(record.id)}
|
|
|
+ okText="确定"
|
|
|
+ cancelText="取消"
|
|
|
+ >
|
|
|
+ <Button type="text" danger icon={<DeleteOutlined />} />
|
|
|
+ </Popconfirm>
|
|
|
+ <Switch
|
|
|
+ checked={record.isEnabled === 1}
|
|
|
+ onChange={() => handleToggleEnabled(record)}
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </Space>
|
|
|
+ )
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ const dataSource = iconsData?.data || [];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div style={{ padding: 24 }}>
|
|
|
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
+ <h2>图标发布管理</h2>
|
|
|
+ <Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
|
|
+ 添加{activeTab === 'banner' ? '轮播图' : '分类图标'}
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ <Card>
|
|
|
+ <Tabs activeKey={activeTab} onChange={(key) => setActiveTab(key as 'banner' | 'category')}>
|
|
|
+ <Tabs.TabPane tab="轮播图管理" key="banner">
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={dataSource}
|
|
|
+ rowKey="id"
|
|
|
+ loading={isLoading}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 1000 }}
|
|
|
+ />
|
|
|
+ </Tabs.TabPane>
|
|
|
+ <Tabs.TabPane tab="分类图标管理" key="category">
|
|
|
+ <Table
|
|
|
+ columns={columns}
|
|
|
+ dataSource={dataSource}
|
|
|
+ rowKey="id"
|
|
|
+ loading={isLoading}
|
|
|
+ pagination={false}
|
|
|
+ scroll={{ x: 1000 }}
|
|
|
+ />
|
|
|
+ </Tabs.TabPane>
|
|
|
+ </Tabs>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ <Modal
|
|
|
+ title={editingRecord ? '编辑图标' : '添加图标'}
|
|
|
+ open={modalOpen}
|
|
|
+ onCancel={() => {
|
|
|
+ setModalOpen(false);
|
|
|
+ form.resetFields();
|
|
|
+ }}
|
|
|
+ onOk={() => form.submit()}
|
|
|
+ width={600}
|
|
|
+ >
|
|
|
+ <Form
|
|
|
+ form={form}
|
|
|
+ layout="vertical"
|
|
|
+ onFinish={handleSubmit}
|
|
|
+ >
|
|
|
+ <Form.Item
|
|
|
+ name="title"
|
|
|
+ label="标题"
|
|
|
+ rules={[{ required: true, message: '请输入标题' }]}
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入图标标题" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="type"
|
|
|
+ label="类型"
|
|
|
+ hidden
|
|
|
+ >
|
|
|
+ <Input />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="fileId"
|
|
|
+ label="图标文件"
|
|
|
+ rules={[{ required: true, message: '请上传图标文件' }]}
|
|
|
+ >
|
|
|
+ <Input type="number" placeholder="请输入文件ID" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="linkUrl"
|
|
|
+ label="链接地址"
|
|
|
+ >
|
|
|
+ <Input placeholder="请输入点击跳转链接(可选)" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="description"
|
|
|
+ label="描述"
|
|
|
+ >
|
|
|
+ <TextArea rows={3} placeholder="请输入描述信息(可选)" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="sortOrder"
|
|
|
+ label="排序顺序"
|
|
|
+ rules={[{ required: true, message: '请输入排序顺序' }]}
|
|
|
+ >
|
|
|
+ <Input type="number" placeholder="数字越小越靠前" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item
|
|
|
+ name="isEnabled"
|
|
|
+ label="启用状态"
|
|
|
+ valuePropName="checked"
|
|
|
+ >
|
|
|
+ <Switch checkedChildren="启用" unCheckedChildren="禁用" />
|
|
|
+ </Form.Item>
|
|
|
+
|
|
|
+ <Form.Item label="文件上传">
|
|
|
+ <MinioUploader
|
|
|
+ uploadPath={`/home-icons/${activeTab}`}
|
|
|
+ accept="image/*"
|
|
|
+ maxSize={5}
|
|
|
+ onUploadSuccess={handleUploadSuccess}
|
|
|
+ buttonText={`上传${activeTab === 'banner' ? '轮播图' : '分类图标'}`}
|
|
|
+ />
|
|
|
+ </Form.Item>
|
|
|
+ </Form>
|
|
|
+ </Modal>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default HomeIconsPage;
|