@@ -1,47 +1,30 @@
import { App , Button , DatePicker , Drawer , Form , Input , Modal , Select , Tabs , Tag } from "antd" ;
import { Alert , App, Button , DatePicker , Select , Tabs , Tag } from "antd" ;
import { Tooltip } from "@/pages/admin/ui/controls" ;
import { useCountUp } from "@/hooks/use-count-up" ;
import { useCallback , useEffect , useMemo , useState , type ReactNode } from "react" ;
import type { ColumnsType } from "antd/es/table" ;
import dayjs , { type Dayjs } from "dayjs" ;
import { AlertTriangle , BarChart3 , CircleDollarSign , Clock3 , Gauge , Pencil , Plus , RefreshCw , Settings2 , Trash2 , UsersRound , Workflow } from "lucide-react" ;
import { AlertTriangle , BarChart3 , CircleDollarSign , Clock3 , Gauge , RefreshCw , UsersRound , Workflow } from "lucide-react" ;
import { Area , CartesianGrid , ComposedChart , Legend , Line , ResponsiveContainer , Tooltip as ChartTooltip , XAxis , YAxis } from "recharts" ;
import { useSearchParams } from "react-router" ;
import { ListToolbar , PaginationBar , AdminDataTable , AdminExportButton , AdminFilterChip , AdminRowActions , AdminStatusBadge , AdminTableEmpty , type AdminStatusTone } from "./admin-ui" ;
import { ListToolbar , PaginationBar , AdminDataTable , AdminExportButton , AdminFilterChip , AdminStatusBadge , AdminTableEmpty , type AdminStatusTone } from "./admin-ui" ;
import {
createAdminModelPricing ,
deleteAdminModelPricing ,
exportAdminAnalytics ,
getAdminAnalytics ,
listAdminUsers ,
listAdminModelPricings ,
updateAdminModelPricing ,
type AdminReferenceData ,
type AdminAnalytics ,
type AnalyticsFilters ,
type ModelPricing ,
} from "@/services/api/auth" ;
import { analyticsFinanceColumns , formatCredits , formatFinanceCost , formatFinanceMargin } from "./analytics-finance" ;
type Props = {
users : AdminReferenceData [ "users" ] ;
channels : AdminReferenceData [ "channels" ] ;
} ;
type PricingFormValues = {
channelId? : string ;
model : string ;
capability : ModelPricing [ "capability" ] ;
currency : string ;
inputPerMillion? : string ;
outputPerMillion? : string ;
cachedPerMillion? : string ;
perRequest? : string ;
perMedia? : string ;
perVideoSecond? : string ;
} ;
type TrendMetric = "volume" | "quality" | "activity" ;
type AnalysisTab = "models" | "users" | "failures" ;
type RangePreset = "7d" | "30d" | "60d" ;
@@ -63,23 +46,14 @@ export default function AnalyticsPanel({ users, channels }: Props) {
const [ channelId , setChannelId ] = useState ( searchParams . get ( "channelId" ) || undefined ) ;
const [ capability , setCapability ] = useState ( searchParams . get ( "capability" ) || undefined ) ;
const [ data , setData ] = useState < AdminAnalytics | null > ( null ) ;
const [ pricings , setPricings ] = useState < ModelPricing [ ] > ( [ ] ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ pricingModalOpen , setPricingModalOpen ] = useState ( false ) ;
const [ editingPricing , setEditingPricing ] = useState < ModelPricing | null > ( null ) ;
const [ savingPricing , setSavingPricing ] = useState ( false ) ;
const [ userOptions , setUserOptions ] = useState ( users ) ;
const [ searchingUsers , setSearchingUsers ] = useState ( false ) ;
const [ modelPage , setModelPage ] = useState ( 1 ) ;
const [ userPage , setUserPage ] = useState ( 1 ) ;
const [ failurePage , setFailurePage ] = useState ( 1 ) ;
const [ pricingPage , setPricingPage ] = useState ( 1 ) ;
const [ trendMetric , setTrendMetric ] = useState < TrendMetric > ( "volume" ) ;
const [ analysisTab , setAnalysisTab ] = useState < AnalysisTab > ( "models" ) ;
const [ pricingWorkspaceOpen , setPricingWorkspaceOpen ] = useState ( false ) ;
const [ pendingPricing , setPendingPricing ] = useState < ModelPricing | null | undefined > ( undefined ) ;
const [ form ] = Form . useForm < PricingFormValues > ( ) ;
const pricingChannelId = Form . useWatch ( "channelId" , form ) ;
const analyticsPageSize = 20 ;
const filters = useMemo < AnalyticsFilters > (
@@ -97,9 +71,8 @@ export default function AnalyticsPanel({ users, channels }: Props) {
const reload = useCallback ( async ( ) = > {
setLoading ( true ) ;
try {
const [ analytics , pricingData ] = await Promise . all ( [ getAdminAnalytics ( filters ) , listAdminModelPricings ( ) ] ) ;
const analytics = await getAdminAnalytics ( filters ) ;
setData ( analytics ) ;
setPricings ( pricingData . pricings ) ;
} catch ( error ) {
message . error ( error instanceof Error ? error . message : "读取统计数据失败" ) ;
} finally {
@@ -123,7 +96,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
setModelPage ( 1 ) ;
setUserPage ( 1 ) ;
setFailurePage ( 1 ) ;
setPricingPage ( 1 ) ;
} , [ filters ] ) ;
useEffect ( ( ) = > {
@@ -149,105 +121,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
return [ . . . names ] . sort ( ) . map ( ( name ) = > ( { label : name , value : name } ) ) ;
} , [ channels , data ? . models ] ) ;
const pricingModelOptions = useMemo ( ( ) = > {
const names = new Set < string > ( ) ;
const sourceChannels = channels . filter ( ( channel ) = > channel . enabled !== false && ( ! pricingChannelId || channel . id === pricingChannelId ) ) ;
sourceChannels . forEach ( ( channel ) = > channel . models ? . forEach ( ( name ) = > names . add ( name ) ) ) ;
if ( editingPricing ? . model && ( ! pricingChannelId || editingPricing . channelId === pricingChannelId ) ) {
names . add ( editingPricing . model ) ;
}
return [ . . . names ] . sort ( ) . map ( ( name ) = > ( { label : name , value : name } ) ) ;
} , [ channels , editingPricing ? . channelId , editingPricing ? . model , pricingChannelId ] ) ;
const preparePricingForm = ( pricing : ModelPricing | null ) = > {
setEditingPricing ( pricing ) ;
form . resetFields ( ) ;
form . setFieldsValue (
pricing
? {
channelId : pricing.channelId || undefined ,
model : pricing.model ,
capability : pricing.capability ,
currency : pricing.currency ,
inputPerMillion : formatPriceInput ( pricing . inputPerMillionMicros ) ,
outputPerMillion : formatPriceInput ( pricing . outputPerMillionMicros ) ,
cachedPerMillion : formatPriceInput ( pricing . cachedPerMillionMicros ) ,
perRequest : formatPriceInput ( pricing . perRequestMicros ) ,
perMedia : formatPriceInput ( pricing . perMediaMicros ) ,
perVideoSecond : formatPriceInput ( pricing . perVideoSecondMicros ) ,
}
: { channelId : undefined , model : "" , capability : "text" , currency : "USD" , inputPerMillion : "" , outputPerMillion : "" , cachedPerMillion : "" , perRequest : "" , perMedia : "" , perVideoSecond : "" } ,
) ;
setPricingModalOpen ( true ) ;
} ;
const openPricing = ( pricing? : ModelPricing ) = > {
const nextPricing = pricing || null ;
if ( pricingWorkspaceOpen ) {
setPendingPricing ( nextPricing ) ;
setPricingWorkspaceOpen ( false ) ;
} else {
preparePricingForm ( nextPricing ) ;
}
} ;
const handlePricingValuesChange = ( changedValues : Partial < PricingFormValues > , values : PricingFormValues ) = > {
if ( Object . prototype . hasOwnProperty . call ( changedValues , "channelId" ) ) {
const nextModels = channels . filter ( ( channel ) = > channel . enabled !== false && ( ! values . channelId || channel . id === values . channelId ) ) . flatMap ( ( channel ) = > channel . models || [ ] ) ;
if ( values . model && ! nextModels . includes ( values . model ) ) form . setFieldValue ( "model" , undefined ) ;
return ;
}
if ( ! Object . prototype . hasOwnProperty . call ( changedValues , "model" ) || ! values . model ) return ;
const matchingChannels = channels . filter ( ( channel ) = > channel . enabled !== false && channel . models ? . includes ( values . model ) ) ;
if ( values . channelId && matchingChannels . some ( ( channel ) = > channel . id === values . channelId ) ) return ;
if ( matchingChannels . length ) form . setFieldValue ( "channelId" , matchingChannels [ 0 ] . id ) ;
} ;
const handlePricingModelChange = ( modelName : string ) = > {
const currentChannelId = form . getFieldValue ( "channelId" ) as string | undefined ;
const matchingChannels = channels . filter ( ( channel ) = > channel . enabled !== false && channel . models ? . includes ( modelName ) ) ;
const matchingChannel = matchingChannels . find ( ( channel ) = > channel . id === currentChannelId ) || matchingChannels [ 0 ] ;
form . setFieldsValue ( { model : modelName , channelId : matchingChannel?.id || currentChannelId } ) ;
} ;
const savePricing = async ( ) = > {
const values = await form . validateFields ( ) ;
const payload = {
channelId : values.channelId || "" ,
model : values.model.trim ( ) ,
capability : values.capability ,
currency : values.currency.trim ( ) . toUpperCase ( ) ,
inputPerMillionMicros : toMicros ( values . inputPerMillion ) ,
outputPerMillionMicros : toMicros ( values . outputPerMillion ) ,
cachedPerMillionMicros : toMicros ( values . cachedPerMillion ) ,
perRequestMicros : toMicros ( values . perRequest ) ,
perMediaMicros : toMicros ( values . perMedia ) ,
perVideoSecondMicros : toMicros ( values . perVideoSecond ) ,
} ;
setSavingPricing ( true ) ;
try {
const result = editingPricing ? await updateAdminModelPricing ( editingPricing . id , payload ) : await createAdminModelPricing ( payload ) ;
setPricings ( ( items ) = > ( editingPricing ? items . map ( ( item ) = > ( item . id === result . pricing . id ? result.pricing : item ) ) : [ . . . items , result . pricing ] ) ) ;
setPricingModalOpen ( false ) ;
message . success ( "模型价格已保存,后续调用将按新价格记录费用快照" ) ;
} catch ( error ) {
message . error ( error instanceof Error ? error . message : "保存价格失败" ) ;
} finally {
setSavingPricing ( false ) ;
}
} ;
const removePricing = async ( id : string ) = > {
try {
await deleteAdminModelPricing ( id ) ;
setPricings ( ( items ) = > items . filter ( ( item ) = > item . id !== id ) ) ;
message . success ( "价格配置已删除" ) ;
} catch ( error ) {
message . error ( error instanceof Error ? error . message : "删除价格失败" ) ;
}
} ;
const modelColumns : ColumnsType < AdminAnalytics [ "models" ] [ number ] > = [
{
title : "模型" ,
@@ -270,7 +143,7 @@ export default function AnalyticsPanel({ users, channels }: Props) {
{ title : "P50 / P95" , width : 145 , render : ( _ , row ) = > ` ${ formatDuration ( row . p50DurationMs ) } / ${ formatDuration ( row . p95DurationMs ) } ` } ,
{ title : "Token( 入 / 出 / 缓存)" , width : 190 , render : ( _ , row ) = > ( row . usageAvailable ? ` ${ formatNumber ( row . inputTokens ) } / ${ formatNumber ( row . outputTokens ) } / ${ formatNumber ( row . cachedTokens ) } ` : "--" ) } ,
{ title : "媒体 / 视频秒" , width : 125 , render : ( _ , row ) = > ` ${ row . mediaCount } / ${ row . videoSeconds } ` } ,
{ title : "估算费用" , width : 120 , render : ( _ , row ) = > formatCost ( row . estimatedCostMicros , row . currency , row . costAvailable ) } ,
. . . analyticsFinanceColumns ,
] ;
const userColumns : ColumnsType < AdminAnalytics [ "users" ] [ number ] > = [
@@ -301,60 +174,16 @@ export default function AnalyticsPanel({ users, channels }: Props) {
{ title : "最近发生" , dataIndex : "lastSeenAt" , width : 170 , render : ( value ) = > dayjs ( value ) . format ( "YYYY-MM-DD HH:mm" ) } ,
] ;
const pricingColumns : ColumnsType < ModelPricing > = [
{
title : "模型" ,
dataIndex : "model" ,
width : 210 ,
render : ( value , row ) = > (
< div >
< div className = "font-medium" > { value } < / div >
< div className = "text-xs text-foreground/45" > { row . channelId ? channels . find ( ( channel ) = > channel . id === row . channelId ) ? . name || row . channelId : "全部渠道" } < / div >
< / div >
) ,
} ,
{ title : "能力" , dataIndex : "capability" , width : 90 , render : capabilityLabel } ,
{
title : "输入 / 输出 / 缓存(每百万 Token) " ,
width : 250 ,
render : ( _ , row ) = > ` ${ formatMoney ( fromMicros ( row . inputPerMillionMicros ) , row . currency ) } / ${ formatMoney ( fromMicros ( row . outputPerMillionMicros ) , row . currency ) } / ${ formatMoney ( fromMicros ( row . cachedPerMillionMicros ) , row . currency ) } ` ,
} ,
{
title : "每请求 / 每媒体 / 每视频秒" ,
width : 220 ,
render : ( _ , row ) = > ` ${ formatMoney ( fromMicros ( row . perRequestMicros ) , row . currency ) } / ${ formatMoney ( fromMicros ( row . perMediaMicros ) , row . currency ) } / ${ formatMoney ( fromMicros ( row . perVideoSecondMicros ) , row . currency ) } ` ,
} ,
{
title : "操作" ,
width : 170 ,
render : ( _ , row ) = > (
< AdminRowActions
primary = { { label : "编辑" , icon : < Pencil className = "size-3.5" / > , onClick : ( ) = > openPricing ( row ) } }
actions = { [
{
key : "delete" ,
label : "删除" ,
icon : < Trash2 className = "size-3.5" / > ,
danger : true ,
confirm : { title : "删除价格配置?" , description : "删除后新的调用不再使用这条价格配置,历史费用不受影响。" , okText : "确认删除" } ,
onClick : ( ) = > removePricing ( row . id ) ,
} ,
] }
/ >
) ,
} ,
] ;
const trend = data ? . trend || [ ] ;
const currentTrend = trend [ trend . length - 1 ] ;
const previousTrend = trend [ trend . length - 2 ] ;
const modelRows = data ? . models || [ ] ;
const userRows = data ? . users || [ ] ;
const failureRows = data ? . failures || [ ] ;
const pricingRows = pricings ;
const failureTotal = failureRows . reduce ( ( sum , item ) = > sum + item . count , 0 ) ;
const topFailure = failureRows . reduce < AdminAnalytics [ "failures" ] [ number ] | undefined > ( ( current , item ) = > ( ! current || item . count > current . count ? item : current ) , undefined ) ;
const pricedModelCount = modelRows . filter ( ( item ) = > item . costAvailable ) . length ;
const finance = data ? . kpi . finance ;
const financeUnavailable = data !== null && ( ! finance || modelRows . some ( ( row ) = > ! row . finance ) ) ;
const trendHasData = trendMetric === "volume" ? trend . some ( ( item ) = > item . tasks > 0 || item . requests > 0 ) : trendMetric === "quality" ? trend . some ( ( item ) = > item . requests > 0 ) : trend . some ( ( item ) = > item . activeUsers > 0 ) ;
const trendTitle = trendMetric === "volume" ? "任务与请求趋势" : trendMetric === "quality" ? "请求质量趋势" : "用户活跃趋势" ;
const pageRows = < T , > ( rows : T [ ] , page : number ) = > rows . slice ( ( page - 1 ) * analyticsPageSize , page * analyticsPageSize ) ;
@@ -443,6 +272,8 @@ export default function AnalyticsPanel({ users, channels }: Props) {
< / div >
< / ListToolbar >
{ financeUnavailable && < Alert type = "warning" showIcon title = "后端未返回完整财务统计,缺失金额显示为 --。请确认后端已更新并重启后刷新。" / > }
< section className = "admin-analytics-health-grid" aria-label = "运营健康指标" >
< AnalyticsHealthCard
icon = { < UsersRound className = "size-4" / > }
@@ -468,10 +299,14 @@ export default function AnalyticsPanel({ users, channels }: Props) {
/ >
< AnalyticsHealthCard
icon = { < CircleDollarSign className = "size-4" / > }
label = "估算 费用"
value = { data ? ( data . kpi . costAvailable ? formatCost ( data . kpi . estimatedCostMicros , data . kpi . currency , true ) : "待配置" ) : "--" }
detail = { data ? ( data . kpi . costAvailable ? ` ${ pricedModelCount } / ${ modelRows . length } 个模型可估算 ` : "价格未完整配置,暂不能汇总" ) : undefined }
tone = { data ? . kpi . costAvailable ? "neutral" : "warning" }
label = "费用统计(积分) "
value = { finance ? < dl className = "admin-analytics-finance-values" >
< div > < dt > 收 入 < / dt > < dd > { formatCredits ( finance . revenueMicrocredits ) } < / dd > < / div >
< div > < dt > 成 本 < / dt > < dd > { formatFinanceCost ( finance ) } < / dd > < / div >
< div > < dt > 利 润 < / dt > < dd > { formatCredits ( finance . profitMicrocredits ) } < / dd > < / div >
< / dl > : "--" }
detail = { finance ? ` 已结算 ${ finance . settledOrders } 笔 · 利润率 ${ formatFinanceMargin ( finance . profitMargin ) } ` : undefined }
tone = { finance && ( finance . costedOrders < finance . settledOrders || ( finance . profitMicrocredits ? ? 0 ) < 0 ) ? "warning" : "neutral" }
/ >
< / section >
@@ -553,11 +388,11 @@ export default function AnalyticsPanel({ users, channels }: Props) {
/ >
< AnalyticsAttentionItem
icon = { < CircleDollarSign className = "size-4" / > }
label = "费用可估算模型 "
value = { data ? ` ${ pricedModelCount } / ${ modelRows . length } ` : "--" }
description = { data ? . kpi . costAvailable ? "当前范围费用可以汇总" : "仍有调用无法完成费用估算 "}
tone = { data ? . kpi . costAvailable ? "success" : "warning" }
onClick = { ( ) = > setPricingWorkspaceOpen ( true ) }
label = "成本覆盖订单 "
value = { finance ? ` ${ finance . costedOrders } / ${ finance . settledOrders } ` : "--" }
description = { ! finance ? "财务统计尚未返回" : finance . settledOrders ? ( finance . costedOrders === finance . settledOrders ? "已结算订单成本完整" : ` ${ finance . settledOrders - finance . costedOrders } 笔缺少成本快照或有效用量 ` ) : "当前范围暂无已结算订单 "}
tone = { finance && finance . costedOrders < finance . settledOrders ? "warning" : "neutral" }
onClick = { ( ) = > openAnalysis ( "models" ) }
/ >
< / div >
< / aside >
@@ -569,9 +404,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
< h2 > 深 度 分 析 < / h2 >
< p > 按 模 型 、 用 户 或 异 常 类 型 继 续 核 对 当 前 统 计 范 围 。 < / p >
< / div >
< Button icon = { < Settings2 className = "size-4" / > } onClick = { ( ) = > setPricingWorkspaceOpen ( true ) } >
模 型 价 格 配 置
< / Button >
< / div >
< Tabs
activeKey = { analysisTab }
@@ -583,9 +415,9 @@ export default function AnalyticsPanel({ users, channels }: Props) {
label : "模型分析" ,
children : (
< AdminDataTable
table = { { rowKey : ( row ) = > ` ${ row . model } : ${ row . capability } ` , size : "small" , loading , columns : modelColumns , dataSource : pageRows ( modelRows , modelPage ) , pagination : false , scroll : { x : 125 0 } } }
table = { { rowKey : ( row ) = > ` ${ row . model } : ${ row . capability } ` , size : "small" , loading , columns : modelColumns , dataSource : pageRows ( modelRows , modelPage ) , pagination : false , scroll : { x : 160 0 } } }
empty = { < AdminTableEmpty / > }
skeletonColumns = { 9 }
skeletonColumns = { 12 }
footer = { < PaginationBar alwaysShow current = { modelPage } pageSize = { analyticsPageSize } total = { modelRows . length } onChange = { ( page ) = > setModelPage ( page ) } pageSizeOptions = { [ analyticsPageSize ] } / > }
/ >
) ,
@@ -618,76 +450,6 @@ export default function AnalyticsPanel({ users, channels }: Props) {
/ >
< / section >
< Drawer
rootClassName = "admin-secondary-drawer admin-analytics-pricing-drawer"
title = "模型价格配置"
open = { pricingWorkspaceOpen }
width = "min(1120px, calc(100vw - 48px))"
onClose = { ( ) = > setPricingWorkspaceOpen ( false ) }
afterOpenChange = { ( open ) = > {
if ( open || pendingPricing === undefined ) return ;
const nextPricing = pendingPricing ;
setPendingPricing ( undefined ) ;
preparePricingForm ( nextPricing ) ;
} }
extra = {
< Button type = "primary" icon = { < Plus className = "size-4" / > } onClick = { ( ) = > openPricing ( ) } >
新 增 价 格
< / Button >
}
>
< p className = "admin-analytics-pricing-description" > 价 格 只 影 响 后 续 调 用 的 费 用 快 照 , 历 史 统 计 不 会 被 重 新 计 算 。 < / p >
< AdminDataTable
table = { { rowKey : "id" , size : "small" , loading , columns : pricingColumns , dataSource : pageRows ( pricingRows , pricingPage ) , pagination : false , scroll : { x : 980 } } }
empty = { < AdminTableEmpty title = "暂无模型价格" description = "新增价格后,后续调用才能形成可汇总的费用快照。" / > }
skeletonColumns = { 5 }
footer = { < PaginationBar alwaysShow current = { pricingPage } pageSize = { analyticsPageSize } total = { pricingRows . length } onChange = { ( page ) = > setPricingPage ( page ) } pageSizeOptions = { [ analyticsPageSize ] } / > }
/ >
< / Drawer >
< Modal
rootClassName = "admin-modal-root admin-analytics-pricing-modal"
title = { editingPricing ? "编辑模型价格" : "新增模型价格" }
open = { pricingModalOpen }
onCancel = { ( ) = > setPricingModalOpen ( false ) }
onOk = { ( ) = > void savePricing ( ) }
confirmLoading = { savingPricing }
okText = "保存"
cancelText = "取消"
width = { 760 }
zIndex = { 1200 }
destroyOnHidden
>
< Form form = { form } layout = "vertical" requiredMark = { false } onValuesChange = { handlePricingValuesChange } >
< div className = "grid grid-cols-1 gap-x-4 sm:grid-cols-2" >
< Form.Item name = "model" label = "模型" rules = { [ { required : true , message : "请选择模型" } ] } >
< Select
showSearch
optionFilterProp = "label"
placeholder = { pricingModelOptions . length ? "选择已启用模型" : "暂无已启用模型" }
options = { pricingModelOptions }
disabled = { ! pricingModelOptions . length }
onChange = { handlePricingModelChange }
/ >
< / Form.Item >
< Form.Item name = "channelId" label = "渠道范围" >
< Select allowClear placeholder = "全部渠道" options = { channels . filter ( ( channel ) = > channel . enabled !== false ) . map ( ( channel ) = > ( { label : channel.name , value : channel.id } ) ) } / >
< / Form.Item >
< Form.Item name = "capability" label = "能力类型" rules = { [ { required : true } ] } >
< Select options = { capabilityOptions } / >
< / Form.Item >
< Form.Item name = "currency" label = "币种" rules = { [ { required : true } ] } >
< Input maxLength = { 12 } / >
< / Form.Item >
< PriceInput name = "inputPerMillion" label = "每百万输入 Token" / >
< PriceInput name = "outputPerMillion" label = "每百万输出 Token" / >
< PriceInput name = "cachedPerMillion" label = "每百万缓存 Token" / >
< PriceInput name = "perRequest" label = "每次请求" / >
< PriceInput name = "perMedia" label = "每个输出媒体" / >
< PriceInput name = "perVideoSecond" label = "每视频秒" / >
< / div >
< / Form >
< / Modal >
< / div >
) ;
}
@@ -765,20 +527,6 @@ function FilterSelect({
) ;
}
function PriceInput ( { name , label } : { name : keyof PricingFormValues ; label : string } ) {
return (
< Form.Item className = "admin-analytics-price-field" name = { name } label = { ` ${ label } (币种单位) ` } rules = { [ { validator : validatePriceInput } ] } >
< Input aria-label = { ` ${ label } 价格 ` } disabled = { false } readOnly = { false } inputMode = "decimal" maxLength = { 30 } className = "admin-analytics-price-input" style = { { width : "100%" } } / >
< / Form.Item >
) ;
}
function validatePriceInput ( _ : unknown , value? : string ) {
const normalized = value ? . trim ( ) || "" ;
if ( ! normalized || /^(?:\d+(?:\.\d{0,6})?|\.\d{1,6})$/ . test ( normalized ) ) return Promise . resolve ( ) ;
return Promise . reject ( new Error ( "请输入非负价格,最多 6 位小数" ) ) ;
}
function capabilityLabel ( value : string ) {
return capabilityOptions . find ( ( item ) = > item . value === value ) ? . label || "未分类" ;
}
@@ -811,32 +559,6 @@ function formatRateDelta(current?: number, previous?: number) {
return { value : ` 较前一日 ${ direction } ${ Math . abs ( delta ) . toFixed ( 1 ) } pp ` , tone : delta >= 0 ? ( "success" as const ) : ( "warning" as const ) } ;
}
function formatCost ( micros : number , currency? : string , available? : boolean ) {
return available ? formatMoney ( fromMicros ( micros ) , currency || "USD" ) : "--" ;
}
function formatMoney ( value : number , currency = "USD" ) {
if ( currency === "MIXED" ) return ` ${ value . toFixed ( 6 ) } (混合币种) ` ;
try {
return new Intl . NumberFormat ( "zh-CN" , { style : "currency" , currency , minimumFractionDigits : 2 , maximumFractionDigits : 6 } ) . format ( value ) ;
} catch {
return ` ${ currency } ${ value . toFixed ( 6 ) } ` ;
}
}
function fromMicros ( value : number ) {
return value / 1 _000_000 ;
}
function formatPriceInput ( micros : number ) {
return fromMicros ( micros ) . toFixed ( 6 ) ;
}
function toMicros ( value? : string | number ) {
const parsed = typeof value === "number" ? value : Number ( value ? . trim ( ) || 0 ) ;
return Number . isFinite ( parsed ) && parsed >= 0 ? Math . round ( parsed * 1 _000_000 ) : 0 ;
}
function filterDate ( value : string | null , fallback : Dayjs ) {
if ( ! value ) return fallback ;
const parsed = dayjs ( value ) ;