@@ -1,4 +1,4 @@
import { App , Button , Modal , Spin } from "antd" ;
import { App , Button , InputNumber , Modal , Select , Spin } from "antd" ;
import { ArrowDown , ArrowUp , GripVertical , ListOrdered } from "lucide-react" ;
import { useRef , useState } from "react" ;
import { AdminEmpty } from "@/pages/admin/components/admin-ui" ;
@@ -6,7 +6,7 @@ import { getChannelOrder, saveChannelOrder, type ChannelOrderItem } from "@/serv
export function moveOrderItem < T extends { id : string } > ( items : T [ ] , id : string , target : number ) : T [ ] {
const from = items . findIndex ( ( item ) = > item . id === id ) ;
if ( from < 0 || target < 0 || target >= items . length || from === target ) return items ;
if ( from < 0 || ! Number . isInteger ( target ) || target < 0 || target >= items . length || from === target ) return items ;
const next = [ . . . items ] ;
const [ item ] = next . splice ( from , 1 ) ;
next . splice ( target , 0 , item ! ) ;
@@ -20,6 +20,8 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
[ saving , setSaving ] = useState ( false ) ;
const [ items , setItems ] = useState < ChannelOrderItem [ ] > ( [ ] ) ,
[ original , setOriginal ] = useState < string [ ] > ( [ ] ) ;
const [ targetId , setTargetId ] = useState < string > ( ) ;
const [ targetPosition , setTargetPosition ] = useState < number | null > ( null ) ;
const dragged = useRef < string | null > ( null ) ,
pending = useRef ( false ) ;
const changed = items . some ( ( item , index ) = > item . id !== original [ index ] ) ;
@@ -28,6 +30,8 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
setLoading ( true ) ;
setItems ( [ ] ) ;
setOriginal ( [ ] ) ;
setTargetId ( undefined ) ;
setTargetPosition ( null ) ;
try {
const result = await getChannelOrder ( channelId ) ;
setItems ( result . items ) ;
@@ -50,8 +54,12 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
original ,
) ;
setOpen ( false ) ;
await onSaved ( ) ;
message . success ( "排序已保存,用户端按此顺序展示" ) ;
try {
await onSaved ( ) ;
} catch ( error ) {
message . warning ( error instanceof Error ? ` 排序已保存,但列表刷新失败: ${ error . message } ` : "排序已保存,请刷新页面查看" ) ;
}
} catch ( error ) {
message . error ( error instanceof Error ? error . message : "保存排序失败" ) ;
} finally {
@@ -62,10 +70,10 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
return (
< >
< Button icon = { < ListOrdered className = "size-4" / > } onClick = { ( ) = > void start ( ) } >
设 置 排 序
{ channelId ? "自定义排序" : "设置排序" }
< / Button >
< Modal
title = { channelId ? "调整 模型顺 序" : "调整渠道顺序" }
title = { channelId ? "自定义 模型排 序" : "调整渠道顺序" }
open = { open }
onCancel = { ( ) = > setOpen ( false ) }
closable = { ! saving && ! loading }
@@ -83,7 +91,31 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
< / div >
}
>
< p className = "mb-4 text-sm text-foreground/60" > 拖 动 条 目 或 点 击 上 下 箭 头 调 整 顺 序 , 保 存 后 用 户 端 按 此 顺 序 展 示 。 这 里 包 含 全 部 条 目 , 不 受 列 表 筛 选 和 分 页 影 响 。 < / p >
< p className = "mb-4 text-sm text-foreground/60" > 选 择 条 目 并 输 入 目 标 位 置 , 或 拖 动 、 点 击 上 下 箭 头 调 整 顺 序 。 点 击 “ 保 存 排 序 ” 后 生 效 , 取 消 会 放 弃 本 次 调 整 。 这 里 包 含 全 部 条 目 , 不 受 列 表 筛 选 和 分 页 影 响 。 < / p >
< div className = "mb-4 flex flex-wrap items-center gap-2" >
< Select
className = "min-w-40 flex-1"
aria-label = { channelId ? "选择要排序的模型" : "选择要排序的渠道" }
placeholder = { channelId ? "搜索并选择模型" : "搜索并选择渠道" }
showSearch = { { optionFilterProp : "label" } }
value = { targetId }
disabled = { loading || saving || ! items . length }
options = { items . map ( ( item , index ) = > ( { value : item.id , label : ` ${ index + 1 } . ${ item . name } ${ item . enabled ? "" : "(已停用)" } ` } ) ) }
onChange = { ( id : string ) = > {
setTargetId ( id ) ;
setTargetPosition ( items . findIndex ( ( item ) = > item . id === id ) + 1 ) ;
} }
/ >
< InputNumber aria-label = "目标位置" placeholder = "位置" min = { 1 } max = { items . length || 1 } precision = { 0 } value = { targetPosition } disabled = { loading || saving || ! targetId } onChange = { setTargetPosition } / >
< Button
disabled = { loading || saving || ! targetId || targetPosition === null || ! Number . isInteger ( targetPosition ) || targetPosition < 1 || targetPosition > items . length }
onClick = { ( ) = > {
if ( targetId && targetPosition !== null ) setItems ( ( current ) = > moveOrderItem ( current , targetId , targetPosition - 1 ) ) ;
} }
>
移 动 到 此 位 置
< / Button >
< / div >
< Spin spinning = { loading } >
< div className = "max-h-[55vh] overflow-y-auto space-y-2" role = "list" aria-label = "展示顺序" >
{ ! loading && ! items . length ? (
@@ -113,6 +145,7 @@ export function ChannelOrderDialog({ channelId, onSaved }: { channelId?: string;
className = "flex items-center gap-3 rounded-lg border border-border bg-background p-3"
>
< GripVertical aria - hidden className = "size-4 shrink-0 cursor-grab text-foreground/40" / >
< span className = "shrink-0 text-xs tabular-nums text-foreground/50" > { index + 1 } < / span >
< span className = "min-w-0 flex-1 break-words" >
{ item . name }
{ ! item . enabled && < span className = "ml-2 text-xs text-foreground/45" > 已 停 用 < / span > }