Files
xin-procurement/web/pages/client/special.tsx
T
2026-08-31 22:21:59 +08:00

266 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useRef, useState } from 'react';
import { Button, Form, Image, message, Modal, Select, Tag, Typography } from 'antd';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import XinTable from '@/components/XinTable';
import AuthButton from '@/components/AuthButton';
import type { XinTableColumn, XinTableInstance, XinTableProps } from '@/components/XinTable/typings.ts';
import type IHomeSpecial from '@/domain/iHomeSpecial.ts';
import { HOME_SPECIAL_STATUS_MAP } from '@/domain/iHomeSpecial.ts';
import type IProduct from '@/domain/iProduct.ts';
import { PRODUCT_STATUS_MAP } from '@/domain/iProduct.ts';
import { getProductOptions } from '@/api/product/goods.ts';
import { batchAddSpecials, batchDeleteSpecials } from '@/api/client/special.ts';
const { Title, Text } = Typography;
/**
* 特价推荐商品配置(小程序首页)
*
* 仅标记推荐商品,不设特价字段:小程序端按门店客户等级价展示;
* 推荐停用或商品下架后不展示,排序越小越靠前。
*/
const HomeSpecialPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IHomeSpecial> | null>(null);
// ===== 批量删除 =====
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
const [batchDeleting, setBatchDeleting] = useState(false);
/** 批量删除(成功后清空勾选并刷新列表) */
const handleBatchDelete = () => {
window.$modal?.confirm({
title: `确定要删除选中的 ${selectedRowKeys.length} 条推荐吗?`,
okText: '删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
setBatchDeleting(true);
try {
await batchDeleteSpecials(selectedRowKeys);
message.success('批量删除成功');
setSelectedRowKeys([]);
await tableRef.current?.reload();
} finally {
setBatchDeleting(false);
}
},
});
};
// ===== 批量添加 =====
const [addOpen, setAddOpen] = useState(false);
const [addSaving, setAddSaving] = useState(false);
const [addForm] = Form.useForm();
const [productOptions, setProductOptions] = useState<IProduct[]>([]);
/** 打开添加弹窗(首次打开时加载上架商品选项) */
const openAdd = () => {
addForm.resetFields();
setAddOpen(true);
if (productOptions.length === 0) {
getProductOptions().then((res) => setProductOptions(res.data.data ?? []));
}
};
/** 提交批量添加(重复推荐后端自动跳过) */
const handleAddSave = async (values: { product_ids: number[] }) => {
setAddSaving(true);
try {
const res = await batchAddSpecials(values.product_ids);
const { added = 0, skipped = 0 } = res.data.data ?? {};
message.success(`已添加 ${added} 件推荐商品${skipped > 0 ? `${skipped} 件已在推荐中自动跳过` : ''}`);
setAddOpen(false);
await tableRef.current?.reload();
} finally {
setAddSaving(false);
}
};
const columns: XinTableColumn<IHomeSpecial>[] = [
{
title: 'ID',
dataIndex: 'id',
hideInForm: true,
hideInSearch: true,
width: 70,
align: 'center',
},
{
title: '商品图片',
dataIndex: 'image',
hideInForm: true,
hideInSearch: true,
align: 'center',
width: 100,
render: (_, record) => {
const url = record.product?.images_arr?.[0]?.preview_url;
if (!url) return '-';
return (
<Image
src={url}
width={60}
height={60}
style={{ objectFit: 'cover', borderRadius: 4 }}
/>
);
},
},
{
title: '商品名称',
dataIndex: 'keyword',
hideInForm: true,
valueType: 'text',
fieldProps: { placeholder: '按商品名称搜索' },
render: (_, record) => {
const product = record.product;
if (!product) return '-';
return `${product.name}${product.spec ? `${product.spec}` : ''}`;
},
},
{
title: '市场',
dataIndex: 'market',
hideInForm: true,
hideInSearch: true,
align: 'center',
render: (_, record) => record.product?.market || '-',
},
{
title: '计价单位',
dataIndex: 'unit',
hideInForm: true,
hideInSearch: true,
align: 'center',
width: 90,
render: (_, record) => record.product?.unit || '-',
},
{
title: '商品状态',
dataIndex: 'product_status',
hideInForm: true,
hideInSearch: true,
align: 'center',
width: 100,
render: (_, record) => {
const item = PRODUCT_STATUS_MAP[record.product?.status ?? 1];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
},
{
title: '排序',
dataIndex: 'sort',
valueType: 'digit',
hideInSearch: true,
initialValue: 0,
fieldProps: { min: 0, precision: 0 },
align: 'center',
width: 80,
},
{
title: '状态',
dataIndex: 'status',
valueType: 'radioButton',
initialValue: 1,
fieldProps: {
options: [
{ value: 1, label: '正常' },
{ value: 0, label: '停用' },
],
},
render: (_, record) => {
const item = HOME_SPECIAL_STATUS_MAP[record.status ?? 1];
return <Tag color={item?.color}>{item?.text}</Tag>;
},
align: 'center',
width: 90,
},
{
title: '创建时间',
dataIndex: 'created_at',
hideInForm: true,
hideInSearch: true,
align: 'center',
},
];
const tableProps: XinTableProps<IHomeSpecial> = {
api: '/client/special',
columns,
rowKey: 'id',
accessName: 'client.special',
tableRef,
addShow: false,
rowSelection: {
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys.map(Number)),
},
actionBarRender: (dom) => [
dom.search,
<AuthButton key="add" auth="client.special.create">
<Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
添加商品
</Button>
</AuthButton>,
<AuthButton key="batchDelete" auth="client.special.delete">
<Button
danger
icon={<DeleteOutlined />}
disabled={selectedRowKeys.length === 0}
loading={batchDeleting}
onClick={handleBatchDelete}
>
批量删除{selectedRowKeys.length > 0 ? ` (${selectedRowKeys.length})` : ''}
</Button>
</AuthButton>,
],
formProps: {
grid: true,
colProps: { span: 12 },
layout: 'vertical',
},
modalProps: { width: 640 },
};
return (
<>
<div className="mb-5">
<Title level={3}>特价推荐</Title>
<Text type="secondary">
小程序首页「特价推荐」区展示的商品;价格按门店客户等级价展示,推荐停用或商品下架后不展示,排序越小越靠前。
</Text>
</div>
<XinTable<IHomeSpecial> {...tableProps} />
<Modal
title="添加推荐商品"
open={addOpen}
onCancel={() => setAddOpen(false)}
onOk={() => addForm.submit()}
confirmLoading={addSaving}
okText="添加"
destroyOnHidden
>
<Form form={addForm} layout="vertical" onFinish={handleAddSave} preserve={false}>
<Form.Item
label="商品"
name="product_ids"
rules={[{ required: true, message: '请选择商品' }]}
>
<Select
mode="multiple"
showSearch={{ optionFilterProp: 'label' }}
placeholder="搜索并选择商品(可多选)"
options={productOptions.map((p) => ({
value: p.id!,
label: `${p.name}${p.spec ? `${p.spec}` : ''}`,
}))}
/>
</Form.Item>
</Form>
</Modal>
</>
);
};
export default HomeSpecialPage;