| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import { useState, useEffect, useMemo } from 'react'; |
| | import { useTranslation } from 'react-i18next'; |
| | import { API, showError, showSuccess } from '../../helpers'; |
| | import { ITEMS_PER_PAGE } from '../../constants'; |
| | import { useTableCompactMode } from '../common/useTableCompactMode'; |
| |
|
| | export const useModelsData = () => { |
| | const { t } = useTranslation(); |
| | const [compactMode, setCompactMode] = useTableCompactMode('models'); |
| |
|
| | |
| | const [models, setModels] = useState([]); |
| | const [loading, setLoading] = useState(true); |
| | const [activePage, setActivePage] = useState(1); |
| | const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); |
| | const [searching, setSearching] = useState(false); |
| | const [modelCount, setModelCount] = useState(0); |
| |
|
| | |
| | const [showEdit, setShowEdit] = useState(false); |
| | const [editingModel, setEditingModel] = useState({ |
| | id: undefined, |
| | }); |
| |
|
| | |
| | const [selectedKeys, setSelectedKeys] = useState([]); |
| | const rowSelection = { |
| | getCheckboxProps: (record) => ({ |
| | name: record.model_name, |
| | }), |
| | selectedRowKeys: selectedKeys.map((model) => model.id), |
| | onChange: (selectedRowKeys, selectedRows) => { |
| | setSelectedKeys(selectedRows); |
| | }, |
| | }; |
| |
|
| | |
| | const formInitValues = { |
| | searchKeyword: '', |
| | searchVendor: '', |
| | }; |
| |
|
| | |
| | |
| | const extractItems = (payload) => { |
| | const items = payload?.items || payload || []; |
| | return Array.isArray(items) ? items : []; |
| | }; |
| |
|
| | |
| | const [formApi, setFormApi] = useState(null); |
| |
|
| | |
| | const getFormValues = () => formApi?.getValues() || formInitValues; |
| |
|
| | |
| | const closeEdit = () => { |
| | setShowEdit(false); |
| | setTimeout(() => { |
| | setEditingModel({ id: undefined }); |
| | }, 500); |
| | }; |
| |
|
| | |
| | const setModelFormat = (models) => { |
| | for (let i = 0; i < models.length; i++) { |
| | models[i].key = models[i].id; |
| | } |
| | setModels(models); |
| | }; |
| |
|
| | |
| | const [vendors, setVendors] = useState([]); |
| | const [vendorCounts, setVendorCounts] = useState({}); |
| | const [activeVendorKey, setActiveVendorKey] = useState('all'); |
| | const [showAddVendor, setShowAddVendor] = useState(false); |
| | const [showEditVendor, setShowEditVendor] = useState(false); |
| | const [editingVendor, setEditingVendor] = useState({ id: undefined }); |
| | const [syncing, setSyncing] = useState(false); |
| | const [previewing, setPreviewing] = useState(false); |
| |
|
| | const vendorMap = useMemo(() => { |
| | const map = {}; |
| | vendors.forEach((v) => { |
| | map[v.id] = v; |
| | }); |
| | return map; |
| | }, [vendors]); |
| |
|
| | |
| | const loadVendors = async () => { |
| | try { |
| | const res = await API.get('/api/vendors/?page_size=1000'); |
| | if (res.data.success) { |
| | const items = res.data.data.items || res.data.data || []; |
| | setVendors(Array.isArray(items) ? items : []); |
| | } |
| | } catch (_) { |
| | |
| | } |
| | }; |
| |
|
| | |
| | const loadModels = async ( |
| | page = 1, |
| | size = pageSize, |
| | vendorKey = activeVendorKey, |
| | ) => { |
| | setLoading(true); |
| | try { |
| | let url = `/api/models/?p=${page}&page_size=${size}`; |
| | if (vendorKey && vendorKey !== 'all') { |
| | |
| | url = `/api/models/search?vendor=${vendorKey}&p=${page}&page_size=${size}`; |
| | } |
| |
|
| | const res = await API.get(url); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | const newPageData = extractItems(data); |
| | setActivePage(data.page || page); |
| | setModelCount(data.total || newPageData.length); |
| | setModelFormat(newPageData); |
| |
|
| | if (data.vendor_counts) { |
| | const sumAll = Object.values(data.vendor_counts).reduce( |
| | (acc, v) => acc + v, |
| | 0, |
| | ); |
| | setVendorCounts({ ...data.vendor_counts, all: sumAll }); |
| | } |
| | } else { |
| | showError(message); |
| | setModels([]); |
| | } |
| | } catch (error) { |
| | console.error(error); |
| | showError(t('获取模型列表失败')); |
| | setModels([]); |
| | } |
| | setLoading(false); |
| | }; |
| |
|
| | |
| | const refresh = async (page = activePage) => { |
| | await loadModels(page, pageSize); |
| | }; |
| |
|
| | |
| | const syncUpstream = async (opts = {}) => { |
| | const locale = opts?.locale; |
| | setSyncing(true); |
| | try { |
| | const body = {}; |
| | if (locale) body.locale = locale; |
| | const res = await API.post('/api/models/sync_upstream', body); |
| | const { success, message, data } = res.data || {}; |
| | if (success) { |
| | const createdModels = data?.created_models || 0; |
| | const createdVendors = data?.created_vendors || 0; |
| | const skipped = (data?.skipped_models || []).length || 0; |
| | showSuccess( |
| | t( |
| | `已同步:新增 ${createdModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`, |
| | ), |
| | ); |
| | await loadVendors(); |
| | await refresh(); |
| | } else { |
| | showError(message || t('同步失败')); |
| | } |
| | } catch (e) { |
| | showError(t('同步失败')); |
| | } |
| | setSyncing(false); |
| | }; |
| |
|
| | |
| | const previewUpstreamDiff = async (opts = {}) => { |
| | const locale = opts?.locale; |
| | setPreviewing(true); |
| | try { |
| | const url = `/api/models/sync_upstream/preview${locale ? `?locale=${locale}` : ''}`; |
| | const res = await API.get(url); |
| | const { success, message, data } = res.data || {}; |
| | if (success) { |
| | return data || { missing: [], conflicts: [] }; |
| | } |
| | showError(message || t('预览失败')); |
| | return { missing: [], conflicts: [] }; |
| | } catch (e) { |
| | showError(t('预览失败')); |
| | return { missing: [], conflicts: [] }; |
| | } finally { |
| | setPreviewing(false); |
| | } |
| | }; |
| |
|
| | |
| | const applyUpstreamOverwrite = async (payloadOrArray = []) => { |
| | const isArray = Array.isArray(payloadOrArray); |
| | const overwrite = isArray ? payloadOrArray : payloadOrArray.overwrite || []; |
| | const locale = isArray ? undefined : payloadOrArray.locale; |
| | setSyncing(true); |
| | try { |
| | const body = { overwrite }; |
| | if (locale) body.locale = locale; |
| | const res = await API.post('/api/models/sync_upstream', body); |
| | const { success, message, data } = res.data || {}; |
| | if (success) { |
| | const createdModels = data?.created_models || 0; |
| | const updatedModels = data?.updated_models || 0; |
| | const createdVendors = data?.created_vendors || 0; |
| | const skipped = (data?.skipped_models || []).length || 0; |
| | showSuccess( |
| | t( |
| | `完成:新增 ${createdModels} 模型,更新 ${updatedModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`, |
| | ), |
| | ); |
| | await loadVendors(); |
| | await refresh(); |
| | return true; |
| | } |
| | showError(message || t('同步失败')); |
| | return false; |
| | } catch (e) { |
| | showError(t('同步失败')); |
| | return false; |
| | } finally { |
| | setSyncing(false); |
| | } |
| | }; |
| |
|
| | |
| | const searchModels = async () => { |
| | const { searchKeyword = '', searchVendor = '' } = getFormValues(); |
| |
|
| | if (searchKeyword === '' && searchVendor === '') { |
| | |
| | await loadModels(1, pageSize); |
| | return; |
| | } |
| |
|
| | setSearching(true); |
| | try { |
| | const res = await API.get( |
| | `/api/models/search?keyword=${searchKeyword}&vendor=${searchVendor}&p=1&page_size=${pageSize}`, |
| | ); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | const newPageData = extractItems(data); |
| | setActivePage(data.page || 1); |
| | setModelCount(data.total || newPageData.length); |
| | setModelFormat(newPageData); |
| | if (data.vendor_counts) { |
| | const sumAll = Object.values(data.vendor_counts).reduce( |
| | (acc, v) => acc + v, |
| | 0, |
| | ); |
| | setVendorCounts({ ...data.vendor_counts, all: sumAll }); |
| | } |
| | } else { |
| | showError(message); |
| | setModels([]); |
| | } |
| | } catch (error) { |
| | console.error(error); |
| | showError(t('搜索模型失败')); |
| | setModels([]); |
| | } |
| | setSearching(false); |
| | }; |
| |
|
| | |
| | const manageModel = async (id, action, record) => { |
| | let res; |
| | switch (action) { |
| | case 'delete': |
| | res = await API.delete(`/api/models/${id}`); |
| | break; |
| | case 'enable': |
| | res = await API.put('/api/models/?status_only=true', { id, status: 1 }); |
| | break; |
| | case 'disable': |
| | res = await API.put('/api/models/?status_only=true', { id, status: 0 }); |
| | break; |
| | default: |
| | return; |
| | } |
| |
|
| | const { success, message } = res.data; |
| | if (success) { |
| | showSuccess(t('操作成功完成!')); |
| | if (action === 'delete') { |
| | await refresh(); |
| | } else { |
| | |
| | setModels((prevModels) => |
| | prevModels.map((model) => |
| | model.id === id |
| | ? { ...model, status: action === 'enable' ? 1 : 0 } |
| | : model, |
| | ), |
| | ); |
| | } |
| | } else { |
| | showError(message); |
| | } |
| | }; |
| |
|
| | |
| | const handlePageChange = (page) => { |
| | setActivePage(page); |
| | loadModels(page, pageSize, activeVendorKey); |
| | }; |
| |
|
| | |
| | useEffect(() => { |
| | loadModels(1, pageSize, activeVendorKey); |
| | }, [activeVendorKey]); |
| |
|
| | |
| | const handlePageSizeChange = async (size) => { |
| | setPageSize(size); |
| | setActivePage(1); |
| | await loadModels(1, size, activeVendorKey); |
| | }; |
| |
|
| | |
| | const handleRow = (record, index) => { |
| | const rowStyle = |
| | record.status !== 1 |
| | ? { |
| | style: { |
| | background: 'var(--semi-color-disabled-border)', |
| | }, |
| | } |
| | : {}; |
| |
|
| | return { |
| | ...rowStyle, |
| | onClick: (event) => { |
| | |
| | if (event.target.closest('button, .semi-button')) { |
| | return; |
| | } |
| | const newSelectedKeys = selectedKeys.some( |
| | (item) => item.id === record.id, |
| | ) |
| | ? selectedKeys.filter((item) => item.id !== record.id) |
| | : [...selectedKeys, record]; |
| | setSelectedKeys(newSelectedKeys); |
| | }, |
| | }; |
| | }; |
| |
|
| | |
| | const batchDeleteModels = async () => { |
| | if (selectedKeys.length === 0) { |
| | showError(t('请至少选择一个模型')); |
| | return; |
| | } |
| |
|
| | try { |
| | const deletePromises = selectedKeys.map((model) => |
| | API.delete(`/api/models/${model.id}`), |
| | ); |
| |
|
| | const results = await Promise.all(deletePromises); |
| | let successCount = 0; |
| |
|
| | results.forEach((res, index) => { |
| | if (res.data.success) { |
| | successCount++; |
| | } else { |
| | showError( |
| | `删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`, |
| | ); |
| | } |
| | }); |
| |
|
| | if (successCount > 0) { |
| | showSuccess(t(`成功删除 ${successCount} 个模型`)); |
| | setSelectedKeys([]); |
| | await refresh(); |
| | } |
| | } catch (error) { |
| | showError(t('批量删除失败')); |
| | } |
| | }; |
| |
|
| | |
| | const copyText = async (text) => { |
| | try { |
| | await navigator.clipboard.writeText(text); |
| | showSuccess(t('复制成功')); |
| | } catch (error) { |
| | console.error('Copy failed:', error); |
| | showError(t('复制失败')); |
| | } |
| | }; |
| |
|
| | |
| | useEffect(() => { |
| | (async () => { |
| | await loadVendors(); |
| | })(); |
| | |
| | }, []); |
| |
|
| | return { |
| | |
| | models, |
| | loading, |
| | searching, |
| | activePage, |
| | pageSize, |
| | modelCount, |
| |
|
| | |
| | selectedKeys, |
| | rowSelection, |
| | handleRow, |
| | setSelectedKeys, |
| |
|
| | |
| | showEdit, |
| | editingModel, |
| | setEditingModel, |
| | setShowEdit, |
| | closeEdit, |
| |
|
| | |
| | formInitValues, |
| | setFormApi, |
| |
|
| | |
| | loadModels, |
| | searchModels, |
| | refresh, |
| | manageModel, |
| | batchDeleteModels, |
| | copyText, |
| |
|
| | |
| | setActivePage, |
| | handlePageChange, |
| | handlePageSizeChange, |
| |
|
| | |
| | compactMode, |
| | setCompactMode, |
| |
|
| | |
| | vendors, |
| | vendorMap, |
| | vendorCounts, |
| | activeVendorKey, |
| | setActiveVendorKey, |
| | showAddVendor, |
| | setShowAddVendor, |
| | showEditVendor, |
| | setShowEditVendor, |
| | editingVendor, |
| | setEditingVendor, |
| | loadVendors, |
| |
|
| | |
| | t, |
| |
|
| | |
| | syncing, |
| | previewing, |
| | syncUpstream, |
| | previewUpstreamDiff, |
| | applyUpstreamOverwrite, |
| | }; |
| | }; |
| |
|