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