157 lines
6.7 KiB
TypeScript
157 lines
6.7 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { Button, Form, Input, InputNumber, Modal, Popconfirm, Select, Space, Switch, Table, message } from 'antd';
|
|
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons';
|
|
import * as AntIcons from '@ant-design/icons';
|
|
import { deleteHomeMaterialCategory, getHomeMaterialCategories, saveHomeMaterialCategory } from '../../api';
|
|
import type { HomeMaterialCategory, HomeMaterialCategoryPayload } from '../../types';
|
|
|
|
|
|
const ICON_COMPONENTS = AntIcons as unknown as Record<string, React.ComponentType<{ style?: React.CSSProperties }>>;
|
|
|
|
const CATEGORY_ICON_OPTIONS = [
|
|
{ label: '通用应用', value: 'AppstoreOutlined' },
|
|
{ label: '图片素材', value: 'PictureOutlined' },
|
|
{ label: '视频素材', value: 'VideoCameraOutlined' },
|
|
{ label: '电商购物', value: 'ShoppingOutlined' },
|
|
{ label: '门店商铺', value: 'ShopOutlined' },
|
|
{ label: '美妆护肤', value: 'SkinOutlined' },
|
|
{ label: '餐饮食品', value: 'CoffeeOutlined' },
|
|
{ label: '家居生活', value: 'HomeOutlined' },
|
|
{ label: '汽车出行', value: 'CarOutlined' },
|
|
{ label: '医疗健康', value: 'MedicineBoxOutlined' },
|
|
{ label: '教育培训', value: 'BookOutlined' },
|
|
{ label: '数码科技', value: 'LaptopOutlined' },
|
|
{ label: '旅游定位', value: 'EnvironmentOutlined' },
|
|
{ label: '摄影影像', value: 'CameraOutlined' },
|
|
{ label: '礼品活动', value: 'GiftOutlined' },
|
|
{ label: '热门爆款', value: 'FireOutlined' },
|
|
{ label: '品牌服务', value: 'CustomerServiceOutlined' },
|
|
{ label: '营销增长', value: 'RocketOutlined' },
|
|
{ label: '机构企业', value: 'BankOutlined' },
|
|
{ label: '娱乐休闲', value: 'SmileOutlined' },
|
|
];
|
|
|
|
function renderIconOption(value?: string | null, label?: string) {
|
|
if (!value) {
|
|
return <span style={{ color: '#999' }}>未设置</span>;
|
|
}
|
|
const Icon = ICON_COMPONENTS[value];
|
|
return (
|
|
<Space size={6}>
|
|
{Icon ? <Icon /> : null}
|
|
<span>{label || value}</span>
|
|
<span style={{ color: '#999' }}>{value}</span>
|
|
</Space>
|
|
);
|
|
}
|
|
|
|
const HomeMaterialCategoryPanel: React.FC<{ onChanged?: () => void }> = ({ onChanged }) => {
|
|
const [items, setItems] = useState<HomeMaterialCategory[]>([]);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [modalOpen, setModalOpen] = useState(false);
|
|
const [editing, setEditing] = useState<HomeMaterialCategory | null>(null);
|
|
const [form] = Form.useForm();
|
|
|
|
const load = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const res = await getHomeMaterialCategories({ page, pageSize: 20 });
|
|
setItems(res.items);
|
|
setTotal(res.total);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
useEffect(() => { load(); }, [page]);
|
|
|
|
const openEdit = (row?: HomeMaterialCategory) => {
|
|
setEditing(row || null);
|
|
form.setFieldsValue(row ? {
|
|
name: row.name,
|
|
key: row.key,
|
|
description: row.description,
|
|
icon: row.icon,
|
|
is_active: row.isActive,
|
|
sort_order: row.sortOrder,
|
|
} : { is_active: true, sort_order: 0 });
|
|
setModalOpen(true);
|
|
};
|
|
|
|
const submit = async () => {
|
|
const values = await form.validateFields();
|
|
await saveHomeMaterialCategory({ ...(values as HomeMaterialCategoryPayload), id: editing?.id });
|
|
message.success('保存成功');
|
|
setModalOpen(false);
|
|
await load();
|
|
onChanged?.();
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
|
<div />
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => openEdit()}>新增行业</Button>
|
|
</div>
|
|
<Table
|
|
rowKey="id"
|
|
loading={loading}
|
|
dataSource={items}
|
|
pagination={{ current: page, total, pageSize: 20, onChange: setPage }}
|
|
columns={[
|
|
{ title: '行业名称', dataIndex: 'name' },
|
|
{ title: 'Key', dataIndex: 'key' },
|
|
{ title: '图标', dataIndex: 'icon', render: (v?: string | null) => renderIconOption(v) },
|
|
{ title: '启用', dataIndex: 'isActive', render: (v: boolean) => v ? '启用' : '禁用' },
|
|
{ title: '素材数', dataIndex: 'assetCount' },
|
|
{ title: '图片/视频', render: (_: unknown, r: HomeMaterialCategory) => `${r.imageCount}/${r.videoCount}` },
|
|
{ title: '排序', dataIndex: 'sortOrder' },
|
|
{
|
|
title: '操作',
|
|
render: (_: unknown, row: HomeMaterialCategory) => (
|
|
<Space>
|
|
<Button size="small" icon={<EditOutlined />} onClick={() => openEdit(row)}>编辑</Button>
|
|
<Popconfirm title="确认删除该行业?" onConfirm={async () => { await deleteHomeMaterialCategory(row.id); message.success('删除成功'); await load(); onChanged?.(); }}>
|
|
<Button size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
),
|
|
},
|
|
]}
|
|
/>
|
|
<Modal title={editing ? '编辑行业' : '新增行业'} open={modalOpen} onOk={submit} onCancel={() => setModalOpen(false)} destroyOnHidden>
|
|
<Form form={form} layout="vertical">
|
|
<Form.Item name="name" label="行业名称" rules={[{ required: true, message: '请输入行业名称' }]}><Input /></Form.Item>
|
|
<Form.Item name="key" label="行业Key" rules={[{ required: true, message: '请输入行业Key' }, { pattern: /^[A-Za-z0-9_-]+$/, message: '只允许字母、数字、下划线、中划线' }]}><Input /></Form.Item>
|
|
<Form.Item name="description" label="描述"><Input.TextArea rows={3} /></Form.Item>
|
|
<Form.Item name="icon" label="图标">
|
|
<Select
|
|
allowClear
|
|
showSearch
|
|
placeholder="请选择行业图标"
|
|
optionFilterProp="searchText"
|
|
filterOption={(input, option) => {
|
|
const text = String((option as any)?.searchText || '').toLowerCase();
|
|
const value = String(option?.value || '').toLowerCase();
|
|
const keyword = input.toLowerCase();
|
|
return text.includes(keyword) || value.includes(keyword);
|
|
}}
|
|
options={CATEGORY_ICON_OPTIONS.map(item => ({
|
|
value: item.value,
|
|
searchText: `${item.label} ${item.value}`,
|
|
label: renderIconOption(item.value, item.label),
|
|
}))}
|
|
/>
|
|
</Form.Item>
|
|
<Form.Item name="sort_order" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
<Form.Item name="is_active" label="启用" valuePropName="checked"><Switch /></Form.Item>
|
|
</Form>
|
|
</Modal>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default HomeMaterialCategoryPanel;
|