|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
}; |
|
|
}; |
|
|
|