@@ -19,18 +19,153 @@ import type {
const { RangePicker } = DatePicker ;
const STATUS_COLOR : Record < string , string > = { success : 'green' , failed : 'red' , cancelled : 'default' } ;
const STATUS_LABEL : Record < string , string > = { success : '成功' , failed : '失败' , cancelled : '中途退出' } ;
// 「卡在哪一步」: platform_results[*].status 翻成人话(找店/加菜/起送/读价)
const STUCK_LABEL : Record < string , string > = {
success : '成功 ' ,
store_not_found : '没找到店 ' ,
items_not_found : '菜没匹配上 ' ,
below_minimum : '未达起送 ' ,
unsupported : '平台不支持 ' ,
failed : '失败 ' ,
// 主状态只对外呈现生命周期口径;below_minimum/store_closed 是迁移前历史兼容值。
const STATUS_COLOR : Record < string , string > = {
success : 'green' ,
below_minimum : 'green' ,
failed : 'red' ,
store_closed : 'red ' ,
store_not_found : 'red ' ,
items_not_found : 'red ' ,
no_delivery : 'red ' ,
unsupported : 'red ' ,
cancelled : 'default ' ,
running : 'blue' ,
} ;
const STATUS_LABEL : Record < string , string > = {
success : '成功' ,
below_minimum : '成功' ,
failed : '失败' ,
store_closed : '失败' ,
store_not_found : '失败' ,
items_not_found : '失败' ,
no_delivery : '失败' ,
unsupported : '失败' ,
cancelled : '中途退出' ,
running : '进行中' ,
} ;
type PlatformResultRow = Record < string , unknown > & {
platform_id? : string ;
platform_name? : string ;
status? : string ;
reason? : string ;
} ;
function isRecord ( value : unknown ) : value is Record < string , unknown > {
return value != null && typeof value === 'object' && ! Array . isArray ( value ) ;
}
function platformResultRows (
platforms : Record < string , unknown > [ ] | null | undefined ,
rawPayload : Record < string , unknown > | null | undefined ,
) : PlatformResultRow [ ] {
if ( ( platforms ? . length ? ? 0 ) > 0 ) return platforms as PlatformResultRow [ ] ;
const rawPlatforms = rawPayload ? . platforms ;
if ( Array . isArray ( rawPlatforms ) ) {
const rows = rawPlatforms . filter ( isRecord ) as PlatformResultRow [ ] ;
if ( rows . length > 0 ) return rows ;
}
// 灰度期旧记录没有 platforms,才回退 platform_results。这里的 success/source 是旧枚举,
// 展示时兼容折算为逐平台成功态 ok。
const resultValue = rawPayload ? . platform_results ;
if ( Array . isArray ( resultValue ) ) return resultValue . filter ( isRecord ) as PlatformResultRow [ ] ;
if ( isRecord ( resultValue ) ) {
return Object . entries ( resultValue ) . flatMap ( ( [ platformId , result ] ) = > (
isRecord ( result ) ? [ { platform_id : platformId , . . . result } as PlatformResultRow ] : [ ]
) ) ;
}
return [ ] ;
}
function stringList ( value : unknown ) : string [ ] {
return Array . isArray ( value ) ? value . filter ( ( item ) : item is string = > typeof item === 'string' ) : [ ] ;
}
function hasDishDiff ( row : PlatformResultRow ) : boolean {
if ( typeof row . has_dish_diff === 'boolean' ) return row . has_dish_diff ;
const items = Array . isArray ( row . items ) ? row . items . filter ( isRecord ) : [ ] ;
return items . some ( ( item ) = > item . similar === true )
|| stringList ( row . approx_dish_names ) . length > 0
|| stringList ( row . skipped_dish_names ) . length > 0
|| Number ( row . skipped_dish_count || 0 ) > 0 ;
}
type PlatformDisplay = {
label : string ;
color : string ;
rawStatus : string ;
category : string ;
dishDiff : boolean ;
fallbackReason : string ;
} ;
function platformDisplay ( row : PlatformResultRow ) : PlatformDisplay {
const rawStatus = typeof row . status === 'string' && row . status ? row . status : 'unknown' ;
const legacySource = rawStatus === 'source' ;
const normalizedStatus = rawStatus === 'success' || legacySource ? 'ok' : rawStatus ;
const dishDiff = hasDishDiff ( row ) ;
if ( normalizedStatus === 'ok' ) {
const isSource = row . role === 'source' || row . is_user_original === true || legacySource ;
const isLowest = row . is_best === true && ! dishDiff ;
return {
label : '成功' ,
color : 'green' ,
rawStatus ,
category : isLowest ? '#1 全网最低赢家' : isSource ? '#2 原选择' : '#3 其他成功' ,
dishDiff ,
fallbackReason : dishDiff ? '已比出价格,但存在菜品差异,仅供参考' : '已成功比出价格' ,
} ;
}
const known : Record < string , Omit < PlatformDisplay , 'rawStatus' | 'dishDiff' > > = {
below_minimum : { label : '未满起送' , color : 'gold' , category : '#5 未满起送' , fallbackReason : '购物车未达到起送门槛' } ,
store_closed : { label : '门店打烊' , color : 'orange' , category : '#6 门店打烊' , fallbackReason : '门店当前已打烊,无法比价' } ,
items_not_found : { label : '商品未找到' , color : 'orange' , category : '#7 没有您点的商品' , fallbackReason : '该店没有找到本次所选商品' } ,
store_not_found : { label : '店铺未找到' , color : 'orange' , category : '#8 无对应商家' , fallbackReason : '未找到可对应的商家' } ,
failed : { label : '比价失败' , color : 'red' , category : '#9 失败兜底' , fallbackReason : '本平台比价流程未完成' } ,
unsupported : { label : '比价失败' , color : 'red' , category : '#9 失败兜底' , fallbackReason : '当前版本暂不支持该平台' } ,
no_delivery : { label : '单点不配送' , color : 'orange' , category : '表外 · 单点不配送' , fallbackReason : '所选商品不支持单点配送' } ,
not_installed : { label : '未安装' , color : 'default' , category : '#10 端侧本地 · 未安装' , fallbackReason : '该状态仅由 App 端根据装机情况补齐' } ,
not_compared_this_time : { label : '本次未比' , color : 'default' , category : '#11 端侧本地 · 未选择' , fallbackReason : '该状态仅由 App 端根据本次勾选情况补齐' } ,
} ;
const mapped = known [ normalizedStatus ] ;
if ( mapped ) return { . . . mapped , rawStatus , dishDiff : false } ;
return {
label : '比价失败' ,
color : 'red' ,
rawStatus ,
category : '#9 未知状态兜底' ,
dishDiff : false ,
fallbackReason : ` 端侧未识别状态 ${ rawStatus } ,按比价失败兜底 ` ,
} ;
}
function dishDiffSummary ( row : PlatformResultRow ) : string | null {
if ( ! hasDishDiff ( row ) ) return null ;
const parts : string [ ] = [ ] ;
const items = Array . isArray ( row . items ) ? row . items . filter ( isRecord ) : [ ] ;
const similarPairs = items
. filter ( ( item ) = > item . similar === true && typeof item . name === 'string' )
. map ( ( item ) = > ( typeof item . orig === 'string' ? ` ${ item . orig } → ${ item . name } ` : String ( item . name ) ) ) ;
if ( similarPairs . length ) parts . push ( ` 近似替换: ${ similarPairs . join ( '、' ) } ` ) ;
const approx = stringList ( row . approx_dish_names ) ;
if ( approx . length ) parts . push ( ` 规格近似: ${ approx . join ( '、' ) } ` ) ;
const skipped = stringList ( row . skipped_dish_names ) ;
if ( skipped . length ) parts . push ( ` 完全缺失: ${ skipped . join ( '、' ) } ` ) ;
if ( ! skipped . length && Number ( row . skipped_dish_count || 0 ) > 0 ) {
parts . push ( ` 缺少 ${ Number ( row . skipped_dish_count ) } 个菜品 ` ) ;
}
return parts . join ( '; ' ) || '存在菜品差异' ;
}
function complexSpecSummary ( row : PlatformResultRow ) : string | null {
const names = stringList ( row . complex_spec_dish_names ) ;
return names . length > 0 ? ` 复杂规格需核对: ${ names . join ( '、' ) } ` : null ;
}
const fmtMs = ( ms : number | null ) = > ( ms == null ? '-' : ` ${ ( ms / 1000 ) . toFixed ( 1 ) } s ` ) ;
const cents = ( c : number | null ) = > ( c == null ? '-' : yuan ( c ) ) ;
@@ -369,8 +504,7 @@ export default function ComparisonRecordsPage() {
} ,
] ;
const platformResults =
( detail ? . raw_payload ? . platform_results as Record < string , unknown > [ ] | undefined ) || [ ] ;
const platformResults = platformResultRows ( detail ? . platforms , detail ? . raw_payload ) ;
return (
< div >
@@ -423,6 +557,7 @@ export default function ComparisonRecordsPage() {
{ value : 'success' , label : '成功' } ,
{ value : 'failed' , label : '失败' } ,
{ value : 'cancelled' , label : '中途退出' } ,
{ value : 'running' , label : '进行中' } ,
] }
/ >
< Button type = "primary" onClick = { search } > 查 询 < / Button >
@@ -579,16 +714,67 @@ export default function ComparisonRecordsPage() {
) }
{ platformResults . length > 0 && (
< Card size = "small" title = "逐平台结局(卡在哪一步) " >
< Card size = "small" title = "逐平台结果卡片判定 " >
< Typography.Paragraph type = "secondary" style = { { marginBottom : 12 } } >
优 先 按 platforms [ ] . status = ok 及 is_best 、 role 、 has_dish_diff 判 定 ; # 10 未 安 装 和 # 11 本 次 未 比
由 App 端 本 地 补 齐 , 正 常 情 况 下 后 台 没 有 对 应 行 。
< / Typography.Paragraph >
< Table
size = "small"
rowKey = { ( _ , i ) = > String ( i ) }
pagination = { false }
dataSource = { platformResults }
scroll = { { x : 1050 } }
columns = { [
{ title : '平台' , dataIndex : 'platform_name' , render : ( v , r : Record < string , unknown > ) = > ( v as string ) || ( r . platform_id as string ) || '-' } ,
{ title : '结局 ' , dataIndex : 'status' , render : ( s : string ) = > < Tag color = { s === 'success' ? 'green' : 'orange' } > { STUCK_LABEL [ s ] || s || '-' } < / Tag > } ,
{ title : '说明' , dataIndex : 'reason' , render : ( v ) = > ( v as string ) || '-' } ,
{
title : '平台 ' , dataIndex : 'platform_name' , width : 150 ,
render : ( v , r : PlatformResultRow ) = > ( v as string ) || r . platform_id || '-' ,
} ,
{
title : '逐平台状态' , dataIndex : 'status' , width : 135 ,
render : ( _ , r : PlatformResultRow ) = > {
const display = platformDisplay ( r ) ;
return (
< Space direction = "vertical" size = { 0 } >
< Tag color = { display . color } > { display . label } < / Tag >
< Typography.Text type = "secondary" code > { display . rawStatus } < / Typography.Text >
< / Space >
) ;
} ,
} ,
{
title : '卡片分类' , key : 'category' , width : 210 ,
render : ( _ , r : PlatformResultRow ) = > {
const display = platformDisplay ( r ) ;
const hasComplexSpec = complexSpecSummary ( r ) != null ;
return (
< Space size = { [ 4 , 4 ] } wrap >
< Tag color = { display . color } > { display . category } < / Tag >
{ display . dishDiff ? < Tag color = "gold" > # 4 菜 品 差 异 < / Tag > : null }
{ hasComplexSpec ? < Tag color = "blue" > 复 杂 规 格 待 核 对 < / Tag > : null }
< / Space >
) ;
} ,
} ,
{
title : '价格' , dataIndex : 'price' , width : 90 ,
render : ( v ) = > ( typeof v === 'number' ? ` ¥ ${ v . toFixed ( 2 ) } ` : '-' ) ,
} ,
{
title : '说明 / 判定字段' , key : 'reason' ,
render : ( _ , r : PlatformResultRow ) = > {
const display = platformDisplay ( r ) ;
const diff = dishDiffSummary ( r ) ;
const complexSpec = complexSpecSummary ( r ) ;
return (
< Space direction = "vertical" size = { 0 } >
< span > { typeof r . reason === 'string' && r . reason ? r.reason : display.fallbackReason } < / span >
{ diff ? < Typography.Text type = "warning" > { diff } < / Typography.Text > : null }
{ complexSpec ? < Typography.Text type = "secondary" > { complexSpec } < / Typography.Text > : null }
< / Space >
) ;
} ,
} ,
] }
/ >
< / Card >