From 3c7efce80b1c50af7df6d5edda2458cb39b3a9ef Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Thu, 12 Feb 2026 19:23:30 +0800 Subject: [PATCH] Consistent library UI --- web/components/UI.tsx | 66 ++++++++++++++++++++++++++++++ web/pages/ASRLibrary.tsx | 84 ++++++++++++++++++-------------------- web/pages/LLMLibrary.tsx | 76 +++++++++++++++++----------------- web/pages/VoiceLibrary.tsx | 81 +++++++++++++++++------------------- 4 files changed, 180 insertions(+), 127 deletions(-) diff --git a/web/components/UI.tsx b/web/components/UI.tsx index 8ffb110..28109bd 100644 --- a/web/components/UI.tsx +++ b/web/components/UI.tsx @@ -52,6 +52,19 @@ export const Input: React.FC = ({ className = '', ...props }) => { ); }; +interface SelectProps extends React.SelectHTMLAttributes {} + +export const Select: React.FC = ({ className = '', children, ...props }) => { + return ( + + ); +}; + // Card - Glassmorphism style, very subtle border interface CardProps extends React.HTMLAttributes { children: React.ReactNode; @@ -104,6 +117,59 @@ interface TableCellProps extends React.TdHTMLAttributes { } export const TableCell: React.FC = ({ children, className = '', ...props }) => {children}; +interface LibraryPageShellProps { + title: string; + primaryAction: React.ReactNode; + filterBar: React.ReactNode; + children: React.ReactNode; +} + +export const LibraryPageShell: React.FC = ({ title, primaryAction, filterBar, children }) => { + return ( +
+
+

{title}

+ {primaryAction} +
+
+ {filterBar} +
+ {children} +
+ ); +}; + +interface TableStatusRowProps { + colSpan: number; + text: string; +} + +export const TableStatusRow: React.FC = ({ colSpan, text }) => { + return ( + + + {text} + + + ); +}; + +interface LibraryActionCellProps { + previewAction?: React.ReactNode; + editAction: React.ReactNode; + deleteAction: React.ReactNode; +} + +export const LibraryActionCell: React.FC = ({ previewAction, editAction, deleteAction }) => { + return ( + + {previewAction} + {editAction} + {deleteAction} + + ); +}; + // Drawer (Side Sheet) interface DrawerProps { isOpen: boolean; diff --git a/web/pages/ASRLibrary.tsx b/web/pages/ASRLibrary.tsx index fbba731..b5a6a24 100644 --- a/web/pages/ASRLibrary.tsx +++ b/web/pages/ASRLibrary.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import { Search, Filter, Plus, Trash2, Key, Server, Ear, Globe, Languages, Pencil, Mic, Square, Upload } from 'lucide-react'; -import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI'; +import { Button, Input, Select, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge, LibraryPageShell, TableStatusRow, LibraryActionCell } from '../components/UI'; import { ASRModel } from '../types'; import { createASRModel, deleteASRModel, fetchASRModels, previewASRModel, updateASRModel } from '../services/backendApi'; @@ -129,25 +129,25 @@ export const ASRLibraryPage: React.FC = () => { }; const handleDelete = async (id: string) => { - if (!confirm('确认删除该语音识别模型吗?')) return; + if (!confirm('确认删除该语音识别模型吗?该操作不可恢复。')) return; await deleteASRModel(id); setModels((prev) => prev.filter((m) => m.id !== id)); }; return ( -
-
-

语音识别

+ setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]"> 添加模型 -
- -
+ )} + filterBar={( + <>
setSearchTerm(e.target.value)} @@ -155,17 +155,15 @@ export const ASRLibraryPage: React.FC = () => {
- +
- +
-
+ + )} + >
@@ -209,29 +209,27 @@ export const ASRLibraryPage: React.FC = () => { {model.modelName || '-'}{model.baseUrl}{maskApiKey(model.apiKey)} - - - - - + setPreviewingModel(model)} title="试听识别"> + + + )} + editAction={( + + )} + deleteAction={( + + )} + /> ))} - {!isLoading && filteredModels.length === 0 && ( - - 暂无语音识别模型 - - )} - {isLoading && ( - - 加载中... - - )} + {!isLoading && filteredModels.length === 0 && } + {isLoading && }
@@ -254,7 +252,7 @@ export const ASRLibraryPage: React.FC = () => { onClose={() => setPreviewingModel(null)} model={previewingModel} /> -
+ ); }; @@ -360,25 +358,23 @@ const ASRModelModal: React.FC<{
- +
- +
diff --git a/web/pages/LLMLibrary.tsx b/web/pages/LLMLibrary.tsx index 017eb99..808ef84 100644 --- a/web/pages/LLMLibrary.tsx +++ b/web/pages/LLMLibrary.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import { Search, Filter, Plus, BrainCircuit, Trash2, Key, Settings2, Server, Thermometer, Pencil, Play } from 'lucide-react'; -import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI'; +import { Button, Input, Select, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge, LibraryPageShell, TableStatusRow, LibraryActionCell } from '../components/UI'; import { LLMModel } from '../types'; import { createLLMModel, deleteLLMModel, fetchLLMModels, previewLLMModel, updateLLMModel } from '../services/backendApi'; @@ -59,25 +59,25 @@ export const LLMLibraryPage: React.FC = () => { }; const handleDelete = async (id: string) => { - if (!confirm('确认删除该模型配置吗?')) return; + if (!confirm('确认删除该模型吗?该操作不可恢复。')) return; await deleteLLMModel(id); setModels((prev) => prev.filter((item) => item.id !== id)); }; return ( -
-
-

模型接入

+ setIsAddModalOpen(true)} className="shadow-[0_0_15px_rgba(6,182,212,0.4)]"> 添加模型 -
- -
+ )} + filterBar={( + <>
setSearchTerm(e.target.value)} @@ -85,17 +85,15 @@ export const LLMLibraryPage: React.FC = () => {
- +
- +
-
+ + )} + >
@@ -138,8 +138,9 @@ export const LLMLibraryPage: React.FC = () => { {model.modelName || '-'}{model.baseUrl}{maskApiKey(model.apiKey)} - - - - - + )} + editAction={( + + )} + deleteAction={( + + )} + /> ))} - {!isLoading && filteredModels.length === 0 && ( - - 暂无模型数据 - - )} - {isLoading && ( - - 加载中... - - )} + {!isLoading && filteredModels.length === 0 && } + {isLoading && }
@@ -185,7 +183,7 @@ export const LLMLibraryPage: React.FC = () => { onClose={() => setPreviewingModel(null)} model={previewingModel} /> -
+ ); }; @@ -273,13 +271,13 @@ const LLMModelModal: React.FC<{
- +
diff --git a/web/pages/VoiceLibrary.tsx b/web/pages/VoiceLibrary.tsx index cb6f574..5c94b1c 100644 --- a/web/pages/VoiceLibrary.tsx +++ b/web/pages/VoiceLibrary.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { Search, Mic2, Play, Pause, Upload, Filter, Plus, Volume2, Pencil, Trash2 } from 'lucide-react'; -import { Button, Input, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge } from '../components/UI'; +import { Button, Input, Select, TableHeader, TableRow, TableHead, TableCell, Dialog, Badge, LibraryPageShell, TableStatusRow, LibraryActionCell } from '../components/UI'; import { Voice } from '../types'; import { createVoice, deleteVoice, fetchVoices, previewVoice, updateVoice } from '../services/backendApi'; @@ -102,15 +102,15 @@ export const VoiceLibraryPage: React.FC = () => { }; const handleDelete = async (id: string) => { - if (!confirm('确认删除这个声音吗?')) return; + if (!confirm('确认删除该声音吗?该操作不可恢复。')) return; await deleteVoice(id); setVoices((prev) => prev.filter((voice) => voice.id !== id)); }; return ( -
-
-

声音资源

+
-
- -
+ )} + filterBar={( + <>
setSearchTerm(e.target.value)} @@ -133,37 +133,36 @@ export const VoiceLibraryPage: React.FC = () => {
- +
- +
- +
-
+ + )} + >
@@ -202,26 +201,22 @@ export const VoiceLibraryPage: React.FC = () => { {playingVoiceId === voice.id ? : } - - - - + setEditingVoice(voice)} title="编辑声音"> + + + )} + deleteAction={( + + )} + /> ))} - {!isLoading && filteredVoices.length === 0 && ( - - 暂无声音数据 - - )} - {isLoading && ( - - 加载中... - - )} + {!isLoading && filteredVoices.length === 0 && } + {isLoading && }
@@ -236,7 +231,7 @@ export const VoiceLibraryPage: React.FC = () => { /> setIsCloneModalOpen(false)} onSuccess={handleAddSuccess} /> -
+ ); }; @@ -433,25 +428,23 @@ const AddVoiceModal: React.FC<{
- +
- +