feat(cost-center): 统一接单模板成本并自动同步商品
This commit is contained in:
@@ -95,7 +95,7 @@ export function ProductCatalogPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const { canConfig, productSourceFilter, refreshAll } = useCostCenter()
|
||||
const [productForm] = Form.useForm<ProductFormValues>()
|
||||
// 初始化/回填只处理最近 30 天,避免很早以前的乱 SKU 再次进入商品与映射。
|
||||
// 历史修复只处理最近 30 天,避免很早以前的乱 SKU 再次进入商品与映射。
|
||||
const [backfillRange, setBackfillRange] = useState<[Dayjs, Dayjs]>([
|
||||
dayjs().subtract(29, 'day').startOf('day'),
|
||||
dayjs().endOf('day'),
|
||||
@@ -177,7 +177,7 @@ export function ProductCatalogPanel() {
|
||||
const data = response.data
|
||||
bootstrapSyncOptionsRef.current.includeDisabledTemplates = false
|
||||
modal.confirm({
|
||||
title: '从接单模板全量同步商品与映射',
|
||||
title: '修复接单模板历史商品与映射',
|
||||
width: 720,
|
||||
content: (
|
||||
<Space direction="vertical" size={8}>
|
||||
@@ -276,7 +276,7 @@ export function ProductCatalogPanel() {
|
||||
? `(含停用模板 ${applied.disabledTemplateCount} 个,已建为归档商品)`
|
||||
: ''
|
||||
message.success(
|
||||
`已同步 ${applied.templateCount} 个模板商品:新建 ${applied.createdProductCount} 个、刷新 ${applied.updatedProductCount} 个、复用 ${applied.reusedProductCount} 个${disabledText};新增 ${applied.createdMappingCount} 条映射,同步 ${applied.templateCostVersionCount} 条模板发单金额,回填 ${applied.mappedOrderItemCount} 个订单项${rebuildText}${conflictText}`,
|
||||
`已同步 ${applied.templateCount} 个模板商品:新建 ${applied.createdProductCount} 个、刷新 ${applied.updatedProductCount} 个、复用 ${applied.reusedProductCount} 个${disabledText};新增 ${applied.createdMappingCount} 条映射,回填 ${applied.mappedOrderItemCount} 个订单项${rebuildText}${conflictText}`,
|
||||
)
|
||||
refreshAll()
|
||||
} catch (error) {
|
||||
@@ -338,6 +338,7 @@ export function ProductCatalogPanel() {
|
||||
}
|
||||
|
||||
function openSetProductPrice(item: ProductCatalogItem) {
|
||||
const isTemplateProduct = item.canonicalSku.startsWith('tpl:')
|
||||
setPricingProduct(item)
|
||||
priceForm.resetFields()
|
||||
priceForm.setFieldsValue({
|
||||
@@ -345,9 +346,9 @@ export function ProductCatalogPanel() {
|
||||
item.activeCostAmountFen !== null && item.activeCostAmountFen !== undefined
|
||||
? (item.activeCostAmountFen / 100).toFixed(2)
|
||||
: undefined,
|
||||
pricingMode: isTemplateProduct ? item.templatePricingMode || 'fixed' : undefined,
|
||||
effectiveScope: 'immediate',
|
||||
changeReason: '在商品与成本中心统一调整价格',
|
||||
syncOpenOrders: true,
|
||||
syncSnapshots: true,
|
||||
})
|
||||
setPriceModalOpen(true)
|
||||
@@ -355,10 +356,10 @@ export function ProductCatalogPanel() {
|
||||
|
||||
async function submitProductPrice(values: {
|
||||
amountYuan: number | string
|
||||
pricingMode?: 'fixed' | 'unit'
|
||||
effectiveScope: 'immediate' | 'custom'
|
||||
effectiveFromCustom?: string
|
||||
changeReason?: string
|
||||
syncOpenOrders?: boolean
|
||||
}) {
|
||||
if (!pricingProduct) return
|
||||
const yuan = Number(values.amountYuan)
|
||||
@@ -376,9 +377,10 @@ export function ProductCatalogPanel() {
|
||||
try {
|
||||
const response = await setAdminProductPrice(pricingProduct.id, {
|
||||
amountFen,
|
||||
pricingMode: values.pricingMode,
|
||||
effectiveFrom,
|
||||
changeReason: values.changeReason || '在商品与成本中心统一调整价格',
|
||||
syncOpenOrders: values.syncOpenOrders !== false,
|
||||
syncOpenOrders: true,
|
||||
})
|
||||
const result = response.data
|
||||
let extraHint = ''
|
||||
@@ -386,6 +388,12 @@ export function ProductCatalogPanel() {
|
||||
extraHint = `,已同步 ${result.syncedOrders.updatedCount} 笔工单大厅待抢单`
|
||||
}
|
||||
message.success(`标准价格配置成功(¥${(amountFen / 100).toFixed(2)})${extraHint}`)
|
||||
if (pricingProduct.canonicalSku.startsWith('tpl:') && !pricingProduct.templateEnabled) {
|
||||
message.info('成本已配置;请回接单模板启用该模板后开始自动接单')
|
||||
}
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-product-rules'],
|
||||
})
|
||||
setPriceModalOpen(false)
|
||||
refreshAll()
|
||||
} catch (error) {
|
||||
@@ -536,6 +544,16 @@ export function ProductCatalogPanel() {
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
单位:{row.unitName || '件'}
|
||||
</Typography.Text>
|
||||
{row.canonicalSku.startsWith('tpl:') ? (
|
||||
<Tag color={row.templateEnabled ? 'green' : 'orange'} style={{ marginInlineEnd: 0 }}>
|
||||
{row.templateEnabled ? '模板已启用' : '模板待启用'}
|
||||
</Tag>
|
||||
) : null}
|
||||
{row.templatePricingMode ? (
|
||||
<Tag color="geekblue" style={{ marginInlineEnd: 0 }}>
|
||||
{row.templatePricingMode === 'unit' ? '按件计价' : '固定金额'}
|
||||
</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
@@ -720,7 +738,7 @@ export function ProductCatalogPanel() {
|
||||
loading={bootstrapPreviewing || bootstrapApplying}
|
||||
onClick={() => void runTemplateBootstrap()}
|
||||
>
|
||||
从接单模板初始化
|
||||
修复历史商品/映射
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
@@ -949,16 +967,38 @@ export function ProductCatalogPanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pricingProduct.canonicalSku.startsWith('tpl:') ? (
|
||||
<Form.Item
|
||||
name="pricingMode"
|
||||
label="接单计价方式"
|
||||
rules={[{ required: true, message: '请选择计价方式' }]}
|
||||
extra="保存后会同步到接单模板;固定金额按整单,按件单价按 SKU 数量计算"
|
||||
>
|
||||
<Radio.Group optionType="button" buttonStyle="solid">
|
||||
<Radio.Button value="fixed">固定金额</Radio.Button>
|
||||
<Radio.Button value="unit">按件计价</Radio.Button>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
|
||||
<Form.Item
|
||||
name="amountYuan"
|
||||
label={
|
||||
pricingProduct.source === 'affiliate_dash' ? '采购单价(元)' : '标准结算单价(元)'
|
||||
pricingProduct.source === 'affiliate_dash'
|
||||
? '采购单价(元)'
|
||||
: pricingProduct.canonicalSku.startsWith('tpl:')
|
||||
? pricingProduct.templateSharingEnabled
|
||||
? '拼单总成本(元)'
|
||||
: '接单成本(元)'
|
||||
: '标准结算单价(元)'
|
||||
}
|
||||
rules={[{ required: true, message: '请填写价格' }]}
|
||||
extra={
|
||||
pricingProduct.source === 'affiliate_dash'
|
||||
? '上游每发货 1 件扣除的采购成本金额'
|
||||
: '接单打手发单/验收的基础结算单价'
|
||||
: pricingProduct.canonicalSku.startsWith('tpl:')
|
||||
? '固定金额按整单计;按件计价时填写每个 SKU 的接单成本'
|
||||
: '接单打手发单/验收的基础结算单价'
|
||||
}
|
||||
>
|
||||
<InputNumber
|
||||
@@ -983,8 +1023,13 @@ export function ProductCatalogPanel() {
|
||||
只影响之后的新订单;历史订单的成本与成交价已固化,不会被改价覆盖
|
||||
</div>
|
||||
</Radio>
|
||||
<Radio value="custom">
|
||||
<Radio value="custom" disabled={pricingProduct.canonicalSku.startsWith('tpl:')}>
|
||||
<strong>指定生效时间(不能早于当前时间)</strong>
|
||||
{pricingProduct.canonicalSku.startsWith('tpl:') ? (
|
||||
<div style={{ color: '#cf1322', fontSize: 12, marginTop: 2 }}>
|
||||
接单模板当前只支持立即生效,避免模板价格与成本版本不同步
|
||||
</div>
|
||||
) : null}
|
||||
</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
@@ -1010,17 +1055,6 @@ export function ProductCatalogPanel() {
|
||||
<Input placeholder="例如:成本中心统一定价调整 / 上游采购成本调整" />
|
||||
</Form.Item>
|
||||
|
||||
{pricingProduct.canonicalSku.startsWith('tpl:') ? (
|
||||
<Form.Item name="syncOpenOrders" valuePropName="checked" style={{ marginBottom: 12 }}>
|
||||
<Checkbox>
|
||||
<strong>立即同步至接单大厅待抢单工单(推荐)</strong>
|
||||
<div style={{ color: '#64748b', fontSize: 12 }}>
|
||||
接单大厅中所有未领取的该模板工单将立即同步为新价格;已领取的工单保持原价不变。
|
||||
</div>
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
) : null}
|
||||
|
||||
{pricingProduct.source === 'affiliate_dash' ? (
|
||||
<Form.Item name="syncSnapshots" valuePropName="checked" style={{ marginBottom: 12 }}>
|
||||
<Checkbox>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/**
|
||||
* 发单价格 Tab:只读展示「接单模板 → 发单金额」的当前生效版本。
|
||||
* 发单价格 Tab:展示由标准商品成本自动投影出的接单模板当前生效版本。
|
||||
*
|
||||
* 接单平台的成本以打手报酬为主,金额由接单模板(work_product_rules)决定,
|
||||
* 这里只做核对视图;改价请改模板后点「同步模板发单金额」。
|
||||
* 这里只做核对视图;改价在「标准商品」中完成并自动同步。
|
||||
* 人工与自动发货的成本分别在各自 SKU 配置里维护,不在这里重复配置。
|
||||
*/
|
||||
import { SearchOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { Alert, App, Button, Card, Input, Select, Space, Tag, Typography } from 'antd'
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Alert, Card, Input, Select, Space, Tag, Typography } from 'antd'
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { fetchAdminCostVersions, syncAdminWorkOrderTemplateCostVersions } from '@/services/admin'
|
||||
import { fetchAdminCostVersions } from '@/services/admin'
|
||||
import type { CostVersion } from '@/types/admin'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
@@ -22,14 +22,12 @@ import { useCostCenter } from './CostCenterContext'
|
||||
import { formatRate } from './shared'
|
||||
|
||||
export function WorkOrderPricePanel() {
|
||||
const { message } = App.useApp()
|
||||
const { canConfig, refreshAll } = useCostCenter()
|
||||
const { canConfig } = useCostCenter()
|
||||
|
||||
const [keyword, setKeyword] = useState('')
|
||||
const [status, setStatus] = useState('active')
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
|
||||
const versionsQuery = useQuery({
|
||||
queryKey: ['admin-cost-center-cost-versions', 'work_order', keyword, status, page, pageSize],
|
||||
@@ -46,22 +44,6 @@ export function WorkOrderPricePanel() {
|
||||
const versions = versionsQuery.data?.data.items || []
|
||||
const total = versionsQuery.data?.data.pagination.total || 0
|
||||
|
||||
async function syncTemplateCosts() {
|
||||
setSyncing(true)
|
||||
try {
|
||||
const response = await syncAdminWorkOrderTemplateCostVersions()
|
||||
const result = response.data
|
||||
message.success(
|
||||
`已同步 ${result.activeVersionCount} 条模板发单金额(新建 ${result.createdCount},停用 ${result.retiredCount}),关联 ${result.linkedSnapshotCount} 条成本快照`,
|
||||
)
|
||||
refreshAll()
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '同步接单模板发单金额失败')
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<CostVersion> = [
|
||||
{
|
||||
title: '接单规则 / 模板',
|
||||
@@ -176,24 +158,14 @@ export function WorkOrderPricePanel() {
|
||||
<Tag color="blue">{total} 条</Tag>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
canConfig ? (
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={syncing}
|
||||
onClick={() => void syncTemplateCosts()}
|
||||
>
|
||||
同步模板发单金额
|
||||
</Button>
|
||||
) : null
|
||||
}
|
||||
extra={canConfig ? <Tag color="green">自动同步</Tag> : null}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="接单平台的成本只看这张表;改价请改接单模板,再点「同步模板发单金额」"
|
||||
description="人工客服的成本来自「人工 SKU 配置」里的消耗积分,自动发货的成本在「自动发货 SKU」里维护,二者都不在这里配置;系统内置成本项只有人工履约、自动发货采购、接单平台报酬 3 条。"
|
||||
message="接单平台成本的唯一写入口是【标准商品 → 配置成本】"
|
||||
description="改价保存后会自动更新成本版本、接单模板运行价格和本表;未领取的大厅工单同步新价,已领取工单保持原价。人工客服和自动发货成本分别在各自 SKU 配置中维护。"
|
||||
/>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
fetchAdminWorkProductRules,
|
||||
reprocessAdminKuaishouWorkOrderMatches,
|
||||
saveAdminWorkProductRule,
|
||||
syncAdminWorkProductRulePrice,
|
||||
} from '@/services/admin'
|
||||
import type {
|
||||
CollectField,
|
||||
@@ -73,7 +71,6 @@ export default function ProductRulesPanel() {
|
||||
const hasInitializedRuleSelection = useRef(false)
|
||||
|
||||
const [editingRule, setEditingRule] = useState<WorkProductRule | null>(null)
|
||||
const [syncingPrice, setSyncingPrice] = useState(false)
|
||||
const [exportingRules, setExportingRules] = useState(false)
|
||||
const [activeMappingId, setActiveMappingId] = useState<number | 'new' | null>(null)
|
||||
const [sourcePrefill, setSourcePrefill] = useState<SourcePrefill | null>(null)
|
||||
@@ -296,9 +293,6 @@ export default function ProductRulesPanel() {
|
||||
itemTitleMatchType: rule.match.itemTitleMatchType,
|
||||
skuNickMatchType: rule.match.skuNickMatchType,
|
||||
categoryId: rule.categoryId || undefined,
|
||||
pricingMode: rule.unitPriceFen > 0 ? 'unit' : 'fixed',
|
||||
rewardAmount: rule.rewardAmount / 100,
|
||||
unitPrice: rule.unitPriceFen / 100,
|
||||
sharingEnabled: rule.sharing?.enabled || false,
|
||||
sharingAutoFromOrder: rule.sharing?.autoFromOrder || false,
|
||||
sharingTotalQuantity: hasManualSharingOverride ? rule.sharing?.totalQuantity : undefined,
|
||||
@@ -342,9 +336,6 @@ export default function ProductRulesPanel() {
|
||||
itemTitleMatchType?: 'contains' | 'exact'
|
||||
skuNickMatchType?: 'contains' | 'exact'
|
||||
categoryId?: number
|
||||
rewardAmount?: number
|
||||
pricingMode?: string
|
||||
unitPrice?: number
|
||||
sharingEnabled?: boolean
|
||||
sharingAutoFromOrder?: boolean
|
||||
sharingTotalQuantity?: number
|
||||
@@ -361,7 +352,6 @@ export default function ProductRulesPanel() {
|
||||
sortOrder?: number
|
||||
}) {
|
||||
try {
|
||||
const unitMode = values.pricingMode === 'unit'
|
||||
const isFriendGift = values.acceptanceMode === 'friend_gift'
|
||||
if (isFriendGift && values.sharingEnabled === true) {
|
||||
message.error('好友赠送验收模式不支持拼单,请先关闭拼单')
|
||||
@@ -372,12 +362,11 @@ export default function ProductRulesPanel() {
|
||||
? Number(values.sharingTotalQuantity || 0) > 0 ||
|
||||
Number(values.sharingUnitReward || 0) > 0
|
||||
: hasManualSharingInput(values.sharingTotalQuantity, values.sharingUnitReward)
|
||||
const { pricingMode: _pricingMode, giftCooldownUnit, giftCooldownValue, ...payload } = values
|
||||
const { giftCooldownUnit, giftCooldownValue, ...payload } = values
|
||||
const res = await saveAdminWorkProductRule({
|
||||
...payload,
|
||||
ruleId: editingRule?.ruleId,
|
||||
ruleKey: editingRule?.ruleKey || '',
|
||||
unitPrice: unitMode ? Number(values.unitPrice || 0) : 0,
|
||||
acceptanceMode: isFriendGift ? 'friend_gift' : 'standard',
|
||||
giftCooldownHours:
|
||||
isFriendGift && giftCooldownUnit !== 'minutes' ? Number(giftCooldownValue || 0) : 0,
|
||||
@@ -385,10 +374,18 @@ export default function ProductRulesPanel() {
|
||||
isFriendGift && giftCooldownUnit === 'minutes' ? Number(giftCooldownValue || 0) : 0,
|
||||
sharingEnabled: isFriendGift ? false : values.sharingEnabled === true,
|
||||
sharingAutoFromOrder:
|
||||
unitMode && values.sharingEnabled === true && !hasManualSharingOverride,
|
||||
editingRule?.unitPriceFen !== undefined &&
|
||||
editingRule.unitPriceFen > 0 &&
|
||||
editingRule.rewardAmount === 0 &&
|
||||
values.sharingEnabled === true &&
|
||||
!hasManualSharingOverride,
|
||||
})
|
||||
message.success(editingRule ? '接单模板已更新' : '接单模板已创建')
|
||||
const savedRule = res.data?.rule
|
||||
if (res.data?.costConfigurationRequired) {
|
||||
message.warning('模板已保存为停用。请到【商品与成本中心】配置成本后再启用')
|
||||
} else {
|
||||
message.success(editingRule ? '接单模板已更新' : '接单模板已创建')
|
||||
}
|
||||
if (savedRule) {
|
||||
editRule(savedRule)
|
||||
} else {
|
||||
@@ -401,42 +398,15 @@ export default function ProductRulesPanel() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-summary'],
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
predicate: (query) => String(query.queryKey[0] || '').startsWith('admin-cost-center'),
|
||||
}),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '保存失败')
|
||||
}
|
||||
}
|
||||
|
||||
function confirmSyncRulePrice() {
|
||||
if (!editingRule) return
|
||||
Modal.confirm({
|
||||
title: '同步模板价格',
|
||||
content:
|
||||
'将同步接单大厅中该模板的待抢单价格。部分拼单仅调整剩余份额价格,已加入份额报酬保持不变。',
|
||||
okText: '确认同步',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
setSyncingPrice(true)
|
||||
try {
|
||||
const response = await syncAdminWorkProductRulePrice(editingRule.ruleId)
|
||||
const result = response.data
|
||||
message.success(
|
||||
`已同步 ${result.updatedCount} 单:待抢单 ${result.updatedNormalCount} 单,部分拼单 ${result.updatedPartialSharingCount} 单`,
|
||||
)
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-rules'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-orders'] }),
|
||||
])
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '同步价格失败')
|
||||
throw error
|
||||
} finally {
|
||||
setSyncingPrice(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function toggleRuleEnabled(rule: WorkProductRule, enabled: boolean) {
|
||||
try {
|
||||
const hasManualSharingOverride = hasManualSharingInput(
|
||||
@@ -450,8 +420,6 @@ export default function ProductRulesPanel() {
|
||||
platform: rule.platform,
|
||||
productName: rule.productName,
|
||||
categoryId: rule.categoryId || undefined,
|
||||
rewardAmount: rule.rewardAmount / 100,
|
||||
unitPrice: rule.unitPriceFen / 100,
|
||||
sharingEnabled: rule.sharing?.enabled || false,
|
||||
sharingAutoFromOrder:
|
||||
rule.unitPriceFen > 0 && rule.sharing?.enabled === true && !hasManualSharingOverride,
|
||||
@@ -473,6 +441,9 @@ export default function ProductRulesPanel() {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['admin-worker-platform-product-rules'],
|
||||
})
|
||||
await queryClient.invalidateQueries({
|
||||
predicate: (query) => String(query.queryKey[0] || '').startsWith('admin-cost-center'),
|
||||
})
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '修改状态失败')
|
||||
}
|
||||
@@ -787,14 +758,22 @@ export default function ProductRulesPanel() {
|
||||
width: '20%',
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Text strong style={{ color: '#3f8600', fontSize: 12 }}>
|
||||
{row.sharing?.autoFromOrder ? '随订单计算' : formatMoney(row.rewardAmount)}
|
||||
</Typography.Text>
|
||||
{row.unitPriceFen > 0 ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
按件: {formatMoney(row.unitPriceFen)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{row.rewardAmount > 0 || row.unitPriceFen > 0 ? (
|
||||
<>
|
||||
<Typography.Text strong style={{ color: '#3f8600', fontSize: 12 }}>
|
||||
{row.sharing?.autoFromOrder ? '随订单计算' : formatMoney(row.rewardAmount)}
|
||||
</Typography.Text>
|
||||
{row.unitPriceFen > 0 ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 11 }}>
|
||||
按件: {formatMoney(row.unitPriceFen)}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Tag color="orange" style={{ width: 'fit-content', margin: 0 }}>
|
||||
待成本配置
|
||||
</Tag>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -891,11 +870,6 @@ export default function ProductRulesPanel() {
|
||||
+ 新建模板
|
||||
</Button>
|
||||
) : null}
|
||||
{editingRule ? (
|
||||
<Button loading={syncingPrice} size="middle" onClick={confirmSyncRulePrice}>
|
||||
同步价格
|
||||
</Button>
|
||||
) : null}
|
||||
<Button type="primary" size="middle" htmlType="submit" form="product-rule-form">
|
||||
{editingRule ? '保存模板' : '创建模板'}
|
||||
</Button>
|
||||
@@ -913,9 +887,7 @@ export default function ProductRulesPanel() {
|
||||
matchType: 'contains',
|
||||
itemTitleMatchType: 'exact',
|
||||
skuNickMatchType: 'exact',
|
||||
pricingMode: 'fixed',
|
||||
rewardAmount: 25,
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
autoCreate: true,
|
||||
sharingEnabled: false,
|
||||
acceptanceMode: 'standard',
|
||||
@@ -926,23 +898,21 @@ export default function ProductRulesPanel() {
|
||||
}}
|
||||
onFinish={saveRule}
|
||||
>
|
||||
{editingRule ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="全系统标准价格已统一收敛至【商品与成本中心】"
|
||||
description={
|
||||
<span>
|
||||
接单单价由商品与成本中心维护并提供同步发单功能。在此处价格为只读展示;如需改价或同步工单价格,请前往
|
||||
<a href="#/admin/cost-center" style={{ marginLeft: 4, fontWeight: 500 }}>
|
||||
【商品与成本中心】
|
||||
</a>
|
||||
进行修改。
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="接单模板只维护匹配和履约规则"
|
||||
description={
|
||||
<span>
|
||||
固定金额、按件单价和价格同步统一由
|
||||
<a href="#/admin/cost-center" style={{ margin: '0 4px', fontWeight: 500 }}>
|
||||
商品与成本中心
|
||||
</a>
|
||||
管理。新模板默认停用,配置成本并启用模板后自动出现在商品目录。
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 基础信息 */}
|
||||
<div className="form-grid-3col">
|
||||
@@ -965,55 +935,8 @@ export default function ProductRulesPanel() {
|
||||
</Form.Item>
|
||||
</div>
|
||||
|
||||
{/* Row 2: 计价方式 + 单价/金额 + 任务时限 */}
|
||||
{/* Row 2: 任务时限 */}
|
||||
<div className="form-grid-3col">
|
||||
<Form.Item label="计价方式" name="pricingMode">
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
className="full-width"
|
||||
options={[
|
||||
{ value: 'fixed', label: '固定金额' },
|
||||
{ value: 'unit', label: '按件计价' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) =>
|
||||
getFieldValue('pricingMode') === 'unit' ? (
|
||||
<Form.Item
|
||||
label="按件单价"
|
||||
name="unitPrice"
|
||||
rules={[{ required: true, message: '请填写单价' }]}
|
||||
extra={editingRule ? '标准单价由商品与成本中心统一管理' : undefined}
|
||||
>
|
||||
<InputNumber
|
||||
min={0.01}
|
||||
step={1}
|
||||
addonAfter="元"
|
||||
className="full-width"
|
||||
disabled={Boolean(editingRule)}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Form.Item
|
||||
label="接单金额"
|
||||
name="rewardAmount"
|
||||
extra={editingRule ? '标准单价由商品与成本中心统一管理' : undefined}
|
||||
>
|
||||
<InputNumber
|
||||
min={0}
|
||||
step={1}
|
||||
addonAfter="元"
|
||||
className="full-width"
|
||||
disabled={Boolean(editingRule)}
|
||||
/>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="任务时限" name="timeoutMinutes">
|
||||
<InputNumber min={0} step={5} addonAfter="分钟" className="full-width" />
|
||||
</Form.Item>
|
||||
@@ -1124,13 +1047,23 @@ export default function ProductRulesPanel() {
|
||||
<Form.Item noStyle shouldUpdate>
|
||||
{({ getFieldValue }) => {
|
||||
const sharingEnabled = getFieldValue('sharingEnabled')
|
||||
const unitMode = getFieldValue('pricingMode') === 'unit'
|
||||
const unitMode =
|
||||
Number(editingRule?.unitPriceFen || 0) > 0 &&
|
||||
Number(editingRule?.rewardAmount || 0) === 0
|
||||
const friendGiftMode = getFieldValue('acceptanceMode') === 'friend_gift'
|
||||
const hasConfiguredPrice =
|
||||
Number(editingRule?.unitPriceFen || 0) > 0 ||
|
||||
Number(editingRule?.rewardAmount || 0) > 0
|
||||
|
||||
return (
|
||||
<div className="switches-inline-group span-2">
|
||||
<Form.Item label="启用模板" name="enabled" valuePropName="checked">
|
||||
<Switch size="small" />
|
||||
<Form.Item
|
||||
label="启用模板"
|
||||
name="enabled"
|
||||
valuePropName="checked"
|
||||
tooltip={!hasConfiguredPrice ? '请先在商品与成本中心配置成本' : undefined}
|
||||
>
|
||||
<Switch size="small" disabled={!hasConfiguredPrice} />
|
||||
</Form.Item>
|
||||
<Form.Item label="自动建单" name="autoCreate" valuePropName="checked">
|
||||
<Switch size="small" />
|
||||
@@ -1139,9 +1072,15 @@ export default function ProductRulesPanel() {
|
||||
label="启用拼单"
|
||||
name="sharingEnabled"
|
||||
valuePropName="checked"
|
||||
tooltip={friendGiftMode ? '好友赠送模式不支持拼单' : undefined}
|
||||
tooltip={
|
||||
friendGiftMode
|
||||
? '好友赠送模式不支持拼单'
|
||||
: !hasConfiguredPrice
|
||||
? '请先在商品与成本中心配置成本'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Switch size="small" disabled={friendGiftMode} />
|
||||
<Switch size="small" disabled={friendGiftMode || !hasConfiguredPrice} />
|
||||
</Form.Item>
|
||||
|
||||
{sharingEnabled ? (
|
||||
|
||||
@@ -64,17 +64,6 @@ export function applyAdminTemplateProductBootstrap(payload?: {
|
||||
)
|
||||
}
|
||||
|
||||
export function syncAdminWorkOrderTemplateCostVersions() {
|
||||
return apiPost<{
|
||||
ruleCount: number
|
||||
unitPriceRuleCount: number
|
||||
activeVersionCount: number
|
||||
createdCount: number
|
||||
retiredCount: number
|
||||
linkedSnapshotCount: number
|
||||
}>('/api/v1/admin/cost-center/work-order-template-costs/sync')
|
||||
}
|
||||
|
||||
export function backfillAdminWorkOrderProductSkus(payload?: {
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
@@ -493,6 +482,7 @@ export function setAdminProductPrice(
|
||||
productId: number,
|
||||
payload: {
|
||||
amountFen: number
|
||||
pricingMode?: 'fixed' | 'unit'
|
||||
effectiveFrom?: string
|
||||
changeReason?: string
|
||||
syncOpenOrders?: boolean
|
||||
@@ -503,6 +493,7 @@ export function setAdminProductPrice(
|
||||
costVersionId: number
|
||||
amountFen: number
|
||||
effectiveFrom: string
|
||||
pricingMode?: 'fixed' | 'unit'
|
||||
syncedOrders?: {
|
||||
candidateCount: number
|
||||
updatedCount: number
|
||||
|
||||
@@ -290,8 +290,6 @@ export function saveAdminWorkProductRule(payload: {
|
||||
categoryId?: number
|
||||
enabled?: boolean
|
||||
autoCreate?: boolean
|
||||
rewardAmount?: number
|
||||
unitPrice?: number
|
||||
sharingEnabled?: boolean
|
||||
sharingAutoFromOrder?: boolean
|
||||
sharingTotalQuantity?: number
|
||||
@@ -305,7 +303,12 @@ export function saveAdminWorkProductRule(payload: {
|
||||
fieldsText?: string
|
||||
sortOrder?: number
|
||||
}) {
|
||||
return apiPost<{ rule: WorkProductRule }>('/api/v1/admin/worker-platform/product-rules', payload)
|
||||
return apiPost<{
|
||||
rule: WorkProductRule
|
||||
productId?: number
|
||||
productStatus?: 'active' | 'archived'
|
||||
costConfigurationRequired?: boolean
|
||||
}>('/api/v1/admin/worker-platform/product-rules', payload)
|
||||
}
|
||||
|
||||
export function syncAdminWorkProductRulePrice(ruleId: number) {
|
||||
|
||||
@@ -51,6 +51,9 @@ export interface ProductCatalogItem {
|
||||
costVersionCount: number
|
||||
activeCostAmountFen?: number | null
|
||||
activeCostEffectiveFrom?: string | null
|
||||
templatePricingMode?: 'fixed' | 'unit' | null
|
||||
templateEnabled?: boolean | null
|
||||
templateSharingEnabled?: boolean | null
|
||||
/** 自动发货履约任务数,实时取自 fulfillment_tasks。 */
|
||||
affiliateTaskCount: number
|
||||
affiliateLastFulfilledAt: string | null
|
||||
@@ -231,9 +234,6 @@ export interface TemplateProductBootstrapResult {
|
||||
resetOrderItemCount: number
|
||||
mappingConflictCount: number
|
||||
mappingConflictItemCount: number
|
||||
templateCostVersionCount: number
|
||||
templateCostCreatedCount: number
|
||||
templateCostRetiredCount: number
|
||||
}
|
||||
|
||||
export interface CostLine {
|
||||
|
||||
Reference in New Issue
Block a user