上传完整的WebUI前端仓库

This commit is contained in:
墨梓柒
2026-01-13 06:24:35 +08:00
parent a9187dc312
commit 812296590e
184 changed files with 47854 additions and 1 deletions

View 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>
)
})

View 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>
)
})

View 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>
)
})

View 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>
)
})

View File

@@ -0,0 +1,8 @@
/**
* Model 配置页面组件导出
*/
export { TaskConfigCard } from './TaskConfigCard'
export { ModelCardList } from './ModelCardList'
export { ModelTable } from './ModelTable'
export { Pagination } from './Pagination'