|
|
|
@@ -0,0 +1,150 @@
|
|
|
|
|
import { useState } from 'react';
|
|
|
|
|
import {
|
|
|
|
|
Button, Descriptions, Drawer, Form, Input, InputNumber, Modal, Select, Space, Table, Tag, Typography, message,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
|
import { request, type Paginated } from '../lib/api';
|
|
|
|
|
import { AROMA_TYPE_LABELS, PRODUCT_STATUS_LABELS, fmtTime } from '../lib/constants';
|
|
|
|
|
import { useAdminList } from '../lib/useAdminList';
|
|
|
|
|
|
|
|
|
|
type Row = {
|
|
|
|
|
id: string;
|
|
|
|
|
skuCode: string;
|
|
|
|
|
barcode69: string;
|
|
|
|
|
name: string;
|
|
|
|
|
subtitle?: string;
|
|
|
|
|
aromaType: string;
|
|
|
|
|
spec: string;
|
|
|
|
|
price: number;
|
|
|
|
|
benefitAmount: number;
|
|
|
|
|
status: string;
|
|
|
|
|
sortOrder: number;
|
|
|
|
|
mainImageUrl?: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default function ProductsPage() {
|
|
|
|
|
const [form] = Form.useForm();
|
|
|
|
|
const [editForm] = Form.useForm();
|
|
|
|
|
const [createForm] = Form.useForm();
|
|
|
|
|
const [filters, setFilters] = useState<Record<string, string>>({});
|
|
|
|
|
const { data, loading, page, pageSize, setPage, setPageSize, reload } = useAdminList<Row>(
|
|
|
|
|
'/admin/products',
|
|
|
|
|
() => {
|
|
|
|
|
const qs = new URLSearchParams();
|
|
|
|
|
if (filters.name) qs.set('name', filters.name);
|
|
|
|
|
if (filters.status) qs.set('status', filters.status);
|
|
|
|
|
if (filters.aromaType) qs.set('aromaType', filters.aromaType);
|
|
|
|
|
return qs;
|
|
|
|
|
},
|
|
|
|
|
[filters],
|
|
|
|
|
);
|
|
|
|
|
const [detail, setDetail] = useState<Record<string, unknown> | null>(null);
|
|
|
|
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
|
|
|
const [createOpen, setCreateOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
const columns: ColumnsType<Row> = [
|
|
|
|
|
{ title: 'SKU', dataIndex: 'skuCode', width: 90 },
|
|
|
|
|
{ title: '商品名', dataIndex: 'name', width: 180, ellipsis: true },
|
|
|
|
|
{ title: '香型', dataIndex: 'aromaType', width: 80, render: (v) => AROMA_TYPE_LABELS[v] || v },
|
|
|
|
|
{ title: '规格', dataIndex: 'spec', width: 120, ellipsis: true },
|
|
|
|
|
{ title: '售价', dataIndex: 'price', width: 80, render: (v) => `¥${v}` },
|
|
|
|
|
{ title: '权益额', dataIndex: 'benefitAmount', width: 80, render: (v) => `¥${v}` },
|
|
|
|
|
{ title: '状态', dataIndex: 'status', width: 80, render: (s) => <Tag>{PRODUCT_STATUS_LABELS[s] || s}</Tag> },
|
|
|
|
|
{ title: '排序', dataIndex: 'sortOrder', width: 60 },
|
|
|
|
|
{ title: '创建', dataIndex: 'createdAt', width: 160, render: fmtTime },
|
|
|
|
|
{
|
|
|
|
|
title: '操作', width: 80,
|
|
|
|
|
render: (_, row) => (
|
|
|
|
|
<Button type="link" size="small" onClick={async () => {
|
|
|
|
|
const d = await request<Record<string, unknown>>(`/admin/products/${row.id}`);
|
|
|
|
|
setDetail(d);
|
|
|
|
|
editForm.setFieldsValue({
|
|
|
|
|
...d,
|
|
|
|
|
coverUrl: (d as { mainImageUrl?: string }).mainImageUrl,
|
|
|
|
|
});
|
|
|
|
|
setDrawerOpen(true);
|
|
|
|
|
}}>编辑</Button>
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
|
|
|
|
<Typography.Title level={4} style={{ margin: 0 }}>商品管理</Typography.Title>
|
|
|
|
|
<Button type="primary" onClick={() => setCreateOpen(true)}>新建商品</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={(v) => { setFilters(v); setPage(1); }}>
|
|
|
|
|
<Form.Item name="name" label="名称"><Input allowClear /></Form.Item>
|
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
<Select allowClear style={{ width: 110 }} options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="aromaType" label="香型">
|
|
|
|
|
<Select allowClear style={{ width: 110 }} options={Object.entries(AROMA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item><Button type="primary" htmlType="submit">查询</Button></Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
<Table rowKey="id" className="admin-table-nowrap" loading={loading} columns={columns} dataSource={data?.items ?? []} scroll={{ x: 1100 }}
|
|
|
|
|
pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); } }} />
|
|
|
|
|
<Drawer title="编辑商品" width={520} open={drawerOpen} onClose={() => setDrawerOpen(false)}
|
|
|
|
|
extra={detail && (
|
|
|
|
|
<Button type="primary" onClick={async () => {
|
|
|
|
|
const v = await editForm.validateFields();
|
|
|
|
|
await request(`/admin/products/${detail.id}`, { method: 'PUT', body: JSON.stringify(v) });
|
|
|
|
|
message.success('已保存');
|
|
|
|
|
setDrawerOpen(false);
|
|
|
|
|
void reload();
|
|
|
|
|
}}>保存</Button>
|
|
|
|
|
)}>
|
|
|
|
|
{detail && (
|
|
|
|
|
<>
|
|
|
|
|
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
|
|
|
|
|
<Descriptions.Item label="SKU">{String(detail.skuCode)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="69码">{String(detail.barcode69)}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="香型">{AROMA_TYPE_LABELS[String(detail.aromaType)] || String(detail.aromaType)}</Descriptions.Item>
|
|
|
|
|
</Descriptions>
|
|
|
|
|
<Form form={editForm} layout="vertical">
|
|
|
|
|
<Form.Item name="name" label="商品名" rules={[{ required: true }]}><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="subtitle" label="副标题"><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="spec" label="规格" rules={[{ required: true }]}><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="price" label="售价" rules={[{ required: true }]}><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="benefitAmount" label="权益额"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Drawer>
|
|
|
|
|
<Modal title="新建商品" open={createOpen} onCancel={() => setCreateOpen(false)} onOk={async () => {
|
|
|
|
|
const v = await createForm.validateFields();
|
|
|
|
|
await request('/admin/products', { method: 'POST', body: JSON.stringify(v) });
|
|
|
|
|
message.success('已创建');
|
|
|
|
|
setCreateOpen(false);
|
|
|
|
|
createForm.resetFields();
|
|
|
|
|
void reload();
|
|
|
|
|
}} width={520}>
|
|
|
|
|
<Form form={createForm} layout="vertical" initialValues={{ aromaType: 'QINGXIANG', status: 'DRAFT', sortOrder: 0 }}>
|
|
|
|
|
<Form.Item name="skuCode" label="SKU" rules={[{ required: true }]}><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="barcode69" label="69码" rules={[{ required: true }]}><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="name" label="商品名" rules={[{ required: true }]}><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="subtitle" label="副标题"><Input /></Form.Item>
|
|
|
|
|
<Form.Item name="aromaType" label="香型" rules={[{ required: true }]}>
|
|
|
|
|
<Select options={Object.entries(AROMA_TYPE_LABELS).map(([value, label]) => ({ value, label }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="spec" label="规格" rules={[{ required: true }]}><Input placeholder="500ml | 52度" /></Form.Item>
|
|
|
|
|
<Form.Item name="price" label="售价" rules={[{ required: true }]}><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="benefitAmount" label="权益额"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="status" label="状态">
|
|
|
|
|
<Select options={Object.entries(PRODUCT_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="sortOrder" label="排序"><InputNumber min={0} style={{ width: '100%' }} /></Form.Item>
|
|
|
|
|
<Form.Item name="coverUrl" label="封面 URL"><Input placeholder="https://..." /></Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|