From 9f6027325c74a68b5ebc3198e9b2c4bba81179e8 Mon Sep 17 00:00:00 2001 From: t0ng7u Date: Mon, 4 Aug 2025 02:54:37 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20prefill=20group=20man?= =?UTF-8?q?agement=20system=20for=20models?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add new PrefillGroup model with CRUD operations * Support for model, tag, and endpoint group types * JSON storage for group items with GORM datatypes * Automatic database migration support - Implement backend API endpoints * GET /api/prefill_group - List groups by type with admin auth * POST /api/prefill_group - Create new groups * PUT /api/prefill_group - Update existing groups * DELETE /api/prefill_group/:id - Delete groups - Add comprehensive frontend management interface * PrefillGroupManagement component for group listing * EditPrefillGroupModal for group creation/editing * Integration with EditModelModal for auto-filling * Responsive design with CardTable and SideSheet - Enhance model editing workflow * Tag group selection with auto-fill functionality * Endpoint group selection with auto-fill functionality * Seamless integration with existing model forms - Create reusable UI components * Extract common rendering utilities to models/ui/ * Shared renderLimitedItems and renderDescription functions * Consistent styling across all model-related components - Improve user experience * Empty state illustrations matching existing patterns * Fixed column positioning for operation buttons * Item content display with +x indicators for overflow * Tooltip support for long descriptions --- controller/prefill_group.go | 72 +++++ go.mod | 8 +- go.sum | 11 + model/main.go | 2 + model/prefill_group.go | 56 ++++ router/api-router.go | 10 + .../components/table/models/ModelsActions.jsx | 16 ++ .../table/models/ModelsColumnDefs.js | 43 +-- .../table/models/modals/EditModelModal.jsx | 57 ++++ .../models/modals/EditPrefillGroupModal.jsx | 234 +++++++++++++++ .../models/modals/MissingModelsModal.jsx | 8 +- .../models/modals/PrefillGroupManagement.jsx | 271 ++++++++++++++++++ .../table/models/ui/RenderUtils.jsx | 60 ++++ 13 files changed, 803 insertions(+), 45 deletions(-) create mode 100644 controller/prefill_group.go create mode 100644 model/prefill_group.go create mode 100644 web/src/components/table/models/modals/EditPrefillGroupModal.jsx create mode 100644 web/src/components/table/models/modals/PrefillGroupManagement.jsx create mode 100644 web/src/components/table/models/ui/RenderUtils.jsx diff --git a/controller/prefill_group.go b/controller/prefill_group.go new file mode 100644 index 00000000..e37082e6 --- /dev/null +++ b/controller/prefill_group.go @@ -0,0 +1,72 @@ +package controller + +import ( + "strconv" + + "one-api/common" + "one-api/model" + + "github.com/gin-gonic/gin" +) + +// GetPrefillGroups 获取预填组列表,可通过 ?type=xxx 过滤 +func GetPrefillGroups(c *gin.Context) { + groupType := c.Query("type") + groups, err := model.GetAllPrefillGroups(groupType) + if err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, groups) +} + +// CreatePrefillGroup 创建新的预填组 +func CreatePrefillGroup(c *gin.Context) { + var g model.PrefillGroup + if err := c.ShouldBindJSON(&g); err != nil { + common.ApiError(c, err) + return + } + if g.Name == "" || g.Type == "" { + common.ApiErrorMsg(c, "组名称和类型不能为空") + return + } + if err := g.Insert(); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, &g) +} + +// UpdatePrefillGroup 更新预填组 +func UpdatePrefillGroup(c *gin.Context) { + var g model.PrefillGroup + if err := c.ShouldBindJSON(&g); err != nil { + common.ApiError(c, err) + return + } + if g.Id == 0 { + common.ApiErrorMsg(c, "缺少组 ID") + return + } + if err := g.Update(); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, &g) +} + +// DeletePrefillGroup 删除预填组 +func DeletePrefillGroup(c *gin.Context) { + idStr := c.Param("id") + id, err := strconv.Atoi(idStr) + if err != nil { + common.ApiError(c, err) + return + } + if err := model.DeletePrefillGroupByID(id); err != nil { + common.ApiError(c, err) + return + } + common.ApiSuccess(c, nil) +} diff --git a/go.mod b/go.mod index 94873c88..fd787a07 100644 --- a/go.mod +++ b/go.mod @@ -34,12 +34,13 @@ require ( golang.org/x/image v0.23.0 golang.org/x/net v0.35.0 golang.org/x/sync v0.11.0 - gorm.io/driver/mysql v1.4.3 + gorm.io/driver/mysql v1.5.6 gorm.io/driver/postgres v1.5.2 - gorm.io/gorm v1.25.2 + gorm.io/gorm v1.30.0 ) require ( + filippo.io/edwards25519 v1.1.0 // indirect github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect @@ -59,7 +60,7 @@ require ( github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/gorilla/context v1.1.1 // indirect @@ -91,6 +92,7 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/datatypes v1.2.6 // indirect modernc.org/libc v1.22.5 // indirect modernc.org/mathutil v1.5.0 // indirect modernc.org/memory v1.5.0 // indirect diff --git a/go.sum b/go.sum index 74eecd4c..8203949a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/Calcium-Ion/go-epay v0.0.4 h1:C96M7WfRLadcIVscWzwLiYs8etI1wrDmtFMuK2zP22A= github.com/Calcium-Ion/go-epay v0.0.4/go.mod h1:cxo/ZOg8ClvE3VAnCmEzbuyAZINSq7kFEN9oHj5WQ2U= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -86,6 +88,8 @@ github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= @@ -274,13 +278,20 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/datatypes v1.2.6 h1:KafLdXvFUhzNeL2ncm03Gl3eTLONQfNKZ+wJ+9Y4Nck= +gorm.io/datatypes v1.2.6/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY= gorm.io/driver/mysql v1.4.3 h1:/JhWJhO2v17d8hjApTltKNADm7K7YI2ogkR7avJUL3k= gorm.io/driver/mysql v1.4.3/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= diff --git a/model/main.go b/model/main.go index 5be43703..1577c50a 100644 --- a/model/main.go +++ b/model/main.go @@ -252,6 +252,7 @@ func migrateDB() error { &Task{}, &Model{}, &Vendor{}, + &PrefillGroup{}, &Setup{}, ) if err != nil { @@ -280,6 +281,7 @@ func migrateDBFast() error { {&Task{}, "Task"}, {&Model{}, "Model"}, {&Vendor{}, "Vendor"}, + {&PrefillGroup{}, "PrefillGroup"}, {&Setup{}, "Setup"}, } // 动态计算migration数量,确保errChan缓冲区足够大 diff --git a/model/prefill_group.go b/model/prefill_group.go new file mode 100644 index 00000000..7a3a6673 --- /dev/null +++ b/model/prefill_group.go @@ -0,0 +1,56 @@ +package model + +import ( + "one-api/common" + + "gorm.io/datatypes" +) + +// PrefillGroup 用于存储可复用的“组”信息,例如模型组、标签组、端点组等。 +// Name 字段保持唯一,用于在前端下拉框中展示。 +// Type 字段用于区分组的类别,可选值如:model、tag、endpoint。 +// Items 字段使用 JSON 数组保存对应类型的字符串集合,示例: +// ["gpt-4o", "gpt-3.5-turbo"] +// 设计遵循 3NF,避免冗余,提供灵活扩展能力。 + +type PrefillGroup struct { + Id int `json:"id"` + Name string `json:"name" gorm:"uniqueIndex;size:64;not null"` + Type string `json:"type" gorm:"size:32;index;not null"` + Items datatypes.JSON `json:"items" gorm:"type:json"` + Description string `json:"description,omitempty" gorm:"type:varchar(255)"` + CreatedTime int64 `json:"created_time" gorm:"bigint"` + UpdatedTime int64 `json:"updated_time" gorm:"bigint"` +} + +// Insert 新建组 +func (g *PrefillGroup) Insert() error { + now := common.GetTimestamp() + g.CreatedTime = now + g.UpdatedTime = now + return DB.Create(g).Error +} + +// Update 更新组 +func (g *PrefillGroup) Update() error { + g.UpdatedTime = common.GetTimestamp() + return DB.Save(g).Error +} + +// DeleteByID 根据 ID 删除组 +func DeletePrefillGroupByID(id int) error { + return DB.Delete(&PrefillGroup{}, id).Error +} + +// GetAllPrefillGroups 获取全部组,可按类型过滤(为空则返回全部) +func GetAllPrefillGroups(groupType string) ([]*PrefillGroup, error) { + var groups []*PrefillGroup + query := DB.Model(&PrefillGroup{}) + if groupType != "" { + query = query.Where("type = ?", groupType) + } + if err := query.Order("updated_time DESC").Find(&groups).Error; err != nil { + return nil, err + } + return groups, nil +} diff --git a/router/api-router.go b/router/api-router.go index a70c2ad4..3baaef14 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -166,6 +166,16 @@ func SetApiRouter(router *gin.Engine) { { groupRoute.GET("/", controller.GetGroups) } + + prefillGroupRoute := apiRouter.Group("/prefill_group") + prefillGroupRoute.Use(middleware.AdminAuth()) + { + prefillGroupRoute.GET("/", controller.GetPrefillGroups) + prefillGroupRoute.POST("/", controller.CreatePrefillGroup) + prefillGroupRoute.PUT("/", controller.UpdatePrefillGroup) + prefillGroupRoute.DELETE("/:id", controller.DeletePrefillGroup) + } + mjRoute := apiRouter.Group("/mj") mjRoute.GET("/self", middleware.UserAuth(), controller.GetUserMidjourney) mjRoute.GET("/", middleware.AdminAuth(), controller.GetAllMidjourney) diff --git a/web/src/components/table/models/ModelsActions.jsx b/web/src/components/table/models/ModelsActions.jsx index b27d51e4..cb91ed29 100644 --- a/web/src/components/table/models/ModelsActions.jsx +++ b/web/src/components/table/models/ModelsActions.jsx @@ -19,6 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import React, { useState } from 'react'; import MissingModelsModal from './modals/MissingModelsModal.jsx'; +import PrefillGroupManagement from './modals/PrefillGroupManagement.jsx'; import { Button, Space, Modal } from '@douyinfe/semi-ui'; import CompactModeToggle from '../../common/ui/CompactModeToggle'; import { showError } from '../../../helpers'; @@ -35,6 +36,7 @@ const ModelsActions = ({ // Modal states const [showDeleteModal, setShowDeleteModal] = useState(false); const [showMissingModal, setShowMissingModal] = useState(false); + const [showGroupManagement, setShowGroupManagement] = useState(false); // Handle delete selected models with confirmation const handleDeleteSelectedModels = () => { @@ -86,6 +88,15 @@ const ModelsActions = ({ {t('未配置模型')} + + + + setShowGroupManagement(false)} + /> ); }; diff --git a/web/src/components/table/models/ModelsColumnDefs.js b/web/src/components/table/models/ModelsColumnDefs.js index e02090d8..a2af1c95 100644 --- a/web/src/components/table/models/ModelsColumnDefs.js +++ b/web/src/components/table/models/ModelsColumnDefs.js @@ -23,14 +23,14 @@ import { Space, Tag, Typography, - Modal, - Popover + Modal } from '@douyinfe/semi-ui'; import { timestamp2string, getLobeHubIcon, stringToColor } from '../../../helpers'; +import { renderLimitedItems, renderDescription } from './ui/RenderUtils.jsx'; const { Text } = Typography; @@ -39,34 +39,6 @@ function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; } -// Generic renderer for list-style tags with limit and popover -function renderLimitedItems({ items, renderItem, maxDisplay = 3 }) { - if (!items || items.length === 0) return '-'; - const displayItems = items.slice(0, maxDisplay); - const remainingItems = items.slice(maxDisplay); - return ( - - {displayItems.map((item, idx) => renderItem(item, idx))} - {remainingItems.length > 0 && ( - - - {remainingItems.map((item, idx) => renderItem(item, idx))} - - - } - position='top' - > - - +{remainingItems.length} - - - )} - - ); -} - // Render vendor column with icon const renderVendorTag = (vendorId, vendorMap, t) => { if (!vendorId || !vendorMap[vendorId]) return '-'; @@ -82,15 +54,6 @@ const renderVendorTag = (vendorId, vendorMap, t) => { ); }; -// Render description with ellipsis -const renderDescription = (text) => { - return ( - - {text || '-'} - - ); -}; - // Render groups (enable_groups) const renderGroups = (groups) => { if (!groups || groups.length === 0) return '-'; @@ -223,7 +186,7 @@ export const getModelsColumns = ({ { title: t('描述'), dataIndex: 'description', - render: renderDescription, + render: (text) => renderDescription(text, 200), }, { title: t('供应商'), diff --git a/web/src/components/table/models/modals/EditModelModal.jsx b/web/src/components/table/models/modals/EditModelModal.jsx index eeff5d2b..1a1c9787 100644 --- a/web/src/components/table/models/modals/EditModelModal.jsx +++ b/web/src/components/table/models/modals/EditModelModal.jsx @@ -61,6 +61,10 @@ const EditModelModal = (props) => { // 供应商列表 const [vendors, setVendors] = useState([]); + // 预填组(标签、端点) + const [tagGroups, setTagGroups] = useState([]); + const [endpointGroups, setEndpointGroups] = useState([]); + // 获取供应商列表 const fetchVendors = async () => { try { @@ -74,9 +78,28 @@ const EditModelModal = (props) => { } }; + // 获取预填组(标签、端点) + const fetchPrefillGroups = async () => { + try { + const [tagRes, endpointRes] = await Promise.all([ + API.get('/api/prefill_group?type=tag'), + API.get('/api/prefill_group?type=endpoint'), + ]); + if (tagRes?.data?.success) { + setTagGroups(tagRes.data.data || []); + } + if (endpointRes?.data?.success) { + setEndpointGroups(endpointRes.data.data || []); + } + } catch (error) { + // ignore + } + }; + useEffect(() => { if (props.visiable) { fetchVendors(); + fetchPrefillGroups(); } }, [props.visiable]); @@ -287,6 +310,23 @@ const EditModelModal = (props) => { showClear /> + + ({ label: g.name, value: g.id }))} + showClear + style={{ width: '100%' }} + onChange={(value) => { + const g = tagGroups.find(item => item.id === value); + if (g && formApiRef.current) { + formApiRef.current.setValue('tags', g.items || []); + } + }} + /> + + { + + ({ label: g.name, value: g.id }))} + showClear + style={{ width: '100%' }} + onChange={(value) => { + const g = endpointGroups.find(item => item.id === value); + if (g && formApiRef.current) { + formApiRef.current.setValue('endpoints', g.items || []); + } + }} + /> + + . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React, { useState, useRef } from 'react'; +import { + SideSheet, + Button, + Form, + Typography, + Space, + Tag, + Row, + Col, + Card, + Avatar, + Spin, +} from '@douyinfe/semi-ui'; +import { + IconLayers, + IconSave, + IconClose, +} from '@douyinfe/semi-icons'; +import { API, showError, showSuccess } from '../../../../helpers'; +import { useTranslation } from 'react-i18next'; +import { useIsMobile } from '../../../../hooks/common/useIsMobile'; + +const { Text, Title } = Typography; + +const EditPrefillGroupModal = ({ visible, onClose, editingGroup, onSuccess }) => { + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const [loading, setLoading] = useState(false); + const formRef = useRef(null); + const isEdit = editingGroup && editingGroup.id !== undefined; + + const typeOptions = [ + { label: t('模型组'), value: 'model' }, + { label: t('标签组'), value: 'tag' }, + { label: t('端点组'), value: 'endpoint' }, + ]; + + // 提交表单 + const handleSubmit = async (values) => { + setLoading(true); + try { + const submitData = { + ...values, + items: Array.isArray(values.items) ? values.items : [], + }; + + if (editingGroup.id) { + submitData.id = editingGroup.id; + const res = await API.put('/api/prefill_group', submitData); + if (res.data.success) { + showSuccess(t('更新成功')); + onSuccess(); + } else { + showError(res.data.message || t('更新失败')); + } + } else { + const res = await API.post('/api/prefill_group', submitData); + if (res.data.success) { + showSuccess(t('创建成功')); + onSuccess(); + } else { + showError(res.data.message || t('创建失败')); + } + } + } catch (error) { + showError(t('操作失败')); + } + setLoading(false); + }; + + return ( + + {isEdit ? ( + + {t('更新')} + + ) : ( + + {t('新建')} + + )} + + {isEdit ? t('更新预填组') : t('创建新的预填组')} + + + } + visible={visible} + onCancel={onClose} + width={isMobile ? '100%' : 600} + bodyStyle={{ padding: '0' }} + footer={ +
+ + + + +
+ } + closeIcon={null} + > + +
(formRef.current = api)} + initValues={{ + name: editingGroup?.name || '', + type: editingGroup?.type || 'tag', + description: editingGroup?.description || '', + items: (() => { + try { + return typeof editingGroup?.items === 'string' + ? JSON.parse(editingGroup.items) + : editingGroup?.items || []; + } catch { + return []; + } + })(), + }} + onSubmit={handleSubmit} + > +
+ {/* 基本信息 */} + +
+ + + +
+ {t('基本信息')} +
{t('设置预填组的基本信息')}
+
+
+ + + + + + + + + + + +
+ + {/* 内容配置 */} + +
+ + + +
+ {t('内容配置')} +
{t('配置组内包含的项目')}
+
+
+ + + + + +
+
+
+
+
+ ); +}; + +export default EditPrefillGroupModal; \ No newline at end of file diff --git a/web/src/components/table/models/modals/MissingModelsModal.jsx b/web/src/components/table/models/modals/MissingModelsModal.jsx index 5bd53944..41ff9d13 100644 --- a/web/src/components/table/models/modals/MissingModelsModal.jsx +++ b/web/src/components/table/models/modals/MissingModelsModal.jsx @@ -22,7 +22,8 @@ import { Modal, Table, Spin, Button, Typography, Empty, Input } from '@douyinfe/ import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IconSearch } from '@douyinfe/semi-icons'; import { API, showError } from '../../../../helpers'; -import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants/index.js'; +import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants'; +import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const MissingModelsModal = ({ visible, @@ -34,6 +35,7 @@ const MissingModelsModal = ({ const [missingModels, setMissingModels] = useState([]); const [searchKeyword, setSearchKeyword] = useState(''); const [currentPage, setCurrentPage] = useState(1); + const isMobile = useIsMobile(); const fetchMissing = async () => { setLoading(true); @@ -87,6 +89,8 @@ const MissingModelsModal = ({ { title: '', dataIndex: 'operate', + fixed: 'right', + width: 100, render: (text, record) => ( + deleteGroup(record.id)} + > + + + + ), + }, + ]; + + useEffect(() => { + if (visible) { + loadGroups(); + } + }, [visible]); + + return ( + <> + + + {t('管理')} + + + {t('预填组管理')} + + + } + visible={visible} + onCancel={onClose} + width={isMobile ? '100%' : 800} + bodyStyle={{ padding: '0' }} + closeIcon={null} + > + +
+ +
+ + + +
+ {t('组列表')} +
{t('管理模型、标签、端点等预填组')}
+
+
+
+ +
+ {groups.length > 0 ? ( + + ) : ( + } + darkModeImage={} + description={t('暂无预填组')} + style={{ padding: 30 }} + /> + )} +
+
+
+
+ + {/* 编辑组件 */} + + + ); +}; + +export default PrefillGroupManagement; \ No newline at end of file diff --git a/web/src/components/table/models/ui/RenderUtils.jsx b/web/src/components/table/models/ui/RenderUtils.jsx new file mode 100644 index 00000000..26a72e16 --- /dev/null +++ b/web/src/components/table/models/ui/RenderUtils.jsx @@ -0,0 +1,60 @@ +/* +Copyright (C) 2025 QuantumNous + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +For commercial licensing, please contact support@quantumnous.com +*/ + +import React from 'react'; +import { Space, Tag, Typography, Popover } from '@douyinfe/semi-ui'; + +const { Text } = Typography; + +// 通用渲染函数:限制项目数量显示,支持popover展开 +export function renderLimitedItems({ items, renderItem, maxDisplay = 3 }) { + if (!items || items.length === 0) return '-'; + const displayItems = items.slice(0, maxDisplay); + const remainingItems = items.slice(maxDisplay); + return ( + + {displayItems.map((item, idx) => renderItem(item, idx))} + {remainingItems.length > 0 && ( + + + {remainingItems.map((item, idx) => renderItem(item, idx))} + + + } + position='top' + > + + +{remainingItems.length} + + + )} + + ); +} + +// 渲染描述字段,长文本支持tooltip +export const renderDescription = (text, maxWidth = 200) => { + return ( + + {text || '-'} + + ); +}; \ No newline at end of file