mirror of
https://github.com/dataelement/bisheng.git
synced 2026-09-24 23:19:52 +08:00
灵思二期更新修改
This commit is contained in:
@@ -146,7 +146,7 @@ const SopFormDrawer = ({
|
||||
ref={nameInputRef}
|
||||
value={sopForm.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className={`mt-1 block w-full border ${errors.name ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-lg`}
|
||||
className={`mt-1 block w-full border ${errors.name ? 'border-red-500' : 'border-gray-300'} rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-[16px]`}
|
||||
placeholder="请输入SOP名称"
|
||||
/>
|
||||
<div className="flex justify-between">
|
||||
@@ -166,7 +166,7 @@ const SopFormDrawer = ({
|
||||
rows={3}
|
||||
value={sopForm.description}
|
||||
onChange={(e) => handleInputChange('description', e.target.value)}
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-lg"
|
||||
className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-blue-500 focus:border-blue-500 text-[16px]"
|
||||
placeholder="请输入SOP描述"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { LoadIcon } from '../bs-icons/loading';
|
||||
import AutoPagination from '../bs-ui/pagination/autoPagination';
|
||||
import SopMarkdown from './SopMarkdown';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../bs-ui/tooltip';
|
||||
|
||||
interface SopRecord {
|
||||
id: number;
|
||||
@@ -24,7 +25,7 @@ interface SopRecord {
|
||||
user_name: string;
|
||||
}
|
||||
|
||||
export default function ImportFromRecordsDialog({ open, tools, onOpenChange, onSuccess }) {
|
||||
export default function ImportFromRecordsDialog({ open, tools, onOpenChange, onSuccess, setDuplicateNames, duplicateNames, duplicateDialogOpen, setDuplicateDialogOpen, importFormData }) {
|
||||
const { toast } = useToast();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [records, setRecords] = useState<SopRecord[]>([]);
|
||||
@@ -37,45 +38,34 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
key: 'create_time',
|
||||
direction: 'desc' as 'asc' | 'desc'
|
||||
});
|
||||
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
const [duplicateNames, setDuplicateNames] = useState<string[]>([]);
|
||||
|
||||
const [pageInputValue, setPageInputValue] = useState(page.toString());
|
||||
const [selectedRecordIds, setSelectedRecordIds] = useState<number[]>([]);
|
||||
|
||||
const [allRecords, setAllRecords] = useState<SopRecord[]>([]);
|
||||
const [selectedRecords, setSelectedRecords] = useState<SopRecord[]>([]);
|
||||
const [isSavingAsNew, setIsSavingAsNew] = useState(false);
|
||||
const [isOverwriting, setIsOverwriting] = useState(false);
|
||||
// 获取SOP记录
|
||||
const fetchRecords = async () => {
|
||||
const fetchRecords = async (isSearch = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await sopApi.GetSopRecord({
|
||||
const params = {
|
||||
keyword: searchTerm,
|
||||
page,
|
||||
page_size: pageSize,
|
||||
sort: sortConfig.direction
|
||||
});
|
||||
...(isSearch ? {} : { page, page_size: pageSize }),
|
||||
sort: sortConfig.direction,
|
||||
};
|
||||
|
||||
const res = await sopApi.GetSopRecord(params);
|
||||
|
||||
console.log('API响应:', res);
|
||||
if (Array.isArray(res)) {
|
||||
setRecords(res);
|
||||
setAllRecords(res); // 存储所有数据
|
||||
setRecords(res); // 当前页数据
|
||||
setTotal(res.length);
|
||||
setCurrentRecord(res[0] || null);
|
||||
} else if (res?.list) {
|
||||
setAllRecords(res.list); // 存储所有数据
|
||||
setRecords(res.list); // 当前页数据
|
||||
setTotal(res.total);
|
||||
}
|
||||
else if (res && Array.isArray(res.list)) {
|
||||
setRecords(res.list);
|
||||
setTotal(res.total || res.list.length);
|
||||
setCurrentRecord(res.list[0] || null);
|
||||
}
|
||||
else {
|
||||
throw new Error('API返回的数据结构不符合预期');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
toast({
|
||||
variant: 'error',
|
||||
description: error.message || '获取数据失败,请检查API响应结构'
|
||||
});
|
||||
setRecords([]);
|
||||
setTotal(0);
|
||||
setCurrentRecord(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -92,14 +82,27 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
setPage(1);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
// 搜索和分页变化时重新获取数据
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (open) fetchRecords();
|
||||
if (open) {
|
||||
if (searchTerm) {
|
||||
// 执行搜索时,重置页码为1并使用全局搜索
|
||||
setPage(1);
|
||||
fetchRecords(true); // 传入true表示是搜索请求
|
||||
} else {
|
||||
// 没有搜索词时,恢复普通分页模式
|
||||
fetchRecords();
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}, [searchTerm, page, pageSize, sortConfig]);
|
||||
}, [searchTerm]);
|
||||
// 搜索和分页变化时重新获取数据
|
||||
useEffect(() => {
|
||||
if (open && !searchTerm) {
|
||||
fetchRecords();
|
||||
}
|
||||
}, [page, pageSize, sortConfig]);
|
||||
|
||||
// 排序处理
|
||||
const sortedRecords = useMemo(() => {
|
||||
@@ -156,11 +159,20 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
|
||||
const handleToggleSelect = (record: SopRecord, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setSelectedRecordIds(prev =>
|
||||
prev.includes(record.id)
|
||||
setSelectedRecordIds(prev => {
|
||||
const newIds = prev.includes(record.id)
|
||||
? prev.filter(id => id !== record.id)
|
||||
: [...prev, record.id]
|
||||
);
|
||||
: [...prev, record.id];
|
||||
|
||||
// 同步更新selectedRecords
|
||||
setSelectedRecords(prevRecords =>
|
||||
prev.includes(record.id)
|
||||
? prevRecords.filter(r => r.id !== record.id)
|
||||
: [...prevRecords, record]
|
||||
);
|
||||
|
||||
return newIds;
|
||||
});
|
||||
setCurrentRecord(record);
|
||||
};
|
||||
// 全选/取消全选当前页
|
||||
@@ -173,6 +185,16 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
? prev.filter(id => !currentPageIds.includes(id))
|
||||
: [...new Set([...prev, ...currentPageIds])]
|
||||
);
|
||||
|
||||
// 同步更新selectedRecords
|
||||
setSelectedRecords(prev => {
|
||||
if (allSelected) {
|
||||
return prev.filter(r => !currentPageIds.includes(r.id));
|
||||
} else {
|
||||
const newRecords = records.filter(r => !prev.some(p => p.id === r.id));
|
||||
return [...prev, ...newRecords];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const importSops = async (recordsToImport: SopRecord[], overwrite = false, saveNew = false) => {
|
||||
@@ -196,13 +218,15 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
|
||||
toast({ variant: 'success', description: '导入成功' });
|
||||
onOpenChange(false);
|
||||
onSuccess?.(); // 导入成功后调用回调
|
||||
|
||||
// 导入成功后清空所有选择状态
|
||||
setSelectedRecordIds([]);
|
||||
setSelectedRecords([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const markdownRef = useRef(null);
|
||||
useEffect(() => {
|
||||
markdownRef.current?.setValue(currentRecord?.content || '');
|
||||
@@ -210,10 +234,10 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent className="w-[87.5vw] min-w-[85vw]">
|
||||
<SheetContent className="w-[87.5vw] min-w-[87.5vw]">
|
||||
<div className="flex h-full" onClick={e => e.stopPropagation()}>
|
||||
{/* 左侧记录列表 */}
|
||||
<div className="p-6 w-[50%]">
|
||||
<div className="p-6 w-[50%] min-w-160">
|
||||
<SheetHeader>
|
||||
<SheetTitle>从运行记录中导入SOP</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -231,8 +255,10 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
|
||||
<div className="flex-1 overflow-y-auto h-[calc(100%-180px)]">
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<LoadIcon className="animate-spin w-8 h-8" />
|
||||
<div className="flex justify-center items-center h-full bg-gray-50 rounded-lg">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<LoadIcon className="animate-spin w-10 h-10 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground py-4">
|
||||
@@ -341,8 +367,8 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
</div>
|
||||
{records.length > 0 && (
|
||||
<div className="px-6 py-3 flex items-center justify-between border-t border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<span className="text-sm text-gray-700">
|
||||
<div className="flex items-center whitespace-nowrap min-w-16">
|
||||
<span className="text-sm text-gray-700">
|
||||
共 {total} 条记录
|
||||
</span>
|
||||
</div>
|
||||
@@ -380,8 +406,7 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
<span>已选择 {selectedRecordIds.length} 项</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const selected = records.filter(r => selectedRecordIds.includes(r.id));
|
||||
importSops(selected, false, false);
|
||||
importSops(selectedRecords, false, false);
|
||||
}}
|
||||
disabled={selectedRecordIds.length === 0 || loading}
|
||||
className="ml-4"
|
||||
@@ -397,7 +422,16 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-semibold truncate">{currentRecord.name}</h3>
|
||||
<p className="text-muted-foreground truncate">{currentRecord.description}</p>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<p className="text-muted-foreground truncate max-w-[200px]">
|
||||
{currentRecord.description}
|
||||
</p>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p className="max-w-[300px] break-words">{currentRecord.description}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto bg-gray-50 rounded-md">
|
||||
<SopMarkdown
|
||||
@@ -413,11 +447,11 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!currentRecord) return;
|
||||
|
||||
setSelectedRecordIds([]);
|
||||
setSelectedRecords([]);
|
||||
// 直接尝试导入当前SOP
|
||||
importSops([currentRecord]).then((hasDuplicate) => {
|
||||
if (hasDuplicate === false) {
|
||||
// 如果有重复,打开重复对话框
|
||||
setDuplicateDialogOpen(true);
|
||||
}
|
||||
});
|
||||
@@ -460,30 +494,90 @@ export default function ImportFromRecordsDialog({ open, tools, onOpenChange, on
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
const recordsToUse = selectedRecordIds.length > 0
|
||||
? records.filter(r => selectedRecordIds.includes(r.id))
|
||||
: currentRecord
|
||||
? [currentRecord]
|
||||
: [];
|
||||
importSops(recordsToUse, false, true);
|
||||
setDuplicateDialogOpen(false);
|
||||
onClick={async () => {
|
||||
setIsSavingAsNew(true);
|
||||
if (importFormData) {
|
||||
const newFormData = new FormData();
|
||||
for (const [key, value] of importFormData.entries()) {
|
||||
if (key === 'override' || key === 'save_new') continue;
|
||||
newFormData.append(key, value);
|
||||
}
|
||||
|
||||
newFormData.append('override', 'false');
|
||||
newFormData.append('save_new', 'true');
|
||||
|
||||
|
||||
try {
|
||||
setLoading(true); // 添加加载状态
|
||||
await captureAndAlertRequestErrorHoc(sopApi.UploadSopRecord(newFormData)) // 等待请求完成
|
||||
toast({ variant: 'success', description: '导入成功' });
|
||||
setDuplicateDialogOpen(false);
|
||||
onOpenChange(false); // 关闭主弹窗
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsSavingAsNew(false);
|
||||
}
|
||||
} else {
|
||||
const recordsToUse = selectedRecords.length > 0
|
||||
? selectedRecords
|
||||
: currentRecord
|
||||
? [currentRecord]
|
||||
: [];
|
||||
importSops(recordsToUse, false, true);
|
||||
setDuplicateDialogOpen(false);
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
不覆盖,另存为新SOP
|
||||
{isSavingAsNew ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LoadIcon className="animate-spin w-4 h-4" />
|
||||
正在另存为新SOP...
|
||||
</div>
|
||||
) : '不覆盖,另存为新SOP'}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const recordsToUse = selectedRecordIds.length > 0
|
||||
? records.filter(r => selectedRecordIds.includes(r.id))
|
||||
: currentRecord
|
||||
? [currentRecord]
|
||||
: [];
|
||||
importSops(recordsToUse, true, false);
|
||||
setDuplicateDialogOpen(false);
|
||||
onClick={async () => {
|
||||
setIsOverwriting(true);
|
||||
if (importFormData) {
|
||||
const newFormData = new FormData();
|
||||
for (const [key, value] of importFormData.entries()) {
|
||||
if (key === 'override' || key === 'save_new') continue;
|
||||
newFormData.append(key, value);
|
||||
}
|
||||
newFormData.append('override', 'true');
|
||||
newFormData.append('save_new', 'false');
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await sopApi.UploadSopRecord(newFormData);
|
||||
toast({ variant: 'success', description: '导入成功' });
|
||||
setDuplicateDialogOpen(false);
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast({ variant: 'error', description: '导入失败' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setIsOverwriting(false);
|
||||
}
|
||||
} else {
|
||||
const recordsToUse = selectedRecordIds.length > 0
|
||||
? records.filter(r => selectedRecordIds.includes(r.id))
|
||||
: currentRecord
|
||||
? [currentRecord]
|
||||
: [];
|
||||
importSops(recordsToUse, true, false);
|
||||
setDuplicateDialogOpen(false);
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
覆盖
|
||||
{isOverwriting ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<LoadIcon className="animate-spin w-4 h-4" />
|
||||
正在覆盖SOP...
|
||||
</div>
|
||||
) : '覆盖'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@@ -230,7 +230,7 @@ const ToolSelector = ({
|
||||
className="w-1/3 flex border rounded-lg bg-white"
|
||||
>
|
||||
<div className="flex-1 p-4">
|
||||
<h3 className="text-sm font-medium">已选工具</h3>
|
||||
<h3 className="text-[16px] font-medium">已选工具</h3>
|
||||
|
||||
{selectedTools.length === 0 ? (
|
||||
<div className="mt-4 border-2 border-dashed border-gray-200 rounded-lg bg-gray-50 flex flex-col items-center justify-center py-6 px-4 text-center">
|
||||
|
||||
@@ -44,4 +44,7 @@ export const sopApi = {
|
||||
SyncSopRecord:(data) => {
|
||||
return axios.post(`/api/v1/linsight/sop/record/sync`, data);
|
||||
},
|
||||
UploadSopRecord:(data) => {
|
||||
return axios.post(`/api/v1/linsight/sop/upload`, data);
|
||||
},
|
||||
}
|
||||
@@ -23,6 +23,9 @@ customAxios.interceptors.request.use(function (config) {
|
||||
customAxios.interceptors.response.use(function (response) {
|
||||
if (response.data.status_code === 200) {
|
||||
return response.data.data;
|
||||
}
|
||||
if (response.data.status_code === 11010) {
|
||||
return response.data;
|
||||
}
|
||||
const i18Msg = i18next.t(`errors.${response.data.status_code}`)
|
||||
const errorMessage = i18Msg === `errors.${response.data.status_code}` ? response.data.status_message : i18Msg
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Button } from "@/components/bs-ui/button";
|
||||
import { Card, CardContent } from "@/components/bs-ui/card";
|
||||
import { toast, useToast } from "@/components/bs-ui/toast/use-toast";
|
||||
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import SopFormDrawer from "@/components/LinSight/SopFormDrawer";
|
||||
import ImportFromRecordsDialog from "@/components/LinSight/SopFromRecord";
|
||||
import SopTable from "@/components/LinSight/SopTable";
|
||||
@@ -21,6 +21,12 @@ import { useNavigate } from "react-router-dom";
|
||||
import { FormInput } from "./FormInput";
|
||||
import { Model } from "./ModelManagement";
|
||||
import Preview from "./Preview";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { locationContext } from "@/contexts/locationContext";
|
||||
import { UploadIcon } from "@/components/bs-icons";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/bs-ui/dialog";
|
||||
import { captureAndAlertRequestErrorHoc } from "@/controllers/request";
|
||||
|
||||
|
||||
export interface FormErrors {
|
||||
@@ -101,6 +107,17 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
const [manuallyExpandedItems, setManuallyExpandedItems] = useState<string[]>([]);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
const [importDialogOpen, setImportDialogOpen] = useState(false);
|
||||
const [localFileDialogOpen, setLocalFileDialogOpen] = useState(false);
|
||||
const [importFile, setImportFile] = useState<File | null>(null);
|
||||
const [importFiles, setImportFiles] = useState<File[]>([]);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const [validationDialog, setValidationDialog] = useState({
|
||||
open: false,
|
||||
status_message: "共计划导入0条SOP,格式正确0条,错误0条"
|
||||
});
|
||||
const [importFilesData, setImportFilesData] = useState<File[]>([]);
|
||||
const [duplicateNames, setDuplicateNames] = useState<string[]>([]);
|
||||
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
||||
const [deleteConfirmModal, setDeleteConfirmModal] = useState({
|
||||
open: false,
|
||||
title: '确认删除',
|
||||
@@ -156,6 +173,8 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
api: [],
|
||||
mcp: []
|
||||
});
|
||||
const [importFormData, setImportFormData] = useState<FormData | null>(null);
|
||||
|
||||
const fetchTools = async (type: 'builtin' | 'api' | 'mcp') => {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -225,13 +244,13 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
.filter(Boolean);
|
||||
}, [toolsData, activeToolTab, toolSearchTerm]);
|
||||
|
||||
const refreshSopList = () => {
|
||||
fetchData({
|
||||
page: page,
|
||||
pageSize: 10,
|
||||
keyword: keywords
|
||||
});
|
||||
};
|
||||
// const refreshSopList = () => {
|
||||
// fetchData({
|
||||
// page: page,
|
||||
// pageSize: 10,
|
||||
// keyword: keywords
|
||||
// });
|
||||
// };
|
||||
const fetchData = async (params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
@@ -243,7 +262,7 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
const res = await sopApi.getSopList({
|
||||
page_size: params.pageSize,
|
||||
page: params.page,
|
||||
keywords: params.keyword || keywords,
|
||||
keywords: params.keyword || '',
|
||||
sort: params.sort,
|
||||
});
|
||||
|
||||
@@ -384,23 +403,25 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
|
||||
const [selectedItems, setSelectedItems] = useState([]);
|
||||
const [sortConfig, setSortConfig] = useState({ key: null, direction: '' });
|
||||
const handleSearch = (keyword: string, resetPage: boolean = false) => {
|
||||
const newKeywords = keyword;
|
||||
const newPage = resetPage || newKeywords.trim() === '' ? 1 : page;
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useContext(locationContext)
|
||||
const handleSearch = (keyword: string, resetPage: boolean = false) => {
|
||||
const newKeywords = keyword;
|
||||
const newPage = resetPage || newKeywords.trim() === '' ? 1 : page;
|
||||
|
||||
fetchData({
|
||||
keyword: newKeywords,
|
||||
page: newPage,
|
||||
pageSize
|
||||
});
|
||||
fetchData({
|
||||
keyword: newKeywords,
|
||||
page: newPage,
|
||||
pageSize
|
||||
});
|
||||
|
||||
// 更新状态
|
||||
setKeywords(newKeywords);
|
||||
setPageInputValue(newPage.toString());
|
||||
if (newPage !== page) {
|
||||
setPage(newPage);
|
||||
}
|
||||
};
|
||||
// 更新状态
|
||||
setKeywords(newKeywords);
|
||||
setPageInputValue(newPage.toString());
|
||||
if (newPage !== page) {
|
||||
setPage(newPage);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭弹窗刷新下数据
|
||||
useEffect(() => {
|
||||
@@ -687,6 +708,80 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
return [...tools];
|
||||
});
|
||||
}, []);
|
||||
const { getRootProps: getLocalFileRootProps, getInputProps: getLocalFileInputProps } = useDropzone({
|
||||
accept: {
|
||||
'application/*': appConfig.enableEtl4lm ?
|
||||
['.PDF', '.TXT', '.DOCX', '.PPT', '.PPTX', '.MD', '.HTML', '.XLS', '.XLSX', '.CSV', '.DOC', '.PNG', '.JPG', '.JPEG', '.BMP']
|
||||
: ['.PDF', '.TXT', '.DOCX', '.DOC', '.PPT', '.PPTX', '.MD', '.HTML', '.XLS', '.XLSX', '.CSV']
|
||||
},
|
||||
multiple: true, // 允许选择多个文件
|
||||
onDrop: (acceptedFiles) => {
|
||||
setImportFiles(prev => [...prev, ...acceptedFiles]); // 追加新文件
|
||||
}
|
||||
});
|
||||
const handleLocalFileImport = async () => {
|
||||
setIsImporting(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', importFiles[0]);
|
||||
|
||||
setImportFilesData([importFiles[0]]); // 只保存一个文件
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
console.log('API Response:', res); // 调试用
|
||||
if(res.status_code === 11010){
|
||||
setValidationDialog({
|
||||
open: true,
|
||||
status_message: res.status_message
|
||||
});
|
||||
}
|
||||
if(res.status_code === 200){
|
||||
const formData = new FormData();
|
||||
importFilesData.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('ignore_error', 'false');
|
||||
formData.append('override', 'false');
|
||||
formData.append('save_new', 'false');
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
if(res?.repeat_name){
|
||||
setImportDialogOpen(true)
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
}else{
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
setIsImporting(false);
|
||||
}
|
||||
};
|
||||
const handleValidationDialogConfirm = async () => {
|
||||
console.log(111);
|
||||
const formData = new FormData();
|
||||
importFilesData.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('ignore_error', 'true');
|
||||
formData.append('override', 'false');
|
||||
formData.append('save_new', 'false');
|
||||
|
||||
setValidationDialog(prev => ({...prev, open: false}));
|
||||
|
||||
|
||||
|
||||
const res = await sopApi.UploadSopRecord(formData);
|
||||
if(res?.repeat_name){
|
||||
setImportDialogOpen(true)
|
||||
setDuplicateNames(res.repeat_name);
|
||||
setDuplicateDialogOpen(true);
|
||||
setImportFormData(formData);
|
||||
return
|
||||
}else{
|
||||
toast({ variant: 'success', description: '提交成功' });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className=" h-full overflow-y-scroll scrollbar-hide relative bg-background-main">
|
||||
{loading && (
|
||||
@@ -744,7 +839,7 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setKeywords(newValue);
|
||||
handleSearch(newValue, newValue.trim() === '');
|
||||
handleSearch(newValue);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -760,12 +855,20 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => setImportDialogOpen(true)}
|
||||
onClick={() => {setImportDialogOpen(true);setImportFilesData(null)}}
|
||||
>
|
||||
从运行记录中导入
|
||||
从运行记录导入
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setLocalFileDialogOpen(true)}
|
||||
>
|
||||
从本地文件导入
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setIsEditing(false);
|
||||
@@ -812,7 +915,12 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
open={importDialogOpen}
|
||||
tools={selectedTools}
|
||||
onOpenChange={setImportDialogOpen}
|
||||
onSuccess={refreshSopList}
|
||||
// onSuccess={refreshSopList}
|
||||
setDuplicateNames={setDuplicateNames}
|
||||
duplicateNames={duplicateNames}
|
||||
duplicateDialogOpen={duplicateDialogOpen}
|
||||
setDuplicateDialogOpen={setDuplicateDialogOpen}
|
||||
importFormData={importFormData}
|
||||
/>
|
||||
{/* 表格区域 */}
|
||||
<SopTable datalist={datalist} selectedItems={selectedItems} handleSelectItem={handleSelectItem} handleSelectAll={handleSelectAll} handleSort={handleSort} handleEdit={handleEdit} handleDelete={handleDelete} page={page} pageSize={pageSize} total={total} loading={loading} pageInputValue={pageInputValue} handlePageChange={handlePageChange} handlePageInputChange={handlePageInputChange} handlePageInputConfirm={handlePageInputConfirm} handleKeyDown={handleKeyDown} />
|
||||
@@ -865,6 +973,114 @@ export default function index({ formData: parentFormData, setFormData: parentSet
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Dialog open={localFileDialogOpen} onOpenChange={setLocalFileDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[1200px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>导入SOP</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-red-500">*</span>
|
||||
<span>请上传文件</span>
|
||||
<button
|
||||
className="text-blue-600 hover:underline ml-auto"
|
||||
|
||||
>
|
||||
<span className="text-black">示例文件:</span>
|
||||
SOP导入格式示例.xlsx
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...getLocalFileRootProps()}
|
||||
className="group h-48 border border-dashed rounded-md flex flex-col justify-center items-center cursor-pointer gap-3 hover:border-primary"
|
||||
>
|
||||
<input {...getLocalFileInputProps()} />
|
||||
<UploadIcon className="group-hover:text-primary size-5" />
|
||||
<p className="text-sm">{t('code.clickOrDragHere')}</p>
|
||||
{importFiles.length > 0 && (
|
||||
<div className="w-full max-h-32 overflow-y-auto">
|
||||
{importFiles.slice(0, 1).map((file, index) => ( // 只显示一个文件
|
||||
<div key={index} className="flex items-center justify-between p-1 bg-gray-200 mt-14">
|
||||
<span className="text-sm text-gray-600 truncate max-w-xs">
|
||||
{file.name}
|
||||
</span>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setImportFiles([]); // 清空文件列表
|
||||
}}
|
||||
className="text-red-500 hover:text-red-700"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setLocalFileDialogOpen(false);
|
||||
setImportFiles([]);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await handleLocalFileImport(); // 等待导入完成
|
||||
setLocalFileDialogOpen(false); // 关闭弹窗
|
||||
}}
|
||||
disabled={isImporting || importFiles.length === 0}
|
||||
>
|
||||
{isImporting ? "导入中..." : "提交"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={validationDialog.open} onOpenChange={(open) => setValidationDialog(prev => ({...prev, open}))}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<AlertTriangle className="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<DialogTitle>文件导入结果</DialogTitle>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">
|
||||
{validationDialog.status_message && (
|
||||
<>
|
||||
<p>
|
||||
{validationDialog.status_message.split(":")[0]}
|
||||
</p>
|
||||
{validationDialog.status_message.includes(":") && (
|
||||
<div className="mt-2 text-sm text-gray-600">
|
||||
{validationDialog.status_message.split(":")[1].split("\n").map((line, index) => (
|
||||
<p key={index} className="mb-1">{line.trim()}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleValidationDialogConfirm}
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user