灵思二期更新修改

This commit is contained in:
lmr
2025-08-08 18:40:09 +08:00
parent 6b406b92ff
commit 8fa3764c32
6 changed files with 417 additions and 101 deletions
@@ -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>
);
}