diff --git a/apps/frontend/src/hooks/usePersistedState.ts b/apps/frontend/src/hooks/usePersistedState.ts new file mode 100644 index 00000000..b13113a1 --- /dev/null +++ b/apps/frontend/src/hooks/usePersistedState.ts @@ -0,0 +1,43 @@ +import { useCallback, useState } from 'react' + +/** + * 把 UI 状态(tab、筛选等)持久化到 localStorage,刷新后保持。 + * + * - 读取失败 / 值不合法时回退到 initial + * - setter 支持值或函数式更新 + * - 写入失败(隐私模式、配额满)时静默忽略,不影响页面 + */ +export function usePersistedState( + key: string, + initial: T, + isValid?: (value: unknown) => value is T, +) { + const [value, setValue] = useState(() => { + try { + const raw = window.localStorage.getItem(key) + if (raw === null) return initial + const parsed: unknown = JSON.parse(raw) + if (isValid && !isValid(parsed)) return initial + return parsed as T + } catch { + return initial + } + }) + + const setPersistedValue = useCallback( + (next: T | ((prev: T) => T)) => { + setValue((prev) => { + const resolved = typeof next === 'function' ? (next as (previous: T) => T)(prev) : next + try { + window.localStorage.setItem(key, JSON.stringify(resolved)) + } catch { + // 本地存储不可用时只保留内存状态。 + } + return resolved + }) + }, + [key], + ) + + return [value, setPersistedValue] as const +} diff --git a/apps/frontend/src/pages/admin/AdminCostCenterPage.tsx b/apps/frontend/src/pages/admin/AdminCostCenterPage.tsx index 13422277..dd598656 100644 --- a/apps/frontend/src/pages/admin/AdminCostCenterPage.tsx +++ b/apps/frontend/src/pages/admin/AdminCostCenterPage.tsx @@ -32,10 +32,11 @@ import { } from 'antd' import type { TableColumnsType } from 'antd' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' +import { useEffect, useState } from 'react' import type { Dayjs } from 'dayjs' import PageHeader from '@/components/admin/PageHeader' +import { usePersistedState } from '@/hooks/usePersistedState' import dayjs from '@/lib/dayjs' import { applyAdminTemplateProductBootstrap, @@ -193,6 +194,92 @@ const CARRIER_WORKSPACES: Array<{ }, ] +function tabKeysForCarrier(carrier: CarrierScope) { + return [ + ...(carrier === 'all' || carrier === 'work_order' ? ['products', 'mappings'] : []), + ...(carrier === 'affiliate_dash' ? ['affiliate-skus'] : []), + 'cost-items', + 'cost-versions', + 'exceptions', + 'preview', + ] +} + +type CostItemPreset = { + key: string + label: string + code: string + name: string + costType: CostItem['costType'] + carrierType: CostCarrierType + pricingMode: CostItem['pricingMode'] + unitName: string +} + +/** 常用成本项模板:避免每次手填编码/类型/计价方式。 */ +const COST_ITEM_PRESETS: CostItemPreset[] = [ + { + key: 'auto_purchase', + label: '自动发货采购成本', + code: 'auto_purchase', + name: '自动发货采购成本', + costType: 'auto_purchase', + carrierType: 'affiliate_dash', + pricingMode: 'per_item', + unitName: '件', + }, + { + key: 'work_order_reward', + label: '接单平台打手报酬', + code: 'work_order_reward', + name: '接单平台打手报酬', + costType: 'work_order_reward', + carrierType: 'work_order', + pricingMode: 'per_item', + unitName: '件', + }, + { + key: 'manual_service', + label: '人工客服成本', + code: 'manual_service', + name: '人工客服成本', + costType: 'manual_service', + carrierType: 'manual', + pricingMode: 'per_item', + unitName: '单', + }, + { + key: 'platform_fee', + label: '平台服务费', + code: 'platform_fee', + name: '平台服务费', + costType: 'platform_fee', + carrierType: 'shared', + pricingMode: 'percent_of_revenue', + unitName: '单', + }, + { + key: 'payment_fee', + label: '支付通道费', + code: 'payment_fee', + name: '支付通道费', + costType: 'payment_fee', + carrierType: 'shared', + pricingMode: 'percent_of_revenue', + unitName: '单', + }, + { + key: 'after_sales_loss', + label: '售后损失', + code: 'after_sales_loss', + name: '售后损失', + costType: 'after_sales_loss', + carrierType: 'shared', + pricingMode: 'per_order', + unitName: '单', + }, +] + const PRICING_OPTIONS = [ { value: 'per_item', label: '按件' }, { value: 'per_order', label: '按单' }, @@ -251,8 +338,13 @@ export default function AdminCostCenterPage() { const [costItemForm] = Form.useForm() const [versionForm] = Form.useForm() - const [activeCarrier, setActiveCarrier] = useState('all') - const [activeTab, setActiveTab] = useState('products') + const [activeCarrier, setActiveCarrier] = usePersistedState( + 'admin-cost-center:carrier', + 'all', + (value): value is CarrierScope => + value === 'all' || CARRIER_WORKSPACES.some((workspace) => workspace.key === value), + ) + const [activeTab, setActiveTab] = usePersistedState('admin-cost-center:tab', 'products') const [productKeyword, setProductKeyword] = useState('') const [productRootCategoryId, setProductRootCategoryId] = useState(null) const [productSubCategoryId, setProductSubCategoryId] = useState(null) @@ -305,15 +397,17 @@ export default function AdminCostCenterPage() { const activeCarrierLabel = activeCarrier === 'all' ? '全部载体' : labelOf(CARRIER_OPTIONS, activeCarrier) // 商品目录 / 渠道映射只服务接单平台;自动发货只需上游 SKU + 采购成本。 - const showsCatalogTabs = activeCarrier === 'all' || activeCarrier === 'work_order' - const visibleTabKeys = [ - ...(showsCatalogTabs ? ['products', 'mappings'] : []), - ...(activeCarrier === 'affiliate_dash' ? ['affiliate-skus'] : []), - 'cost-items', - 'cost-versions', - 'exceptions', - 'preview', - ] + const visibleTabKeys = tabKeysForCarrier(activeCarrier) + useEffect(() => { + const keys = tabKeysForCarrier(activeCarrier) + if (!keys.includes(activeTab)) { + setActiveTab(keys[0] ?? 'cost-items') + } + }, [activeCarrier, activeTab, setActiveTab]) + const visibleCostItemPresets = + activeCarrier === 'all' + ? COST_ITEM_PRESETS + : COST_ITEM_PRESETS.filter((preset) => preset.carrierType === activeCarrier) const productsQuery = useQuery({ queryKey: [ @@ -682,9 +776,22 @@ export default function AdminCostCenterPage() { } } - function openCreateCostItem() { + function openCreateCostItem(preset?: CostItemPreset) { setEditingCostItem(null) costItemForm.resetFields() + if (preset) { + costItemForm.setFieldsValue({ + code: preset.code, + name: preset.name, + costType: preset.costType, + carrierType: preset.carrierType, + pricingMode: preset.pricingMode, + unitName: preset.unitName, + status: 'active', + }) + setCostItemModalOpen(true) + return + } const defaultCostType = activeCarrier === 'affiliate_dash' ? 'auto_purchase' @@ -744,10 +851,20 @@ export default function AdminCostCenterPage() { } } - function openCreateVersion() { + function openCreateVersion(costItem?: CostItem) { versionForm.resetFields() + const defaultScope: CostVersionFormValues['scopeType'] = costItem + ? costItem.costType === 'auto_purchase' + ? 'product' + : costItem.costType === 'work_order_reward' + ? 'rule' + : 'global' + : activeCarrier === 'affiliate_dash' + ? 'product' + : 'global' versionForm.setFieldsValue({ - scopeType: activeCarrier === 'affiliate_dash' ? 'product' : 'global', + costItemId: costItem?.id, + scopeType: defaultScope, effectiveFrom: dayjs(), status: 'active', }) @@ -983,6 +1100,8 @@ export default function AdminCostCenterPage() { { title: '商品 / 分类', dataIndex: 'canonicalName', + width: 460, + ellipsis: true, render: (value: string, row) => (
( - - {value} - + + + {value} + + {row.canonicalSku} · {row.unitName} @@ -1269,8 +1390,8 @@ export default function AdminCostCenterPage() { ] const costItemColumns: TableColumnsType = [ - { title: '编码', dataIndex: 'code', width: 180 }, - { title: '名称', dataIndex: 'name', minWidth: 160 }, + { title: '编码', dataIndex: 'code', width: 160 }, + { title: '名称', dataIndex: 'name', width: 320, ellipsis: true }, { title: '类型', dataIndex: 'costType', @@ -1302,11 +1423,14 @@ export default function AdminCostCenterPage() { }, { title: '操作', - width: 140, + width: 210, fixed: 'right', render: (_, row) => canConfig ? ( + @@ -1322,11 +1446,16 @@ export default function AdminCostCenterPage() { { title: '成本项', dataIndex: 'costItemName', - minWidth: 160, + width: 240, + ellipsis: true, render: (value: string, row) => ( - - {value} - {row.costItemCode} + + + {value} + + + {row.costItemCode} + ), }, @@ -1341,7 +1470,8 @@ export default function AdminCostCenterPage() { { title: '范围', dataIndex: 'scopeType', - width: 110, + width: 200, + ellipsis: true, render: (value: string, row) => { if (value === 'global') return '全局' if (value === 'provider') return providerLabel(row.provider) @@ -1417,7 +1547,7 @@ export default function AdminCostCenterPage() { { title: '原因', dataIndex: 'reason', - minWidth: 220, + width: 360, render: (value: string, row) => ( {value} @@ -1688,13 +1818,14 @@ export default function AdminCostCenterPage() { ) : null} (row.mappingCount > 0 ? 'product-rule-row-mapped' : '')} - scroll={{ x: 1080 }} + scroll={{ x: 1180 }} pagination={buildAdminTablePagination({ current: productPage, pageSize: productPageSize, @@ -1780,12 +1911,13 @@ export default function AdminCostCenterPage() { + tableLayout="fixed" rowKey={(row) => String(row.canonicalProductId)} size="small" columns={mappingGroupColumns} dataSource={mappingGroups} loading={mappingsQuery.isLoading} - scroll={{ x: 1210 }} + scroll={{ x: 1100 }} expandable={{ expandedRowKeys: expandedMappingProductIds, onExpandedRowsChange: (keys) => @@ -1797,6 +1929,7 @@ export default function AdminCostCenterPage() { 该标准商品下共有 {group.mappings.length} 条渠道映射 + tableLayout="fixed" rowKey="id" size="small" columns={mappingDetailColumns} @@ -1866,11 +1999,12 @@ export default function AdminCostCenterPage() { message="自动发货只按上游货源 SKU 配采购成本:这里不维护游戏分类、渠道映射和商品上下架,那些只属于接单平台。" />
( - - {value} - + + + {value} + + {String(row.attributes?.skuCode || row.canonicalSku)} @@ -1974,12 +2118,48 @@ export default function AdminCostCenterPage() { } extra={ canConfig ? ( - ) : null } > + + + 1. 先建成本项目:如「自动发货采购成本」「接单平台打手报酬」「平台服务费」; + + + 2. 再建成本版本:同一成本项可有多条,按范围优先级 接单规则 > 标准商品 + > 店铺 > 来源 > 全局 命中; + + 3. 改价只新增版本,历史订单走成本快照。 + + } + /> + {canConfig && visibleCostItemPresets.length > 0 ? ( + + 常用成本项: + {visibleCostItemPresets.map((preset) => ( + + ))} + + ) : null}
} onClick={openCreateVersion}> + ) : null } > +
`${row.costItemId}-${row.costVersionId}`} size="small" pagination={false} dataSource={preview.lines} columns={[ - { title: '成本项', dataIndex: 'costItemName' }, - { title: '类型', dataIndex: 'costType' }, - { title: '范围', dataIndex: 'scopeType' }, + { title: '成本项', dataIndex: 'costItemName', width: 220 }, + { title: '类型', dataIndex: 'costType', width: 120 }, + { title: '范围', dataIndex: 'scopeType', width: 160 }, { title: '数量', dataIndex: 'quantity', + width: 90, render: (value: number) => Number(value || 0).toFixed(3), }, { title: '单位成本', dataIndex: 'unitAmountFen', + width: 110, render: (value: number) => formatMoney(value), }, { title: '合计', dataIndex: 'totalAmountFen', + width: 110, render: (value: number) => formatMoney(value), }, ]} @@ -2398,19 +2596,34 @@ export default function AdminCostCenterPage() { destroyOnClose > - + - + - + @@ -2501,6 +2721,7 @@ export default function AdminCostCenterPage() {