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