样式修改

This commit is contained in:
liu
2026-08-31 20:48:11 +08:00
parent d0ce897a0f
commit f8cc40a609
179 changed files with 228 additions and 1199 deletions
+228 -129
View File
@@ -68,9 +68,22 @@ import { getProductOptions } from '@/api/product/goods.ts';
import type ISupplier from '@/domain/iSupplier.ts';
import type IProduct from '@/domain/iProduct.ts';
import AuthButton from '@/components/AuthButton';
import useAuth from '@/hooks/useAuth.ts';
const { Title, Text } = Typography;
/** 商品明细可行内编辑的字段 */
type InlineEditField = 'product_name' | 'supplier_id' | 'product_spec' | 'unit' | 'cost_price';
/** 行内编辑字段中文名(校验提示用) */
const INLINE_FIELD_LABELS: Record<InlineEditField, string> = {
product_name: '品名',
supplier_id: '供应商',
product_spec: '包规',
unit: '单位',
cost_price: '成本',
};
/** 每单位参考价 = 整单价(成本/售价) ÷ 包规数值(包规解析不出正数时按 1 处理,与后端同口径;仅展示参考,不参与金额计算) */
const calcUnitRefPrice = (total: number, spec: string): number => {
const pack = parseFloat(spec);
@@ -83,6 +96,7 @@ const calcUnitRefPrice = (total: number, spec: string): number => {
*/
const PurchaseOrderPage: React.FC = () => {
const tableRef = useRef<XinTableInstance<IPurchaseOrder>>(null);
const { auth } = useAuth();
// 详情抽屉
const [detailOpen, setDetailOpen] = useState(false);
@@ -91,10 +105,8 @@ const PurchaseOrderPage: React.FC = () => {
const [detailLoading, setDetailLoading] = useState(false);
const [completing, setCompleting] = useState(false);
// 行修改弹窗
const [editingRow, setEditingRow] = useState<IPurchaseDetailRow | null>(null);
const [rowSaving, setRowSaving] = useState(false);
const [editForm] = Form.useForm<PurchaseRowUpdateParams>();
// 行内编辑:正在编辑的单元格(点击 品名/供应商/包规/单位/成本 进入编辑态)
const [editingCell, setEditingCell] = useState<{ productId: number; dataIndex: InlineEditField } | null>(null);
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
// 单元格下钻弹窗(门店 × 商品订货明细)
@@ -131,6 +143,9 @@ const PurchaseOrderPage: React.FC = () => {
/** 市场筛选('' = 全部市场) */
const [marketFilter, setMarketFilter] = useState<string>('');
// 商品明细页签:供应商/市场列筛选值(antd Table 受控筛选,用于底部统计行联动)
const [itemColumnFilters, setItemColumnFilters] = useState<Record<string, (React.Key | boolean)[] | null>>({});
// 导出弹窗:商品明细(供应商筛选)/ 门店购买详情 / 供应商采购明细
const [itemExportOpen, setItemExportOpen] = useState(false);
const [itemExportSupplier, setItemExportSupplier] = useState<number>(0);
@@ -180,6 +195,26 @@ const PurchaseOrderPage: React.FC = () => {
}));
}, [detail, supplierId, marketFilter]);
/** 商品明细市场列筛选选项(采购单内出现的全部市场) */
const itemMarketOptions = useMemo(() => {
const markets = new Set<string>();
(detail?.items ?? []).forEach((row) => {
if (row.market) {
markets.add(row.market);
}
});
return Array.from(markets);
}, [detail?.items]);
/** 商品明细当前可见行:按列筛选值本地过滤,驱动底部成本统计与列筛选联动 */
const summaryItems = useMemo<IPurchaseDetailRow[]>(() => {
const supplierKeys = itemColumnFilters.supplier;
const marketKeys = itemColumnFilters.market;
return (detail?.items ?? [])
.filter((row) => !supplierKeys?.length || supplierKeys.includes(row.supplier_id))
.filter((row) => !marketKeys?.length || marketKeys.includes(row.market ?? ''));
}, [detail?.items, itemColumnFilters]);
// 生成账单(已完成采购单按门店生成:填写配送费/周转筐/托盘数量,金额只读)
const [billOpen, setBillOpen] = useState(false);
const [billPrepare, setBillPrepare] = useState<IBillPrepare | null>(null);
@@ -251,6 +286,7 @@ const PurchaseOrderPage: React.FC = () => {
const openDetail = async (id: number) => {
setDetailTab('items');
setStoreSummary(null);
setItemColumnFilters({});
setDetailOpen(true);
await loadDetail(id);
};
@@ -396,35 +432,142 @@ const PurchaseOrderPage: React.FC = () => {
}
};
const openEdit = (row: IPurchaseDetailRow) => {
setEditingRow(row);
editForm.setFieldsValue({
/** 商品明细是否可行内编辑(进行中采购单 + 修改权限) */
const canUpdateRow = detail?.purchase.status === 0 && auth('purchase.order.update');
/**
* 行内编辑保存:合并该行当前值整行提交(后端要求全字段);未变更 / 校验失败时不请求。
* Enter 或失焦触发保存,保存后关闭编辑态并刷新详情与列表。
*/
const handleInlineSave = async (row: IPurchaseDetailRow, dataIndex: InlineEditField, rawValue: string | number) => {
if (!detail) {
return;
}
setEditingCell(null);
const params: PurchaseRowUpdateParams = {
product_name: row.product_name,
supplier_id: row.supplier_id > 0 ? row.supplier_id : undefined,
supplier_id: row.supplier_id,
product_spec: row.product_spec,
unit: row.unit,
cost_price: Number(row.cost_price),
});
};
};
/** 提交行修改:同步该商品全部订货明细;syncTarget=product 时追加同步商品档案 */
const handleEditSave = async (values: PurchaseRowUpdateParams) => {
if (!detail || !editingRow) {
if (dataIndex === 'cost_price') {
const cost = Number(rawValue);
if (rawValue === '' || !Number.isFinite(cost) || cost < 0) {
message.warning('成本无效,已取消修改');
return;
}
if (cost === Number(row.cost_price)) {
return;
}
params.cost_price = cost;
} else if (dataIndex === 'supplier_id') {
const supplierId = Number(rawValue);
if (supplierId === row.supplier_id) {
return;
}
params.supplier_id = supplierId;
} else {
const text = String(rawValue).trim();
if (!text) {
message.warning(`${INLINE_FIELD_LABELS[dataIndex]}不能为空,已取消修改`);
return;
}
if (text === row[dataIndex]) {
return;
}
params[dataIndex] = text;
}
if (params.supplier_id <= 0) {
message.warning('请先通过供应商单元格为该商品设置供应商');
return;
}
setRowSaving(true);
try {
const res = await updatePurchaseRow(detail.purchase.id!, editingRow.product_id, {
...values,
supplier_id: values.supplier_id ?? 0,
});
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
setEditingRow(null);
await loadDetail(detail.purchase.id!);
await tableRef.current?.reload();
} finally {
setRowSaving(false);
const res = await updatePurchaseRow(detail.purchase.id!, row.product_id, params);
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
await loadDetail(detail.purchase.id!);
await tableRef.current?.reload();
};
/** 可编辑单元格:点击进入编辑态(文本 Input / 供应商 Select / 成本 InputNumber),Esc 取消 */
const renderEditableCell = (
row: IPurchaseDetailRow,
dataIndex: InlineEditField,
display: React.ReactNode,
): React.ReactNode => {
if (!canUpdateRow) {
return display;
}
const editing = editingCell?.productId === row.product_id && editingCell.dataIndex === dataIndex;
if (!editing) {
return (
<div
className="-mx-1 cursor-pointer rounded px-1 transition-colors hover:bg-blue-50"
title="点击修改"
onClick={() => setEditingCell({ productId: row.product_id, dataIndex })}
>
{display}
</div>
);
}
const cancelOnEscape = {
onKeyDown: (e: React.KeyboardEvent) => {
if (e.key === 'Escape') {
setEditingCell(null);
}
},
};
if (dataIndex === 'supplier_id') {
return (
<Select
size="small"
autoFocus
defaultOpen
className="w-full"
defaultValue={row.supplier_id > 0 ? row.supplier_id : undefined}
placeholder="选择供应商"
showSearch={{ optionFilterProp: 'label' }}
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
onChange={(value) => void handleInlineSave(row, dataIndex, value)}
onBlur={() => setEditingCell(null)}
/>
);
}
if (dataIndex === 'cost_price') {
return (
<InputNumber
size="small"
autoFocus
min={0}
precision={2}
prefix="¥"
className="w-full"
defaultValue={Number(row.cost_price)}
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
{...cancelOnEscape}
/>
);
}
return (
<Input
size="small"
autoFocus
defaultValue={row[dataIndex]}
maxLength={dataIndex === 'unit' ? 20 : 100}
onPressEnter={(e) => void handleInlineSave(row, dataIndex, (e.target as HTMLInputElement).value)}
onBlur={(e) => void handleInlineSave(row, dataIndex, e.target.value)}
{...cancelOnEscape}
/>
);
};
/** 打开生成账单弹窗:拉取按门店汇总的商品金额(只读),初始化配送费/周转筐/托盘数量 */
@@ -483,19 +626,40 @@ const PurchaseOrderPage: React.FC = () => {
};
/** 明细矩阵列:品名/供应商/单价/包规/单位/成本 + 每门店一列(数量)+ 合计 + 操作 */
/** 明细矩阵列:品名/供应商/单价/包规/单位/成本(可点击行内编辑)+ 每门店一列(数量)+ 合计 */
const buildItemColumns = (): TableProps<IPurchaseDetailRow>['columns'] => {
const fixed: NonNullable<TableProps<IPurchaseDetailRow>['columns']> = [
{ title: '品名', dataIndex: 'product_name', width: 160, align: 'center', fixed: 'left', ellipsis: true },
{
title: '品名',
dataIndex: 'product_name',
width: 160,
align: 'center',
fixed: 'left',
ellipsis: true,
render: (_, row) => renderEditableCell(row, 'product_name', row.product_name),
},
{
title: '供应商',
dataIndex: 'supplier',
width: 110,
align: 'center',
fixed: 'left',
render: (_, row) => row.supplier?.name ?? '-',
filters: purchaseSuppliers.map((s) => ({ text: s.name, value: s.id })),
filteredValue: itemColumnFilters.supplier ?? null,
onFilter: (value, row) => row.supplier_id === Number(value),
render: (_, row) => renderEditableCell(row, 'supplier_id', row.supplier?.name ?? '-'),
},
{
title: '市场',
fixed: 'left',
dataIndex: 'market',
width: 90,
align: 'center',
filters: itemMarketOptions.map((m) => ({ text: m, value: m })),
filteredValue: itemColumnFilters.market ?? null,
onFilter: (value, row) => (row.market ?? '') === value,
render: (v) => v || '-',
},
{ title: '市场', fixed: 'left', dataIndex: 'market', width: 90, align: 'center', render: (v) => v || '-' },
{
title: '单价',
key: 'retail_price',
@@ -509,9 +673,27 @@ const PurchaseOrderPage: React.FC = () => {
return `¥${calcUnitRefPrice(weightedPrice, row.product_spec ?? '').toFixed(2)}`;
},
},
{ title: '包规', dataIndex: 'product_spec', align: 'center', width: 90, render: (v) => v || '-' },
{ title: '单位', dataIndex: 'unit', width: 70, align: 'center', render: (v) => v || '-' },
{ title: '成本', dataIndex: 'cost_price', width: 90, align: 'center', render: (v) => `¥${Number(v).toFixed(2)}` },
{
title: '包规',
dataIndex: 'product_spec',
align: 'center',
width: 90,
render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'),
},
{
title: '单位',
dataIndex: 'unit',
width: 70,
align: 'center',
render: (v, row) => renderEditableCell(row, 'unit', v || '-'),
},
{
title: '成本',
dataIndex: 'cost_price',
width: 100,
align: 'center',
render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`),
},
{
title: '合计数量',
key: 'total_quantity',
@@ -536,39 +718,13 @@ const PurchaseOrderPage: React.FC = () => {
},
}));
const tail: NonNullable<TableProps<IPurchaseDetailRow>['columns']> = [
{
title: '操作',
key: 'action',
width: 90,
fixed: 'right',
align: 'center',
render: (_, row) => (
<>
{ detail?.purchase.status === 0 ? (
<AuthButton auth="purchase.order.update">
<Button
size="small"
type="link"
icon={<EditOutlined />}
onClick={() => openEdit(row)}
>
</Button>
</AuthButton>
) : '-' }
</>
),
},
];
return [...fixed, ...storeColumns, ...tail];
return [...fixed, ...storeColumns];
};
/** 底部统计行:按门店统计金额(Σ 门店数量 × 行单价)+ 合计 */
/** 底部统计行:按门店统计金额(Σ 门店数量 × 行单价)+ 合计(随供应商/市场列筛选联动) */
const renderSummary = () => {
const stores = detail?.stores ?? [];
const items = detail?.items ?? [];
const items = summaryItems;
const storeTotals = stores.map((store) =>
items.reduce(
(sum, row) => sum + (row.cells[store.id] ?? 0) * Number(row.cost_price),
@@ -581,9 +737,12 @@ const PurchaseOrderPage: React.FC = () => {
return (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={7} align="center">
<Text strong></Text>
<Space size={16}>
<Text strong></Text>
<Text strong type="danger"> ¥{totalAmount.toFixed(2)}</Text>
</Space>
</Table.Summary.Cell>
<Table.Summary.Cell index={8} align="center">
<Table.Summary.Cell index={7} align="center">
<Text strong>{totalQuantity}</Text>
</Table.Summary.Cell>
{storeTotals.map((amount, index) => (
@@ -591,10 +750,6 @@ const PurchaseOrderPage: React.FC = () => {
<Text strong>¥{amount.toFixed(2)}</Text>
</Table.Summary.Cell>
))}
<Table.Summary.Cell index={9 + stores.length} align="center">
<Text strong>¥{totalAmount.toFixed(2)}</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
);
};
@@ -1195,6 +1350,11 @@ const PurchaseOrderPage: React.FC = () => {
</Button>
</AuthButton>
{canUpdateRow && (
<Text type="secondary" className="text-xs">
////
</Text>
)}
</Space>
<Table<IPurchaseDetailRow>
rowKey="product_id"
@@ -1202,6 +1362,7 @@ const PurchaseOrderPage: React.FC = () => {
bordered
columns={buildItemColumns()}
dataSource={detail.items}
onChange={(_pagination, filters) => setItemColumnFilters(filters)}
pagination={false}
scroll={{ x: 1200, y: 800 }}
summary={renderSummary}
@@ -1212,68 +1373,6 @@ const PurchaseOrderPage: React.FC = () => {
)}
</Drawer>
{/* 行修改:品名/供应商/包规/单位/成本 */}
<Modal
title={editingRow ? `修改「${editingRow.product_name}` : '修改明细行'}
open={editingRow !== null}
onCancel={() => setEditingRow(null)}
destroyOnHidden
footer={[
<Button key="cancel" onClick={() => setEditingRow(null)}>
</Button>,
<Button key="purchase" type="primary" loading={rowSaving} onClick={() => editForm.submit()}>
</Button>
]}
>
<div className="py-2 text-gray-500">
</div>
<Form form={editForm} layout="vertical" onFinish={handleEditSave}>
<Form.Item
label="品名"
name="product_name"
rules={[{ required: true, message: '请输入品名' }, { max: 100 }]}
>
<Input maxLength={100} />
</Form.Item>
<Form.Item
label="供应商"
name="supplier_id"
rules={[{ required: true, message: '请选择供应商' }]}
>
<Select
allowClear
showSearch={{ optionFilterProp: 'label' }}
placeholder="选择供应商"
options={suppliers.map((s) => ({ value: s.id, label: s.name }))}
/>
</Form.Item>
<Form.Item
label="包规"
name="product_spec"
rules={[{ required: true, message: '请输入包规' }, { max: 100 }]}
>
<Input maxLength={100} placeholder="如:10斤/箱" />
</Form.Item>
<Form.Item
label="单位"
name="unit"
rules={[{ required: true, message: '请输入单位' },{ max: 20 }]}
>
<Input maxLength={20} placeholder="如:斤" />
</Form.Item>
<Form.Item
label="成本"
name="cost_price"
rules={[{ required: true, message: '请输入成本' }]}
>
<InputNumber min={0} precision={2} prefix="¥" className="w-full" />
</Form.Item>
</Form>
</Modal>
{/* 单元格下钻 */}
<Modal
title={cellData ? `${cellData.store?.name ?? ''} · ${cellData.product?.name ?? ''}` : '门店商品明细'}