|
|
@@ -0,0 +1,378 @@
|
|
|
+/*
|
|
|
+Copyright (C) 2025 QuantumNous
|
|
|
+
|
|
|
+This program is free software: you can redistribute it and/or modify
|
|
|
+it under the terms of the GNU Affero General Public License as
|
|
|
+published by the Free Software Foundation, either version 3 of the
|
|
|
+License, or (at your option) any later version.
|
|
|
+
|
|
|
+This program is distributed in the hope that it will be useful,
|
|
|
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+GNU Affero General Public License for more details.
|
|
|
+
|
|
|
+You should have received a copy of the GNU Affero General Public License
|
|
|
+along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+For commercial licensing, please contact support@quantumnous.com
|
|
|
+*/
|
|
|
+
|
|
|
+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');
|
|
|
+
|
|
|
+ // State management
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // Modal states
|
|
|
+ const [showEdit, setShowEdit] = useState(false);
|
|
|
+ const [editingModel, setEditingModel] = useState({
|
|
|
+ id: undefined,
|
|
|
+ });
|
|
|
+
|
|
|
+ // Row selection
|
|
|
+ const [selectedKeys, setSelectedKeys] = useState([]);
|
|
|
+ const rowSelection = {
|
|
|
+ getCheckboxProps: (record) => ({
|
|
|
+ name: record.model_name,
|
|
|
+ }),
|
|
|
+ selectedRowKeys: selectedKeys.map((model) => model.id),
|
|
|
+ onChange: (selectedRowKeys, selectedRows) => {
|
|
|
+ setSelectedKeys(selectedRows);
|
|
|
+ },
|
|
|
+ };
|
|
|
+
|
|
|
+ // Form initial values
|
|
|
+ const formInitValues = {
|
|
|
+ searchKeyword: '',
|
|
|
+ searchVendor: '',
|
|
|
+ };
|
|
|
+
|
|
|
+ // Form API reference
|
|
|
+ const [formApi, setFormApi] = useState(null);
|
|
|
+
|
|
|
+ // Get form values helper function
|
|
|
+ const getFormValues = () => {
|
|
|
+ const formValues = formApi ? formApi.getValues() : {};
|
|
|
+ return {
|
|
|
+ searchKeyword: formValues.searchKeyword || '',
|
|
|
+ searchVendor: formValues.searchVendor || '',
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // Close edit modal
|
|
|
+ const closeEdit = () => {
|
|
|
+ setShowEdit(false);
|
|
|
+ setTimeout(() => {
|
|
|
+ setEditingModel({ id: undefined });
|
|
|
+ }, 500);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Set model format with key field
|
|
|
+ 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 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 (_) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Load models data
|
|
|
+ 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') {
|
|
|
+ // 按供应商筛选,通过vendor搜索接口
|
|
|
+ const vendor = vendors.find(v => String(v.id) === vendorKey);
|
|
|
+ if (vendor) {
|
|
|
+ url = `/api/models/search?vendor=${vendor.name}&p=${page}&page_size=${size}`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await API.get(url);
|
|
|
+ const { success, message, data } = res.data;
|
|
|
+ if (success) {
|
|
|
+ const items = data.items || data || [];
|
|
|
+ const newPageData = Array.isArray(items) ? items : [];
|
|
|
+ setActivePage(data.page || page);
|
|
|
+ setModelCount(data.total || newPageData.length);
|
|
|
+ setModelFormat(newPageData);
|
|
|
+
|
|
|
+ // 更新供应商统计
|
|
|
+ updateVendorCounts(newPageData);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ setModels([]);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ showError(t('获取模型列表失败'));
|
|
|
+ setModels([]);
|
|
|
+ }
|
|
|
+ setLoading(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Refresh data
|
|
|
+ const refresh = async (page = activePage) => {
|
|
|
+ await loadModels(page, pageSize);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Search models with keyword and vendor
|
|
|
+ const searchModels = async () => {
|
|
|
+ const formValues = getFormValues();
|
|
|
+ const { searchKeyword, searchVendor } = formValues;
|
|
|
+
|
|
|
+ if (searchKeyword === '' && searchVendor === '') {
|
|
|
+ // If keyword is blank, load models instead
|
|
|
+ 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 items = data.items || data || [];
|
|
|
+ const newPageData = Array.isArray(items) ? items : [];
|
|
|
+ setActivePage(data.page || 1);
|
|
|
+ setModelCount(data.total || newPageData.length);
|
|
|
+ setModelFormat(newPageData);
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ setModels([]);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ showError(t('搜索模型失败'));
|
|
|
+ setModels([]);
|
|
|
+ }
|
|
|
+ setSearching(false);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Manage model (enable/disable/delete)
|
|
|
+ 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 {
|
|
|
+ // Update local state for enable/disable
|
|
|
+ setModels(prevModels =>
|
|
|
+ prevModels.map(model =>
|
|
|
+ model.id === id ? { ...model, status: action === 'enable' ? 1 : 0 } : model
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ showError(message);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 更新供应商统计
|
|
|
+ const updateVendorCounts = (models) => {
|
|
|
+ const counts = { all: models.length };
|
|
|
+ models.forEach(model => {
|
|
|
+ if (model.vendor_id) {
|
|
|
+ counts[model.vendor_id] = (counts[model.vendor_id] || 0) + 1;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ setVendorCounts(counts);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle page change
|
|
|
+ const handlePageChange = (page) => {
|
|
|
+ setActivePage(page);
|
|
|
+ loadModels(page, pageSize, activeVendorKey);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle page size change
|
|
|
+ const handlePageSizeChange = async (size) => {
|
|
|
+ setPageSize(size);
|
|
|
+ setActivePage(1);
|
|
|
+ await loadModels(1, size, activeVendorKey);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Handle row click
|
|
|
+ const handleRow = (record, index) => {
|
|
|
+ return {
|
|
|
+ onClick: (event) => {
|
|
|
+ // Don't trigger row selection when clicking on buttons
|
|
|
+ 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);
|
|
|
+ },
|
|
|
+ };
|
|
|
+ };
|
|
|
+
|
|
|
+ // Batch delete models
|
|
|
+ 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('批量删除失败'));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Copy text helper
|
|
|
+ const copyText = async (text) => {
|
|
|
+ try {
|
|
|
+ await navigator.clipboard.writeText(text);
|
|
|
+ showSuccess(t('复制成功'));
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Copy failed:', error);
|
|
|
+ showError(t('复制失败'));
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // Initial load
|
|
|
+ useEffect(() => {
|
|
|
+ loadVendors();
|
|
|
+ loadModels();
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ return {
|
|
|
+ // Data state
|
|
|
+ models,
|
|
|
+ loading,
|
|
|
+ searching,
|
|
|
+ activePage,
|
|
|
+ pageSize,
|
|
|
+ modelCount,
|
|
|
+
|
|
|
+ // Selection state
|
|
|
+ selectedKeys,
|
|
|
+ rowSelection,
|
|
|
+ handleRow,
|
|
|
+
|
|
|
+ // Modal state
|
|
|
+ showEdit,
|
|
|
+ editingModel,
|
|
|
+ setEditingModel,
|
|
|
+ setShowEdit,
|
|
|
+ closeEdit,
|
|
|
+
|
|
|
+ // Form state
|
|
|
+ formInitValues,
|
|
|
+ setFormApi,
|
|
|
+
|
|
|
+ // Actions
|
|
|
+ loadModels,
|
|
|
+ searchModels,
|
|
|
+ refresh,
|
|
|
+ manageModel,
|
|
|
+ batchDeleteModels,
|
|
|
+ copyText,
|
|
|
+
|
|
|
+ // Pagination
|
|
|
+ handlePageChange,
|
|
|
+ handlePageSizeChange,
|
|
|
+
|
|
|
+ // UI state
|
|
|
+ compactMode,
|
|
|
+ setCompactMode,
|
|
|
+
|
|
|
+ // Vendor data
|
|
|
+ vendors,
|
|
|
+ vendorMap,
|
|
|
+ vendorCounts,
|
|
|
+ activeVendorKey,
|
|
|
+ setActiveVendorKey,
|
|
|
+ showAddVendor,
|
|
|
+ setShowAddVendor,
|
|
|
+ showEditVendor,
|
|
|
+ setShowEditVendor,
|
|
|
+ editingVendor,
|
|
|
+ setEditingVendor,
|
|
|
+ loadVendors,
|
|
|
+
|
|
|
+ // Translation
|
|
|
+ t,
|
|
|
+ };
|
|
|
+};
|