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(true)} + > + {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={ + + + formRef.current?.submitForm()} + icon={} + loading={loading} + > + {t('提交')} + + } + > + {t('取消')} + + + + } + 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) => ( diff --git a/web/src/components/table/models/modals/PrefillGroupManagement.jsx b/web/src/components/table/models/modals/PrefillGroupManagement.jsx new file mode 100644 index 00000000..569fcdcd --- /dev/null +++ b/web/src/components/table/models/modals/PrefillGroupManagement.jsx @@ -0,0 +1,271 @@ +/* +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, { useState, useEffect } from 'react'; +import { + SideSheet, + Button, + Typography, + Space, + Tag, + Popconfirm, + Card, + Avatar, + Spin, + Empty, +} from '@douyinfe/semi-ui'; +import { + IconPlus, + IconLayers, +} from '@douyinfe/semi-icons'; +import { + IllustrationNoResult, + IllustrationNoResultDark, +} from '@douyinfe/semi-illustrations'; +import { API, showError, showSuccess, stringToColor } from '../../../../helpers'; +import { useTranslation } from 'react-i18next'; +import { useIsMobile } from '../../../../hooks/common/useIsMobile'; +import CardTable from '../../../common/ui/CardTable.js'; +import EditPrefillGroupModal from './EditPrefillGroupModal.jsx'; +import { renderLimitedItems, renderDescription } from '../ui/RenderUtils.jsx'; + +const { Text, Title } = Typography; + +const PrefillGroupManagement = ({ visible, onClose }) => { + const { t } = useTranslation(); + const isMobile = useIsMobile(); + const [loading, setLoading] = useState(false); + const [groups, setGroups] = useState([]); + const [showEdit, setShowEdit] = useState(false); + const [editingGroup, setEditingGroup] = useState({ id: undefined }); + + const typeOptions = [ + { label: t('模型组'), value: 'model' }, + { label: t('标签组'), value: 'tag' }, + { label: t('端点组'), value: 'endpoint' }, + ]; + + // 加载组列表 + const loadGroups = async () => { + setLoading(true); + try { + const res = await API.get('/api/prefill_group'); + if (res.data.success) { + setGroups(res.data.data || []); + } else { + showError(res.data.message || t('获取组列表失败')); + } + } catch (error) { + showError(t('获取组列表失败')); + } + setLoading(false); + }; + + // 删除组 + const deleteGroup = async (id) => { + try { + const res = await API.delete(`/api/prefill_group/${id}`); + if (res.data.success) { + showSuccess(t('删除成功')); + loadGroups(); + } else { + showError(res.data.message || t('删除失败')); + } + } catch (error) { + showError(t('删除失败')); + } + }; + + // 编辑组 + const handleEdit = (group = {}) => { + setEditingGroup(group); + setShowEdit(true); + }; + + // 关闭编辑 + const closeEdit = () => { + setShowEdit(false); + setTimeout(() => { + setEditingGroup({ id: undefined }); + }, 300); + }; + + // 编辑成功回调 + const handleEditSuccess = () => { + closeEdit(); + loadGroups(); + }; + + // 表格列定义 + const columns = [ + { + title: t('组名'), + dataIndex: 'name', + key: 'name', + render: (text, record) => ( + + {text} + + {typeOptions.find(opt => opt.value === record.type)?.label || record.type} + + + ), + }, + { + title: t('描述'), + dataIndex: 'description', + key: 'description', + render: (text) => renderDescription(text, 150), + }, + { + title: t('项目内容'), + dataIndex: 'items', + key: 'items', + render: (items) => { + try { + const itemsArray = typeof items === 'string' ? JSON.parse(items) : items; + if (!Array.isArray(itemsArray) || itemsArray.length === 0) { + return {t('暂无项目')}; + } + return renderLimitedItems({ + items: itemsArray, + renderItem: (item, idx) => ( + + {item} + + ), + maxDisplay: 3, + }); + } catch { + return {t('数据格式错误')}; + } + }, + }, + { + title: '', + key: 'action', + fixed: 'right', + width: 140, + render: (_, record) => ( + + handleEdit(record)} + > + {t('编辑')} + + deleteGroup(record.id)} + > + + {t('删除')} + + + + ), + }, + ]; + + useEffect(() => { + if (visible) { + loadGroups(); + } + }, [visible]); + + return ( + <> + + + {t('管理')} + + + {t('预填组管理')} + + + } + visible={visible} + onCancel={onClose} + width={isMobile ? '100%' : 800} + bodyStyle={{ padding: '0' }} + closeIcon={null} + > + + + + + + + + + {t('组列表')} + {t('管理模型、标签、端点等预填组')} + + + + } + onClick={() => handleEdit()} + > + {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