diff --git a/web/src/components/table/model-pricing/filter/PricingVendors.jsx b/web/src/components/table/model-pricing/filter/PricingVendors.jsx
index 192d166..6d9906c 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).sort(),
+ vendors: Array.from(vendors),
vendorIcons,
hasUnknownVendor,
};
diff --git a/web/src/components/table/models/ModelsColumnDefs.jsx b/web/src/components/table/models/ModelsColumnDefs.jsx
index 4520a1b..fc5afd4 100644
--- a/web/src/components/table/models/ModelsColumnDefs.jsx
+++ b/web/src/components/table/models/ModelsColumnDefs.jsx
@@ -26,6 +26,7 @@ import {
Modal,
Tooltip,
} from '@douyinfe/semi-ui';
+import { IconMenu } from '@douyinfe/semi-icons';
import {
timestamp2string,
getLobeHubIcon,
@@ -282,6 +283,16 @@ export const getModelsColumns = ({
vendorMap,
}) => {
return [
+ {
+ key: 'drag',
+ width: 40,
+ fixed: 'left',
+ render: () => (
+
+
+
+ ),
+ },
{
title: t('图标'),
dataIndex: 'icon',
diff --git a/web/src/components/table/models/ModelsTable.jsx b/web/src/components/table/models/ModelsTable.jsx
index 643bff6..9515ac0 100644
--- a/web/src/components/table/models/ModelsTable.jsx
+++ b/web/src/components/table/models/ModelsTable.jsx
@@ -19,6 +19,9 @@ 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,
@@ -26,6 +29,21 @@ 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,
@@ -43,10 +61,14 @@ const ModelsTable = (modelsData) => {
setShowEdit,
refresh,
vendorMap,
+ reorderModels,
t,
} = modelsData;
- // Get all columns
+ const sensors = useSensors(
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
+ );
+
const columns = useMemo(() => {
return getModelsColumns({
t,
@@ -58,7 +80,6 @@ const ModelsTable = (modelsData) => {
});
}, [t, manageModel, setEditingModel, setShowEdit, refresh, vendorMap]);
- // Handle compact mode by removing fixed positioning
const tableColumns = useMemo(() => {
return compactMode
? columns.map((col) => {
@@ -71,37 +92,62 @@ 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 (
- }
- darkModeImage={
-
+
+ m.id)}
+ strategy={verticalListSortingStrategy}
+ >
+ }
+ darkModeImage={}
+ description={t('搜索无结果')}
+ style={{ padding: 30 }}
+ />
}
- description={t('搜索无结果')}
- style={{ padding: 30 }}
+ className='rounded-xl overflow-hidden'
+ size='middle'
/>
- }
- 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 820d707..c0f5e14 100644
--- a/web/src/components/table/models/ModelsTabs.jsx
+++ b/web/src/components/table/models/ModelsTabs.jsx
@@ -18,11 +18,76 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React from 'react';
-import { Tabs, TabPane, Tag, Button, Dropdown, Modal } from '@douyinfe/semi-ui';
+import { 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,
@@ -36,8 +101,13 @@ const ModelsTabs = ({
setShowEditVendor,
setEditingVendor,
loadVendors,
+ reorderVendors,
t,
}) => {
+ const sensors = useSensors(
+ useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
+ );
+
const handleTabChange = (key) => {
setActiveVendorKey(key);
setActivePage(1);
@@ -45,25 +115,24 @@ const ModelsTabs = ({
};
const handleEditVendor = (vendor, e) => {
- e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换
+ e.stopPropagation();
setEditingVendor(vendor);
setShowEditVendor(true);
};
const handleDeleteVendor = async (vendor, e) => {
- e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换
+ e.stopPropagation();
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('删除失败'));
}
@@ -72,106 +141,62 @@ 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 (
- setShowAddVendor(true)}
- >
- {t('新增供应商')}
-
- }
- >
-
- {t('全部')}
-
- {vendorCounts['all'] || 0}
-
-
- }
- />
+
+ {/* "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) => (
+
+ ))}
+
+
- {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/hooks/models/useModelsData.jsx b/web/src/hooks/models/useModelsData.jsx
index b1bd8b6..fbf73f7 100644
--- a/web/src/hooks/models/useModelsData.jsx
+++ b/web/src/hooks/models/useModelsData.jsx
@@ -119,6 +119,48 @@ export const useModelsData = () => {
}
};
+ // 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,
@@ -483,6 +525,8 @@ 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 4f4f76d..ed91d2f 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).sort();
+ const vendors = Array.from(vendorSet);
const getVendorCount = () => {
// 计算供应商数量(包含未知供应商)
const count = vendorSet.size + (hasUnknown ? 1 : 0);