回退 @dnd-kit 拖拽实现,改为在编辑弹窗中直接设置 sort_order 数值: - 模型编辑弹窗新增「排序」InputNumber 字段 - 供应商编辑弹窗新增「排序」InputNumber 字段 - 模型表格新增 sort_order 显示列 - 移除 @dnd-kit 依赖及所有 DnD 相关代码 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>master
| @@ -4,10 +4,6 @@ | |||||
| "private": true, | "private": true, | ||||
| "type": "module", | "type": "module", | ||||
| "dependencies": { | "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-icons": "^2.63.1", | ||||
| "@douyinfe/semi-ui": "^2.69.1", | "@douyinfe/semi-ui": "^2.69.1", | ||||
| "@lobehub/icons": "^5.0.1", | "@lobehub/icons": "^5.0.1", | ||||
| @@ -56,7 +56,7 @@ const PricingVendors = ({ | |||||
| }); | }); | ||||
| return { | return { | ||||
| vendors: Array.from(vendors), | |||||
| vendors: Array.from(vendors).sort(), | |||||
| vendorIcons, | vendorIcons, | ||||
| hasUnknownVendor, | hasUnknownVendor, | ||||
| }; | }; | ||||
| @@ -26,7 +26,6 @@ import { | |||||
| Modal, | Modal, | ||||
| Tooltip, | Tooltip, | ||||
| } from '@douyinfe/semi-ui'; | } from '@douyinfe/semi-ui'; | ||||
| import { IconMenu } from '@douyinfe/semi-icons'; | |||||
| import { | import { | ||||
| timestamp2string, | timestamp2string, | ||||
| getLobeHubIcon, | getLobeHubIcon, | ||||
| @@ -284,14 +283,9 @@ export const getModelsColumns = ({ | |||||
| }) => { | }) => { | ||||
| return [ | return [ | ||||
| { | { | ||||
| key: 'drag', | |||||
| width: 40, | |||||
| fixed: 'left', | |||||
| render: () => ( | |||||
| <span style={{ cursor: 'grab', color: 'var(--semi-color-text-3)' }}> | |||||
| <IconMenu /> | |||||
| </span> | |||||
| ), | |||||
| title: t('排序'), | |||||
| dataIndex: 'sort_order', | |||||
| width: 60, | |||||
| }, | }, | ||||
| { | { | ||||
| title: t('图标'), | title: t('图标'), | ||||
| @@ -19,9 +19,6 @@ For commercial licensing, please contact support@quantumnous.com | |||||
| import React, { useMemo } from 'react'; | import React, { useMemo } from 'react'; | ||||
| import { Empty } from '@douyinfe/semi-ui'; | 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 CardTable from '../../common/ui/CardTable'; | ||||
| import { | import { | ||||
| IllustrationNoResult, | IllustrationNoResult, | ||||
| @@ -29,21 +26,6 @@ import { | |||||
| } from '@douyinfe/semi-illustrations'; | } from '@douyinfe/semi-illustrations'; | ||||
| import { getModelsColumns } from './ModelsColumnDefs'; | 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 ( | |||||
| <tr ref={setNodeRef} style={style} {...attributes} {...listeners} {...props} /> | |||||
| ); | |||||
| }; | |||||
| const ModelsTable = (modelsData) => { | const ModelsTable = (modelsData) => { | ||||
| const { | const { | ||||
| models, | models, | ||||
| @@ -61,14 +43,10 @@ const ModelsTable = (modelsData) => { | |||||
| setShowEdit, | setShowEdit, | ||||
| refresh, | refresh, | ||||
| vendorMap, | vendorMap, | ||||
| reorderModels, | |||||
| t, | t, | ||||
| } = modelsData; | } = modelsData; | ||||
| const sensors = useSensors( | |||||
| useSensor(PointerSensor, { activationConstraint: { distance: 5 } }) | |||||
| ); | |||||
| // Get all columns | |||||
| const columns = useMemo(() => { | const columns = useMemo(() => { | ||||
| return getModelsColumns({ | return getModelsColumns({ | ||||
| t, | t, | ||||
| @@ -80,6 +58,7 @@ const ModelsTable = (modelsData) => { | |||||
| }); | }); | ||||
| }, [t, manageModel, setEditingModel, setShowEdit, refresh, vendorMap]); | }, [t, manageModel, setEditingModel, setShowEdit, refresh, vendorMap]); | ||||
| // Handle compact mode by removing fixed positioning | |||||
| const tableColumns = useMemo(() => { | const tableColumns = useMemo(() => { | ||||
| return compactMode | return compactMode | ||||
| ? columns.map((col) => { | ? columns.map((col) => { | ||||
| @@ -92,62 +71,37 @@ const ModelsTable = (modelsData) => { | |||||
| : columns; | : columns; | ||||
| }, [compactMode, 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 ( | return ( | ||||
| <DndContext | |||||
| sensors={sensors} | |||||
| collisionDetection={closestCenter} | |||||
| modifiers={[restrictToVerticalAxis]} | |||||
| onDragEnd={handleDragEnd} | |||||
| > | |||||
| <SortableContext | |||||
| items={models.map((m) => m.id)} | |||||
| strategy={verticalListSortingStrategy} | |||||
| > | |||||
| <CardTable | |||||
| columns={tableColumns} | |||||
| dataSource={models} | |||||
| scroll={compactMode ? undefined : { x: 'max-content' }} | |||||
| pagination={{ | |||||
| currentPage: activePage, | |||||
| pageSize: pageSize, | |||||
| total: modelCount, | |||||
| showSizeChanger: true, | |||||
| pageSizeOptions: [10, 20, 50, 100], | |||||
| onPageSizeChange: handlePageSizeChange, | |||||
| onPageChange: handlePageChange, | |||||
| }} | |||||
| hidePagination={true} | |||||
| loading={loading} | |||||
| rowSelection={rowSelection} | |||||
| onRow={handleRow} | |||||
| components={{ | |||||
| body: { row: SortableRow }, | |||||
| }} | |||||
| empty={ | |||||
| <Empty | |||||
| image={<IllustrationNoResult style={{ width: 150, height: 150 }} />} | |||||
| darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />} | |||||
| description={t('搜索无结果')} | |||||
| style={{ padding: 30 }} | |||||
| /> | |||||
| <CardTable | |||||
| columns={tableColumns} | |||||
| dataSource={models} | |||||
| scroll={compactMode ? undefined : { x: 'max-content' }} | |||||
| pagination={{ | |||||
| currentPage: activePage, | |||||
| pageSize: pageSize, | |||||
| total: modelCount, | |||||
| showSizeChanger: true, | |||||
| pageSizeOptions: [10, 20, 50, 100], | |||||
| onPageSizeChange: handlePageSizeChange, | |||||
| onPageChange: handlePageChange, | |||||
| }} | |||||
| hidePagination={true} | |||||
| loading={loading} | |||||
| rowSelection={rowSelection} | |||||
| onRow={handleRow} | |||||
| empty={ | |||||
| <Empty | |||||
| image={<IllustrationNoResult style={{ width: 150, height: 150 }} />} | |||||
| darkModeImage={ | |||||
| <IllustrationNoResultDark style={{ width: 150, height: 150 }} /> | |||||
| } | } | ||||
| className='rounded-xl overflow-hidden' | |||||
| size='middle' | |||||
| description={t('搜索无结果')} | |||||
| style={{ padding: 30 }} | |||||
| /> | /> | ||||
| </SortableContext> | |||||
| </DndContext> | |||||
| } | |||||
| className='rounded-xl overflow-hidden' | |||||
| size='middle' | |||||
| /> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -18,76 +18,11 @@ For commercial licensing, please contact support@quantumnous.com | |||||
| */ | */ | ||||
| import React from 'react'; | 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 { 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 { getLobeHubIcon, showError, showSuccess } from '../../../helpers'; | ||||
| import { API } 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 ( | |||||
| <div ref={setNodeRef} style={style} {...attributes} {...listeners}> | |||||
| {getLobeHubIcon(vendor.icon || 'Layers', 14)} | |||||
| {vendor.name} | |||||
| <Tag color={activeVendorKey === String(vendor.id) ? 'red' : 'grey'} shape='circle' size='small'> | |||||
| {count} | |||||
| </Tag> | |||||
| <Dropdown | |||||
| trigger='click' | |||||
| position='bottomRight' | |||||
| render={ | |||||
| <Dropdown.Menu> | |||||
| <Dropdown.Item icon={<IconEdit />} onClick={(e) => handleEditVendor(vendor, e)}> | |||||
| {t('编辑')} | |||||
| </Dropdown.Item> | |||||
| <Dropdown.Item | |||||
| type='danger' | |||||
| icon={<IconDelete />} | |||||
| 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('删除')} | |||||
| </Dropdown.Item> | |||||
| </Dropdown.Menu> | |||||
| } | |||||
| onClickOutSide={(e) => e.stopPropagation()} | |||||
| > | |||||
| <Button size='small' type='tertiary' theme='outline' onClick={(e) => e.stopPropagation()} style={{ marginLeft: 4 }}> | |||||
| {t('操作')} | |||||
| </Button> | |||||
| </Dropdown> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| const ModelsTabs = ({ | const ModelsTabs = ({ | ||||
| activeVendorKey, | activeVendorKey, | ||||
| setActiveVendorKey, | setActiveVendorKey, | ||||
| @@ -101,13 +36,8 @@ const ModelsTabs = ({ | |||||
| setShowEditVendor, | setShowEditVendor, | ||||
| setEditingVendor, | setEditingVendor, | ||||
| loadVendors, | loadVendors, | ||||
| reorderVendors, | |||||
| t, | t, | ||||
| }) => { | }) => { | ||||
| const sensors = useSensors( | |||||
| useSensor(PointerSensor, { activationConstraint: { distance: 8 } }) | |||||
| ); | |||||
| const handleTabChange = (key) => { | const handleTabChange = (key) => { | ||||
| setActiveVendorKey(key); | setActiveVendorKey(key); | ||||
| setActivePage(1); | setActivePage(1); | ||||
| @@ -115,24 +45,25 @@ const ModelsTabs = ({ | |||||
| }; | }; | ||||
| const handleEditVendor = (vendor, e) => { | const handleEditVendor = (vendor, e) => { | ||||
| e.stopPropagation(); | |||||
| e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换 | |||||
| setEditingVendor(vendor); | setEditingVendor(vendor); | ||||
| setShowEditVendor(true); | setShowEditVendor(true); | ||||
| }; | }; | ||||
| const handleDeleteVendor = async (vendor, e) => { | const handleDeleteVendor = async (vendor, e) => { | ||||
| e.stopPropagation(); | |||||
| e.stopPropagation(); // 阻止事件冒泡,避免触发tab切换 | |||||
| try { | try { | ||||
| const res = await API.delete(`/api/vendors/${vendor.id}`); | const res = await API.delete(`/api/vendors/${vendor.id}`); | ||||
| if (res.data.success) { | if (res.data.success) { | ||||
| showSuccess(t('供应商删除成功')); | showSuccess(t('供应商删除成功')); | ||||
| // 如果删除的是当前选中的供应商,切换到"全部" | |||||
| if (activeVendorKey === String(vendor.id)) { | if (activeVendorKey === String(vendor.id)) { | ||||
| setActiveVendorKey('all'); | setActiveVendorKey('all'); | ||||
| loadModels(1, pageSize, 'all'); | loadModels(1, pageSize, 'all'); | ||||
| } else { | } else { | ||||
| loadModels(activePage, pageSize, activeVendorKey); | loadModels(activePage, pageSize, activeVendorKey); | ||||
| } | } | ||||
| loadVendors(); | |||||
| loadVendors(); // 重新加载供应商列表 | |||||
| } else { | } else { | ||||
| showError(res.data.message || t('删除失败')); | 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 ( | return ( | ||||
| <div className='flex items-center gap-2 flex-wrap mb-2'> | |||||
| {/* "All" tab - not draggable */} | |||||
| <div | |||||
| onClick={() => 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('全部')} | |||||
| <Tag color={activeVendorKey === 'all' ? 'red' : 'grey'} shape='circle' size='small'> | |||||
| {vendorCounts['all'] || 0} | |||||
| </Tag> | |||||
| </div> | |||||
| {/* Draggable vendor tabs */} | |||||
| <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}> | |||||
| <SortableContext items={vendors.map((v) => String(v.id))} strategy={horizontalListSortingStrategy}> | |||||
| {vendors.map((vendor) => ( | |||||
| <SortableVendorItem | |||||
| key={String(vendor.id)} | |||||
| vendor={vendor} | |||||
| activeVendorKey={activeVendorKey} | |||||
| vendorCounts={vendorCounts} | |||||
| handleEditVendor={handleEditVendor} | |||||
| handleDeleteVendor={handleDeleteVendor} | |||||
| t={t} | |||||
| /> | |||||
| ))} | |||||
| </SortableContext> | |||||
| </DndContext> | |||||
| <Tabs | |||||
| activeKey={activeVendorKey} | |||||
| type='card' | |||||
| collapsible | |||||
| onChange={handleTabChange} | |||||
| className='mb-2' | |||||
| tabBarExtraContent={ | |||||
| <Button | |||||
| type='primary' | |||||
| size='small' | |||||
| onClick={() => setShowAddVendor(true)} | |||||
| > | |||||
| {t('新增供应商')} | |||||
| </Button> | |||||
| } | |||||
| > | |||||
| <TabPane | |||||
| itemKey='all' | |||||
| tab={ | |||||
| <span className='flex items-center gap-2'> | |||||
| {t('全部')} | |||||
| <Tag | |||||
| color={activeVendorKey === 'all' ? 'red' : 'grey'} | |||||
| shape='circle' | |||||
| > | |||||
| {vendorCounts['all'] || 0} | |||||
| </Tag> | |||||
| </span> | |||||
| } | |||||
| /> | |||||
| <Button type='primary' size='small' onClick={() => setShowAddVendor(true)}> | |||||
| {t('新增供应商')} | |||||
| </Button> | |||||
| </div> | |||||
| {vendors.map((vendor) => { | |||||
| const key = String(vendor.id); | |||||
| const count = vendorCounts[vendor.id] || 0; | |||||
| return ( | |||||
| <TabPane | |||||
| key={key} | |||||
| itemKey={key} | |||||
| tab={ | |||||
| <span className='flex items-center gap-2'> | |||||
| {getLobeHubIcon(vendor.icon || 'Layers', 14)} | |||||
| {vendor.name} | |||||
| <Tag | |||||
| color={activeVendorKey === key ? 'red' : 'grey'} | |||||
| shape='circle' | |||||
| > | |||||
| {count} | |||||
| </Tag> | |||||
| <Dropdown | |||||
| trigger='click' | |||||
| position='bottomRight' | |||||
| render={ | |||||
| <Dropdown.Menu> | |||||
| <Dropdown.Item | |||||
| icon={<IconEdit />} | |||||
| onClick={(e) => handleEditVendor(vendor, e)} | |||||
| > | |||||
| {t('编辑')} | |||||
| </Dropdown.Item> | |||||
| <Dropdown.Item | |||||
| type='danger' | |||||
| icon={<IconDelete />} | |||||
| 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('删除')} | |||||
| </Dropdown.Item> | |||||
| </Dropdown.Menu> | |||||
| } | |||||
| onClickOutSide={(e) => e.stopPropagation()} | |||||
| > | |||||
| <Button | |||||
| size='small' | |||||
| type='tertiary' | |||||
| theme='outline' | |||||
| onClick={(e) => e.stopPropagation()} | |||||
| > | |||||
| {t('操作')} | |||||
| </Button> | |||||
| </Dropdown> | |||||
| </span> | |||||
| } | |||||
| /> | |||||
| ); | |||||
| })} | |||||
| </Tabs> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -126,6 +126,7 @@ const EditModelModal = (props) => { | |||||
| name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配 | name_rule: props.editingModel?.model_name ? 0 : undefined, // 通过未配置模型过来的固定为精确匹配 | ||||
| status: true, | status: true, | ||||
| sync_official: true, | sync_official: true, | ||||
| sort_order: 0, | |||||
| }); | }); | ||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| @@ -332,6 +333,15 @@ const EditModelModal = (props) => { | |||||
| /> | /> | ||||
| </Col> | </Col> | ||||
| <Col span={24}> | |||||
| <Form.InputNumber | |||||
| field='sort_order' | |||||
| label={t('排序')} | |||||
| precision={0} | |||||
| defaultValue={0} | |||||
| /> | |||||
| </Col> | |||||
| <Col span={24}> | <Col span={24}> | ||||
| <Form.Select | <Form.Select | ||||
| field='type' | field='type' | ||||
| @@ -38,6 +38,7 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => { | |||||
| description: '', | description: '', | ||||
| icon: '', | icon: '', | ||||
| status: true, | status: true, | ||||
| sort_order: 0, | |||||
| }); | }); | ||||
| const handleCancel = () => { | const handleCancel = () => { | ||||
| @@ -140,6 +141,14 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => { | |||||
| showClear | showClear | ||||
| /> | /> | ||||
| </Col> | </Col> | ||||
| <Col span={24}> | |||||
| <Form.InputNumber | |||||
| field='sort_order' | |||||
| label={t('排序')} | |||||
| precision={0} | |||||
| defaultValue={0} | |||||
| /> | |||||
| </Col> | |||||
| <Col span={24}> | <Col span={24}> | ||||
| <Form.TextArea | <Form.TextArea | ||||
| field='description' | field='description' | ||||
| @@ -119,48 +119,6 @@ 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 | // Load models data | ||||
| const loadModels = async ( | const loadModels = async ( | ||||
| page = 1, | page = 1, | ||||
| @@ -525,8 +483,6 @@ export const useModelsData = () => { | |||||
| editingVendor, | editingVendor, | ||||
| setEditingVendor, | setEditingVendor, | ||||
| loadVendors, | loadVendors, | ||||
| reorderVendors, | |||||
| reorderModels, | |||||
| // Translation | // Translation | ||||
| t, | t, | ||||
| @@ -90,7 +90,7 @@ const HomePricingFilters = ({ t }) => { | |||||
| hasUnknown = true; | hasUnknown = true; | ||||
| } | } | ||||
| }); | }); | ||||
| const vendors = Array.from(vendorSet); | |||||
| const vendors = Array.from(vendorSet).sort(); | |||||
| const getVendorCount = () => { | const getVendorCount = () => { | ||||
| // 计算供应商数量(包含未知供应商) | // 计算供应商数量(包含未知供应商) | ||||
| const count = vendorSet.size + (hasUnknown ? 1 : 0); | const count = vendorSet.size + (hasUnknown ? 1 : 0); | ||||