上传完整的WebUI前端仓库
This commit is contained in:
105
dashboard/src/routes/config/model/components/ModelCardList.tsx
Normal file
105
dashboard/src/routes/config/model/components/ModelCardList.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 模型列表 - 移动端卡片视图
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Pencil, Trash2 } from 'lucide-react'
|
||||
import type { ModelInfo } from '../types'
|
||||
|
||||
interface ModelCardListProps {
|
||||
/** 当前页显示的模型 (分页后的) */
|
||||
paginatedModels: ModelInfo[]
|
||||
/** 所有模型列表 (未分页) */
|
||||
allModels: ModelInfo[]
|
||||
/** 编辑模型回调 */
|
||||
onEdit: (model: ModelInfo, index: number) => void
|
||||
/** 删除模型回调 */
|
||||
onDelete: (index: number) => void
|
||||
/** 检查模型是否被使用 */
|
||||
isModelUsed: (modelName: string) => boolean
|
||||
/** 搜索关键词 */
|
||||
searchQuery: string
|
||||
}
|
||||
|
||||
export const ModelCardList = React.memo(function ModelCardList({
|
||||
paginatedModels,
|
||||
allModels,
|
||||
onEdit,
|
||||
onDelete,
|
||||
isModelUsed,
|
||||
searchQuery,
|
||||
}: ModelCardListProps) {
|
||||
if (paginatedModels.length === 0) {
|
||||
return (
|
||||
<div className="md:hidden text-center text-muted-foreground py-8 rounded-lg border bg-card">
|
||||
{searchQuery ? '未找到匹配的模型' : '暂无模型配置'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="md:hidden space-y-3">
|
||||
{paginatedModels.map((model, displayIndex) => {
|
||||
const actualIndex = allModels.findIndex(m => m === model)
|
||||
const used = isModelUsed(model.name)
|
||||
return (
|
||||
<div key={displayIndex} className="rounded-lg border bg-card p-4 space-y-3">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="font-semibold text-base">{model.name}</h3>
|
||||
<Badge
|
||||
variant={used ? "default" : "secondary"}
|
||||
className={used ? "bg-green-600 hover:bg-green-700" : ""}
|
||||
>
|
||||
{used ? '已使用' : '未使用'}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground break-all" title={model.model_identifier}>
|
||||
{model.model_identifier}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onEdit(model, actualIndex)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onDelete(actualIndex)}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">提供商</span>
|
||||
<p className="font-medium">{model.api_provider}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">模型温度</span>
|
||||
<p className="font-medium">{model.temperature != null ? model.temperature : <span className="text-muted-foreground">默认</span>}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">输入价格</span>
|
||||
<p className="font-medium">¥{model.price_in}/M</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground text-xs">输出价格</span>
|
||||
<p className="font-medium">¥{model.price_out}/M</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
142
dashboard/src/routes/config/model/components/ModelTable.tsx
Normal file
142
dashboard/src/routes/config/model/components/ModelTable.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 模型列表 - 桌面端表格视图
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { Pencil, Trash2 } from 'lucide-react'
|
||||
import type { ModelInfo } from '../types'
|
||||
|
||||
interface ModelTableProps {
|
||||
/** 当前页显示的模型 (分页后的) */
|
||||
paginatedModels: ModelInfo[]
|
||||
/** 所有模型列表 (未分页) */
|
||||
allModels: ModelInfo[]
|
||||
/** 过滤后的模型列表 */
|
||||
filteredModels: ModelInfo[]
|
||||
/** 已选中的模型索引集合 */
|
||||
selectedModels: Set<number>
|
||||
/** 编辑模型回调 */
|
||||
onEdit: (model: ModelInfo, index: number) => void
|
||||
/** 删除模型回调 */
|
||||
onDelete: (index: number) => void
|
||||
/** 切换选中状态回调 */
|
||||
onToggleSelection: (index: number) => void
|
||||
/** 切换全选回调 */
|
||||
onToggleSelectAll: () => void
|
||||
/** 检查模型是否被使用 */
|
||||
isModelUsed: (modelName: string) => boolean
|
||||
/** 搜索关键词 */
|
||||
searchQuery: string
|
||||
}
|
||||
|
||||
export const ModelTable = React.memo(function ModelTable({
|
||||
paginatedModels,
|
||||
allModels,
|
||||
filteredModels,
|
||||
selectedModels,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggleSelection,
|
||||
onToggleSelectAll,
|
||||
isModelUsed,
|
||||
searchQuery,
|
||||
}: ModelTableProps) {
|
||||
return (
|
||||
<div className="hidden md:block rounded-lg border bg-card overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-12">
|
||||
<Checkbox
|
||||
checked={selectedModels.size === filteredModels.length && filteredModels.length > 0}
|
||||
onCheckedChange={onToggleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-24">使用状态</TableHead>
|
||||
<TableHead>模型名称</TableHead>
|
||||
<TableHead>模型标识符</TableHead>
|
||||
<TableHead>提供商</TableHead>
|
||||
<TableHead className="text-center">温度</TableHead>
|
||||
<TableHead className="text-right">输入价格</TableHead>
|
||||
<TableHead className="text-right">输出价格</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedModels.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
|
||||
{searchQuery ? '未找到匹配的模型' : '暂无模型配置'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedModels.map((model, displayIndex) => {
|
||||
const actualIndex = allModels.findIndex(m => m === model)
|
||||
const used = isModelUsed(model.name)
|
||||
return (
|
||||
<TableRow key={displayIndex}>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedModels.has(actualIndex)}
|
||||
onCheckedChange={() => onToggleSelection(actualIndex)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={used ? "default" : "secondary"}
|
||||
className={used ? "bg-green-600 hover:bg-green-700" : ""}
|
||||
>
|
||||
{used ? '已使用' : '未使用'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{model.name}</TableCell>
|
||||
<TableCell className="max-w-xs truncate" title={model.model_identifier}>
|
||||
{model.model_identifier}
|
||||
</TableCell>
|
||||
<TableCell>{model.api_provider}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{model.temperature != null ? model.temperature : <span className="text-muted-foreground">-</span>}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">¥{model.price_in}/M</TableCell>
|
||||
<TableCell className="text-right">¥{model.price_out}/M</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => onEdit(model, actualIndex)}
|
||||
>
|
||||
<Pencil className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => onDelete(actualIndex)}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-1" strokeWidth={2} fill="none" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
142
dashboard/src/routes/config/model/components/Pagination.tsx
Normal file
142
dashboard/src/routes/config/model/components/Pagination.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* 模型列表分页组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react'
|
||||
import { PAGE_SIZE_OPTIONS } from '../constants'
|
||||
|
||||
interface PaginationProps {
|
||||
page: number
|
||||
pageSize: number
|
||||
totalItems: number
|
||||
jumpToPage: string
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (size: number) => void
|
||||
onJumpToPageChange: (value: string) => void
|
||||
onJumpToPage: () => void
|
||||
onSelectionClear?: () => void
|
||||
}
|
||||
|
||||
export const Pagination = React.memo(function Pagination({
|
||||
page,
|
||||
pageSize,
|
||||
totalItems,
|
||||
jumpToPage,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
onJumpToPageChange,
|
||||
onJumpToPage,
|
||||
onSelectionClear,
|
||||
}: PaginationProps) {
|
||||
const totalPages = Math.ceil(totalItems / pageSize)
|
||||
|
||||
const handlePageSizeChange = (value: string) => {
|
||||
onPageSizeChange(parseInt(value))
|
||||
onPageChange(1)
|
||||
onSelectionClear?.()
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
onJumpToPage()
|
||||
}
|
||||
}
|
||||
|
||||
if (totalItems === 0) return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label htmlFor="page-size-model" className="text-sm whitespace-nowrap">每页显示</Label>
|
||||
<Select
|
||||
value={pageSize.toString()}
|
||||
onValueChange={handlePageSizeChange}
|
||||
>
|
||||
<SelectTrigger id="page-size-model" className="w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAGE_SIZE_OPTIONS.map((size) => (
|
||||
<SelectItem key={size} value={size.toString()}>
|
||||
{size}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
显示 {(page - 1) * pageSize + 1} 到{' '}
|
||||
{Math.min(page * pageSize, totalItems)} 条,共 {totalItems} 条
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(1)}
|
||||
disabled={page === 1}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<ChevronsLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(Math.max(1, page - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4 sm:mr-1" />
|
||||
<span className="hidden sm:inline">上一页</span>
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
value={jumpToPage}
|
||||
onChange={(e) => onJumpToPageChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={page.toString()}
|
||||
className="w-16 h-8 text-center"
|
||||
min={1}
|
||||
max={totalPages}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onJumpToPage}
|
||||
disabled={!jumpToPage}
|
||||
className="h-8"
|
||||
>
|
||||
跳转
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(page + 1)}
|
||||
disabled={page >= totalPages}
|
||||
>
|
||||
<span className="hidden sm:inline">下一页</span>
|
||||
<ChevronRight className="h-4 w-4 sm:ml-1" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(totalPages)}
|
||||
disabled={page >= totalPages}
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<ChevronsRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
155
dashboard/src/routes/config/model/components/TaskConfigCard.tsx
Normal file
155
dashboard/src/routes/config/model/components/TaskConfigCard.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* 任务配置卡片组件
|
||||
*/
|
||||
import React from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { MultiSelect } from '@/components/ui/multi-select'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import type { TaskConfig } from '../types'
|
||||
|
||||
interface TaskConfigCardProps {
|
||||
title: string
|
||||
description: string
|
||||
taskConfig: TaskConfig
|
||||
modelNames: string[]
|
||||
onChange: (field: keyof TaskConfig, value: string[] | number | string) => void
|
||||
hideTemperature?: boolean
|
||||
hideMaxTokens?: boolean
|
||||
dataTour?: string
|
||||
}
|
||||
|
||||
export const TaskConfigCard = React.memo(function TaskConfigCard({
|
||||
title,
|
||||
description,
|
||||
taskConfig,
|
||||
modelNames,
|
||||
onChange,
|
||||
hideTemperature = false,
|
||||
hideMaxTokens = false,
|
||||
dataTour,
|
||||
}: TaskConfigCardProps) {
|
||||
const handleModelChange = (values: string[]) => {
|
||||
onChange('model_list', values)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card p-4 sm:p-6 space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold text-base sm:text-lg">{title}</h4>
|
||||
<p className="text-xs sm:text-sm text-muted-foreground mt-1">{description}</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{/* 模型列表 */}
|
||||
<div className="grid gap-2" data-tour={dataTour}>
|
||||
<Label>模型列表</Label>
|
||||
<MultiSelect
|
||||
options={modelNames.map((name) => ({ label: name, value: name }))}
|
||||
selected={taskConfig.model_list || []}
|
||||
onChange={handleModelChange}
|
||||
placeholder="选择模型..."
|
||||
emptyText="暂无可用模型"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 温度和最大 Token */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
{!hideTemperature && (
|
||||
<div className="grid gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>温度</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
value={taskConfig.temperature ?? 0.3}
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.target.value)
|
||||
if (!isNaN(value) && value >= 0 && value <= 1) {
|
||||
onChange('temperature', value)
|
||||
}
|
||||
}}
|
||||
className="w-20 h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<Slider
|
||||
value={[taskConfig.temperature ?? 0.3]}
|
||||
onValueChange={(values) => onChange('temperature', values[0])}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.1}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hideMaxTokens && (
|
||||
<div className="grid gap-2">
|
||||
<Label>最大 Token</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={taskConfig.max_tokens ?? 1024}
|
||||
onChange={(e) => onChange('max_tokens', parseInt(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 慢请求阈值 */}
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>慢请求阈值 (秒)</Label>
|
||||
<span className="text-xs text-muted-foreground">超时警告</span>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
step="1"
|
||||
min="1"
|
||||
value={taskConfig.slow_threshold ?? 15}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value)
|
||||
if (!isNaN(value) && value >= 1) {
|
||||
onChange('slow_threshold', value)
|
||||
}
|
||||
}}
|
||||
placeholder="15"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
模型响应时间超过此阈值将输出警告日志
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 模型选择策略 */}
|
||||
<div className="grid gap-2">
|
||||
<Label>模型选择策略</Label>
|
||||
<Select
|
||||
value={taskConfig.selection_strategy ?? 'balance'}
|
||||
onValueChange={(value) => onChange('selection_strategy', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择模型选择策略" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="balance">负载均衡(balance)</SelectItem>
|
||||
<SelectItem value="random">随机选择(random)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
负载均衡:优先选择使用次数少的模型。随机选择:完全随机从模型列表中选择
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
8
dashboard/src/routes/config/model/components/index.ts
Normal file
8
dashboard/src/routes/config/model/components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Model 配置页面组件导出
|
||||
*/
|
||||
|
||||
export { TaskConfigCard } from './TaskConfigCard'
|
||||
export { ModelCardList } from './ModelCardList'
|
||||
export { ModelTable } from './ModelTable'
|
||||
export { Pagination } from './Pagination'
|
||||
Reference in New Issue
Block a user