批量对账

This commit is contained in:
liu
2026-09-07 22:53:50 +08:00
parent 379aeac2b3
commit 7de51391e4
9 changed files with 469 additions and 39 deletions
+104 -31
View File
@@ -43,6 +43,7 @@ import type {
import { PURCHASE_STATUS_MAP, BILL_STATUS_MAP } from '@/domain/iPurchaseOrder.ts';
import {
addPurchaseStoreItem,
batchPurchaseItemCheck,
exportPurchase,
exportPurchaseStores,
exportPurchaseSuppliers,
@@ -51,6 +52,7 @@ import {
getPurchaseDetail,
getPurchaseStoreSummary,
removePurchaseStoreItem,
togglePurchaseItemCheck,
type BillGenerateStoreParams,
type PurchaseRowUpdateParams,
updatePurchaseRow,
@@ -99,8 +101,9 @@ const PurchaseOrderPage: React.FC = () => {
const [detailLoading, setDetailLoading] = useState(false);
const [completing, setCompleting] = useState(false);
// 商品明细:单品「已对账」勾选标记(前端本地控制不入库,按采购单持久化到 localStorage
// 商品明细:单品「已对账」勾选标记(入库持久化,随详情接口回显
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set());
const [checkAllSaving, setCheckAllSaving] = useState(false);
// 行内编辑:供应商选项(品名/供应商/包规/单位/成本与各门店订货量直接在表格中编辑)
const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
@@ -250,35 +253,85 @@ const PurchaseOrderPage: React.FC = () => {
}
}, [detailOpen, detailTab, storeId, detail?.purchase.id]);
// 切换采购单时从 localStorage 恢复单品「已对账」标记
// 切换采购单时从详情接口恢复单品「已对账」标记(入库持久化)
useEffect(() => {
if (!detail) {
setCheckedItems(new Set());
return;
}
try {
const raw = localStorage.getItem(`purchase-checked-items:${detail.purchase.id}`);
setCheckedItems(new Set(raw ? (JSON.parse(raw) as number[]) : []));
} catch {
setCheckedItems(new Set());
}
setCheckedItems(new Set(detail?.checked_product_ids ?? []));
}, [detail]);
/** 切换单品「已对账」标记(前端本地控制,不入库) */
const toggleItemChecked = (productId: number) => {
/** 切换单品「已对账」标记(入库持久化;乐观更新,请求失败回滚本地勾选 */
const toggleItemChecked = async (productId: number) => {
if (!detail) {
return;
}
const willCheck = !checkedItems.has(productId);
setCheckedItems((prev) => {
const next = new Set(prev);
if (next.has(productId)) {
next.delete(productId);
} else {
if (willCheck) {
next.add(productId);
} else {
next.delete(productId);
}
localStorage.setItem(`purchase-checked-items:${detail.purchase.id}`, JSON.stringify([...next]));
return next;
});
try {
await togglePurchaseItemCheck(detail.purchase.id!, productId);
} catch {
// 错误提示由请求封装统一弹出,这里回滚本地勾选
setCheckedItems((prev) => {
const next = new Set(prev);
if (willCheck) {
next.delete(productId);
} else {
next.add(productId);
}
return next;
});
}
};
/** 商品明细当前可见行的商品ID(列筛选联动,全选仅作用于筛选后的数据) */
const visibleProductIds = useMemo(() => summaryItems.map((row) => row.product_id), [summaryItems]);
/** 当前可见行是否全部已标记 */
const allVisibleChecked =
visibleProductIds.length > 0 && visibleProductIds.every((id) => checkedItems.has(id));
/** 全选/取消全选「已对账」标记(仅作用于列筛选后的可见行;乐观更新,请求失败回滚) */
const toggleAllVisibleChecked = async () => {
if (!detail || visibleProductIds.length === 0 || checkAllSaving) {
return;
}
const willCheck = !allVisibleChecked;
setCheckedItems((prev) => {
const next = new Set(prev);
visibleProductIds.forEach((id) => {
if (willCheck) {
next.add(id);
} else {
next.delete(id);
}
});
return next;
});
setCheckAllSaving(true);
try {
await batchPurchaseItemCheck(detail.purchase.id!, visibleProductIds, willCheck);
} catch {
// 错误提示由请求封装统一弹出,这里回滚本地勾选
setCheckedItems((prev) => {
const next = new Set(prev);
visibleProductIds.forEach((id) => {
if (willCheck) {
next.delete(id);
} else {
next.add(id);
}
});
return next;
});
} finally {
setCheckAllSaving(false);
}
};
const loadDetail = async (id: number) => {
@@ -599,7 +652,26 @@ const PurchaseOrderPage: React.FC = () => {
render: (_, row) => renderEditableCell(row, 'supplier_id', row.supplier?.name ?? '-'),
},
{
title: '市场',
title: (
<Space size={4}>
<span></span>
<Tooltip
title={`${allVisibleChecked ? '取消全选' : '全选对账'}(仅作用于当前筛选显示的 ${visibleProductIds.length} 行)`}
>
<Button
size="small"
shape="circle"
style={{ minWidth: 16, height: 16, width: 16 }}
icon={<CheckOutlined />}
color={allVisibleChecked ? 'green' : 'default'}
variant={allVisibleChecked ? 'solid' : 'outlined'}
loading={checkAllSaving}
disabled={visibleProductIds.length === 0}
onClick={() => void toggleAllVisibleChecked()}
/>
</Tooltip>
</Space>
),
fixed: 'left',
dataIndex: 'market',
width: 90,
@@ -608,22 +680,20 @@ const PurchaseOrderPage: React.FC = () => {
filteredValue: itemColumnFilters.market ?? null,
onFilter: (value, row) => (row.market ?? '') === value,
render: (v, row) => {
// ✔ 按钮标记本单品已对账(前端本地标记,不入库)
// ✔ 按钮标记本单品已对账(入库持久化,随详情接口回显
const checked = checkedItems.has(row.product_id);
return (
<Space size={4}>
<span>{v || '-'}</span>
<Tooltip title={checked ? '取消对账标记' : '标记本单品已对账'}>
<Button
size="small"
shape="circle"
style={{ minWidth: 16, height: 16, width: 16 }}
icon={<CheckOutlined />}
color={checked ? 'green' : 'default'}
variant={checked ? 'solid' : 'outlined'}
onClick={() => toggleItemChecked(row.product_id)}
/>
</Tooltip>
<Button
size="small"
shape="circle"
style={{ minWidth: 16, height: 16, width: 16 }}
icon={<CheckOutlined />}
color={checked ? 'green' : 'default'}
variant={checked ? 'solid' : 'outlined'}
onClick={() => void toggleItemChecked(row.product_id)}
/>
</Space>
);
},
@@ -1332,6 +1402,9 @@ const PurchaseOrderPage: React.FC = () => {
</Button>
</AuthButton>
<Text type="secondary" className="text-xs">
{visibleProductIds.filter((id) => checkedItems.has(id)).length}/{visibleProductIds.length}
</Text>
{canUpdateRow && (
<Text type="secondary" className="text-xs">