feat(cost-center): 收敛第三方卡券商品与成本链路
This commit is contained in:
@@ -737,6 +737,18 @@ export function ProductCatalogPanel() {
|
||||
>
|
||||
新增人工 SKU
|
||||
</Button>
|
||||
) : productSourceFilter === 'third_party_voucher' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() =>
|
||||
void navigate(
|
||||
'/admin/cost-center?scope=third_party_voucher&tab=cost&voucherNew=1',
|
||||
)
|
||||
}
|
||||
>
|
||||
新增卡券 SKU
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateProduct}>
|
||||
新建商品
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 商品编码固定为 `tpv:<sku>`;成本写入 `sys_third_party_voucher_cost` 的
|
||||
* 商品级版本,由成本快照管道消费。
|
||||
*/
|
||||
import { DeleteOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -19,13 +19,14 @@ import {
|
||||
message,
|
||||
} from 'antd'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router'
|
||||
|
||||
import PercentTable from '@/components/common/PercentTable'
|
||||
import {
|
||||
deleteAdminVoucherProduct,
|
||||
ensureVoucherChannelRules,
|
||||
fetchAdminProductCatalog,
|
||||
saveAdminVoucherProduct,
|
||||
setAdminProductPrice,
|
||||
} from '@/services/admin'
|
||||
import type { ProductCatalogItem } from '@/types/admin'
|
||||
@@ -35,12 +36,20 @@ import { useCostCenter } from './CostCenterContext'
|
||||
|
||||
export default function VoucherSkuPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [searchParams] = useSearchParams()
|
||||
const { canConfig, refreshAll } = useCostCenter()
|
||||
const handledCreateLinkRef = useRef(false)
|
||||
const voucherNewRequested = searchParams.get('voucherNew') === '1'
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||||
const [selected, setSelected] = useState<ProductCatalogItem | null>(null)
|
||||
const [ensuringRules, setEnsuringRules] = useState(false)
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [form] = Form.useForm<{ amountFen: number; changeReason?: string }>()
|
||||
const [createForm] = Form.useForm<{
|
||||
skuKey: string
|
||||
amountYuan: number
|
||||
note?: string
|
||||
}>()
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['cost-center-voucher-products', page, pageSize],
|
||||
@@ -68,6 +77,25 @@ export default function VoucherSkuPanel() {
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '保存失败'),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (values: { skuKey: string; amountYuan: number; note?: string }) =>
|
||||
saveAdminVoucherProduct({
|
||||
skuKey: values.skuKey.trim(),
|
||||
amountFen: Math.round(values.amountYuan * 100),
|
||||
note: values.note || '',
|
||||
}),
|
||||
onSuccess: (response) => {
|
||||
message.success(
|
||||
`卡券 SKU 已保存并回填 ${response.data.linkedItemCount} 个订单项,补建成本快照 ${response.data.snapshotCreatedCount} 条`,
|
||||
)
|
||||
setCreateOpen(false)
|
||||
createForm.resetFields()
|
||||
void queryClient.invalidateQueries({ queryKey: ['cost-center-voucher-products'] })
|
||||
refreshAll()
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '保存卡券 SKU 失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (productId: number) => deleteAdminVoucherProduct(productId),
|
||||
onSuccess: (response) => {
|
||||
@@ -82,23 +110,15 @@ export default function VoucherSkuPanel() {
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '删除失败'),
|
||||
})
|
||||
|
||||
async function handleEnsureRules() {
|
||||
setEnsuringRules(true)
|
||||
try {
|
||||
const result = await ensureVoucherChannelRules()
|
||||
message.success(
|
||||
`卡券商品 ${result.data.productCount} 个,已补齐渠道规则 ${result.data.created} 条`,
|
||||
)
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '补齐卡券渠道规则失败')
|
||||
} finally {
|
||||
setEnsuringRules(false)
|
||||
}
|
||||
}
|
||||
|
||||
const items = query.data?.data.items || []
|
||||
const total = query.data?.data.pagination.total || 0
|
||||
|
||||
useEffect(() => {
|
||||
if (!canConfig || !voucherNewRequested || handledCreateLinkRef.current) return
|
||||
handledCreateLinkRef.current = true
|
||||
setCreateOpen(true)
|
||||
}, [canConfig, voucherNewRequested])
|
||||
|
||||
const columns = [
|
||||
{ title: '商品', dataIndex: 'canonicalName', ellipsis: true },
|
||||
{ title: '编码', dataIndex: 'canonicalSku', width: 220 },
|
||||
@@ -169,14 +189,11 @@ export default function VoucherSkuPanel() {
|
||||
title={`卡券商品采购价(${total})`}
|
||||
extra={
|
||||
<Space size={8}>
|
||||
<Button
|
||||
icon={<SyncOutlined />}
|
||||
loading={ensuringRules}
|
||||
disabled={!canConfig}
|
||||
onClick={() => void handleEnsureRules()}
|
||||
>
|
||||
批量生成渠道规则
|
||||
</Button>
|
||||
{canConfig ? (
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
||||
新增卡券 SKU
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
@@ -184,8 +201,8 @@ export default function VoucherSkuPanel() {
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
卡券渠道只统计:命中规则后不创建任务、不发领取页,只记收入与采购成本。 商品编码以{' '}
|
||||
<Typography.Text code>tpv:</Typography.Text> 开头。
|
||||
新增时只需填写内部 SKU 和采购价,系统会自动创建 <Typography.Text code>tpv:</Typography.Text>{' '}
|
||||
商品、卡券渠道规则并回填已有订单。卡券不创建任务、不发领取页,只记收入与采购成本。
|
||||
</Typography.Paragraph>
|
||||
<PercentTable
|
||||
rowKey="id"
|
||||
@@ -204,6 +221,50 @@ export default function VoucherSkuPanel() {
|
||||
})}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title="新增第三方卡券 SKU"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={() => createForm.submit()}
|
||||
confirmLoading={createMutation.isPending}
|
||||
destroyOnHidden
|
||||
>
|
||||
<Form
|
||||
form={createForm}
|
||||
layout="vertical"
|
||||
onFinish={(values) => createMutation.mutate(values)}
|
||||
>
|
||||
<Form.Item
|
||||
name="skuKey"
|
||||
label="内部 SKU"
|
||||
rules={[
|
||||
{ required: true, message: '请输入内部 SKU' },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
const text = String(value || '').trim()
|
||||
if (text && !/\p{Script=Han}/u.test(text)) {
|
||||
throw new Error('内部 SKU 必须包含中文')
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra="保存后自动生成 tpv:<内部 SKU> 标准商品和卡券渠道规则。"
|
||||
>
|
||||
<Input placeholder="例如:11月改名卡" maxLength={60} showCount />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="amountYuan"
|
||||
label="采购价(元)"
|
||||
rules={[{ required: true, message: '请输入采购价' }]}
|
||||
>
|
||||
<InputNumber min={0.01} precision={2} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="note" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={300} showCount />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`设置采购价:${selected?.canonicalName || ''}`}
|
||||
open={Boolean(selected)}
|
||||
|
||||
@@ -525,10 +525,21 @@ export function syncAdminProductPrice(productId: number) {
|
||||
}>(`/api/v1/admin/cost-center/products/${productId}/sync-price`, {})
|
||||
}
|
||||
|
||||
export function ensureVoucherChannelRules() {
|
||||
return apiPost<{ created: number; productCount: number }>(
|
||||
'/api/v1/admin/cost-center/voucher-products/ensure-rules',
|
||||
)
|
||||
export function saveAdminVoucherProduct(payload: {
|
||||
skuKey: string
|
||||
amountFen: number
|
||||
effectiveFrom?: string
|
||||
note?: string
|
||||
}) {
|
||||
return apiPost<{
|
||||
product: ProductCatalogItem
|
||||
costVersionId: number
|
||||
amountFen: number
|
||||
effectiveFrom: string
|
||||
linkedItemCount: number
|
||||
snapshotCreatedCount: number
|
||||
refreshedDays: number
|
||||
}>('/api/v1/admin/cost-center/voucher-products', payload)
|
||||
}
|
||||
|
||||
export function deleteAdminVoucherProduct(productId: number) {
|
||||
|
||||
Reference in New Issue
Block a user