@@ -14,6 +14,7 @@ import {
Descriptions ,
Input ,
InputNumber ,
Popconfirm ,
Select ,
Space ,
Table ,
@@ -33,7 +34,7 @@ import {
checkAdminKuaishouIndustryVoucherAvailable ,
consumeAdminKuaishouIndustryVoucher ,
disagreeAdminKuaishouIndustryRefund ,
fetchAdminKuaishouIndustrySourceConfig ,
fetchAdminKuaishouIndustryShops ,
fetchAdminKuaishouIndustryVouchers ,
listAdminKuaishouIndustryRefunds ,
resendAdminKuaishouIndustryVoucherCode ,
@@ -44,11 +45,11 @@ import type {
AdminKuaishouIndustryRefundApprovePayload ,
AdminKuaishouIndustryRefundDisagreePayload ,
AdminKuaishouIndustryRefundListPayload ,
AdminKuaishouIndustryShopConfig ,
AdminKuaishouIndustryShopOption ,
AdminKuaishouIndustryVoucher ,
AdminKuaishouIndustryVoucherToolPayload ,
} from '@/types/admin'
import { hasAdminRole } from '@/utils/admin-auth'
import { getAdminRole , hasAdminRole } from '@/utils/admin-auth'
import {
ADMIN_DEFAULT_PAGE_SIZE ,
buildAdminLocalTablePagination ,
@@ -58,7 +59,7 @@ import { formatAdminDateTime } from '@/utils/admin-time'
import { stringifyDisplayJson } from '@/utils/date-time'
type DateRangeValue = [ Dayjs | null , Dayjs | null ] | null
type IndustryTab = 'vouchers' | 'refunds' | 'result'
type IndustryTab = 'support-consume' | ' vouchers' | 'refunds' | 'result'
type IndustryActionScope = 'vouchers' | 'refunds'
type IndustryActionKind =
| 'check'
@@ -109,10 +110,14 @@ type RefundListState = {
const DEFAULT_ETICKET_TYPE = 'DINING_OPEN_TICKET'
export default function AdminKuaishouIndustryPage() {
const [ activeTab , setActiveTab ] = useState < IndustryTab > ( 'vouchers' )
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 [ vouchers , setVouchers ] = useState < AdminKuaishouIndustryVoucher [ ] > ( [ ] )
const [ voucherTotal , setVoucherTotal ] = useState ( 0 )
const [ voucherFilters , setVoucherFilters ] = useState < VoucherFilterState > ( {
@@ -158,7 +163,17 @@ export default function AdminKuaishouIndustryPage() {
} )
const [ lastActionResult , setLastActionResult ] = useState < IndustryActionResultView | null > ( null )
const [ refundRows , setRefundRows ] = useState < Array < Record < string , unknown > > > ( [ ] )
const [ industryShops , setIndustryShops ] = useState < AdminKuaishouIndustryShopConfig [ ] > ( [ ] )
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
@@ -169,6 +184,24 @@ export default function AdminKuaishouIndustryPage() {
} ) ) ,
[ 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 > > (
( ) = > [
@@ -209,14 +242,14 @@ export default function AdminKuaishouIndustryPage() {
) ,
} ,
{
title : '卖家 ' ,
minWidth : 160 ,
render : ( _ , row ) = > (
< div className = "cell-stack" >
< span > { row . sellerId || '- ' } < / span >
< span className = "muted" > Token : { row . tokenMasked || '-' } < / span >
< / div >
) ,
title : '店铺 ' ,
minWidth : 180 ,
render : ( _ , row ) = >
renderShopCell (
row . shopName || shopNameBySellerId . get ( String ( row . sellerId || '' ) . trim ( ) ) || '' ,
row . sellerId ,
isSupportOnly ? '' : row . tokenMasked ,
) ,
} ,
{
title : '核销' ,
@@ -228,30 +261,69 @@ export default function AdminKuaishouIndustryPage() {
< / div >
) ,
} ,
{
title : '更新时间' ,
width : 170 ,
render : ( _ , row ) = > formatAdminDateTime ( row . updatedAt ) ,
} ,
. . . ( isSupportOnly
? [ ]
: [
{
title : '更新时间' ,
width : 170 ,
render : ( _ : unknown , row : AdminKuaishouIndustryVoucher ) = >
formatAdminDateTime ( row . updatedAt ) ,
} ,
] ) ,
{
title : '操作' ,
fixed : 'right' ,
width : 120 ,
render : ( _ , row ) = > (
< Button size = "small" type = "primary" ghost onClick = { ( ) = > selectVoucher ( row ) } >
选 中
< / Button >
) ,
fixed : 'right' as const ,
width : isSupportOnly ? 100 : 120 ,
render : ( _ : unknown , row : AdminKuaishouIndustryVoucher ) = > {
if ( isSupportOnly ) {
const canConsume = row . status === 'UNUSED'
return (
< 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 >
)
}
return (
< Button size = "small" type = "primary" ghost onClick = { ( ) = > selectVoucher ( row ) } >
选 中
< / Button >
)
} ,
} ,
] ,
[ ] ,
[ consumingVoucherId , isSupportOnly , shopNameBySellerId ] ,
)
const refundColumns = useMemo < TableColumnsType < Record < string , unknown > > > (
( ) = > [
{ title : '售后单' , dataIndex : 'refundId' , minWidth : 160 } ,
{ title : '订单' , dataIndex : 'oid' , minWidth : 160 } ,
{ title : '卖家' , dataIndex : 'sellerId' , minWidth : 140 } ,
{
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 } ,
@@ -271,18 +343,18 @@ export default function AdminKuaishouIndustryPage() {
) ,
} ,
] ,
[ ] ,
[ shopNameBySellerId ] ,
)
useEffect ( ( ) = > {
void loadVouchers ( )
void loadIndustryShops ( )
void loadVouchers ( )
} , [ ] )
async function loadIndustryShops() {
try {
const response = await fetchAdminKuaishouIndustrySourceConfig ( )
const shops = response . data . source . shops || [ ]
const response = await fetchAdminKuaishouIndustryShops ( )
const shops = response . data . shops || [ ]
setIndustryShops ( shops )
const enabledSellerIds = shops
@@ -292,7 +364,8 @@ export default function AdminKuaishouIndustryPage() {
applyDefaultSellerId ( enabledSellerIds [ 0 ] as string )
}
} catch ( error ) {
showError ( error instanceof Error ? error . message : '读取快手授权店铺失败' )
// 店铺下拉失败不阻断列表,仅回退为手动输入店铺 ID
console . warn ( error instanceof Error ? error . message : '读取店铺列表失败' )
}
}
@@ -372,6 +445,50 @@ export default function AdminKuaishouIndustryPage() {
}
}
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 : DEFAULT_ETICKET_TYPE ,
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 runResendCode() {
setActionLoading ( 'resend' )
try {
@@ -403,7 +520,7 @@ export default function AdminKuaishouIndustryPage() {
const [ begin , end ] = refundDateRange || [ ]
const sellerId = resolveRefundSellerId ( refundListForm . sellerId )
if ( ! sellerId && refundShopOptions . length > 1 ) {
showError ( '请先选择快手售后退款店铺 sellerId ' )
showError ( '请先选择快手售后退款店铺' )
return
}
@@ -429,7 +546,7 @@ export default function AdminKuaishouIndustryPage() {
async function runApproveRefund() {
const sellerId = resolveRefundSellerId ( approveForm . sellerId )
if ( ! sellerId && refundShopOptions . length > 1 ) {
showError ( '请先选择快手售后退款店铺 sellerId ' )
showError ( '请先选择快手售后退款店铺' )
return
}
@@ -445,7 +562,7 @@ export default function AdminKuaishouIndustryPage() {
async function runDisagreeRefund() {
const sellerId = resolveRefundSellerId ( disagreeForm . sellerId )
if ( ! sellerId && refundShopOptions . length > 1 ) {
showError ( '请先选择快手售后退款店铺 sellerId ' )
showError ( '请先选择快手售后退款店铺' )
return
}
@@ -537,33 +654,47 @@ export default function AdminKuaishouIndustryPage() {
showSuccess ( '已填入退款操作区' )
}
const tabItems : TabsProps [ 'items' ] = [
{
key : 'vouchers' ,
label : '券码操作 ' ,
children : renderVoucherTab ( ) ,
} ,
. . . ( canManageRefund
? [
{
key : 'refunds' ,
label : '售后退款 ' ,
children : renderRefundTab ( ) ,
} ,
]
: [ ] ) ,
{
key : 'result' ,
label : lastActionResult ? ` 接口结果 · ${ lastActionResult . title } ` : '接口结果 ',
children : renderResultTab ( ) ,
} ,
]
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 = { canManageRefund ? '行业电子凭证接口工具与售后处理' : '行业电子凭证接口工具' }
description = {
isSupportOnly
? '客服核销:查询本地券码并执行核销'
: canManageRefund
? '行业电子凭证接口工具与售后处理'
: '行业电子凭证接口工具'
}
/ >
< Tabs
@@ -574,47 +705,63 @@ export default function AdminKuaishouIndustryPage() {
< / div >
)
function renderVoucherTab () {
function renderVoucherFilters () {
return (
< div className = "page-stack" >
< Card >
< Space wrap className = "filter-form" >
< Input
< Card >
< Space wrap className = "filter-form" >
< Input
allowClear
placeholder = "订单号 oid"
value = { voucherFilters . oid }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
oid : event.target.value ,
} )
}
/ >
< Input
allowClear
placeholder = "券码"
value = { voucherFilters . voucherCode }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
voucherCode : event.target.value ,
} )
}
/ >
< Input
allowClear
placeholder = "任务 ID"
value = { voucherFilters . taskId }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
taskId : event.target.value ,
} )
}
/ >
{ shopFilterOptions . length > 0 ? (
< Select
allowClear
placeholder = "订单号 oid"
value = { voucherFilters . oid }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
oid : event.target.value ,
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 = "券码 "
value = { voucherFilters . voucherCode }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
voucherCode : event.target.value ,
} )
}
/ >
< Input
allowClear
placeholder = "任务 ID"
value = { voucherFilters . taskId }
onChange = { ( event ) = >
setVoucherFilters ( {
. . . voucherFilters ,
taskId : event.target.value ,
} )
}
/ >
< Input
allowClear
placeholder = "卖家 ID"
placeholder = "店铺 ID "
value = { voucherFilters . sellerId }
onChange = { ( event ) = >
setVoucherFilters ( {
@@ -623,50 +770,71 @@ export default function AdminKuaishouIndustryPage() {
} )
}
/ >
< Select
allowClear
placeholder = "券码状态"
style = { { width : 132 } }
value = { voucherFilters . status || undefined }
options = { [
{ label : '未使用' , value : 'UNUSED' } ,
{ label : '已核销 ' , value : 'CONSUM ED' } ,
{ label : '已销毁 ' , value : 'DESTROY ED' } ,
] }
onChange = { ( value ) = > setVoucherFilters ( { . . . voucherFilters , status : value || '' } ) }
/ >
< Button
type = "primary"
icon = { < SearchOutlined / > }
loading = { voucherLoading }
onClick = { ( ) = > loadVouchers ( { page : 1 } ) }
>
查 询
< / Button >
< / Space >
< / Card >
) }
< Select
allowClear
placeholder = "券码状态"
style = { { width : 132 } }
value = { voucherFilters . status || undefined }
options = { [
{ label : '未使用 ' , value : 'UNUS ED' } ,
{ label : '已核 销' , value : 'CONSUM ED' } ,
{ label : '已销毁' , value : 'DESTROYED' } ,
] }
onChange = { ( value ) = > setVoucherFilters ( { . . . voucherFilters , status : value || '' } ) }
/ >
< Button
type = "primary"
icon = { < SearchOutlined / > }
loading = { voucherLoading }
onClick = { ( ) = > loadVouchers ( { page : 1 } ) }
>
查 询
< / Button >
< / Space >
< / Card >
)
}
function renderVoucherTable ( scrollX = 1180 ) {
return (
< Table
rowKey = "id"
columns = { columns }
dataSource = { vouchers }
loading = { voucherLoading }
scroll = { { x : scrollX } }
pagination = { buildAdminTablePagination ( {
current : voucherFilters.page ,
pageSize : voucherFilters.pageSize ,
total : voucherTotal ,
onChange : ( page , pageSize ) = > loadVouchers ( { page , pageSize } ) ,
} ) }
/ >
)
}
function renderSupportConsumeTab() {
return (
< div className = "page-stack" >
{ renderVoucherFilters ( ) }
< Card title = "本地券码" > { renderVoucherTable ( 1080 ) } < / Card >
< / div >
)
}
function renderVoucherTab() {
return (
< div className = "page-stack" >
{ renderVoucherFilters ( ) }
< div className = "kuaishou-industry-split" >
< Card title = "本地券码" >
< Table
rowKey = "id"
columns = { columns }
dataSource = { vouchers }
loading = { voucherLoading }
scroll = { { x : 1180 } }
pagination = { buildAdminTablePagination ( {
current : voucherFilters.page ,
pageSize : voucherFilters.pageSize ,
total : voucherTotal ,
onChange : ( page , pageSize ) = > loadVouchers ( { page , pageSize } ) ,
} ) }
/ >
< / Card >
< Card title = "本地券码" > { renderVoucherTable ( ) } < / Card >
< Card title = "接口操作" >
< div className = "kuaishou-industry-tool-grid" >
< Input
placeholder = "卖家 ID"
placeholder = "店铺 ID"
value = { toolForm . sellerId }
onChange = { ( event ) = > updateToolForm ( { sellerId : event.target.value } ) }
/ >
@@ -1115,7 +1283,7 @@ export default function AdminKuaishouIndustryPage() {
return (
< Input
allowClear
placeholder = "卖家 ID"
placeholder = "店铺 ID"
value = { normalizedValue }
onChange = { ( event ) = > onChange ( event . target . value ) }
/ >
@@ -1123,6 +1291,28 @@ export default function AdminKuaishouIndustryPage() {
}
}
function renderShopCell (
shopName? : string | null ,
sellerId? : string | null ,
tokenMasked? : string | null ,
) {
const name = String ( shopName || '' ) . trim ( )
const id = String ( sellerId || '' ) . trim ( )
const token = String ( tokenMasked || '' ) . trim ( )
if ( ! name && ! id ) {
return < span > - < / span >
}
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 > > {
@@ -1409,11 +1599,12 @@ function getVoucherStatusLabel(status: string) {
return status || '-'
}
function formatShopOptionLabel ( shop : AdminKuaishouIndustryShopConfig ) {
const name = shop . customShopName || shop . shopName || '未命名店铺'
const tokenStatus = shop . hasAccessToken ? '' : ' / 未授权 '
function formatShopOptionLabel ( shop : AdminKuaishouIndustryShopOption ) {
const name = String ( shop . shopName || '' ) . trim ( ) || '未命名店铺'
const id = shop . sellerId || shop . shopId || ' '
const disabledSuffix = shop . enabled === false ? ' · 已停用' : ''
return ` ${ name } / ${ shop . sellerId } ${ tokenStatus } `
return id ? ` ${ name } ( ${ id } ) ${ disabledSuffix } ` : ` ${ name } ${ disabledSuffix } `
}
function formatTimestamp ( value : unknown ) {