- Frontend V3 architecture migration to modules/pkb - Implement three work modes: full-text reading, deep reading, batch processing - Complete batch processing: template selection, progress display, result export (CSV) - Integrate Ant Design X Chat component with streaming support - Add document upload modal with drag-and-drop support - Optimize UI: multi-line table display, citation formatting, auto-scroll - Fix 10+ technical issues: API mapping, state sync, form clearing - Update documentation: development records and module status Performance: 3 docs batch processing ~17-28s Status: PKB module now production-ready (90% complete)
342 lines
9.1 KiB
TypeScript
342 lines
9.1 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { Modal, Radio, Button, Slider, Alert, App, Statistic, Row, Col, Checkbox } from 'antd';
|
||
import { Trash2, AlertTriangle } from 'lucide-react';
|
||
|
||
interface Props {
|
||
visible: boolean;
|
||
onClose: () => void;
|
||
onApply: (newData: any[]) => void;
|
||
columns: Array<{ id: string; name: string }>;
|
||
data: any[];
|
||
sessionId: string | null;
|
||
}
|
||
|
||
const DropnaDialog: React.FC<Props> = ({
|
||
visible,
|
||
onClose,
|
||
onApply,
|
||
columns,
|
||
data,
|
||
sessionId,
|
||
}) => {
|
||
const { message } = App.useApp();
|
||
const [method, setMethod] = useState<'row' | 'column' | 'both'>('row');
|
||
const [threshold, setThreshold] = useState<number>(50); // 百分比
|
||
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
// 计算缺失值统计
|
||
const [missingStats, setMissingStats] = useState<{
|
||
totalMissing: number;
|
||
rowsWithMissing: number;
|
||
colsWithMissing: number;
|
||
}>({ totalMissing: 0, rowsWithMissing: 0, colsWithMissing: 0 });
|
||
|
||
useEffect(() => {
|
||
if (visible && data.length > 0) {
|
||
// 计算缺失值
|
||
let totalMissing = 0;
|
||
let rowsWithMissing = 0;
|
||
const colsMissing: Record<string, number> = {};
|
||
|
||
data.forEach(row => {
|
||
let rowHasMissing = false;
|
||
Object.keys(row).forEach(key => {
|
||
const value = row[key];
|
||
if (value === null || value === undefined || value === '') {
|
||
totalMissing++;
|
||
colsMissing[key] = (colsMissing[key] || 0) + 1;
|
||
rowHasMissing = true;
|
||
}
|
||
});
|
||
if (rowHasMissing) {
|
||
rowsWithMissing++;
|
||
}
|
||
});
|
||
|
||
setMissingStats({
|
||
totalMissing,
|
||
rowsWithMissing,
|
||
colsWithMissing: Object.keys(colsMissing).length,
|
||
});
|
||
}
|
||
}, [visible, data]);
|
||
|
||
// 执行
|
||
const handleApply = async () => {
|
||
if (!sessionId) {
|
||
message.error('Session ID不存在');
|
||
return;
|
||
}
|
||
|
||
// 验证
|
||
if (method === 'row' && selectedColumns.length === 0) {
|
||
// 删除所有行:不需要指定列
|
||
}
|
||
|
||
setLoading(true);
|
||
|
||
try {
|
||
const response = await fetch('/api/v1/dc/tool-c/quick-action', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
sessionId,
|
||
action: 'dropna',
|
||
params: {
|
||
method,
|
||
threshold: threshold / 100, // 转换为0-1
|
||
subset: selectedColumns.length > 0 ? selectedColumns : undefined,
|
||
},
|
||
}),
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
throw new Error(result.error || '删除缺失值失败');
|
||
}
|
||
|
||
message.success('删除缺失值成功!');
|
||
|
||
// 更新父组件数据
|
||
if (result.data?.newDataPreview) {
|
||
onApply(result.data.newDataPreview);
|
||
}
|
||
|
||
// 成功后关闭
|
||
onClose();
|
||
} catch (error: any) {
|
||
message.error(error.message || '执行失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Modal
|
||
title={
|
||
<div className="flex items-center gap-2">
|
||
<Trash2 size={20} className="text-red-500" />
|
||
<span>删除缺失值</span>
|
||
</div>
|
||
}
|
||
open={visible}
|
||
onCancel={onClose}
|
||
width={700}
|
||
footer={[
|
||
<Button key="cancel" onClick={onClose}>
|
||
取消
|
||
</Button>,
|
||
<Button
|
||
key="apply"
|
||
type="primary"
|
||
danger
|
||
onClick={handleApply}
|
||
loading={loading}
|
||
icon={<Trash2 size={16} />}
|
||
>
|
||
执行删除
|
||
</Button>,
|
||
]}
|
||
>
|
||
<div className="space-y-4">
|
||
{/* 缺失值统计 */}
|
||
<Alert
|
||
message="当前数据缺失值统计"
|
||
description={
|
||
<Row gutter={16} className="mt-3">
|
||
<Col span={8}>
|
||
<Statistic
|
||
title="总缺失值"
|
||
value={missingStats.totalMissing}
|
||
suffix={`/ ${data.length * columns.length} 单元格`}
|
||
/>
|
||
</Col>
|
||
<Col span={8}>
|
||
<Statistic
|
||
title="含缺失值的行"
|
||
value={missingStats.rowsWithMissing}
|
||
suffix={`/ ${data.length} 行`}
|
||
/>
|
||
</Col>
|
||
<Col span={8}>
|
||
<Statistic
|
||
title="含缺失值的列"
|
||
value={missingStats.colsWithMissing}
|
||
suffix={`/ ${columns.length} 列`}
|
||
/>
|
||
</Col>
|
||
</Row>
|
||
}
|
||
type="info"
|
||
showIcon
|
||
className="mb-4"
|
||
/>
|
||
|
||
{/* 删除方式 */}
|
||
<div>
|
||
<label className="text-sm font-medium text-slate-700 mb-2 block">
|
||
删除方式:
|
||
</label>
|
||
<Radio.Group
|
||
value={method}
|
||
onChange={(e) => setMethod(e.target.value)}
|
||
className="w-full"
|
||
>
|
||
<div className="space-y-3">
|
||
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
|
||
<Radio value="row">
|
||
<div className="ml-2">
|
||
<div className="font-medium">按行删除</div>
|
||
<div className="text-xs text-slate-500 mt-1">
|
||
删除包含任何缺失值的行(推荐)
|
||
</div>
|
||
</div>
|
||
</Radio>
|
||
</div>
|
||
|
||
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
|
||
<Radio value="column">
|
||
<div className="ml-2">
|
||
<div className="font-medium">按列删除</div>
|
||
<div className="text-xs text-slate-500 mt-1">
|
||
删除缺失值比例超过阈值的列
|
||
</div>
|
||
</div>
|
||
</Radio>
|
||
</div>
|
||
|
||
<div className="border rounded-lg p-3 hover:border-blue-400 transition-colors">
|
||
<Radio value="both">
|
||
<div className="ml-2">
|
||
<div className="font-medium">先删列,再删行</div>
|
||
<div className="text-xs text-slate-500 mt-1">
|
||
先删除缺失列,再删除剩余的缺失行
|
||
</div>
|
||
</div>
|
||
</Radio>
|
||
</div>
|
||
</div>
|
||
</Radio.Group>
|
||
</div>
|
||
|
||
{/* 缺失率阈值(仅对按列删除和先删列再删行有效) */}
|
||
{(method === 'column' || method === 'both') && (
|
||
<div>
|
||
<label className="text-sm font-medium text-slate-700 mb-2 block">
|
||
缺失率阈值:{threshold}%
|
||
</label>
|
||
<Slider
|
||
min={0}
|
||
max={100}
|
||
value={threshold}
|
||
onChange={setThreshold}
|
||
marks={{
|
||
0: '0%',
|
||
30: '30%',
|
||
50: '50%',
|
||
70: '70%',
|
||
100: '100%',
|
||
}}
|
||
/>
|
||
<div className="text-xs text-slate-500 mt-2">
|
||
删除缺失率超过 {threshold}% 的列
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 仅检查指定列(仅对按行删除有效) */}
|
||
{method === 'row' && (
|
||
<div>
|
||
<label className="text-sm font-medium text-slate-700 mb-2 block">
|
||
仅检查指定列的缺失值(可选):
|
||
</label>
|
||
<div className="border rounded-lg p-3 max-h-48 overflow-y-auto">
|
||
<Checkbox.Group
|
||
value={selectedColumns}
|
||
onChange={setSelectedColumns}
|
||
className="flex flex-col gap-2"
|
||
>
|
||
{columns.map((col) => (
|
||
<Checkbox key={col.id} value={col.id}>
|
||
{col.name}
|
||
</Checkbox>
|
||
))}
|
||
</Checkbox.Group>
|
||
</div>
|
||
<div className="text-xs text-slate-500 mt-2">
|
||
留空表示检查所有列
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 警告提示 */}
|
||
<Alert
|
||
message={
|
||
<div className="flex items-center gap-2">
|
||
<AlertTriangle size={16} />
|
||
<span className="font-semibold">重要提示</span>
|
||
</div>
|
||
}
|
||
description={
|
||
<div className="text-xs space-y-1 mt-1">
|
||
<div>• 删除操作不可撤销,请谨慎操作</div>
|
||
<div>• 建议先使用AI对话功能探索数据</div>
|
||
<div>• 如果删除后数据为空,操作会失败</div>
|
||
<div>• 考虑使用"多重插补"代替直接删除</div>
|
||
</div>
|
||
}
|
||
type="warning"
|
||
showIcon={false}
|
||
className="bg-orange-50 border-orange-200"
|
||
/>
|
||
</div>
|
||
</Modal>
|
||
);
|
||
};
|
||
|
||
export default DropnaDialog;
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|