From 518f1ab87f009e0f0cf2a40e32bbd34074c6b27f Mon Sep 17 00:00:00 2001 From: fengsilin Date: Thu, 23 Apr 2026 10:45:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20=E7=A7=BB=E9=99=A4?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E6=8E=92=E5=BA=8F=EF=BC=8C=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=20sort=5Forder=20=E6=95=B0=E5=80=BC=E8=BE=93=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回退 @dnd-kit 拖拽实现,改为在编辑弹窗中直接设置 sort_order 数值: - 模型编辑弹窗新增「排序」InputNumber 字段 - 供应商编辑弹窗新增「排序」InputNumber 字段 - 模型表格新增 sort_order 显示列 - 移除 @dnd-kit 依赖及所有 DnD 相关代码 Co-Authored-By: Claude Opus 4.7 --- web/package.json | 4 - .../model-pricing/filter/PricingVendors.jsx | 2 +- .../table/models/ModelsColumnDefs.jsx | 12 +- .../components/table/models/ModelsTable.jsx | 106 +++----- .../components/table/models/ModelsTabs.jsx | 231 ++++++++---------- .../table/models/modals/EditModelModal.jsx | 10 + .../table/models/modals/EditVendorModal.jsx | 9 + web/src/hooks/models/useModelsData.jsx | 44 ---- web/src/pages/Home/HomePricingFilters.jsx | 2 +- 9 files changed, 157 insertions(+), 263 deletions(-) diff --git a/web/package.json b/web/package.json index 706c5a6..145448d 100644 --- a/web/package.json +++ b/web/package.json @@ -4,10 +4,6 @@ "private": true, "type": "module", "dependencies": { - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/modifiers": "^9.0.0", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", "@douyinfe/semi-icons": "^2.63.1", "@douyinfe/semi-ui": "^2.69.1", "@lobehub/icons": "^5.0.1", diff --git a/web/src/components/table/model-pricing/filter/PricingVendors.jsx b/web/src/components/table/model-pricing/filter/PricingVendors.jsx index 6d9906c..192d166 100644 --- a/web/src/components/table/model-pricing/filter/PricingVendors.jsx +++ b/web/src/components/table/model-pricing/filter/PricingVendors.jsx @@ -56,7 +56,7 @@ const PricingVendors = ({ }); return { - vendors: Array.from(vendors), + vendors: Array.from(vendors).sort(), vendorIcons, hasUnknownVendor, }; diff --git a/web/src/components/table/models/ModelsColumnDefs.jsx b/web/src/components/table/models/ModelsColumnDefs.jsx index fc5afd4..4fa136c 100644 --- a/web/src/components/table/models/ModelsColumnDefs.jsx +++ b/web/src/components/table/models/ModelsColumnDefs.jsx @@ -26,7 +26,6 @@ import { Modal, Tooltip, } from '@douyinfe/semi-ui'; -import { IconMenu } from '@douyinfe/semi-icons'; import { timestamp2string, getLobeHubIcon, @@ -284,14 +283,9 @@ export const getModelsColumns = ({ }) => { return [ { - key: 'drag', - width: 40, - fixed: 'left', - render: () => ( - - - - ), + title: t('排序'), + dataIndex: 'sort_order', + width: 60, }, { title: t('图标'), diff --git a/web/src/components/table/models/ModelsTable.jsx b/web/src/components/table/models/ModelsTable.jsx index 9515ac0..643bff6 100644 --- a/web/src/components/table/models/ModelsTable.jsx +++ b/web/src/components/table/models/ModelsTable.jsx @@ -19,9 +19,6 @@ For commercial licensing, please contact support@quantumnous.com import React, { useMemo } from 'react'; import { Empty } from '@douyinfe/semi-ui'; -import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; -import { SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; -import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import CardTable from '../../common/ui/CardTable'; import { IllustrationNoResult, @@ -29,21 +26,6 @@ import { } from '@douyinfe/semi-illustrations'; import { getModelsColumns } from './ModelsColumnDefs'; -const SortableRow = (props) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: props['data-row-key'], - }); - const style = { - transform: transform ? `translate3d(0, ${transform.y}px, 0)` : undefined, - transition, - opacity: isDragging ? 0.5 : 1, - ...(props.style || {}), - }; - return ( - - ); -}; - const ModelsTable = (modelsData) => { const { models, @@ -61,14 +43,10 @@ const ModelsTable = (modelsData) => { setShowEdit, refresh, vendorMap, - reorderModels, t, } = modelsData; - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) - ); - + // Get all columns const columns = useMemo(() => { return getModelsColumns({ t, @@ -80,6 +58,7 @@ const ModelsTable = (modelsData) => { }); }, [t, manageModel, setEditingModel, setShowEdit, refresh, vendorMap]); + // Handle compact mode by removing fixed positioning const tableColumns = useMemo(() => { return compactMode ? columns.map((col) => { @@ -92,62 +71,37 @@ const ModelsTable = (modelsData) => { : columns; }, [compactMode, columns]); - const handleDragEnd = (event) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - const oldIndex = models.findIndex((m) => m.id === active.id); - const newIndex = models.findIndex((m) => m.id === over.id); - if (oldIndex === -1 || newIndex === -1) return; - const reordered = [...models]; - const [moved] = reordered.splice(oldIndex, 1); - reordered.splice(newIndex, 0, moved); - reorderModels(reordered); - }; - return ( - - m.id)} - strategy={verticalListSortingStrategy} - > - } - darkModeImage={} - description={t('搜索无结果')} - style={{ padding: 30 }} - /> + } + darkModeImage={ + } - className='rounded-xl overflow-hidden' - size='middle' + description={t('搜索无结果')} + style={{ padding: 30 }} /> - - + } + className='rounded-xl overflow-hidden' + size='middle' + /> ); }; diff --git a/web/src/components/table/models/ModelsTabs.jsx b/web/src/components/table/models/ModelsTabs.jsx index c0f5e14..820d707 100644 --- a/web/src/components/table/models/ModelsTabs.jsx +++ b/web/src/components/table/models/ModelsTabs.jsx @@ -18,76 +18,11 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Tag, Button, Dropdown, Modal } from '@douyinfe/semi-ui'; +import { Tabs, TabPane, Tag, Button, Dropdown, Modal } from '@douyinfe/semi-ui'; import { IconEdit, IconDelete } from '@douyinfe/semi-icons'; -import { DndContext, closestCenter, PointerSensor, useSensor, useSensors } from '@dnd-kit/core'; -import { SortableContext, horizontalListSortingStrategy, useSortable } from '@dnd-kit/sortable'; -import { CSS } from '@dnd-kit/utilities'; import { getLobeHubIcon, showError, showSuccess } from '../../../helpers'; import { API } from '../../../helpers'; -const SortableVendorItem = ({ vendor, activeVendorKey, vendorCounts, handleEditVendor, handleDeleteVendor, t }) => { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: String(vendor.id) }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - display: 'inline-flex', - alignItems: 'center', - gap: 6, - padding: '4px 12px', - borderRadius: 6, - cursor: 'grab', - background: activeVendorKey === String(vendor.id) ? 'var(--semi-color-primary-light-default)' : 'var(--semi-color-bg-2)', - border: activeVendorKey === String(vendor.id) ? '1px solid var(--semi-color-primary)' : '1px solid var(--semi-color-border)', - userSelect: 'none', - }; - const count = vendorCounts[vendor.id] || 0; - return ( -
- {getLobeHubIcon(vendor.icon || 'Layers', 14)} - {vendor.name} - - {count} - - - } onClick={(e) => handleEditVendor(vendor, e)}> - {t('编辑')} - - } - onClick={(e) => { - e.stopPropagation(); - Modal.confirm({ - title: t('确认删除'), - content: t('确定要删除供应商 "{{name}}" 吗?此操作不可撤销。', { name: vendor.name }), - onOk: () => handleDeleteVendor(vendor, e), - okText: t('删除'), - cancelText: t('取消'), - type: 'warning', - okType: 'danger', - }); - }} - > - {t('删除')} - - - } - onClickOutSide={(e) => e.stopPropagation()} - > - - -
- ); -}; - const ModelsTabs = ({ activeVendorKey, setActiveVendorKey, @@ -101,13 +36,8 @@ const ModelsTabs = ({ setShowEditVendor, setEditingVendor, loadVendors, - reorderVendors, t, }) => { - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 8 } }) - ); - const handleTabChange = (key) => { setActiveVendorKey(key); setActivePage(1); @@ -115,24 +45,25 @@ const ModelsTabs = ({ }; const handleEditVendor = (vendor, e) => { - e.stopPropagation(); + e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换 setEditingVendor(vendor); setShowEditVendor(true); }; const handleDeleteVendor = async (vendor, e) => { - e.stopPropagation(); + e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换 try { const res = await API.delete(`/api/vendors/${vendor.id}`); if (res.data.success) { showSuccess(t('供应商删除成功')); + // 如果删除的是当前选中的供应商,切换到"全部" if (activeVendorKey === String(vendor.id)) { setActiveVendorKey('all'); loadModels(1, pageSize, 'all'); } else { loadModels(activePage, pageSize, activeVendorKey); } - loadVendors(); + loadVendors(); // 重新加载供应商列表 } else { showError(res.data.message || t('删除失败')); } @@ -141,62 +72,106 @@ const ModelsTabs = ({ } }; - const handleDragEnd = (event) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - const oldIndex = vendors.findIndex((v) => String(v.id) === active.id); - const newIndex = vendors.findIndex((v) => String(v.id) === over.id); - if (oldIndex === -1 || newIndex === -1) return; - const reordered = [...vendors]; - const [moved] = reordered.splice(oldIndex, 1); - reordered.splice(newIndex, 0, moved); - reorderVendors(reordered); - }; - return ( -
- {/* "All" tab - not draggable */} -
handleTabChange('all')} - style={{ - display: 'inline-flex', - alignItems: 'center', - gap: 6, - padding: '4px 12px', - borderRadius: 6, - cursor: 'pointer', - background: activeVendorKey === 'all' ? 'var(--semi-color-primary-light-default)' : 'var(--semi-color-bg-2)', - border: activeVendorKey === 'all' ? '1px solid var(--semi-color-primary)' : '1px solid var(--semi-color-border)', - userSelect: 'none', - }} - > - {t('全部')} - - {vendorCounts['all'] || 0} - -
- - {/* Draggable vendor tabs */} - - String(v.id))} strategy={horizontalListSortingStrategy}> - {vendors.map((vendor) => ( - - ))} - - + setShowAddVendor(true)} + > + {t('新增供应商')} + + } + > + + {t('全部')} + + {vendorCounts['all'] || 0} + + + } + /> - -
+ {vendors.map((vendor) => { + const key = String(vendor.id); + const count = vendorCounts[vendor.id] || 0; + return ( + + {getLobeHubIcon(vendor.icon || 'Layers', 14)} + {vendor.name} + + {count} + + + } + onClick={(e) => handleEditVendor(vendor, e)} + > + {t('编辑')} + + } + onClick={(e) => { + e.stopPropagation(); + Modal.confirm({ + title: t('确认删除'), + content: t( + '确定要删除供应商 "{{name}}" 吗?此操作不可撤销。', + { name: vendor.name }, + ), + onOk: () => handleDeleteVendor(vendor, e), + okText: t('删除'), + cancelText: t('取消'), + type: 'warning', + okType: 'danger', + }); + }} + > + {t('删除')} + + + } + onClickOutSide={(e) => e.stopPropagation()} + > + + + + } + /> + ); + })} + ); }; diff --git a/web/src/components/table/models/modals/EditModelModal.jsx b/web/src/components/table/models/modals/EditModelModal.jsx index 683d13d..e2c7f51 100644 --- a/web/src/components/table/models/modals/EditModelModal.jsx +++ b/web/src/components/table/models/modals/EditModelModal.jsx @@ -126,6 +126,7 @@ const EditModelModal = (props) => { name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配 status: true, sync_official: true, + sort_order: 0, }); const handleCancel = () => { @@ -332,6 +333,15 @@ const EditModelModal = (props) => { /> + + + + { description: '', icon: '', status: true, + sort_order: 0, }); const handleCancel = () => { @@ -140,6 +141,14 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => { showClear /> + + + { } }; - // Reorder vendors via drag-and-drop - const reorderVendors = async (reorderedVendors) => { - try { - const items = reorderedVendors.map((v, index) => ({ - id: v.id, - sort_order: index, - })); - const res = await API.put('/api/vendors/reorder', { items }); - if (res.data.success) { - setVendors(reorderedVendors); - showSuccess(t('供应商排序已更新')); - } else { - showError(res.data.message || t('排序更新失败')); - await loadVendors(); - } - } catch (error) { - showError(t('排序更新失败')); - await loadVendors(); - } - }; - - // Reorder models via drag-and-drop - const reorderModels = async (reorderedModels) => { - try { - const items = reorderedModels.map((m, index) => ({ - id: m.id, - sort_order: index, - })); - const res = await API.put('/api/models/reorder', { items }); - if (res.data.success) { - showSuccess(t('模型排序已更新')); - await refresh(); - } else { - showError(res.data.message || t('排序更新失败')); - await refresh(); - } - } catch (error) { - showError(t('排序更新失败')); - await refresh(); - } - }; - // Load models data const loadModels = async ( page = 1, @@ -525,8 +483,6 @@ export const useModelsData = () => { editingVendor, setEditingVendor, loadVendors, - reorderVendors, - reorderModels, // Translation t, diff --git a/web/src/pages/Home/HomePricingFilters.jsx b/web/src/pages/Home/HomePricingFilters.jsx index ed91d2f..4f4f76d 100644 --- a/web/src/pages/Home/HomePricingFilters.jsx +++ b/web/src/pages/Home/HomePricingFilters.jsx @@ -90,7 +90,7 @@ const HomePricingFilters = ({ t }) => { hasUnknown = true; } }); - const vendors = Array.from(vendorSet); + const vendors = Array.from(vendorSet).sort(); const getVendorCount = () => { // 计算供应商数量(包含未知供应商) const count = vendorSet.size + (hasUnknown ? 1 : 0);