feat(fulfillment): 增加统一结算与多渠道人工履约

This commit is contained in:
yml2213
2026-11-05 16:14:56 +08:00
parent 548070c2ad
commit 4e83dbb84c
43 changed files with 1582 additions and 334 deletions
@@ -68,6 +68,7 @@ const CHANNEL_OPTIONS = [
{ value: 'affiliate_dash', label: '自动发货' },
{ value: 'third_party_voucher', label: '第三方卡券' },
{ value: 'manual', label: '人工客服' },
{ value: 'on_account', label: '统一结算' },
]
const CHANNEL_COLOR: Record<string, string> = {
@@ -75,6 +76,7 @@ const CHANNEL_COLOR: Record<string, string> = {
affiliate_dash: 'green',
third_party_voucher: 'purple',
manual: 'orange',
on_account: 'gold',
}
const CHANNEL_SOURCE_LABEL: Record<string, string> = {
@@ -133,6 +135,7 @@ function PendingPanel() {
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
const [selections, setSelections] = useState<Record<string, string>>({})
const [internalSkuDrafts, setInternalSkuDrafts] = useState<Record<string, string>>({})
const query = useQuery({
queryKey: ['fulfillment-pending', keyword, page, pageSize],
@@ -142,16 +145,26 @@ function PendingPanel() {
const assignMutation = useMutation({
mutationFn: (row: PendingChannelGroup) =>
assignChannelByInternalSku({
internalSku: row.internal_sku,
groupKey: row.group_key,
internalSku: row.internal_sku || '',
mappedInternalSku: row.internal_sku
? ''
: String(internalSkuDrafts[rowKey(row)] || '').trim(),
channel: selections[rowKey(row)] || '',
}),
onSuccess: (result) => {
const productMessage = result.data.draftProductId ? '和标准商品' : ''
if (result.data.carrierFailed > 0) {
const firstFailure = result.data.carrierFailures?.[0]
message.warning(
`已指定 ${result.data.updated} 个订单项,${result.data.carrierFailed} 个因载体创建失败仍待处理`,
`渠道${productMessage}已保存,${result.data.carrierFailed} 个订单项仍待补配置${
firstFailure?.message ? `:${firstFailure.message}` : ''
}`,
)
} else {
message.success(`已指定 ${result.data.updated} 个订单项`)
message.success(
`已指定 ${result.data.updated} 个订单项${productMessage ? `并创建${productMessage.slice(1)}` : ''}`,
)
}
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
},
@@ -183,7 +196,7 @@ function PendingPanel() {
const retryCarrierMutation = useMutation({
mutationFn: (row: PendingChannelGroup) =>
retryPendingCarriers({ internalSku: row.internal_sku }),
retryPendingCarriers({ internalSku: row.internal_sku || '' }),
onSuccess: (result) => {
const data = result.data
const detail = [
@@ -205,11 +218,11 @@ function PendingPanel() {
const deleteMutation = useMutation({
mutationFn: (row: PendingChannelGroup) =>
deletePendingChannelGroup({ internalSku: row.internal_sku }),
deletePendingChannelGroup({ groupKey: row.group_key }),
onSuccess: (result) => {
const data = result.data
message.success(
`已删除 ${data.archivedItems} 个待分类订单项,原始订单保留;财务事实已重算 ${data.refreshedDays} 天`,
`已忽略归档 ${data.archivedItems} 个待分类订单项,原始订单保留;财务事实已重算 ${data.refreshedDays} 天`,
)
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
void queryClient.invalidateQueries({ queryKey: ['fulfillment-exceptions'] })
@@ -261,9 +274,23 @@ function PendingPanel() {
title: '内部SKU',
dataIndex: 'internal_sku',
width: 220,
render: (value: string, row: PendingChannelGroup) => (
<Space direction="vertical" size={2}>
<Typography.Text delete={!value}>{value || '未识别内部 SKU'}</Typography.Text>
render: (value: string | null, row: PendingChannelGroup) => (
<Space direction="vertical" size={4}>
{value ? (
<Typography.Text>{value}</Typography.Text>
) : (
<Input
size="small"
placeholder="填写内部 SKU"
value={internalSkuDrafts[rowKey(row)] || ''}
onChange={(event) =>
setInternalSkuDrafts((current) => ({
...current,
[rowKey(row)]: event.target.value,
}))
}
/>
)}
{row.configured_channels?.length ? (
<Space size={2} wrap>
{row.configured_channels.map((channel) => (
@@ -345,7 +372,9 @@ function PendingPanel() {
<Select
size="small"
style={{ width: 180 }}
placeholder="选择渠道"
placeholder={
row.internal_sku || internalSkuDrafts[rowKey(row)] ? '选择渠道' : '先补内部 SKU'
}
options={CHANNEL_OPTIONS}
value={selections[rowKey(row)]}
onChange={(value) => setSelections((prev) => ({ ...prev, [rowKey(row)]: value }))}
@@ -353,11 +382,14 @@ function PendingPanel() {
<Button
size="small"
type="primary"
disabled={!selections[rowKey(row)]}
disabled={
(!row.internal_sku && !String(internalSkuDrafts[rowKey(row)] || '').trim()) ||
!selections[rowKey(row)]
}
loading={assignMutation.isPending}
onClick={() => assignMutation.mutate(row)}
>
指定
保存渠道
</Button>
{row.configured_channels?.length ? (
<Tooltip title="重新确认渠道并补建工单或自动发货任务">
@@ -371,26 +403,26 @@ function PendingPanel() {
</Button>
</Tooltip>
) : null}
<Tooltip title="删除待分类记录,不删除原始订单">
<Tooltip title="从待办中忽略并归档,不删除原始订单">
<Button
danger
icon={<DeleteOutlined />}
loading={deleteMutation.isPending}
onClick={() =>
Modal.confirm({
title: '删除待分类记录',
title: '忽略并归档待分类记录',
content: (
<div>
<p>
将归档 <strong>{row.item_count}</strong> 个订单项,并清除已生成的成本快照、
分类异常,重算对应销售事实与总账。
将从待办中归档 <strong>{row.item_count}</strong> 个订单项,并清除已生成的
派生成本快照、分类异常;可重建的财务事实会按开放期间重算。
</p>
<p style={{ marginBottom: 0 }}>
原始订单和订单商品不会被删除;已关账期间禁止删除。
原始订单和订单商品不会删除;已关账期间只跳过重算,不再阻止归档。
</p>
</div>
),
okText: '确认删除',
okText: '确认归档',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: () => deleteMutation.mutateAsync(row),
@@ -441,7 +473,7 @@ function PendingPanel() {
type="warning"
showIcon
style={{ marginBottom: 12 }}
message="删除只会归档对应订单项并清理财务成本记录,不会删除原始订单。配置错误优先修正模板/映射后重试匹配;确认无需履约或核算时,可直接删除。"
message="指定渠道会先保存渠道和草稿标准商品;载体或成本未配置时会继续留在队列中。忽略归档只影响派生流程,不删除原始订单。"
/>
<PercentTable
rowKey={(row) => rowKey(row)}
@@ -1196,7 +1228,7 @@ function PipelinesPanel() {
}
function rowKey(row: PendingChannelGroup) {
return row.internal_sku
return row.group_key
}
function formatCarrierParams(row: ChannelRule) {
@@ -40,6 +40,7 @@ const FULFILLMENT_MODE_OPTIONS = [
{ value: 'affiliate_dash', label: '自动发货' },
{ value: 'third_party_voucher', label: '第三方卡券' },
{ value: 'manual', label: '人工客服' },
{ value: 'on_account', label: '统一结算' },
{ value: 'unclassified', label: '未分类' },
{ value: 'archived', label: '历史归档' },
]
@@ -49,6 +50,7 @@ const FULFILLMENT_MODE_LABEL: Record<string, string> = {
affiliate_dash: '自动发货',
third_party_voucher: '第三方卡券',
manual: '人工客服',
on_account: '统一结算',
archived: '历史归档',
unclassified: '未分类',
}
@@ -13,6 +13,8 @@
import {
DeleteOutlined,
EditOutlined,
MinusCircleOutlined,
PlusOutlined,
ReloadOutlined,
ScanOutlined,
SyncOutlined,
@@ -50,14 +52,21 @@ import {
saveAdminManualConfig,
scanAdminManualSkus,
} from '@/services/admin'
import type { ManualSkuItem } from '@/types/admin'
import type { ManualPlatformKey, ManualSkuItem } from '@/types/admin'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
import {
formatCreditQuantity,
formatCreditUnitPriceLabel,
formatCreditUnitPriceYuan,
} from '@/utils/credit-unit-price'
import dayjs from '@/lib/dayjs'
import type { Dayjs } from 'dayjs'
type ConfigFormValues = {
platformKey: string
creditQuantity: number
channels: Array<{
platformKey: string
creditQuantity: number
}>
note?: string
}
@@ -66,13 +75,21 @@ const PLATFORM_TAG_COLORS: Record<string, string> = {
lewan: 'purple',
rock: 'cyan',
fund_cash: 'orange',
on_account: 'gold',
}
/** 按换算率把「消耗单位」翻译成人话。 */
function unitLabel(platformKey: string, creditUnitPriceFen: number) {
if (platformKey === 'fund_cash') return '元'
if (creditUnitPriceFen >= 100) return '积分(1积分=1元)'
return '积分(1积分=1分)'
function isCashChannel(platformKey: string) {
return platformKey === 'fund_cash' || platformKey === 'on_account'
}
function channelQuantityText(platformKey: string, creditQuantity: number) {
const quantity = formatCreditQuantity(creditQuantity)
return isCashChannel(platformKey) ? `¥${quantity}` : `${quantity} 积分`
}
function platformOptionLabel(platformKey: string, label: string, creditUnitPriceFen: number) {
if (isCashChannel(platformKey)) return `${label} · 按元填写`
return `${label} · ${formatCreditUnitPriceLabel(creditUnitPriceFen)}`
}
export function ManualSkuConfigPanel() {
@@ -139,8 +156,10 @@ export function ManualSkuConfigPanel() {
mutationFn: (values: ConfigFormValues) =>
saveAdminManualConfig({
skuKey: editing?.skuKey || '',
platformKey: values.platformKey,
creditQuantity: values.creditQuantity,
channels: values.channels.map((channel) => ({
platformKey: channel.platformKey,
creditQuantity: Number(channel.creditQuantity || 0),
})),
note: values.note || '',
}),
onSuccess: (response) => {
@@ -191,9 +210,21 @@ export function ManualSkuConfigPanel() {
function openConfig(item: ManualSkuItem) {
setEditing(item)
const configuredChannels = item.config?.channels?.length
? item.config.channels
: item.config
? [
{
platformKey: item.config.platformKey,
creditQuantity: item.config.creditQuantity,
},
]
: [{ platformKey: '', creditQuantity: 0 }]
form.setFieldsValue({
platformKey: item.config?.platformKey || undefined,
creditQuantity: item.config?.creditQuantity ?? undefined,
channels: configuredChannels.map((channel) => ({
platformKey: channel.platformKey,
creditQuantity: channel.creditQuantity,
})),
note: item.config?.note || '',
})
setModalOpen(true)
@@ -229,20 +260,37 @@ export function ManualSkuConfigPanel() {
title: '履约渠道',
key: 'platform',
width: 150,
render: (_, row) =>
row.config ? (
render: (_, row) => {
if (!row.config) {
return <Typography.Text type="secondary">未配置</Typography.Text>
}
const channels = row.config.channels?.length
? row.config.channels
: [
{
platformKey: row.config.platformKey,
platformLabel: row.config.platformLabel,
creditQuantity: row.config.creditQuantity,
creditUnitPriceFen: row.config.creditUnitPriceFen,
costFen: row.config.costFen,
},
]
return (
<Space size={4} direction="vertical">
<Tag color={PLATFORM_TAG_COLORS[row.config.platformKey] || 'default'}>
{row.config.platformLabel || row.config.platformKey}
</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.config.creditQuantity}{' '}
{unitLabel(row.config.platformKey, row.config.creditUnitPriceFen)}
</Typography.Text>
{channels.map((channel) => (
<div key={`${row.id}-${channel.platformKey}`}>
<Tag color={PLATFORM_TAG_COLORS[channel.platformKey] || 'default'}>
{channel.platformLabel}
</Tag>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{channelQuantityText(channel.platformKey, channel.creditQuantity)}
{' · '}¥{((channel.costFen || 0) / 100).toFixed(2)}
</Typography.Text>
</div>
))}
</Space>
) : (
<Typography.Text type="secondary">未配置</Typography.Text>
),
)
},
},
{
title: '成本',
@@ -251,7 +299,9 @@ export function ManualSkuConfigPanel() {
align: 'right',
render: (_, row) =>
row.config ? (
row.config.costMode === 'fixed' ? (
(row.config.channels?.length || 0) > 1 ? (
<Tag color="blue">按实际渠道</Tag>
) : row.config.costMode === 'fixed' ? (
<Typography.Text>¥{(row.config.costFen / 100).toFixed(2)}</Typography.Text>
) : (
<Tag color="orange">不固定·客服填</Tag>
@@ -286,11 +336,10 @@ export function ManualSkuConfigPanel() {
},
]
const selectedPlatform = form.getFieldValue('platformKey')
const selectedCredit = form.getFieldValue('creditQuantity')
const previewUnitPrice = platformMap.get(selectedPlatform)?.creditUnitPriceFen ?? 0
const previewCostFen =
selectedPlatform && Number(selectedCredit) > 0 ? Number(selectedCredit) * previewUnitPrice : 0
function previewChannelCost(platformKey: string, creditQuantity: number) {
const unitPrice = platformMap.get(platformKey as ManualPlatformKey)?.creditUnitPriceFen ?? 0
return Number(creditQuantity) > 0 ? Math.round(Number(creditQuantity) * unitPrice) : 0
}
return (
<Card bordered={false} title="人工 SKU 配置">
@@ -426,38 +475,138 @@ export function ManualSkuConfigPanel() {
showIcon
style={{ marginBottom: 14 }}
message={`商品名称:${editing?.displayName || '(无)'}(仅供识别)`}
description="保存后自动同步标准商品、统一渠道规则和订单匹配;不需要再去其它页面重复设置。消耗积分填 0 表示成本不固定,客服会在确认履约时手填实际成本。"
description="一个 SKU 可以配置多个渠道,客服确认履约时选择本次实际渠道。积分渠道填整数积分;备用金现金和统一结算填金额,最多两位小数。填 0 表示成本不固定,由客服确认时填写。"
/>
<Form<ConfigFormValues> form={form} layout="vertical">
<Form.Item
name="platformKey"
label="履约渠道"
rules={[{ required: true, message: '请选择履约渠道' }]}
<Form.List
name="channels"
rules={[
{
validator: async (_, channels) => {
if (!Array.isArray(channels) || channels.length === 0) {
throw new Error('请至少配置一个履约渠道')
}
},
},
]}
>
<Select
placeholder="请选择渠道"
options={platforms.map((item) => ({
value: item.key,
label: `${item.label}(${unitLabel(item.key, item.creditUnitPriceFen)})`,
}))}
onChange={() => form.validateFields(['creditQuantity'])}
/>
</Form.Item>
<Form.Item
name="creditQuantity"
label="每单消耗积分"
rules={[{ required: true, message: '请输入消耗积分' }]}
extra="填 0 表示成本不固定,客服确认履约时手填实际成本。"
>
<InputNumber min={0} precision={0} style={{ width: '100%' }} placeholder="例如 10" />
</Form.Item>
<Form.Item label="折算成本">
<Typography.Text strong>
{previewCostFen > 0
? `¥${(previewCostFen / 100).toFixed(2)}(${previewCostFen} 分)`
: '成本不固定,由客服手填'}
</Typography.Text>
</Form.Item>
{(fields, { add, remove }, { errors }) => (
<Space direction="vertical" size={12} style={{ width: '100%' }}>
{fields.map((field, index) => (
<div key={field.key} className="manual-channel-config-item">
<div className="manual-channel-config-head">
<Typography.Text strong>履约渠道 {index + 1}</Typography.Text>
<Button
type="text"
danger
title="移除此渠道"
aria-label={`移除履约渠道 ${index + 1}`}
icon={<MinusCircleOutlined />}
disabled={fields.length <= 1}
onClick={() => remove(field.name)}
/>
</div>
<Form.Item
name={[field.name, 'platformKey']}
label="履约渠道"
rules={[{ required: true, message: '请选择履约渠道' }]}
>
<Select
placeholder="请选择履约渠道"
options={platforms.map((item) => ({
value: item.key,
label: platformOptionLabel(item.key, item.label, item.creditUnitPriceFen),
}))}
/>
</Form.Item>
<Form.Item noStyle shouldUpdate>
{() => {
const platformKey = String(
form.getFieldValue(['channels', field.name, 'platformKey']) || '',
)
const creditQuantity = Number(
form.getFieldValue(['channels', field.name, 'creditQuantity']) || 0,
)
const cashChannel = isCashChannel(platformKey)
const unitPriceFen =
platformMap.get(platformKey as ManualPlatformKey)?.creditUnitPriceFen ?? 0
const costFen = previewChannelCost(platformKey, creditQuantity)
return (
<Row gutter={12}>
<Col xs={24} sm={12}>
<Form.Item
name={[field.name, 'creditQuantity']}
label={
!platformKey
? '消耗 / 金额'
: cashChannel
? '每单金额'
: '每单消耗积分'
}
rules={[
{ required: true, message: '请输入消耗或金额' },
{
validator: async (_, value) => {
const quantity = Number(value)
if (!Number.isFinite(quantity) || quantity < 0) {
throw new Error('请输入大于等于 0 的数字')
}
if (!cashChannel && !Number.isInteger(quantity)) {
throw new Error('积分必须是整数')
}
},
},
]}
extra={
platformKey
? cashChannel
? '单位:元,最多两位小数'
: `换算率:${formatCreditUnitPriceLabel(unitPriceFen)}`
: '积分填整数,现金和统一结算填金额'
}
style={{ marginBottom: 0 }}
>
<InputNumber
min={0}
precision={cashChannel ? 2 : 0}
step={cashChannel ? 0.01 : 1}
style={{ width: '100%' }}
placeholder={cashChannel ? '例如 12.50' : '例如 10'}
addonAfter={
platformKey ? (cashChannel ? '元' : '积分') : undefined
}
/>
</Form.Item>
</Col>
<Col xs={24} sm={12}>
<Form.Item label="折算成本" style={{ marginBottom: 0 }}>
<div className="manual-channel-cost-preview">
<Typography.Text strong={costFen > 0}>
{costFen > 0
? `¥${(costFen / 100).toFixed(2)}`
: '成本不固定,由客服确认时填写'}
</Typography.Text>
{platformKey && !cashChannel ? (
<Typography.Text type="secondary">
1积分={formatCreditUnitPriceYuan(unitPriceFen)}
</Typography.Text>
) : null}
</div>
</Form.Item>
</Col>
</Row>
)
}}
</Form.Item>
</div>
))}
<Button type="dashed" icon={<PlusOutlined />} onClick={() => add()}>
添加履约渠道
</Button>
<Form.ErrorList errors={errors} />
</Space>
)}
</Form.List>
<Form.Item name="note" label="备注">
<Input.TextArea rows={2} maxLength={300} showCount />
</Form.Item>
@@ -130,6 +130,7 @@ export const FULFILLMENT_MODE_LABELS: Record<string, string> = {
affiliate_dash: '自动发货',
third_party_voucher: '第三方卡券',
manual: '人工客服',
on_account: '统一结算',
archived: '历史归档',
unclassified: '未分类',
}
@@ -139,6 +140,7 @@ export const FULFILLMENT_MODE_COLORS: Record<string, string> = {
affiliate_dash: 'green',
third_party_voucher: 'cyan',
manual: 'orange',
on_account: 'gold',
archived: 'default',
unclassified: 'gold',
}
@@ -46,6 +46,7 @@ import type {
} from '@/types/admin'
import type { UploadedFile } from '@/types/worker-platform'
import { formatAdminDateTime } from '@/utils/admin-time'
import { formatCreditUnitPriceLabel } from '@/utils/credit-unit-price'
import {
FUND_ACCOUNT_STATUS_LABELS,
FUND_CATEGORY_LABELS,
@@ -387,7 +388,7 @@ export default function FundSupportView() {
<section className="page-stack">
<PageHeader
title="我的钱包"
description="四个平台钱包:南航 / 乐玩 / ROCK 积分与备用金现金。余额由流水自动计算,不能手工修改。"
description="五种履约方式:南航 / 乐玩 / ROCK 积分、备用金现金、统一结算。积分余额与现金余额由流水自动计算,不能手工修改。"
extra={
<Button
icon={<ReloadOutlined />}
@@ -418,51 +419,61 @@ export default function FundSupportView() {
/>
) : null}
<Row gutter={[16, 16]}>
<div className="support-wallet-grid">
{myCreditWallets.map((wallet) => (
<Col xs={24} sm={12} lg={6} key={wallet.platform.key}>
<Card>
<Statistic
title={
<Space size={6}>
<span>{wallet.platform.label}</span>
<Tag color={wallet.platform.creditUnitPriceFen >= 100 ? 'cyan' : 'blue'}>
1积分=
{wallet.platform.creditUnitPriceFen >= 100 ? '1元' : '1分'}
</Tag>
</Space>
}
value={wallet.balanceQuantity}
suffix="积分"
valueStyle={{
color: wallet.balanceQuantity > 0 ? undefined : '#999',
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
折合 ¥{(wallet.balanceAmountFen / 100).toFixed(2)}
</Typography.Text>
</Card>
</Col>
))}
<Col xs={24} sm={12} lg={6}>
<Card>
<Card key={wallet.platform.key}>
<Statistic
title="备用金现金"
value={account ? account.balanceFen / 100 : 0}
precision={2}
prefix="¥"
title={
<Space size={6}>
<span>{wallet.platform.label}</span>
<Tag color={wallet.platform.creditUnitPriceFen >= 100 ? 'cyan' : 'blue'}>
{formatCreditUnitPriceLabel(wallet.platform.creditUnitPriceFen)}
</Tag>
</Space>
}
value={wallet.balanceQuantity}
suffix="积分"
valueStyle={{
color: account && account.balanceFen > 0 ? undefined : '#999',
color: wallet.balanceQuantity > 0 ? undefined : '#999',
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{account
? `本月发放 ¥${(account.monthInFen / 100).toFixed(2)} · 支出/缴回 ¥${(account.monthOutFen / 100).toFixed(2)}`
: '尚未开通备用金账户'}
折合 ¥{(wallet.balanceAmountFen / 100).toFixed(2)}
</Typography.Text>
</Card>
</Col>
</Row>
))}
<Card>
<Statistic
title="备用金现金"
value={account ? account.balanceFen / 100 : 0}
precision={2}
prefix="¥"
valueStyle={{
color: account && account.balanceFen > 0 ? undefined : '#999',
}}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{account
? `本月发放 ¥${(account.monthInFen / 100).toFixed(2)} · 支出/缴回 ¥${(account.monthOutFen / 100).toFixed(2)}`
: '尚未开通备用金账户'}
</Typography.Text>
</Card>
<Card>
<Statistic
title={
<Space size={6}>
<span>统一结算</span>
<Tag color="gold">后对账</Tag>
</Space>
}
value="按单生成"
valueStyle={{ color: '#8c6d1f', fontSize: 22 }}
/>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
生成虚拟账单,不扣积分或备用金
</Typography.Text>
</Card>
</div>
{account ? (
<Card>
@@ -42,6 +42,7 @@ import type { PlatformCreditAccount, PlatformCreditEntry, PlatformCreditOwner }
import { hasAdminPermission } from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { buildAdminLocalTablePagination } from '@/utils/admin-pagination'
import { formatCreditUnitPriceLabel } from '@/utils/credit-unit-price'
const PLATFORM_TAG_COLORS: Record<string, string> = {
nanhang: 'blue',
@@ -342,7 +343,7 @@ export function PlatformCreditPanel() {
showIcon
style={{ marginBottom: 14 }}
message="南航 / 乐玩 / ROCK 三平台积分互不通用,每平台一个钱包"
description="换算率按平台:南航、乐玩 1 积分 = 1 分;ROCK 1 积分 = 1 元。可在下方按客服直接分配或调整积分;备用金现金在「现金钱包 / 对账中心」管理。"
description="换算率按平台展示为人民币金额;南航、乐玩默认 1 积分 = 0.01 元,ROCK 默认 1 积分 = 1.00 元。可在下方按客服直接分配或调整积分;备用金现金在「现金钱包 / 对账中心」管理。"
/>
<Row gutter={[12, 12]} style={{ marginBottom: 12 }}>
@@ -359,7 +360,7 @@ export function PlatformCreditPanel() {
suffix="积分"
prefix={
<Tag color={PLATFORM_TAG_COLORS[platform.key] || 'default'}>
1积分={platform.creditUnitPriceFen >= 100 ? '1元' : '1分'}
{formatCreditUnitPriceLabel(platform.creditUnitPriceFen)}
</Tag>
}
/>
@@ -459,7 +460,7 @@ export function PlatformCreditPanel() {
<Select
options={platforms.map((item) => ({
value: item.key,
label: `${item.label}(1积分=${item.creditUnitPriceFen >= 100 ? '1元' : '1分'})`,
label: `${item.label} · ${formatCreditUnitPriceLabel(item.creditUnitPriceFen)}`,
}))}
/>
</Form.Item>
@@ -38,6 +38,7 @@ const CHANNEL_LABEL: Record<VoucherConsumeChannel, string> = {
affiliate_dash: '自动发货',
work_order: '接单工单',
manual: '人工客服',
on_account: '统一结算',
third_party_voucher: '第三方卡券',
}
@@ -68,6 +69,10 @@ const MODE_OPTIONS: Record<
{ value: 'after_order_received', label: '收到订单后' },
{ value: 'manual', label: '人工核销' },
],
on_account: [
{ value: 'after_order_received', label: '收到订单后' },
{ value: 'manual', label: '人工核销' },
],
}
type PolicyRow = {