@@ -0,0 +1,198 @@
import { useEffect , useState } from 'react' ;
import { Button , Descriptions , Drawer , Form , Input , Segmented , Space , Table , Tag , Typography } from 'antd' ;
import type { ColumnsType } from 'antd/es/table' ;
import { useSearchParams } from 'react-router-dom' ;
import {
STORE_LOG_CATEGORY_OPTIONS ,
STORE_LOG_CATEGORY_LABELS ,
resolveStoreLogCategory ,
type StoreLogCategory ,
} from '../lib/store-log' ;
import { request } from '../lib/api' ;
import { fmtTime } from '../lib/constants' ;
import { useAdminList } from '../lib/useAdminList' ;
type Row = {
id : string ;
source : 'analytics' | 'redeem_record' | 'store_payout' ;
storeId : string ;
storeAccountId : string | null ;
storeName : string | null ;
accountName : string | null ;
accountPhone : string | null ;
category : StoreLogCategory | null ;
eventName : string ;
clientApp : string | null ;
refType : string | null ;
refId : string | null ;
extraJson : Record < string , unknown > | null ;
createdAt : string ;
} ;
const SOURCE_LABELS : Record < Row [ 'source' ] , string > = {
analytics : '行为埋点' ,
redeem_record : '核销记录' ,
store_payout : '打款记录' ,
} ;
function summarizeExtra ( json : Record < string , unknown > | null ) {
if ( ! json ) return '—' ;
const text = JSON . stringify ( json ) ;
return text . length > 80 ? ` ${ text . slice ( 0 , 80 ) } … ` : text ;
}
function parseCompositeId ( id : string ) {
const idx = id . indexOf ( ':' ) ;
if ( idx <= 0 ) return null ;
return { source : id.slice ( 0 , idx ) , rawId : id.slice ( idx + 1 ) } ;
}
export default function StoreLogsPage() {
const [ searchParams , setSearchParams ] = useSearchParams ( ) ;
const [ form ] = Form . useForm ( ) ;
const [ category , setCategory ] = useState ( searchParams . get ( 'category' ) ? ? '' ) ;
const [ filters , setFilters ] = useState < Record < string , string > > ( ( ) = > ( {
storeId : searchParams.get ( 'storeId' ) ? ? '' ,
storeAccountId : searchParams.get ( 'storeAccountId' ) ? ? '' ,
phone : searchParams.get ( 'phone' ) ? ? '' ,
storeName : searchParams.get ( 'storeName' ) ? ? '' ,
eventName : searchParams.get ( 'eventName' ) ? ? '' ,
} ) ) ;
const { data , loading , page , pageSize , setPage , setPageSize } = useAdminList < Row > (
'/admin/logs/stores' ,
( ) = > {
const qs = new URLSearchParams ( ) ;
if ( filters . storeId ) qs . set ( 'storeId' , filters . storeId ) ;
if ( filters . storeAccountId ) qs . set ( 'storeAccountId' , filters . storeAccountId ) ;
if ( filters . phone ) qs . set ( 'phone' , filters . phone ) ;
if ( filters . storeName ) qs . set ( 'storeName' , filters . storeName ) ;
if ( filters . eventName ) qs . set ( 'eventName' , filters . eventName ) ;
if ( category ) qs . set ( 'category' , category ) ;
return qs ;
} ,
[ filters , category ] ,
) ;
const [ detail , setDetail ] = useState < Record < string , unknown > | null > ( null ) ;
const [ drawerOpen , setDrawerOpen ] = useState ( false ) ;
useEffect ( ( ) = > {
form . setFieldsValue ( filters ) ;
} , [ form , filters ] ) ;
const columns : ColumnsType < Row > = [
{ title : '时间' , dataIndex : 'createdAt' , width : 160 , render : fmtTime } ,
{
title : '门店' , width : 180 ,
render : ( _ , r ) = > (
< div >
< div > { r . storeName || '—' } < / div >
< div style = { { color : '#999' , fontSize : 12 } } > { r . storeId } < / div >
< / div >
) ,
} ,
{
title : '账号' , width : 150 ,
render : ( _ , r ) = > (
< div >
< div > { r . accountName || '—' } < / div >
< div style = { { color : '#999' , fontSize : 12 } } > { r . accountPhone || r . storeAccountId || '系统/HQ' } < / div >
< / div >
) ,
} ,
{
title : '分类' , dataIndex : 'category' , width : 100 ,
render : ( v : StoreLogCategory | null , r ) = > (
< Tag > { STORE_LOG_CATEGORY_LABELS [ v ? ? '' ] || resolveStoreLogCategory ( r . eventName ) || '其他' } < / Tag >
) ,
} ,
{ title : '事件' , dataIndex : 'eventName' , width : 160 } ,
{
title : '来源' , dataIndex : 'source' , width : 100 ,
render : ( v : Row [ 'source' ] ) = > SOURCE_LABELS [ v ] || v ,
} ,
{
title : '关联' , width : 120 ,
render : ( _ , r ) = > ( r . refType && r . refId ? ` ${ r . refType } # ${ r . refId } ` : '—' ) ,
} ,
{
title : '摘要' , ellipsis : true ,
render : ( _ , r ) = > summarizeExtra ( r . extraJson ) ,
} ,
{
title : '操作' , width : 80 ,
render : ( _ , row ) = > (
< Button type = "link" size = "small" onClick = { async ( ) = > {
const parsed = parseCompositeId ( row . id ) ;
if ( ! parsed ) return ;
setDetail ( await request ( ` /admin/logs/stores/ ${ parsed . source } / ${ parsed . rawId } ` ) ) ;
setDrawerOpen ( true ) ;
} } > 详 情 < / Button >
) ,
} ,
] ;
return (
< div >
< Typography.Title level = { 4 } > 商 户 日 志 < / Typography.Title >
< Typography.Paragraph type = "secondary" style = { { marginTop : - 8 } } >
门 店 登 录 、 微 信 授 权 、 核 销 、 打 款 与 营 业 状 态 等 操 作 记 录 ; 历 史 核 销 / 打 款 数 据 来 自 业 务 表 归 档 。
< / Typography.Paragraph >
< Segmented
style = { { marginBottom : 16 } }
options = { STORE_LOG_CATEGORY_OPTIONS . map ( ( o ) = > ( { value : o.value , label : o.label } ) ) }
value = { category }
onChange = { ( v ) = > {
setCategory ( String ( v ) ) ;
setPage ( 1 ) ;
const next = new URLSearchParams ( searchParams ) ;
if ( v ) next . set ( 'category' , String ( v ) ) ;
else next . delete ( 'category' ) ;
setSearchParams ( next ) ;
} }
/ >
< Form form = { form } layout = "inline" style = { { marginBottom : 16 } } onFinish = { ( v ) = > {
setFilters ( v ) ;
setPage ( 1 ) ;
const next = new URLSearchParams ( searchParams ) ;
for ( const key of [ 'storeId' , 'storeAccountId' , 'phone' , 'storeName' , 'eventName' ] as const ) {
if ( v [ key ] ) next . set ( key , v [ key ] ) ;
else next . delete ( key ) ;
}
setSearchParams ( next ) ;
} } >
< Form.Item name = "storeId" label = "门店ID" > < Input allowClear style = { { width : 120 } } / > < / Form.Item >
< Form.Item name = "storeName" label = "门店名称" > < Input allowClear style = { { width : 140 } } / > < / Form.Item >
< Form.Item name = "storeAccountId" label = "账号ID" > < Input allowClear style = { { width : 120 } } / > < / Form.Item >
< Form.Item name = "phone" label = "手机号" > < Input allowClear style = { { width : 130 } } / > < / Form.Item >
< Form.Item name = "eventName" label = "事件名" > < Input allowClear style = { { width : 160 } } placeholder = "store_login_success" / > < / Form.Item >
< Form.Item > < Button type = "primary" htmlType = "submit" > 筛 选 < / Button > < / Form.Item >
< Form.Item > < Button onClick = { ( ) = > { form . resetFields ( ) ; setFilters ( { } ) ; setCategory ( '' ) ; setSearchParams ( { } ) ; setPage ( 1 ) ; } } > 重 置 < / Button > < / Form.Item >
< / Form >
< Table rowKey = "id" loading = { loading } columns = { columns } dataSource = { data ? . items ? ? [ ] } scroll = { { x : 1200 } }
pagination = { { current : page , pageSize , total : data?.total ? ? 0 , showSizeChanger : true , onChange : ( p , ps ) = > { setPage ( p ) ; setPageSize ( ps ) ; } } } / >
< Drawer title = "商户日志详情" width = { 560 } open = { drawerOpen } onClose = { ( ) = > setDrawerOpen ( false ) } >
{ detail && (
< Descriptions column = { 1 } bordered size = "small" >
< Descriptions.Item label = "ID" > { String ( detail . id ) } < / Descriptions.Item >
< Descriptions.Item label = "门店" > { String ( detail . storeName || detail . storeId || '—' ) } < / Descriptions.Item >
< Descriptions.Item label = "账号" > { String ( detail . accountName || detail . storeAccountId || '系统/HQ' ) } < / Descriptions.Item >
< Descriptions.Item label = "手机" > { String ( detail . accountPhone || '—' ) } < / Descriptions.Item >
< Descriptions.Item label = "分类" >
{ ( STORE_LOG_CATEGORY_LABELS as Record < string , string > ) [ String ( detail . category ? ? '' ) ] || String ( detail . category || '—' ) }
< / Descriptions.Item >
< Descriptions.Item label = "事件" > { String ( detail . eventName ) } < / Descriptions.Item >
< Descriptions.Item label = "来源" > { SOURCE_LABELS [ String ( detail . source ) as Row [ 'source' ] ] || String ( detail . source || '—' ) } < / Descriptions.Item >
< Descriptions.Item label = "客户端" > { String ( detail . clientApp || '—' ) } < / Descriptions.Item >
< Descriptions.Item label = "关联" > { detail . refType ? ` ${ String ( detail . refType ) } # ${ String ( detail . refId ) } ` : '—' } < / Descriptions.Item >
< Descriptions.Item label = "时间" > { fmtTime ( String ( detail . createdAt ) ) } < / Descriptions.Item >
< Descriptions.Item label = "参数" >
< pre style = { { margin : 0 , whiteSpace : 'pre-wrap' , wordBreak : 'break-all' } } >
{ JSON . stringify ( detail . extraJson ? ? { } , null , 2 ) }
< / pre >
< / Descriptions.Item >
< / Descriptions >
) }
< / Drawer >
< / div >
) ;
}