1978 lines
64 KiB
TypeScript
1978 lines
64 KiB
TypeScript
import {
|
||
CheckCircleOutlined,
|
||
DeleteOutlined,
|
||
ReloadOutlined,
|
||
RollbackOutlined,
|
||
SearchOutlined,
|
||
SendOutlined,
|
||
SyncOutlined,
|
||
} from '@ant-design/icons'
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Card,
|
||
Collapse,
|
||
Descriptions,
|
||
Input,
|
||
InputNumber,
|
||
Popconfirm,
|
||
Select,
|
||
Space,
|
||
Tabs,
|
||
Tag,
|
||
Typography,
|
||
} from 'antd'
|
||
import PercentTable from '@/components/common/PercentTable'
|
||
import type { TableColumnsType, TabsProps } from 'antd'
|
||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||
|
||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||
import PageHeader from '@/components/admin/PageHeader'
|
||
import dayjs, { type Dayjs } from '@/lib/dayjs'
|
||
import { showError, showSuccess } from '@/lib/feedback'
|
||
import {
|
||
approveAdminKuaishouIndustryRefund,
|
||
checkAdminKuaishouIndustryVoucherAvailable,
|
||
consumeAdminKuaishouIndustryVoucher,
|
||
destroyAdminKuaishouIndustryVoucher,
|
||
disagreeAdminKuaishouIndustryRefund,
|
||
fetchAdminKuaishouIndustryShops,
|
||
fetchAdminKuaishouIndustryVouchers,
|
||
listAdminKuaishouIndustryRefunds,
|
||
resendAdminKuaishouIndustryVoucherCode,
|
||
reverseAdminKuaishouIndustryVoucher,
|
||
} from '@/services/admin'
|
||
import type {
|
||
AdminKuaishouIndustryOpenApiResult,
|
||
AdminKuaishouIndustryRefundApprovePayload,
|
||
AdminKuaishouIndustryRefundDisagreePayload,
|
||
AdminKuaishouIndustryRefundListPayload,
|
||
AdminKuaishouIndustryShopOption,
|
||
AdminKuaishouIndustryVoucher,
|
||
AdminKuaishouIndustryVoucherToolPayload,
|
||
} from '@/types/admin'
|
||
import { getAdminRole, hasAdminRole } from '@/utils/admin-auth'
|
||
import {
|
||
ADMIN_DEFAULT_PAGE_SIZE,
|
||
buildAdminLocalTablePagination,
|
||
buildAdminTablePagination,
|
||
} from '@/utils/admin-pagination'
|
||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||
import { stringifyDisplayJson } from '@/utils/date-time'
|
||
|
||
type DateRangeValue = [Dayjs | null, Dayjs | null] | null
|
||
type IndustryTab = 'support-consume' | 'vouchers' | 'refunds' | 'result'
|
||
type IndustryActionScope = 'vouchers' | 'refunds'
|
||
type IndustryActionKind =
|
||
| 'check'
|
||
| 'consume'
|
||
| 'destroy'
|
||
| 'reverse'
|
||
| 'resend'
|
||
| 'refund-list'
|
||
| 'refund-approve'
|
||
| 'refund-disagree'
|
||
|
||
type IndustryActionHighlight = {
|
||
label: string
|
||
value: string
|
||
}
|
||
|
||
type IndustryActionResultView = {
|
||
kind: IndustryActionKind
|
||
scope: IndustryActionScope
|
||
title: string
|
||
success: boolean
|
||
summary: string
|
||
highlights: IndustryActionHighlight[]
|
||
raw: unknown
|
||
executedAt: string
|
||
}
|
||
|
||
type VoucherFilterState = {
|
||
oid: string
|
||
voucherCode: string
|
||
taskId: string
|
||
sellerId: string
|
||
status: string
|
||
page: number
|
||
pageSize: number
|
||
}
|
||
|
||
type RefundListState = {
|
||
sellerId: string
|
||
orderId: string
|
||
type: string
|
||
pageSize: string
|
||
currentPage: string
|
||
pcursor: string
|
||
status: string
|
||
negotiateStatus: string
|
||
}
|
||
|
||
export default function AdminKuaishouIndustryPage() {
|
||
const isSupportOnly = getAdminRole() === 'support'
|
||
const canManageRefund = hasAdminRole('operator')
|
||
const [activeTab, setActiveTab] = useState<IndustryTab>(
|
||
isSupportOnly ? 'support-consume' : 'vouchers',
|
||
)
|
||
const [voucherLoading, setVoucherLoading] = useState(false)
|
||
const [actionLoading, setActionLoading] = useState('')
|
||
const [consumingVoucherId, setConsumingVoucherId] = useState<number | null>(null)
|
||
const [reversingVoucherId, setReversingVoucherId] = useState<number | null>(null)
|
||
const [vouchers, setVouchers] = useState<AdminKuaishouIndustryVoucher[]>([])
|
||
const [selectedVoucher, setSelectedVoucher] = useState<AdminKuaishouIndustryVoucher | null>(null)
|
||
const [voucherTotal, setVoucherTotal] = useState(0)
|
||
const [voucherFilters, setVoucherFilters] = useState<VoucherFilterState>({
|
||
oid: '',
|
||
voucherCode: '',
|
||
taskId: '',
|
||
sellerId: '',
|
||
status: '',
|
||
page: 1,
|
||
pageSize: 50,
|
||
})
|
||
// 异步接口返回后刷新列表时,仍使用用户最后一次输入的筛选条件。
|
||
const voucherFiltersRef = useRef(voucherFilters)
|
||
const [toolForm, setToolForm] = useState<AdminKuaishouIndustryVoucherToolPayload>({
|
||
eticketType: '',
|
||
consumeType: 'consume',
|
||
reason: 'USER_APPLY_REFUND',
|
||
})
|
||
const [refundDateRange, setRefundDateRange] = useState<DateRangeValue>(() => [
|
||
dayjs().subtract(1, 'day'),
|
||
dayjs(),
|
||
])
|
||
const [refundListForm, setRefundListForm] = useState<RefundListState>({
|
||
sellerId: '',
|
||
orderId: '',
|
||
type: '8',
|
||
pageSize: '50',
|
||
currentPage: '1',
|
||
pcursor: '',
|
||
status: '10',
|
||
negotiateStatus: '',
|
||
})
|
||
const [approveForm, setApproveForm] = useState<AdminKuaishouIndustryRefundApprovePayload>({
|
||
refundId: '',
|
||
refundAmount: '',
|
||
status: '',
|
||
negotiateStatus: '',
|
||
refundHandingWay: '10',
|
||
})
|
||
const [disagreeForm, setDisagreeForm] = useState<AdminKuaishouIndustryRefundDisagreePayload>({
|
||
refundId: '',
|
||
sellerDisagreeReason: '100',
|
||
sellerDisagreeDesc: '',
|
||
status: '10',
|
||
negotiateStatus: '1',
|
||
})
|
||
const [lastActionResult, setLastActionResult] = useState<IndustryActionResultView | null>(null)
|
||
const [refundRows, setRefundRows] = useState<Array<Record<string, unknown>>>([])
|
||
const [industryShops, setIndustryShops] = useState<AdminKuaishouIndustryShopOption[]>([])
|
||
const shopFilterOptions = useMemo(
|
||
() =>
|
||
industryShops
|
||
.filter((shop) => shop.sellerId)
|
||
.map((shop) => ({
|
||
label: formatShopOptionLabel(shop),
|
||
value: shop.sellerId,
|
||
})),
|
||
[industryShops],
|
||
)
|
||
const refundShopOptions = useMemo(
|
||
() =>
|
||
industryShops
|
||
.filter((shop) => shop.enabled !== false && shop.sellerId)
|
||
.map((shop) => ({
|
||
label: formatShopOptionLabel(shop),
|
||
value: shop.sellerId,
|
||
})),
|
||
[industryShops],
|
||
)
|
||
const shopNameBySellerId = useMemo(() => {
|
||
const map = new Map<string, string>()
|
||
for (const shop of industryShops) {
|
||
const name = String(shop.shopName || '').trim()
|
||
if (!name) {
|
||
continue
|
||
}
|
||
const sellerId = String(shop.sellerId || '').trim()
|
||
const shopId = String(shop.shopId || '').trim()
|
||
if (sellerId) {
|
||
map.set(sellerId, name)
|
||
}
|
||
if (shopId) {
|
||
map.set(shopId, name)
|
||
}
|
||
}
|
||
return map
|
||
}, [industryShops])
|
||
|
||
const columns = useMemo<TableColumnsType<AdminKuaishouIndustryVoucher>>(
|
||
() => [
|
||
{
|
||
title: '卡券号码',
|
||
width: 236,
|
||
render: (_, row) => (
|
||
<div className="cell-stack">
|
||
<Typography.Text
|
||
strong
|
||
copyable={Boolean(row.voucherCode)}
|
||
className="kuaishou-voucher-code-text"
|
||
title={row.voucherCode || undefined}
|
||
>
|
||
{row.voucherCode || '-'}
|
||
</Typography.Text>
|
||
<span className="muted kuaishou-voucher-meta">
|
||
发码:{formatAdminDateTime(row.sendCallbackSentAt)}
|
||
</span>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '订单商品',
|
||
width: 348,
|
||
// 主列自适应:订单号 + 商品名,避免额外「商品」列占宽
|
||
ellipsis: true,
|
||
render: (_, row) => {
|
||
const skuName = String(row.skuName || '').trim()
|
||
const skuCode = String(row.skuCode || '').trim()
|
||
const productLabel = skuName || (skuCode ? `编码 ${skuCode}` : '')
|
||
const taskLabel = row.taskNo
|
||
? `${row.taskNo}${row.taskId ? ` (#${row.taskId})` : ''}`
|
||
: row.taskId
|
||
? `#${row.taskId}`
|
||
: '-'
|
||
|
||
return (
|
||
<div className="cell-stack">
|
||
<Typography.Text
|
||
copyable={Boolean(row.oid)}
|
||
ellipsis={{ tooltip: row.oid || undefined }}
|
||
>
|
||
{row.oid || '-'}
|
||
</Typography.Text>
|
||
{productLabel ? (
|
||
<Typography.Text
|
||
ellipsis={{ tooltip: skuCode ? `${skuName || '-'}(${skuCode})` : productLabel }}
|
||
>
|
||
{productLabel}
|
||
</Typography.Text>
|
||
) : (
|
||
<span className="muted">未关联商品</span>
|
||
)}
|
||
<span className="muted">
|
||
任务 {taskLabel}
|
||
{row.orderId ? ` · 订单 #${row.orderId}` : ''}
|
||
</span>
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
title: '状态',
|
||
width: 148,
|
||
render: (_, row) => (
|
||
<div className="cell-stack">
|
||
<Tag color={getVoucherStatusColor(row.status)}>{getVoucherStatusLabel(row.status)}</Tag>
|
||
<Tag color={row.sendCallbackStatus === 'success' ? 'green' : 'orange'}>
|
||
发码:{row.sendCallbackStatus || '-'}
|
||
</Tag>
|
||
{row.eticketType ? (
|
||
<Typography.Text
|
||
type="secondary"
|
||
ellipsis={{ tooltip: row.eticketType }}
|
||
className="muted"
|
||
>
|
||
{row.eticketType}
|
||
</Typography.Text>
|
||
) : (
|
||
<span className="muted">类型未知</span>
|
||
)}
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
title: '店铺',
|
||
width: 128,
|
||
ellipsis: true,
|
||
render: (_, row) =>
|
||
renderShopCell(
|
||
row.shopName || shopNameBySellerId.get(String(row.sellerId || '').trim()) || '',
|
||
row.sellerId,
|
||
isSupportOnly ? '' : row.tokenMasked,
|
||
{ compact: true },
|
||
),
|
||
},
|
||
{
|
||
title: '核销',
|
||
width: 132,
|
||
render: (_, row) => {
|
||
const serial = String(row.consumeSerialNum || '').trim()
|
||
const consumedAt = formatAdminDateTime(row.consumedAt)
|
||
return (
|
||
<div className="cell-stack">
|
||
<span title={serial || undefined}>
|
||
{consumedAt !== '-' ? consumedAt : serial || '-'}
|
||
</span>
|
||
{serial && consumedAt !== '-' ? (
|
||
<Typography.Text type="secondary" ellipsis={{ tooltip: serial }} className="muted">
|
||
{serial}
|
||
</Typography.Text>
|
||
) : null}
|
||
</div>
|
||
)
|
||
},
|
||
},
|
||
{
|
||
title: '操作',
|
||
fixed: 'right' as const,
|
||
width: isSupportOnly ? 164 : 72,
|
||
render: (_: unknown, row: AdminKuaishouIndustryVoucher) => {
|
||
if (isSupportOnly) {
|
||
const canConsume = row.status === 'UNUSED'
|
||
const canReverse = row.status === 'CONSUMED'
|
||
return (
|
||
<Space size={4}>
|
||
<Popconfirm
|
||
title="确认核销该券码?"
|
||
description={row.voucherCode}
|
||
okText="核销"
|
||
cancelText="取消"
|
||
disabled={!canConsume}
|
||
onConfirm={() => consumeVoucherRow(row)}
|
||
>
|
||
<Button
|
||
size="small"
|
||
type="primary"
|
||
icon={<CheckCircleOutlined />}
|
||
disabled={!canConsume}
|
||
loading={consumingVoucherId === row.id}
|
||
>
|
||
核销
|
||
</Button>
|
||
</Popconfirm>
|
||
<Popconfirm
|
||
title="确认冲正该券码?"
|
||
description="冲正后券码将恢复为未使用状态。"
|
||
okText="冲正"
|
||
cancelText="取消"
|
||
disabled={!canReverse}
|
||
onConfirm={() => reverseVoucherRow(row)}
|
||
>
|
||
<Button
|
||
size="small"
|
||
danger
|
||
icon={<RollbackOutlined />}
|
||
disabled={!canReverse}
|
||
loading={reversingVoucherId === row.id}
|
||
>
|
||
冲正
|
||
</Button>
|
||
</Popconfirm>
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
const isSelected = selectedVoucher?.id === row.id
|
||
return (
|
||
<Button
|
||
size="small"
|
||
type="primary"
|
||
ghost={!isSelected}
|
||
onClick={() => selectVoucher(row)}
|
||
>
|
||
{isSelected ? '已选' : '选择'}
|
||
</Button>
|
||
)
|
||
},
|
||
},
|
||
],
|
||
[
|
||
consumingVoucherId,
|
||
isSupportOnly,
|
||
reversingVoucherId,
|
||
selectedVoucher?.id,
|
||
shopNameBySellerId,
|
||
],
|
||
)
|
||
|
||
const refundColumns = useMemo<TableColumnsType<Record<string, unknown>>>(
|
||
() => [
|
||
{ title: '售后单', dataIndex: 'refundId', minWidth: 160 },
|
||
{ title: '订单', dataIndex: 'oid', minWidth: 160 },
|
||
{
|
||
title: '店铺',
|
||
dataIndex: 'sellerId',
|
||
minWidth: 180,
|
||
render: (_, row) => {
|
||
const sellerId = String(row.sellerId || '').trim()
|
||
return renderShopCell(shopNameBySellerId.get(sellerId) || '', sellerId)
|
||
},
|
||
},
|
||
{ title: '状态', dataIndex: 'status', width: 100 },
|
||
{ title: '协商', dataIndex: 'negotiateStatus', width: 100 },
|
||
{ title: '金额', dataIndex: 'refundFee', width: 100 },
|
||
{
|
||
title: '提交时间',
|
||
minWidth: 170,
|
||
render: (_, row) => formatTimestamp(row.submitTime || row.createTime),
|
||
},
|
||
{
|
||
title: '操作',
|
||
fixed: 'right',
|
||
width: 120,
|
||
render: (_, row) => (
|
||
<Button size="small" onClick={() => fillRefundAction(row)}>
|
||
填入
|
||
</Button>
|
||
),
|
||
},
|
||
],
|
||
[shopNameBySellerId],
|
||
)
|
||
|
||
useEffect(() => {
|
||
void loadIndustryShops()
|
||
void loadVouchers()
|
||
}, [])
|
||
|
||
async function loadIndustryShops() {
|
||
try {
|
||
const response = await fetchAdminKuaishouIndustryShops()
|
||
const shops = response.data.shops || []
|
||
setIndustryShops(shops)
|
||
|
||
const enabledSellerIds = shops
|
||
.filter((shop) => shop.enabled !== false && shop.sellerId)
|
||
.map((shop) => shop.sellerId)
|
||
if (enabledSellerIds.length === 1) {
|
||
applyDefaultSellerId(enabledSellerIds[0] as string)
|
||
}
|
||
} catch (error) {
|
||
// 店铺下拉失败不阻断列表,仅回退为手动输入店铺 ID
|
||
console.warn(error instanceof Error ? error.message : '读取店铺列表失败')
|
||
}
|
||
}
|
||
|
||
async function loadVouchers(nextFilters: Partial<VoucherFilterState> = {}) {
|
||
const filters = updateVoucherFilters(nextFilters)
|
||
setVoucherLoading(true)
|
||
|
||
try {
|
||
const response = await fetchAdminKuaishouIndustryVouchers({
|
||
oid: filters.oid.trim(),
|
||
voucherCode: filters.voucherCode.trim(),
|
||
taskId: filters.taskId.trim(),
|
||
sellerId: filters.sellerId.trim(),
|
||
status: filters.status,
|
||
page: filters.page,
|
||
pageSize: filters.pageSize,
|
||
})
|
||
const items = response.data.items
|
||
setVouchers(items)
|
||
setSelectedVoucher((current) => {
|
||
if (!current) {
|
||
return null
|
||
}
|
||
return items.find((item) => item.id === current.id) || current
|
||
})
|
||
setVoucherTotal(response.data.pagination.total)
|
||
} catch (error) {
|
||
showError(error instanceof Error ? error.message : '查询电子凭证失败')
|
||
} finally {
|
||
setVoucherLoading(false)
|
||
}
|
||
}
|
||
|
||
function updateVoucherFilters(patch: Partial<VoucherFilterState>): VoucherFilterState {
|
||
const nextFilters = { ...voucherFiltersRef.current, ...patch }
|
||
voucherFiltersRef.current = nextFilters
|
||
setVoucherFilters(nextFilters)
|
||
return nextFilters
|
||
}
|
||
|
||
function clearVoucherFilters() {
|
||
void loadVouchers({
|
||
oid: '',
|
||
voucherCode: '',
|
||
taskId: '',
|
||
sellerId: '',
|
||
status: '',
|
||
page: 1,
|
||
})
|
||
}
|
||
|
||
function selectVoucher(row: AdminKuaishouIndustryVoucher) {
|
||
setSelectedVoucher(row)
|
||
showSuccess(`已选择券码 ${row.voucherCode}`)
|
||
}
|
||
|
||
async function runCheckAvailable() {
|
||
await runOpenApiAction('check', '检查电子凭证有效性', 'check', () =>
|
||
checkAdminKuaishouIndustryVoucherAvailable(buildVoucherToolPayload()),
|
||
)
|
||
}
|
||
|
||
async function runReverse() {
|
||
await runOpenApiAction('reverse', '电子凭证冲正回调', 'reverse', () =>
|
||
reverseAdminKuaishouIndustryVoucher({
|
||
...buildVoucherToolPayload(),
|
||
reason: '后台手动冲正',
|
||
}),
|
||
)
|
||
await loadVouchers()
|
||
}
|
||
|
||
async function runConsume() {
|
||
setActionLoading('consume')
|
||
try {
|
||
const response = await consumeAdminKuaishouIndustryVoucher(buildVoucherToolPayload())
|
||
publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data))
|
||
showSuccess('手动核销已完成')
|
||
await loadVouchers()
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '手动核销失败'
|
||
publishActionResult(
|
||
buildIndustryActionResultView('consume', '手动核销', {
|
||
success: false,
|
||
error: message,
|
||
}),
|
||
)
|
||
showError(message)
|
||
} finally {
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
async function consumeVoucherRow(row: AdminKuaishouIndustryVoucher) {
|
||
if (row.status !== 'UNUSED') {
|
||
showError('仅未使用券码可核销')
|
||
return
|
||
}
|
||
|
||
setConsumingVoucherId(row.id)
|
||
setActionLoading('consume')
|
||
try {
|
||
const response = await consumeAdminKuaishouIndustryVoucher({
|
||
sellerId: row.sellerId,
|
||
oid: row.oid,
|
||
orderId: row.oid,
|
||
taskId: row.taskId || '',
|
||
voucherCode: row.voucherCode,
|
||
eticketType: row.eticketType || undefined,
|
||
consumeType: 'consume',
|
||
serialNum: row.consumeSerialNum || undefined,
|
||
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
|
||
})
|
||
if (!isSupportOnly) {
|
||
publishActionResult(buildIndustryActionResultView('consume', '手动核销', response.data))
|
||
}
|
||
showSuccess(`券码 ${row.voucherCode} 已核销`)
|
||
await loadVouchers()
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '核销失败'
|
||
if (!isSupportOnly) {
|
||
publishActionResult(
|
||
buildIndustryActionResultView('consume', '手动核销', {
|
||
success: false,
|
||
error: message,
|
||
}),
|
||
)
|
||
}
|
||
showError(message)
|
||
} finally {
|
||
setConsumingVoucherId(null)
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
async function reverseVoucherRow(row: AdminKuaishouIndustryVoucher) {
|
||
if (row.status !== 'CONSUMED') {
|
||
showError('仅已核销券码可冲正')
|
||
return
|
||
}
|
||
|
||
setReversingVoucherId(row.id)
|
||
setActionLoading('reverse')
|
||
try {
|
||
const response = await reverseAdminKuaishouIndustryVoucher({
|
||
sellerId: row.sellerId,
|
||
oid: row.oid,
|
||
orderId: row.oid,
|
||
taskId: row.taskId || '',
|
||
voucherCode: row.voucherCode,
|
||
eticketType: row.eticketType || undefined,
|
||
reason: '客服手动冲正',
|
||
etickets: [{ id: row.voucherCode, code: row.voucherCode, num: 1 }],
|
||
})
|
||
if (response.data.success) {
|
||
showSuccess(`券码 ${row.voucherCode} 已冲正`)
|
||
} else {
|
||
showError(response.data.error || '冲正失败')
|
||
}
|
||
await loadVouchers()
|
||
} catch (error) {
|
||
showError(error instanceof Error ? error.message : '冲正失败')
|
||
} finally {
|
||
setReversingVoucherId(null)
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
async function runResendCode() {
|
||
setActionLoading('resend')
|
||
try {
|
||
const response = await resendAdminKuaishouIndustryVoucherCode(buildVoucherToolPayload())
|
||
publishActionResult(buildIndustryActionResultView('resend', '重发发码回调', response.data))
|
||
if (response.data.success) {
|
||
showSuccess('发码回调已重发')
|
||
} else {
|
||
showError(response.data.error || '发码回调重发失败')
|
||
}
|
||
await loadVouchers()
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '重发发码回调失败'
|
||
publishActionResult(
|
||
buildIndustryActionResultView('resend', '重发发码回调', {
|
||
success: false,
|
||
error: message,
|
||
}),
|
||
)
|
||
showError(message)
|
||
} finally {
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
async function runDestroy() {
|
||
setActionLoading('destroy')
|
||
try {
|
||
const response = await destroyAdminKuaishouIndustryVoucher({
|
||
...buildVoucherToolPayload(),
|
||
reason: toolForm.reason || 'USER_APPLY_REFUND',
|
||
})
|
||
publishActionResult(buildIndustryActionResultView('destroy', '手动销毁', response.data))
|
||
showSuccess(
|
||
response.data.alreadyDestroyed
|
||
? `券码已是销毁状态,销毁回调已重试(${response.data.reason || 'USER_APPLY_REFUND'})`
|
||
: `券码已销毁(${response.data.reason || 'USER_APPLY_REFUND'})`,
|
||
)
|
||
await loadVouchers()
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : '手动销毁失败'
|
||
publishActionResult(
|
||
buildIndustryActionResultView('destroy', '手动销毁', {
|
||
success: false,
|
||
error: message,
|
||
}),
|
||
)
|
||
showError(message)
|
||
} finally {
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
async function runRefundList() {
|
||
const [begin, end] = refundDateRange || []
|
||
const sellerId = resolveRefundSellerId(refundListForm.sellerId)
|
||
if (!sellerId && refundShopOptions.length > 1) {
|
||
showError('请先选择快手售后退款店铺')
|
||
return
|
||
}
|
||
|
||
await runOpenApiAction(
|
||
'refund-list',
|
||
'售后单列表',
|
||
'refund-list',
|
||
() =>
|
||
listAdminKuaishouIndustryRefunds({
|
||
...refundListForm,
|
||
sellerId,
|
||
beginTime: begin?.valueOf(),
|
||
endTime: end?.valueOf(),
|
||
} satisfies AdminKuaishouIndustryRefundListPayload),
|
||
(result) => {
|
||
const rows = extractRefundRows(result.response)
|
||
setRefundRows(rows)
|
||
return { refundCount: rows.length }
|
||
},
|
||
)
|
||
}
|
||
|
||
async function runApproveRefund() {
|
||
const sellerId = resolveRefundSellerId(approveForm.sellerId)
|
||
if (!sellerId && refundShopOptions.length > 1) {
|
||
showError('请先选择快手售后退款店铺')
|
||
return
|
||
}
|
||
|
||
await runOpenApiAction('refund-approve', '同意退款', 'refund-approve', () =>
|
||
approveAdminKuaishouIndustryRefund({
|
||
...approveForm,
|
||
sellerId,
|
||
orderId: approveForm.orderId || refundListForm.orderId,
|
||
}),
|
||
)
|
||
}
|
||
|
||
async function runDisagreeRefund() {
|
||
const sellerId = resolveRefundSellerId(disagreeForm.sellerId)
|
||
if (!sellerId && refundShopOptions.length > 1) {
|
||
showError('请先选择快手售后退款店铺')
|
||
return
|
||
}
|
||
|
||
await runOpenApiAction('refund-disagree', '不同意退款', 'refund-disagree', () =>
|
||
disagreeAdminKuaishouIndustryRefund({
|
||
...disagreeForm,
|
||
sellerId,
|
||
orderId: disagreeForm.orderId || refundListForm.orderId,
|
||
}),
|
||
)
|
||
}
|
||
|
||
async function runOpenApiAction(
|
||
kind: IndustryActionKind,
|
||
title: string,
|
||
loadingKey: string,
|
||
action: () => Promise<{ data: AdminKuaishouIndustryOpenApiResult }>,
|
||
afterSuccess?: (result: AdminKuaishouIndustryOpenApiResult) => { refundCount?: number } | void,
|
||
) {
|
||
setActionLoading(loadingKey)
|
||
try {
|
||
const response = await action()
|
||
const meta = afterSuccess?.(response.data) || undefined
|
||
publishActionResult(
|
||
buildIndustryActionResultView(kind, title, response.data, meta || undefined),
|
||
)
|
||
if (response.data.success) {
|
||
showSuccess(`${title}已执行`)
|
||
} else {
|
||
showError(response.data.error || `${title}返回失败`)
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : `${title}失败`
|
||
publishActionResult(
|
||
buildIndustryActionResultView(kind, title, {
|
||
success: false,
|
||
error: message,
|
||
}),
|
||
)
|
||
showError(message)
|
||
} finally {
|
||
setActionLoading('')
|
||
}
|
||
}
|
||
|
||
function publishActionResult(view: IndustryActionResultView) {
|
||
setLastActionResult(view)
|
||
}
|
||
|
||
function buildVoucherToolPayload(): AdminKuaishouIndustryVoucherToolPayload {
|
||
const voucherCode = String(
|
||
selectedVoucher ? selectedVoucher.voucherCode : toolForm.voucherCode || '',
|
||
).trim()
|
||
const oid = String(
|
||
selectedVoucher ? selectedVoucher.oid : toolForm.oid || toolForm.orderId || '',
|
||
).trim()
|
||
return {
|
||
...toolForm,
|
||
sellerId: selectedVoucher ? selectedVoucher.sellerId : toolForm.sellerId,
|
||
oid,
|
||
orderId: oid,
|
||
taskId: selectedVoucher ? selectedVoucher.taskId || '' : toolForm.taskId,
|
||
voucherCode,
|
||
eticketType: selectedVoucher ? selectedVoucher.eticketType || '' : toolForm.eticketType,
|
||
serialNum: toolForm.serialNum || selectedVoucher?.consumeSerialNum,
|
||
etickets: voucherCode ? [{ id: voucherCode, code: voucherCode, num: 1 }] : undefined,
|
||
}
|
||
}
|
||
|
||
function fillRefundAction(row: Record<string, unknown>) {
|
||
const refundId = String(row.refundId || '').trim()
|
||
const sellerId = String(row.sellerId || refundListForm.sellerId || '').trim()
|
||
const orderId = String(row.oid || row.orderId || refundListForm.orderId || '').trim()
|
||
const status = String(row.status || '').trim()
|
||
const negotiateStatus = String(row.negotiateStatus || '').trim()
|
||
setApproveForm((current) => ({
|
||
...current,
|
||
sellerId: sellerId || current.sellerId,
|
||
orderId: orderId || current.orderId,
|
||
refundId,
|
||
status,
|
||
negotiateStatus,
|
||
refundAmount: String(row.refundFee || current.refundAmount || ''),
|
||
refundHandingWay: String(row.handlingWay || current.refundHandingWay || ''),
|
||
}))
|
||
setDisagreeForm((current) => ({
|
||
...current,
|
||
sellerId: sellerId || current.sellerId,
|
||
orderId: orderId || current.orderId,
|
||
refundId,
|
||
status,
|
||
negotiateStatus: negotiateStatus || current.negotiateStatus,
|
||
}))
|
||
setRefundListForm((current) => ({
|
||
...current,
|
||
sellerId: sellerId || current.sellerId,
|
||
orderId: orderId || current.orderId,
|
||
}))
|
||
showSuccess('已填入退款操作区')
|
||
}
|
||
|
||
const tabItems: TabsProps['items'] = isSupportOnly
|
||
? [
|
||
{
|
||
key: 'support-consume',
|
||
label: '券码核销',
|
||
children: renderSupportConsumeTab(),
|
||
},
|
||
]
|
||
: [
|
||
{
|
||
key: 'vouchers',
|
||
label: '券码操作',
|
||
children: renderVoucherTab(),
|
||
},
|
||
...(canManageRefund
|
||
? [
|
||
{
|
||
key: 'refunds',
|
||
label: '售后退款',
|
||
children: renderRefundTab(),
|
||
},
|
||
]
|
||
: []),
|
||
{
|
||
key: 'result',
|
||
label: lastActionResult ? `接口结果 · ${lastActionResult.title}` : '接口结果',
|
||
children: renderResultTab(),
|
||
},
|
||
]
|
||
|
||
return (
|
||
<div className="page-stack kuaishou-industry-page">
|
||
<PageHeader
|
||
title="快手电子凭证"
|
||
description={
|
||
isSupportOnly
|
||
? '客服核销:查询本地券码并执行核销'
|
||
: canManageRefund
|
||
? '行业电子凭证接口工具与售后处理'
|
||
: '行业电子凭证接口工具'
|
||
}
|
||
/>
|
||
|
||
<Tabs
|
||
activeKey={activeTab}
|
||
onChange={(key) => setActiveTab(key as IndustryTab)}
|
||
items={tabItems}
|
||
/>
|
||
</div>
|
||
)
|
||
|
||
function renderVoucherFilters() {
|
||
return (
|
||
<Card>
|
||
<Space wrap className="filter-form">
|
||
<Input
|
||
allowClear
|
||
placeholder="订单号 oid"
|
||
value={voucherFilters.oid}
|
||
onChange={(event) =>
|
||
updateVoucherFilters({
|
||
oid: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
allowClear
|
||
placeholder="券码"
|
||
value={voucherFilters.voucherCode}
|
||
onChange={(event) =>
|
||
updateVoucherFilters({
|
||
voucherCode: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
allowClear
|
||
placeholder="任务 ID"
|
||
value={voucherFilters.taskId}
|
||
onChange={(event) =>
|
||
updateVoucherFilters({
|
||
taskId: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
{shopFilterOptions.length > 0 ? (
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
placeholder="按店铺筛选"
|
||
style={{ minWidth: 240 }}
|
||
value={voucherFilters.sellerId || undefined}
|
||
options={shopFilterOptions}
|
||
onChange={(value) => {
|
||
void loadVouchers({
|
||
page: 1,
|
||
sellerId: String(value || '').trim(),
|
||
})
|
||
}}
|
||
/>
|
||
) : (
|
||
<Input
|
||
allowClear
|
||
placeholder="店铺 ID"
|
||
value={voucherFilters.sellerId}
|
||
onChange={(event) =>
|
||
updateVoucherFilters({
|
||
sellerId: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
)}
|
||
<Select
|
||
allowClear
|
||
placeholder="券码状态"
|
||
style={{ width: 132 }}
|
||
value={voucherFilters.status || undefined}
|
||
options={[
|
||
{ label: '未使用', value: 'UNUSED' },
|
||
{ label: '已核销', value: 'CONSUMED' },
|
||
{ label: '已销毁', value: 'DESTROYED' },
|
||
]}
|
||
onChange={(value) => updateVoucherFilters({ status: value || '' })}
|
||
/>
|
||
<Button
|
||
type="primary"
|
||
icon={<SearchOutlined />}
|
||
loading={voucherLoading}
|
||
onClick={() => loadVouchers({ page: 1 })}
|
||
>
|
||
查询
|
||
</Button>
|
||
<Button onClick={clearVoucherFilters}>清空筛选</Button>
|
||
</Space>
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function renderVoucherTable(scrollX?: number) {
|
||
return (
|
||
<PercentTable
|
||
rowKey="id"
|
||
size="middle"
|
||
tableLayout="fixed"
|
||
columns={columns}
|
||
dataSource={vouchers}
|
||
loading={voucherLoading}
|
||
rowClassName={(row) =>
|
||
selectedVoucher?.id === row.id ? 'kuaishou-industry-row-selected' : ''
|
||
}
|
||
// 列已压缩;仅在极窄容器时横向滚动
|
||
scroll={{ x: scrollX || 1064 }}
|
||
pagination={buildAdminTablePagination({
|
||
current: voucherFilters.page,
|
||
pageSize: voucherFilters.pageSize,
|
||
total: voucherTotal,
|
||
onChange: (page, pageSize) => {
|
||
void loadVouchers({ page, pageSize })
|
||
},
|
||
})}
|
||
/>
|
||
)
|
||
}
|
||
|
||
function renderSupportConsumeTab() {
|
||
return (
|
||
<div className="page-stack">
|
||
{renderVoucherFilters()}
|
||
<Card title="本地券码">{renderVoucherTable()}</Card>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function renderVoucherTab() {
|
||
const hasVoucherTarget = Boolean(
|
||
selectedVoucher ||
|
||
String(toolForm.voucherCode || '').trim() ||
|
||
String(toolForm.taskId || '').trim() ||
|
||
String(toolForm.oid || toolForm.orderId || '').trim(),
|
||
)
|
||
const selectedShopName = selectedVoucher
|
||
? selectedVoucher.shopName ||
|
||
shopNameBySellerId.get(String(selectedVoucher.sellerId || '').trim()) ||
|
||
'未命名店铺'
|
||
: ''
|
||
|
||
return (
|
||
<div className="page-stack">
|
||
{renderVoucherFilters()}
|
||
|
||
<div className="kuaishou-industry-split">
|
||
<Card title="本地券码" className="kuaishou-industry-voucher-card">
|
||
{renderVoucherTable(1064)}
|
||
</Card>
|
||
|
||
<Card title="券码操作" className="kuaishou-industry-tool-card">
|
||
<div className="kuaishou-industry-tool-section">
|
||
<div className="kuaishou-industry-tool-section-title">当前卡券</div>
|
||
{selectedVoucher ? (
|
||
<div className="kuaishou-industry-selected-voucher">
|
||
<div className="kuaishou-industry-selected-header">
|
||
<Typography.Text
|
||
strong
|
||
copyable={{ text: selectedVoucher.voucherCode }}
|
||
className="kuaishou-industry-selected-code"
|
||
>
|
||
{selectedVoucher.voucherCode}
|
||
</Typography.Text>
|
||
<Space size={4}>
|
||
<Tag color={getVoucherStatusColor(selectedVoucher.status)}>
|
||
{getVoucherStatusLabel(selectedVoucher.status)}
|
||
</Tag>
|
||
<Button type="link" size="small" onClick={() => setSelectedVoucher(null)}>
|
||
清除
|
||
</Button>
|
||
</Space>
|
||
</div>
|
||
<div className="kuaishou-industry-selected-meta">
|
||
<div>
|
||
<span>订单号</span>
|
||
<Typography.Text copyable={{ text: selectedVoucher.oid }}>
|
||
{selectedVoucher.oid || '-'}
|
||
</Typography.Text>
|
||
</div>
|
||
<div>
|
||
<span>任务</span>
|
||
<strong>{selectedVoucher.taskNo || selectedVoucher.taskId || '-'}</strong>
|
||
</div>
|
||
<div>
|
||
<span>店铺</span>
|
||
<strong>{selectedShopName}</strong>
|
||
<small>{selectedVoucher.sellerId || '-'}</small>
|
||
</div>
|
||
<div>
|
||
<span>凭证类型</span>
|
||
<strong title={selectedVoucher.eticketType || undefined}>
|
||
{selectedVoucher.eticketType || '-'}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
<div className="kuaishou-industry-selected-empty">
|
||
<strong>尚未选择卡券</strong>
|
||
<span>请从左侧列表选择,或在下方手动指定。</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="kuaishou-industry-tool-section">
|
||
<div className="kuaishou-industry-tool-section-title">操作参数</div>
|
||
<div className="kuaishou-industry-tool-grid">
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>核销类型</span>
|
||
<Input
|
||
placeholder="consume"
|
||
value={toolForm.consumeType}
|
||
onChange={(event) => updateToolForm({ consumeType: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>核销序列号(可选)</span>
|
||
<Input
|
||
placeholder={selectedVoucher?.consumeSerialNum || '默认使用卡券记录'}
|
||
value={toolForm.serialNum}
|
||
onChange={(event) => updateToolForm({ serialNum: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field kuaishou-industry-tool-field-wide">
|
||
<span>销毁原因</span>
|
||
<Select
|
||
value={toolForm.reason || undefined}
|
||
onChange={(value) => updateToolForm({ reason: value || '' })}
|
||
options={[
|
||
{ label: '用户申请退款', value: 'USER_APPLY_REFUND' },
|
||
{ label: '后台作废', value: 'SYS_ADMIN_DESTROY' },
|
||
{ label: '供应商作废', value: 'SUPPLY_DESTROY' },
|
||
{ label: '凭证过期', value: 'ETICKET_EXPIRED' },
|
||
]}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>门店名称(可选)</span>
|
||
<Input
|
||
placeholder="用于核销明细"
|
||
value={toolForm.storeName}
|
||
onChange={(event) => updateToolForm({ storeName: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>门店地址(可选)</span>
|
||
<Input
|
||
placeholder="用于核销明细"
|
||
value={toolForm.storeAddress}
|
||
onChange={(event) => updateToolForm({ storeAddress: event.target.value })}
|
||
/>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<Collapse
|
||
ghost
|
||
size="small"
|
||
className="kuaishou-industry-manual-target"
|
||
items={[
|
||
{
|
||
key: 'manual-target',
|
||
label: selectedVoucher ? '手动指定卡券(清除当前选择后可用)' : '手动指定卡券',
|
||
children: (
|
||
<div className="kuaishou-industry-tool-grid">
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>店铺 ID</span>
|
||
<Input
|
||
disabled={Boolean(selectedVoucher)}
|
||
value={toolForm.sellerId}
|
||
onChange={(event) => updateToolForm({ sellerId: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>订单号 oid</span>
|
||
<Input
|
||
disabled={Boolean(selectedVoucher)}
|
||
value={toolForm.oid}
|
||
onChange={(event) =>
|
||
updateToolForm({
|
||
oid: event.target.value,
|
||
orderId: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>任务 ID</span>
|
||
<Input
|
||
disabled={Boolean(selectedVoucher)}
|
||
value={String(toolForm.taskId || '')}
|
||
onChange={(event) => updateToolForm({ taskId: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field">
|
||
<span>券码</span>
|
||
<Input
|
||
disabled={Boolean(selectedVoucher)}
|
||
value={toolForm.voucherCode}
|
||
onChange={(event) => updateToolForm({ voucherCode: event.target.value })}
|
||
/>
|
||
</label>
|
||
<label className="kuaishou-industry-tool-field kuaishou-industry-tool-field-wide">
|
||
<span>电子凭证类型</span>
|
||
<Input
|
||
disabled={Boolean(selectedVoucher)}
|
||
value={toolForm.eticketType}
|
||
onChange={(event) => updateToolForm({ eticketType: event.target.value })}
|
||
/>
|
||
</label>
|
||
</div>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
|
||
<div className="kuaishou-industry-actions">
|
||
<Button
|
||
icon={<SyncOutlined />}
|
||
loading={actionLoading === 'check'}
|
||
disabled={!hasVoucherTarget}
|
||
onClick={runCheckAvailable}
|
||
>
|
||
检查有效性
|
||
</Button>
|
||
<Button
|
||
icon={<SendOutlined />}
|
||
loading={actionLoading === 'resend'}
|
||
disabled={!hasVoucherTarget}
|
||
onClick={runResendCode}
|
||
>
|
||
重发发码
|
||
</Button>
|
||
<Button
|
||
type="primary"
|
||
icon={<CheckCircleOutlined />}
|
||
loading={actionLoading === 'consume'}
|
||
disabled={!hasVoucherTarget}
|
||
onClick={runConsume}
|
||
>
|
||
手动核销
|
||
</Button>
|
||
<Button
|
||
danger
|
||
icon={<RollbackOutlined />}
|
||
loading={actionLoading === 'reverse'}
|
||
disabled={!hasVoucherTarget}
|
||
onClick={runReverse}
|
||
>
|
||
冲正
|
||
</Button>
|
||
<Popconfirm
|
||
title="确认销毁该券码?"
|
||
description="将先检查快手卡券状态,仅 frozen 状态会发起销毁并关闭关联任务。"
|
||
okText="销毁"
|
||
cancelText="取消"
|
||
disabled={!canManageRefund || !hasVoucherTarget}
|
||
onConfirm={() => runDestroy()}
|
||
>
|
||
<Button
|
||
danger
|
||
type="primary"
|
||
icon={<DeleteOutlined />}
|
||
loading={actionLoading === 'destroy'}
|
||
disabled={!canManageRefund || !hasVoucherTarget}
|
||
>
|
||
手动销毁
|
||
</Button>
|
||
</Popconfirm>
|
||
</div>
|
||
{renderInlineActionResult('vouchers')}
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function renderRefundTab() {
|
||
return (
|
||
<div className="page-stack">
|
||
<Card title="售后单列表">
|
||
<Space wrap className="filter-form">
|
||
{renderSellerIdControl(refundListForm.sellerId, (sellerId) =>
|
||
updateRefundListForm({ sellerId }),
|
||
)}
|
||
<Input
|
||
placeholder="订单号"
|
||
value={refundListForm.orderId}
|
||
onChange={(event) => updateRefundListForm({ orderId: event.target.value })}
|
||
/>
|
||
<AdminRangePicker
|
||
showTime
|
||
value={refundDateRange}
|
||
onChange={(value) => setRefundDateRange(value)}
|
||
/>
|
||
<Select
|
||
style={{ width: 140 }}
|
||
value={refundListForm.type}
|
||
options={[
|
||
{ label: '等待退款', value: '8' },
|
||
{ label: '全部退款', value: '9' },
|
||
]}
|
||
onChange={(value) => updateRefundListForm({ type: value })}
|
||
/>
|
||
<Input
|
||
placeholder="状态"
|
||
value={refundListForm.status}
|
||
onChange={(event) => updateRefundListForm({ status: event.target.value })}
|
||
/>
|
||
<Input
|
||
placeholder="游标"
|
||
value={refundListForm.pcursor}
|
||
onChange={(event) => updateRefundListForm({ pcursor: event.target.value })}
|
||
/>
|
||
<InputNumber
|
||
min={1}
|
||
max={100}
|
||
value={Number(refundListForm.pageSize)}
|
||
onChange={(value) => updateRefundListForm({ pageSize: String(value || 50) })}
|
||
/>
|
||
<Button
|
||
type="primary"
|
||
icon={<SearchOutlined />}
|
||
loading={actionLoading === 'refund-list'}
|
||
onClick={runRefundList}
|
||
>
|
||
查询售后
|
||
</Button>
|
||
</Space>
|
||
<PercentTable
|
||
rowKey={(row) => String(row.refundId || row.oid || JSON.stringify(row))}
|
||
columns={refundColumns}
|
||
dataSource={refundRows}
|
||
scroll={{ x: 1120 }}
|
||
pagination={buildAdminLocalTablePagination({ pageSize: ADMIN_DEFAULT_PAGE_SIZE })}
|
||
style={{ marginTop: 16 }}
|
||
/>
|
||
</Card>
|
||
|
||
<div className="kuaishou-industry-split">
|
||
<Card title="同意退款">
|
||
<div className="kuaishou-industry-tool-grid">
|
||
{renderSellerIdControl(approveForm.sellerId || refundListForm.sellerId, (sellerId) =>
|
||
setApproveForm({ ...approveForm, sellerId }),
|
||
)}
|
||
<Input
|
||
placeholder="退款单编号"
|
||
value={String(approveForm.refundId || '')}
|
||
onChange={(event) =>
|
||
setApproveForm({
|
||
...approveForm,
|
||
refundId: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="退款金额(分)"
|
||
value={String(approveForm.refundAmount || '')}
|
||
onChange={(event) =>
|
||
setApproveForm({
|
||
...approveForm,
|
||
refundAmount: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="退款单状态"
|
||
value={String(approveForm.status || '')}
|
||
onChange={(event) => setApproveForm({ ...approveForm, status: event.target.value })}
|
||
/>
|
||
<Input
|
||
placeholder="协商状态"
|
||
value={String(approveForm.negotiateStatus || '')}
|
||
onChange={(event) =>
|
||
setApproveForm({
|
||
...approveForm,
|
||
negotiateStatus: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="退款方式"
|
||
value={String(approveForm.refundHandingWay || '')}
|
||
onChange={(event) =>
|
||
setApproveForm({
|
||
...approveForm,
|
||
refundHandingWay: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="说明"
|
||
value={approveForm.desc}
|
||
onChange={(event) => setApproveForm({ ...approveForm, desc: event.target.value })}
|
||
/>
|
||
</div>
|
||
<Button
|
||
type="primary"
|
||
icon={<CheckCircleOutlined />}
|
||
loading={actionLoading === 'refund-approve'}
|
||
onClick={runApproveRefund}
|
||
>
|
||
同意退款
|
||
</Button>
|
||
</Card>
|
||
|
||
<Card title="不同意退款">
|
||
<div className="kuaishou-industry-tool-grid">
|
||
{renderSellerIdControl(disagreeForm.sellerId || refundListForm.sellerId, (sellerId) =>
|
||
setDisagreeForm({ ...disagreeForm, sellerId }),
|
||
)}
|
||
<Input
|
||
placeholder="退款单编号"
|
||
value={String(disagreeForm.refundId || '')}
|
||
onChange={(event) =>
|
||
setDisagreeForm({
|
||
...disagreeForm,
|
||
refundId: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="拒绝原因枚举"
|
||
value={String(disagreeForm.sellerDisagreeReason || '')}
|
||
onChange={(event) =>
|
||
setDisagreeForm({
|
||
...disagreeForm,
|
||
sellerDisagreeReason: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="拒绝说明"
|
||
value={disagreeForm.sellerDisagreeDesc}
|
||
onChange={(event) =>
|
||
setDisagreeForm({
|
||
...disagreeForm,
|
||
sellerDisagreeDesc: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="退款单状态"
|
||
value={String(disagreeForm.status || '')}
|
||
onChange={(event) =>
|
||
setDisagreeForm({
|
||
...disagreeForm,
|
||
status: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
<Input
|
||
placeholder="协商状态"
|
||
value={String(disagreeForm.negotiateStatus || '')}
|
||
onChange={(event) =>
|
||
setDisagreeForm({
|
||
...disagreeForm,
|
||
negotiateStatus: event.target.value,
|
||
})
|
||
}
|
||
/>
|
||
</div>
|
||
<Button
|
||
danger
|
||
icon={<RollbackOutlined />}
|
||
loading={actionLoading === 'refund-disagree'}
|
||
onClick={runDisagreeRefund}
|
||
>
|
||
不同意退款
|
||
</Button>
|
||
</Card>
|
||
</div>
|
||
{renderInlineActionResult('refunds')}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function renderInlineActionResult(scope: IndustryActionScope) {
|
||
if (!lastActionResult || lastActionResult.scope !== scope) {
|
||
return null
|
||
}
|
||
|
||
return (
|
||
<div className="kuaishou-industry-action-result">
|
||
{renderActionResultBody(lastActionResult, {
|
||
compact: true,
|
||
onOpenRaw: () => setActiveTab('result'),
|
||
onClear: () => setLastActionResult(null),
|
||
})}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function renderResultTab() {
|
||
return (
|
||
<Card
|
||
title={lastActionResult?.title || '接口结果'}
|
||
extra={
|
||
lastActionResult ? (
|
||
<Button icon={<ReloadOutlined />} onClick={() => setLastActionResult(null)}>
|
||
清空
|
||
</Button>
|
||
) : null
|
||
}
|
||
>
|
||
{lastActionResult ? (
|
||
renderActionResultBody(lastActionResult, { compact: false })
|
||
) : (
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
message="暂无接口结果"
|
||
description="在「券码操作」或「售后退款」执行接口后,结果会展示在对应操作区;这里保留完整原始响应。"
|
||
/>
|
||
)}
|
||
</Card>
|
||
)
|
||
}
|
||
|
||
function renderActionResultBody(
|
||
result: IndustryActionResultView,
|
||
options: {
|
||
compact?: boolean
|
||
onOpenRaw?: () => void
|
||
onClear?: () => void
|
||
} = {},
|
||
) {
|
||
return (
|
||
<Space direction="vertical" size={12} className="full-width">
|
||
<Alert
|
||
type={result.success ? 'success' : 'error'}
|
||
showIcon
|
||
message={
|
||
<Space wrap size={[8, 4]}>
|
||
<span>{result.title}</span>
|
||
<Tag color={result.success ? 'success' : 'error'}>
|
||
{result.success ? '成功' : '失败'}
|
||
</Tag>
|
||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||
{formatAdminDateTime(result.executedAt)}
|
||
</Typography.Text>
|
||
</Space>
|
||
}
|
||
description={result.summary}
|
||
action={
|
||
options.compact ? (
|
||
<Space>
|
||
{options.onOpenRaw ? (
|
||
<Button size="small" type="link" onClick={options.onOpenRaw}>
|
||
原始响应
|
||
</Button>
|
||
) : null}
|
||
{options.onClear ? (
|
||
<Button size="small" type="link" onClick={options.onClear}>
|
||
关闭
|
||
</Button>
|
||
) : null}
|
||
</Space>
|
||
) : undefined
|
||
}
|
||
/>
|
||
|
||
{result.highlights.length > 0 ? (
|
||
<Descriptions
|
||
size="small"
|
||
bordered
|
||
column={options.compact ? 1 : 2}
|
||
items={result.highlights.map((item) => ({
|
||
key: item.label,
|
||
label: item.label,
|
||
children: item.value || '-',
|
||
}))}
|
||
/>
|
||
) : null}
|
||
|
||
<Collapse
|
||
size="small"
|
||
items={[
|
||
{
|
||
key: 'raw',
|
||
label: '原始响应 JSON',
|
||
children: <pre className="json-preview">{stringifyDisplayJson(result.raw)}</pre>,
|
||
},
|
||
]}
|
||
defaultActiveKey={options.compact ? [] : ['raw']}
|
||
/>
|
||
</Space>
|
||
)
|
||
}
|
||
|
||
function updateToolForm(patch: Partial<AdminKuaishouIndustryVoucherToolPayload>) {
|
||
setToolForm((current) => ({ ...current, ...patch }))
|
||
}
|
||
|
||
function updateRefundListForm(patch: Partial<RefundListState>) {
|
||
setRefundListForm((current) => ({ ...current, ...patch }))
|
||
}
|
||
|
||
function applyDefaultSellerId(sellerId: string) {
|
||
setRefundListForm((current) => (current.sellerId ? current : { ...current, sellerId }))
|
||
setApproveForm((current) => (current.sellerId ? current : { ...current, sellerId }))
|
||
setDisagreeForm((current) => (current.sellerId ? current : { ...current, sellerId }))
|
||
setToolForm((current) => (current.sellerId ? current : { ...current, sellerId }))
|
||
}
|
||
|
||
function resolveRefundSellerId(value?: unknown): string {
|
||
return (
|
||
String(value || refundListForm.sellerId || '').trim() ||
|
||
(refundShopOptions.length === 1 ? refundShopOptions[0]?.value || '' : '')
|
||
)
|
||
}
|
||
|
||
function renderSellerIdControl(value: string | undefined, onChange: (sellerId: string) => void) {
|
||
const normalizedValue = String(value || '').trim()
|
||
if (refundShopOptions.length > 0) {
|
||
return (
|
||
<Select
|
||
allowClear
|
||
showSearch
|
||
optionFilterProp="label"
|
||
placeholder="授权店铺"
|
||
style={{ minWidth: 220 }}
|
||
value={normalizedValue || undefined}
|
||
options={refundShopOptions}
|
||
onChange={(nextValue) => onChange(String(nextValue || '').trim())}
|
||
/>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Input
|
||
allowClear
|
||
placeholder="店铺 ID"
|
||
value={normalizedValue}
|
||
onChange={(event) => onChange(event.target.value)}
|
||
/>
|
||
)
|
||
}
|
||
}
|
||
|
||
function renderShopCell(
|
||
shopName?: string | null,
|
||
sellerId?: string | null,
|
||
tokenMasked?: string | null,
|
||
options: { compact?: boolean } = {},
|
||
) {
|
||
const name = String(shopName || '').trim()
|
||
const id = String(sellerId || '').trim()
|
||
const token = String(tokenMasked || '').trim()
|
||
const compact = options.compact === true
|
||
|
||
if (!name && !id) {
|
||
return <span>-</span>
|
||
}
|
||
|
||
const tipParts = [name || null, id ? `ID:${id}` : null, token ? `Token:${token}` : null].filter(
|
||
Boolean,
|
||
)
|
||
const tip = tipParts.join('\n')
|
||
|
||
if (compact) {
|
||
return (
|
||
<div className="cell-stack" title={tip}>
|
||
<Typography.Text ellipsis={{ tooltip: tip }}>{name || id}</Typography.Text>
|
||
{name && id ? <span className="muted">ID:{id}</span> : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="cell-stack">
|
||
<span>{name || id}</span>
|
||
{name && id ? <span className="muted">ID:{id}</span> : null}
|
||
{token ? <span className="muted">Token:{token}</span> : null}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function extractRefundRows(
|
||
response: Record<string, unknown> | null,
|
||
): Array<Record<string, unknown>> {
|
||
const data = response?.data
|
||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||
return []
|
||
}
|
||
|
||
const rows = (data as { refundOrderInfoList?: unknown }).refundOrderInfoList
|
||
return Array.isArray(rows)
|
||
? rows.filter((item): item is Record<string, unknown> =>
|
||
Boolean(item && typeof item === 'object' && !Array.isArray(item)),
|
||
)
|
||
: []
|
||
}
|
||
|
||
function buildIndustryActionResultView(
|
||
kind: IndustryActionKind,
|
||
title: string,
|
||
raw: unknown,
|
||
meta?: { refundCount?: number },
|
||
): IndustryActionResultView {
|
||
const scope: IndustryActionScope = kind.startsWith('refund') ? 'refunds' : 'vouchers'
|
||
const record = asRecord(raw) ?? {}
|
||
const openApi = asOpenApiShape(record)
|
||
const voucher = asRecord(record.voucher)
|
||
const task = asRecord(record.task)
|
||
const response = openApi.response
|
||
const responseData = asRecord(response?.data)
|
||
const success = resolveActionSuccess(kind, record, openApi)
|
||
const platformMessage = pickFirstString([
|
||
response?.error_msg,
|
||
response?.msg,
|
||
response?.sub_msg,
|
||
response?.message,
|
||
openApi.error,
|
||
record.error,
|
||
])
|
||
const platformResult = pickFirstString([response?.result, response?.code, response?.sub_code])
|
||
const highlights: IndustryActionHighlight[] = []
|
||
|
||
pushHighlight(highlights, '接口', title)
|
||
if (openApi.httpStatus) {
|
||
pushHighlight(highlights, 'HTTP', String(openApi.httpStatus))
|
||
}
|
||
if (openApi.durationMs) {
|
||
pushHighlight(highlights, '耗时', `${openApi.durationMs}ms`)
|
||
}
|
||
if (platformResult) {
|
||
pushHighlight(highlights, '平台 result', platformResult)
|
||
}
|
||
if (openApi.skippedReason) {
|
||
pushHighlight(highlights, '跳过原因', openApi.skippedReason)
|
||
}
|
||
|
||
if (kind === 'check') {
|
||
const etickets = extractAvailableEtickets(response)
|
||
pushHighlight(highlights, '返回券数', String(etickets.length || 0))
|
||
const resolvedType = pickFirstString([
|
||
record.eticketType,
|
||
voucher?.eticketType,
|
||
asRecord(openApi.request)?.eticketType,
|
||
asRecord(asRecord(openApi.request)?.param)?.eticketType,
|
||
])
|
||
if (resolvedType) {
|
||
pushHighlight(highlights, '请求类型', resolvedType)
|
||
}
|
||
if (etickets[0]) {
|
||
pushHighlight(
|
||
highlights,
|
||
'首张券码',
|
||
pickFirstString([etickets[0].code, etickets[0].id]) || '-',
|
||
)
|
||
pushHighlight(
|
||
highlights,
|
||
'展示状态',
|
||
pickFirstString([etickets[0].displayStatus, etickets[0].status]) || '-',
|
||
)
|
||
pushHighlight(highlights, '平台类型', pickFirstString([etickets[0].eticketType]) || '-')
|
||
pushHighlight(highlights, '剩余数量', String(etickets[0].leftNum ?? etickets[0].num ?? 1))
|
||
}
|
||
}
|
||
|
||
if (
|
||
kind === 'consume' ||
|
||
kind === 'destroy' ||
|
||
kind === 'reverse' ||
|
||
kind === 'resend' ||
|
||
voucher
|
||
) {
|
||
if (voucher) {
|
||
pushHighlight(highlights, '券码', String(voucher.voucherCode || '-'))
|
||
pushHighlight(highlights, '券状态', getVoucherStatusLabel(String(voucher.status || '')))
|
||
if (voucher.eticketType) {
|
||
pushHighlight(highlights, '凭证类型', String(voucher.eticketType))
|
||
}
|
||
if (kind === 'destroy' && record.reason) {
|
||
pushHighlight(highlights, '销毁原因', String(record.reason))
|
||
}
|
||
if (kind === 'destroy' && typeof record.alreadyDestroyed === 'boolean') {
|
||
pushHighlight(highlights, '已是销毁态', record.alreadyDestroyed ? '是(重试回调)' : '否')
|
||
}
|
||
if (voucher.consumeSerialNum) {
|
||
pushHighlight(highlights, '核销序列号', String(voucher.consumeSerialNum))
|
||
}
|
||
if (voucher.sendCallbackStatus) {
|
||
pushHighlight(highlights, '发码状态', String(voucher.sendCallbackStatus))
|
||
}
|
||
if (voucher.sendCallbackLastError) {
|
||
pushHighlight(highlights, '发码错误', String(voucher.sendCallbackLastError))
|
||
}
|
||
}
|
||
if (task) {
|
||
pushHighlight(highlights, '任务号', String(task.taskNo || task.taskId || '-'))
|
||
pushHighlight(highlights, '任务状态', String(task.status || '-'))
|
||
if (task.deliveryStatus) {
|
||
pushHighlight(highlights, '交付状态', String(task.deliveryStatus))
|
||
}
|
||
}
|
||
}
|
||
|
||
if (kind === 'refund-list') {
|
||
const count =
|
||
typeof meta?.refundCount === 'number' ? meta.refundCount : extractRefundRows(response).length
|
||
pushHighlight(highlights, '售后单数量', String(count))
|
||
const pcursor = pickFirstString([
|
||
responseData?.pcursor,
|
||
responseData?.cursor,
|
||
asRecord(response)?.pcursor,
|
||
])
|
||
if (pcursor) {
|
||
pushHighlight(highlights, '下一页游标', pcursor)
|
||
}
|
||
}
|
||
|
||
if (kind === 'refund-approve' || kind === 'refund-disagree') {
|
||
const refundId = pickFirstString([responseData?.refundId, asRecord(openApi.request)?.refundId])
|
||
if (refundId) {
|
||
pushHighlight(highlights, '退款单号', refundId)
|
||
}
|
||
}
|
||
|
||
if (platformMessage) {
|
||
pushHighlight(highlights, '平台消息', platformMessage)
|
||
}
|
||
|
||
return {
|
||
kind,
|
||
scope,
|
||
title,
|
||
success,
|
||
summary: buildActionSummary(kind, success, {
|
||
platformMessage,
|
||
platformResult,
|
||
refundCount: meta?.refundCount,
|
||
voucherStatus: voucher ? getVoucherStatusLabel(String(voucher.status || '')) : '',
|
||
skippedReason: openApi.skippedReason,
|
||
}),
|
||
highlights,
|
||
raw,
|
||
executedAt: new Date().toISOString(),
|
||
}
|
||
}
|
||
|
||
function buildActionSummary(
|
||
kind: IndustryActionKind,
|
||
success: boolean,
|
||
context: {
|
||
platformMessage: string
|
||
platformResult: string
|
||
refundCount?: number
|
||
voucherStatus: string
|
||
skippedReason: string
|
||
},
|
||
) {
|
||
if (context.skippedReason) {
|
||
return `请求被跳过:${context.skippedReason}${context.platformMessage ? `,${context.platformMessage}` : ''}`
|
||
}
|
||
|
||
if (kind === 'check') {
|
||
return success
|
||
? context.platformMessage || '检查成功,请结合展示状态(如 frozen)判断是否可核销。'
|
||
: context.platformMessage || '检查失败,请核对卖家、券码与电子凭证类型。'
|
||
}
|
||
|
||
if (kind === 'consume') {
|
||
return success
|
||
? `核销成功${context.voucherStatus ? `,券状态:${context.voucherStatus}` : ''}。`
|
||
: context.platformMessage || '核销失败。'
|
||
}
|
||
|
||
if (kind === 'destroy') {
|
||
return success
|
||
? `销毁成功${context.voucherStatus ? `,券状态:${context.voucherStatus}` : ''}。`
|
||
: context.platformMessage || '销毁失败。'
|
||
}
|
||
|
||
if (kind === 'reverse') {
|
||
return success
|
||
? `冲正成功${context.voucherStatus ? `,券状态已回写为 ${context.voucherStatus}` : ''}。`
|
||
: context.platformMessage || '冲正失败。'
|
||
}
|
||
|
||
if (kind === 'resend') {
|
||
return success
|
||
? '发码回调已重发,请确认发码状态是否更新为 success。'
|
||
: context.platformMessage || '重发发码失败。'
|
||
}
|
||
|
||
if (kind === 'refund-list') {
|
||
return success
|
||
? `查询成功,共 ${context.refundCount ?? 0} 条售后单,已填充列表。`
|
||
: context.platformMessage || '查询售后单失败。'
|
||
}
|
||
|
||
if (kind === 'refund-approve') {
|
||
return success
|
||
? context.platformMessage || '同意退款已提交。'
|
||
: context.platformMessage || '同意退款失败。'
|
||
}
|
||
|
||
if (kind === 'refund-disagree') {
|
||
return success
|
||
? context.platformMessage || '不同意退款已提交。'
|
||
: context.platformMessage || '不同意退款失败。'
|
||
}
|
||
|
||
return (
|
||
context.platformMessage ||
|
||
(success ? '接口执行成功。' : '接口执行失败。') +
|
||
(context.platformResult ? `(result=${context.platformResult})` : '')
|
||
)
|
||
}
|
||
|
||
function resolveActionSuccess(
|
||
kind: IndustryActionKind,
|
||
record: Record<string, unknown>,
|
||
openApi: ReturnType<typeof asOpenApiShape>,
|
||
) {
|
||
if (typeof record.success === 'boolean') {
|
||
return record.success
|
||
}
|
||
if (kind === 'consume' || kind === 'destroy') {
|
||
return Boolean(record.voucher)
|
||
}
|
||
return openApi.success
|
||
}
|
||
|
||
function asOpenApiShape(record: Record<string, unknown>) {
|
||
const response = asRecord(record.response)
|
||
return {
|
||
success: Boolean(record.success),
|
||
error: String(record.error || '').trim(),
|
||
response,
|
||
request: asRecord(record.request),
|
||
durationMs: Number(record.durationMs || 0) || 0,
|
||
httpStatus: Number(record.httpStatus || 0) || 0,
|
||
skippedReason: String(record.skippedReason || '').trim(),
|
||
}
|
||
}
|
||
|
||
function extractAvailableEtickets(data: Record<string, unknown> | null) {
|
||
const candidates = [data?.etickets, data?.eTicketList, data?.eticketList, data?.list]
|
||
for (const candidate of candidates) {
|
||
if (!Array.isArray(candidate)) {
|
||
continue
|
||
}
|
||
return candidate
|
||
.map((item) => asRecord(item))
|
||
.filter((item): item is Record<string, unknown> => Boolean(item))
|
||
}
|
||
return [] as Array<Record<string, unknown>>
|
||
}
|
||
|
||
function asRecord(value: unknown): Record<string, unknown> | null {
|
||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||
return null
|
||
}
|
||
return value as Record<string, unknown>
|
||
}
|
||
|
||
function pickFirstString(values: unknown[]) {
|
||
for (const value of values) {
|
||
const text = String(value ?? '').trim()
|
||
if (text) {
|
||
return text
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
function pushHighlight(list: IndustryActionHighlight[], label: string, value: string) {
|
||
if (!value || value === '-') {
|
||
return
|
||
}
|
||
list.push({ label, value })
|
||
}
|
||
|
||
function getVoucherStatusColor(status: string) {
|
||
const normalized = String(status || '').toUpperCase()
|
||
if (normalized === 'CONSUMED') return 'green'
|
||
if (normalized === 'DESTROYED') return 'red'
|
||
return 'blue'
|
||
}
|
||
|
||
function getVoucherStatusLabel(status: string) {
|
||
const normalized = String(status || '').toUpperCase()
|
||
if (normalized === 'CONSUMED') return '已核销'
|
||
if (normalized === 'DESTROYED') return '已销毁'
|
||
if (normalized === 'UNUSED') return '未使用'
|
||
return status || '-'
|
||
}
|
||
|
||
function formatShopOptionLabel(shop: AdminKuaishouIndustryShopOption) {
|
||
const name = String(shop.shopName || '').trim() || '未命名店铺'
|
||
const id = shop.sellerId || shop.shopId || ''
|
||
const disabledSuffix = shop.enabled === false ? ' · 已停用' : ''
|
||
|
||
return id ? `${name}(${id})${disabledSuffix}` : `${name}${disabledSuffix}`
|
||
}
|
||
|
||
function formatTimestamp(value: unknown) {
|
||
const timestamp = Number(value || 0)
|
||
if (!Number.isFinite(timestamp) || timestamp <= 0) {
|
||
return '-'
|
||
}
|
||
|
||
return formatAdminDateTime(new Date(timestamp).toISOString())
|
||
}
|