344 lines
10 KiB
TypeScript
344 lines
10 KiB
TypeScript
/**
|
||
* 人工履约待办页:客服确认履约。
|
||
*
|
||
* 口径(2026-10-24 确认):
|
||
* * 渠道与消耗积分在成本中心提前配好,客服不选、不填;
|
||
* * 固定成本 → 点「确认履约」零输入;
|
||
* * 成本不固定(消耗积分 = 0)→ 只填本次实际成本;
|
||
* * 确认后自动扣对应平台的积分并写成本快照。
|
||
*/
|
||
import { CheckOutlined, ReloadOutlined } from '@ant-design/icons'
|
||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||
import {
|
||
Alert,
|
||
App,
|
||
Button,
|
||
Card,
|
||
Form,
|
||
Input,
|
||
InputNumber,
|
||
Modal,
|
||
Space,
|
||
Statistic,
|
||
Tag,
|
||
Typography,
|
||
} from 'antd'
|
||
import PercentTable from '@/components/common/PercentTable'
|
||
import type { TableColumnsType } from 'antd'
|
||
import { useState } from 'react'
|
||
|
||
import PageHeader from '@/components/admin/PageHeader'
|
||
import { confirmAdminManualFulfillment, fetchAdminManualFulfillmentPending } from '@/services/admin'
|
||
import type { ManualFulfillmentPendingItem } from '@/types/admin'
|
||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||
|
||
type ConfirmFormValues = {
|
||
costYuan?: number
|
||
payeeName?: string
|
||
note?: string
|
||
}
|
||
|
||
const PLATFORM_TAG_COLORS: Record<string, string> = {
|
||
nanhang: 'blue',
|
||
lewan: 'purple',
|
||
rock: 'cyan',
|
||
fund_cash: 'orange',
|
||
}
|
||
|
||
export default function AdminManualFulfillmentPage() {
|
||
const { message } = App.useApp()
|
||
const queryClient = useQueryClient()
|
||
const [form] = Form.useForm<ConfirmFormValues>()
|
||
|
||
const [status, setStatus] = useState<'pending' | 'fulfilled' | ''>('pending')
|
||
const [keyword, setKeyword] = useState('')
|
||
const [page, setPage] = useState(1)
|
||
const [pageSize, setPageSize] = useState(ADMIN_DEFAULT_PAGE_SIZE)
|
||
const [confirming, setConfirming] = useState<ManualFulfillmentPendingItem | null>(null)
|
||
const [showCostInput, setShowCostInput] = useState(false)
|
||
|
||
const listQuery = useQuery({
|
||
queryKey: ['admin-manual-fulfillment-pending', status, keyword, page, pageSize],
|
||
queryFn: () =>
|
||
fetchAdminManualFulfillmentPending({
|
||
status: status || undefined,
|
||
keyword: keyword || undefined,
|
||
page,
|
||
pageSize,
|
||
}),
|
||
})
|
||
|
||
const confirmMutation = useMutation({
|
||
mutationFn: (values: ConfirmFormValues) =>
|
||
confirmAdminManualFulfillment({
|
||
orderItemId: confirming?.orderItemId || 0,
|
||
costFen: values.costYuan === undefined ? undefined : Math.round(values.costYuan * 100),
|
||
payeeName: values.payeeName || '',
|
||
note: values.note || '',
|
||
}),
|
||
onSuccess: (response) => {
|
||
const result = response.data
|
||
message.success(
|
||
`履约已确认,扣 ${result.creditQuantity} 积分,成本 ¥${(result.costFen / 100).toFixed(2)}`,
|
||
)
|
||
setConfirming(null)
|
||
setShowCostInput(false)
|
||
void queryClient.invalidateQueries({
|
||
queryKey: ['admin-manual-fulfillment-pending'],
|
||
})
|
||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-my'] })
|
||
void queryClient.invalidateQueries({ queryKey: ['admin-fund-accounts'] })
|
||
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-accounts'] })
|
||
void queryClient.invalidateQueries({ queryKey: ['admin-platform-credit-entries'] })
|
||
},
|
||
onError: (error) => {
|
||
message.error(error instanceof Error ? error.message : '确认履约失败')
|
||
},
|
||
})
|
||
|
||
function openConfirm(item: ManualFulfillmentPendingItem) {
|
||
setConfirming(item)
|
||
setShowCostInput(item.costFen === null)
|
||
form.resetFields()
|
||
}
|
||
|
||
const columns: TableColumnsType<ManualFulfillmentPendingItem> = [
|
||
{
|
||
title: '商品',
|
||
key: 'sku',
|
||
width: 260,
|
||
render: (_, row) => (
|
||
<div className="cell-stack">
|
||
<Typography.Text strong ellipsis={{ tooltip: row.skuName }}>
|
||
{row.skuName || row.skuCode}
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
{row.internalSku || row.skuCode}
|
||
</Typography.Text>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '平台单号',
|
||
dataIndex: 'platformOrderId',
|
||
width: 170,
|
||
render: (value: string) => (
|
||
<Typography.Text copyable={{ text: value }} style={{ fontSize: 12 }}>
|
||
{value}
|
||
</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '履约渠道',
|
||
key: 'platform',
|
||
width: 110,
|
||
render: (_, row) => (
|
||
<Tag color={PLATFORM_TAG_COLORS[row.platformKey] || 'default'}>{row.platformLabel}</Tag>
|
||
),
|
||
},
|
||
{
|
||
title: '消耗',
|
||
key: 'credit',
|
||
width: 120,
|
||
render: (_, row) =>
|
||
row.isFundCash ? (
|
||
<Typography.Text type="secondary">—</Typography.Text>
|
||
) : (
|
||
<Typography.Text>{row.creditQuantity} 积分</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '成本',
|
||
key: 'cost',
|
||
width: 120,
|
||
align: 'right',
|
||
render: (_, row) =>
|
||
row.costFen === null ? (
|
||
<Tag color="orange">待填写</Tag>
|
||
) : (
|
||
<Typography.Text strong>¥{(row.costFen / 100).toFixed(2)}</Typography.Text>
|
||
),
|
||
},
|
||
{
|
||
title: '订单金额',
|
||
key: 'revenue',
|
||
width: 100,
|
||
align: 'right',
|
||
render: (_, row) => `¥${(row.revenueFen / 100).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '支付时间',
|
||
dataIndex: 'paidAt',
|
||
width: 150,
|
||
render: (value: string) => formatAdminDateTime(value),
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'actions',
|
||
width: 110,
|
||
fixed: 'right',
|
||
render: (_, row) =>
|
||
row.fulfilled ? (
|
||
<Tag color="green">已履约</Tag>
|
||
) : (
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
icon={<CheckOutlined />}
|
||
onClick={() => openConfirm(row)}
|
||
>
|
||
确认履约
|
||
</Button>
|
||
),
|
||
},
|
||
]
|
||
|
||
const total = listQuery.data?.data.pagination.total ?? 0
|
||
|
||
return (
|
||
<section>
|
||
<PageHeader
|
||
title="人工履约待办"
|
||
description="渠道与消耗积分已提前配好,确认即扣额度并计入成本。"
|
||
/>
|
||
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
style={{ marginBottom: 14 }}
|
||
message="确认履约会从对应平台的积分钱包扣减,并写入成本快照"
|
||
description="固定成本的商品直接确认即可;成本不固定的商品需要填写本次实际成本。"
|
||
/>
|
||
|
||
<Space wrap style={{ marginBottom: 12 }}>
|
||
<Button icon={<ReloadOutlined />} onClick={() => void listQuery.refetch()}>
|
||
刷新
|
||
</Button>
|
||
<Button
|
||
type={status === 'pending' ? 'primary' : 'default'}
|
||
onClick={() => {
|
||
setStatus('pending')
|
||
setPage(1)
|
||
}}
|
||
>
|
||
待确认
|
||
</Button>
|
||
<Button
|
||
type={status === 'fulfilled' ? 'primary' : 'default'}
|
||
onClick={() => {
|
||
setStatus('fulfilled')
|
||
setPage(1)
|
||
}}
|
||
>
|
||
已履约
|
||
</Button>
|
||
<Button
|
||
type={status === '' ? 'primary' : 'default'}
|
||
onClick={() => {
|
||
setStatus('')
|
||
setPage(1)
|
||
}}
|
||
>
|
||
全部
|
||
</Button>
|
||
</Space>
|
||
|
||
<Card bordered={false}>
|
||
<PercentTable
|
||
rowKey="orderItemId"
|
||
size="small"
|
||
columns={columns}
|
||
dataSource={listQuery.data?.data.items || []}
|
||
loading={listQuery.isLoading}
|
||
scroll={{ x: 1080 }}
|
||
pagination={buildAdminTablePagination({
|
||
page,
|
||
pageSize,
|
||
total,
|
||
onChange: (nextPage, nextPageSize) => {
|
||
setPage(nextPage)
|
||
if (nextPageSize !== pageSize) setPageSize(nextPageSize)
|
||
},
|
||
})}
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
open={Boolean(confirming)}
|
||
title="确认履约"
|
||
okText="确认"
|
||
cancelText="取消"
|
||
confirmLoading={confirmMutation.isPending}
|
||
onOk={() => {
|
||
void form.validateFields().then((values) => confirmMutation.mutate(values))
|
||
}}
|
||
onCancel={() => {
|
||
setConfirming(null)
|
||
setShowCostInput(false)
|
||
}}
|
||
destroyOnHidden
|
||
width={520}
|
||
>
|
||
{confirming ? (
|
||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||
<div>
|
||
<Typography.Text type="secondary">商品:</Typography.Text>
|
||
<Typography.Text strong>{confirming.skuName || confirming.skuCode}</Typography.Text>
|
||
</div>
|
||
<div>
|
||
<Typography.Text type="secondary">内部 SKU:</Typography.Text>
|
||
<Typography.Text copyable={{ text: confirming.internalSku }}>
|
||
{confirming.internalSku}
|
||
</Typography.Text>
|
||
</div>
|
||
<div>
|
||
<Typography.Text type="secondary">履约渠道:</Typography.Text>
|
||
<Tag color={PLATFORM_TAG_COLORS[confirming.platformKey] || 'default'}>
|
||
{confirming.platformLabel}
|
||
</Tag>
|
||
{!confirming.isFundCash ? (
|
||
<Typography.Text>消耗 {confirming.creditQuantity} 积分</Typography.Text>
|
||
) : null}
|
||
</div>
|
||
{showCostInput ? (
|
||
<Alert type="warning" showIcon message="该商品成本不固定,请填写本次实际成本" />
|
||
) : (
|
||
<Statistic
|
||
title="本次成本"
|
||
value={(confirming.costFen || 0) / 100}
|
||
precision={2}
|
||
prefix="¥"
|
||
/>
|
||
)}
|
||
</Space>
|
||
) : null}
|
||
|
||
<Form<ConfirmFormValues> form={form} layout="vertical">
|
||
{showCostInput ? (
|
||
<Form.Item
|
||
name="costYuan"
|
||
label="实际成本(元)"
|
||
rules={[{ required: true, message: '请填写实际成本' }]}
|
||
>
|
||
<InputNumber
|
||
min={0.01}
|
||
precision={2}
|
||
style={{ width: '100%' }}
|
||
placeholder="例如 12.50"
|
||
prefix="¥"
|
||
/>
|
||
</Form.Item>
|
||
) : null}
|
||
<Form.Item name="payeeName" label="收款方 / 处理人">
|
||
<Input placeholder="例如:客服小王、上游商家" maxLength={120} />
|
||
</Form.Item>
|
||
<Form.Item name="note" label="备注">
|
||
<Input.TextArea rows={2} maxLength={300} showCount />
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</section>
|
||
)
|
||
}
|