11import { Dialog , DialogContent , DialogDescription , DialogHeader , DialogTitle , DialogTrigger } from "@/components/ui/dialog" ;
2- import { IconCoin , IconCrown , IconGift , IconLockCode , IconLogout , IconMail , IconUserHexagon , IconCpu , IconPencil , IconCamera } from "@tabler/icons-react" ;
2+ import { IconChevronDown , IconCoin , IconCrown , IconGift , IconLockCode , IconLogout , IconMail , IconUserHexagon , IconCpu , IconPencil , IconCamera } from "@tabler/icons-react" ;
33import { useEffect , useState , useRef , useCallback , type ChangeEvent } from "react" ;
44import { apiRequest } from "@/utils/requestUtils" ;
55import { Input } from "@/components/ui/input" ;
@@ -11,6 +11,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
1111import { Badge } from "@/components/ui/badge" ;
1212import { Api , ConstsTransactionKind , ConstsUploadUsage , type DomainInvitationItem , type DomainTransactionLog } from "@/api/Api" ;
1313import { Item , ItemContent , ItemGroup , ItemSeparator , ItemTitle } from "@/components/ui/item" ;
14+ import Icon from "@/components/common/Icon" ;
1415import dayjs from "dayjs" ;
1516import { cn } from "@/lib/utils" ;
1617import {
@@ -24,6 +25,12 @@ import {
2425 AlertDialogTitle ,
2526} from "@/components/ui/alert-dialog" ;
2627import { Label } from "@/components/ui/label" ;
28+ import {
29+ DropdownMenu ,
30+ DropdownMenuContent ,
31+ DropdownMenuItem ,
32+ DropdownMenuTrigger ,
33+ } from "@/components/ui/dropdown-menu" ;
2734import {
2835 Sidebar ,
2936 SidebarContent ,
@@ -36,7 +43,7 @@ import {
3643 SidebarProvider ,
3744} from "@/components/ui/sidebar" ;
3845import { useCommonData } from "../data-provider" ;
39- import { captchaChallenge , getSubscriptionPlanLabel , getSubscriptionPlanShortLabel , hasProSubscription , isValidEmail } from "@/utils/common" ;
46+ import { captchaChallenge , getBrandFromModelName , getSubscriptionPlanLabel , getSubscriptionPlanShortLabel , hasProSubscription , isValidEmail } from "@/utils/common" ;
4047import { useNavigate } from "react-router-dom" ;
4148
4249interface NavBalanceProps {
@@ -56,19 +63,22 @@ const BALANCE_NAV = [
5663] as const
5764
5865const MODEL_PRICING = [
59- { model : "minimax-m2.7" , credits : 0 } ,
60- { model : "gpt5.4" , credits : 1000 } ,
61- { model : "gpt5.2" , credits : 600 } ,
62- { model : "gpt5.3-codex" , credits : 600 } ,
63- { model : "gpt5.1" , credits : 500 } ,
64- { model : "glm-5.1" , credits : 600 } ,
65- { model : "glm-5" , credits : 400 } ,
66- { model : "glm-4.7" , credits : 200 } ,
67- { model : "qwen3-max" , credits : 400 } ,
68- { model : "qwen3.6-plus" , credits : 200 } ,
69- { model : "kimi-k2.5" , credits : 400 } ,
66+ { model : "minimax-m2.7" , credits : 0 , score : 637 } ,
67+ { model : "qwen3.5-plus" , credits : 0 , score : 538 } ,
68+ { model : "gpt5.4" , credits : 1000 , score : 922 } ,
69+ { model : "gpt5.2" , credits : 600 , score : 887 } ,
70+ { model : "gpt5.3-codex" , credits : 600 , score : 918 } ,
71+ { model : "gpt5.1" , credits : 500 , score : 883 } ,
72+ { model : "glm-5.1" , credits : 600 , score : 904 } ,
73+ { model : "glm-5" , credits : 400 , score : 847 } ,
74+ { model : "glm-4.7" , credits : 200 , score : 709 } ,
75+ { model : "qwen3-max" , credits : 400 , score : 840 } ,
76+ { model : "qwen3.6-plus" , credits : 200 , score : 751 } ,
77+ { model : "kimi-k2.5" , credits : 400 , score : 889 } ,
7078] as const
7179
80+ const TOP_MODEL_COUNT = 3
81+
7282type BalanceSectionId = ( typeof BALANCE_NAV ) [ number ] [ "id" ]
7383type WalletSectionId = BalanceSectionId | "profile" | "plan" | "balance"
7484
@@ -105,6 +115,7 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
105115 const [ newName , setNewName ] = useState ( "" ) ;
106116 const [ changingName , setChangingName ] = useState ( false ) ;
107117 const [ uploadingAvatar , setUploadingAvatar ] = useState ( false ) ;
118+ const [ pricingSort , setPricingSort ] = useState < "value" | "score" | "name" > ( "value" )
108119 const loadMoreRef = useRef < HTMLDivElement > ( null ) ;
109120 const contentScrollRef = useRef < HTMLDivElement > ( null ) ;
110121 const avatarInputRef = useRef < HTMLInputElement > ( null ) ;
@@ -168,6 +179,35 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
168179 return normalized . toString ( )
169180 }
170181 const getInvitationInitial = ( name ?: string ) => name ?. trim ( ) . charAt ( 0 ) . toUpperCase ( ) || "?"
182+ const pricingSortLabel = pricingSort === "value"
183+ ? "性价比排序"
184+ : pricingSort === "score"
185+ ? "代码能力排序"
186+ : "按名字排序"
187+ const topScoreModels = [ ...MODEL_PRICING ]
188+ . sort ( ( a , b ) => b . score - a . score )
189+ . slice ( 0 , TOP_MODEL_COUNT )
190+ const scoreRankMap = new Map ( topScoreModels . map ( ( item , index ) => [ item . model , index + 1 ] ) )
191+ const sortedModelPricing = [ ...MODEL_PRICING ] . sort ( ( a , b ) => {
192+ if ( pricingSort === "name" ) {
193+ return a . model . localeCompare ( b . model )
194+ }
195+
196+ if ( pricingSort === "score" ) {
197+ return b . score - a . score
198+ }
199+
200+ if ( a . credits === 0 && b . credits === 0 ) {
201+ return b . score - a . score
202+ }
203+ if ( a . credits === 0 ) {
204+ return - 1
205+ }
206+ if ( b . credits === 0 ) {
207+ return 1
208+ }
209+ return ( b . score / b . credits ) - ( a . score / a . credits )
210+ } )
171211
172212 const formatSubscriptionExpiry = ( expiresAt ?: string ) => {
173213 if ( ! expiresAt ) {
@@ -1133,32 +1173,69 @@ export default function NavBalance({ variant = "sidebar", hideTrigger = false, t
11331173
11341174 const pricingContent = (
11351175 < div className = "space-y-4" >
1136- < div className = "rounded-md border p-5" >
1137- < div className = "text-md font-medium" > 计费说明</ div >
1138- < div className = "mt-2 text-sm text-muted-foreground" >
1139- 以下价格单位为每百万 token 消耗多少积分。免费模型不会扣减积分。
1176+ < div className = "flex items-center justify-between gap-4" >
1177+ < div className = "text-sm text-muted-foreground" >
1178+ 价格按每百万 Token 计算
11401179 </ div >
1180+ < DropdownMenu >
1181+ < DropdownMenuTrigger asChild >
1182+ < Button type = "button" size = "sm" variant = "outline" className = "gap-2" >
1183+ < span > { pricingSortLabel } </ span >
1184+ < IconChevronDown className = "size-4" />
1185+ </ Button >
1186+ </ DropdownMenuTrigger >
1187+ < DropdownMenuContent align = "end" >
1188+ < DropdownMenuItem onClick = { ( ) => setPricingSort ( "value" ) } >
1189+ 性价比排序
1190+ </ DropdownMenuItem >
1191+ < DropdownMenuItem onClick = { ( ) => setPricingSort ( "score" ) } >
1192+ 代码能力排序
1193+ </ DropdownMenuItem >
1194+ < DropdownMenuItem onClick = { ( ) => setPricingSort ( "name" ) } >
1195+ 按名字排序
1196+ </ DropdownMenuItem >
1197+ </ DropdownMenuContent >
1198+ </ DropdownMenu >
11411199 </ div >
1142- < div className = "grid gap-3 md:grid-cols-2 xl:grid-cols-3" >
1143- { MODEL_PRICING . map ( ( item ) => (
1144- < div key = { item . model } className = "rounded-md border p-4" >
1145- < div className = "flex items-start justify-between gap-3" >
1146- < div className = "min-w-0" >
1147- < div className = "truncate font-mono text-sm font-medium" > { item . model } </ div >
1148- < div className = "mt-1 text-xs text-muted-foreground" > 每百万 token</ div >
1149- </ div >
1150- < div
1151- className = { cn (
1152- "shrink-0 rounded-full px-2.5 py-1 text-xs font-medium" ,
1153- item . credits === 0 ? "bg-muted text-muted-foreground" : "bg-primary/10 text-primary" ,
1154- ) }
1155- >
1156- { item . credits === 0 ? "免费" : `${ formatPoints ( item . credits ) } 积分` }
1157- </ div >
1158- </ div >
1200+ < ItemGroup className = "flex flex-col gap-0 has-data-[size=sm]:gap-0 has-data-[size=xs]:gap-0" >
1201+ { sortedModelPricing . map ( ( item , index ) => (
1202+ < div key = { item . model } >
1203+ < Item variant = "default" size = "sm" className = "px-2 py-3" >
1204+ < ItemContent >
1205+ < ItemTitle className = "grid w-full grid-cols-[minmax(0,1.6fr)_minmax(120px,0.8fr)_minmax(100px,0.6fr)] items-center gap-4" >
1206+ < div className = "flex min-w-0 items-center gap-2" >
1207+ < Icon name = { getBrandFromModelName ( item . model ) } className = "size-4 shrink-0" />
1208+ < div className = "truncate font-mono text-sm font-medium" > { item . model } </ div >
1209+ </ div >
1210+ < div >
1211+ < Badge
1212+ variant = { item . credits === 0 ? "default" : "outline" }
1213+ className = { cn (
1214+ "h-6 rounded-full px-2.5 text-xs font-medium" ,
1215+ item . credits === 0 && "hover:bg-primary" ,
1216+ ) }
1217+ >
1218+ { item . credits === 0 ? "免费" : `${ formatPoints ( item . credits ) } 积分 / 1M` }
1219+ </ Badge >
1220+ </ div >
1221+ < div className = "flex justify-end" >
1222+ < Badge
1223+ variant = { scoreRankMap . has ( item . model ) ? "default" : "secondary" }
1224+ className = { cn (
1225+ "h-6 rounded-full px-2.5 text-xs font-medium tabular-nums" ,
1226+ scoreRankMap . has ( item . model ) && "bg-amber-500 text-white hover:bg-amber-500" ,
1227+ ) }
1228+ >
1229+ { item . score } 分
1230+ </ Badge >
1231+ </ div >
1232+ </ ItemTitle >
1233+ </ ItemContent >
1234+ </ Item >
1235+ { index < sortedModelPricing . length - 1 && < ItemSeparator className = "my-0" /> }
11591236 </ div >
11601237 ) ) }
1161- </ div >
1238+ </ ItemGroup >
11621239 </ div >
11631240 )
11641241
0 commit comments