feat(iit): V3.2 data consistency + project isolation + admin config redesign + Chinese labels

Summary:
- Refactor timeline API to read from qc_field_status (SSOT) instead of qc_logs
- Add field-issues paginated API with severity/dimension/recordId filters
- Add LEFT JOIN field_metadata + qc_event_status for Chinese display names
- Implement per-project ChatOrchestrator cache and SessionMemory isolation
- Redesign admin IIT config tabs (REDCap -> Fields -> KB -> Rules -> Members)
- Add AI-powered QC rule generation (D3 programmatic + D1/D5/D6 LLM-based)
- Add clickable warning/critical detail Modal in ReportsPage
- Auto-dispatch eQuery after batch QC via DailyQcOrchestrator
- Update module status documentation to v3.2

Backend changes:
- iitQcCockpitController: rewrite getTimeline from qc_field_status, add getFieldIssues
- iitQcCockpitRoutes: add field-issues route
- ChatOrchestrator: per-projectId cached instances
- SessionMemory: keyed by userId::projectId
- WechatCallbackController: resolve projectId from iitUserMapping
- iitRuleSuggestionService: dimension-based suggest + generateD3Rules
- iitBatchController: call DailyQcOrchestrator after batch QC

Frontend changes:
- AiStreamPage: adapt to new timeline structure with dimension tags
- ReportsPage: clickable stats cards with issue detail Modal
- IitProjectDetailPage: reorder tabs, add AI rule generation UI
- iitProjectApi: add TimelineIssue, FieldIssueItem types and APIs

Status: TypeScript compilation verified, no new lint errors
Made-with: Cursor
This commit is contained in:
2026-03-02 14:29:59 +08:00
parent 72928d3116
commit 71d32d11ee
38 changed files with 1597 additions and 546 deletions

View File

@@ -8,9 +8,7 @@
import { Layout, Menu } from 'antd';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import {
FileTextOutlined,
SearchOutlined,
FolderOpenOutlined,
FilterOutlined,
FileSearchOutlined,
DatabaseOutlined,
@@ -32,29 +30,15 @@ const ASLLayout = () => {
// 菜单项配置
const menuItems: MenuItem[] = [
{
key: 'research-plan',
icon: <FileTextOutlined />,
label: '1. 研究方案生成',
disabled: true,
title: '敬请期待'
},
{
key: '/literature/research/deep',
icon: <SearchOutlined />,
label: '2. 智能文献检索',
},
{
key: 'literature-management',
icon: <FolderOpenOutlined />,
label: '3. 文献管理',
disabled: true,
title: '敬请期待'
label: '1. 智能文献检索',
},
{
key: 'title-screening',
icon: <FilterOutlined />,
label: '4. 标题摘要初筛',
label: '2. 标题摘要初筛',
children: [
{
key: '/literature/screening/title/settings',
@@ -76,7 +60,7 @@ const ASLLayout = () => {
{
key: 'fulltext-screening',
icon: <FileSearchOutlined />,
label: '5. 全文复筛',
label: '3. 全文复筛',
children: [
{
key: '/literature/screening/fulltext/settings',
@@ -98,7 +82,7 @@ const ASLLayout = () => {
{
key: 'extraction',
icon: <DatabaseOutlined />,
label: '6. 全文智能提取',
label: '4. 全文智能提取',
children: [
{
key: '/literature/extraction/setup',
@@ -115,12 +99,12 @@ const ASLLayout = () => {
{
key: '/literature/charting',
icon: <ApartmentOutlined />,
label: '7. SR 图表生成器',
label: '5. SR 图表生成器',
},
{
key: '/literature/meta-analysis',
icon: <BarChartOutlined />,
label: '8. Meta 分析引擎',
label: '6. Meta 分析引擎',
},
];

View File

@@ -6,16 +6,13 @@
*/
import { useState, useEffect } from 'react';
import { Card, Checkbox, Button, Input, Select, Spin, Divider, Typography, Tag } from 'antd';
import { Card, Button, Input, Typography, Tag } from 'antd';
import {
ArrowLeftOutlined,
ThunderboltOutlined,
GlobalOutlined,
EditOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { aslApi } from '../../api';
import type { DataSourceConfig } from '../../types/deepResearch';
const { Text } = Typography;
@@ -43,24 +40,11 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
initialQuery, onSubmit, onBack, loading, collapsed, onExpand,
}) => {
const [query, setQuery] = useState(initialQuery);
const [dataSources, setDataSources] = useState<DataSourceConfig[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [yearRange, setYearRange] = useState<string>('近5年');
const [targetCount, setTargetCount] = useState<string>('全面检索');
const [loadingSources, setLoadingSources] = useState(true);
const [loadingTextIdx, setLoadingTextIdx] = useState(0);
useEffect(() => {
aslApi.getDeepResearchDataSources().then(res => {
const sources = res.data || [];
setDataSources(sources);
setSelectedIds(sources.filter((s: DataSourceConfig) => s.defaultChecked).map((s: DataSourceConfig) => s.id));
}).catch(() => {
setDataSources([]);
}).finally(() => {
setLoadingSources(false);
});
}, []);
// 默认使用 PubMed + 近5年 + 全面检索(数据源/年限/篇数 UI 暂时隐藏)
const yearRange = '近5年';
const targetCount = '全面检索';
useEffect(() => {
if (!loading) { setLoadingTextIdx(0); return; }
@@ -70,21 +54,10 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
return () => clearInterval(timer);
}, [loading]);
const handleToggle = (id: string) => {
setSelectedIds(prev =>
prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]
);
};
const handleSubmit = () => {
const domains = dataSources
.filter(s => selectedIds.includes(s.id))
.map(s => s.domainScope);
onSubmit(query, domains, { yearRange, targetCount });
onSubmit(query, ['PubMed'], { yearRange, targetCount });
};
const selectedNames = dataSources.filter(s => selectedIds.includes(s.id)).map(s => s.label);
if (collapsed) {
return (
<Card size="small" className="!bg-white">
@@ -94,9 +67,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
<div className="min-w-0">
<Text strong className="block truncate">{query}</Text>
<div className="flex items-center gap-2 mt-1 flex-wrap">
{selectedNames.map(name => (
<Tag key={name} className="!text-xs !m-0">{name}</Tag>
))}
<Tag className="!text-xs !m-0">PubMed</Tag>
<Text type="secondary" className="text-xs">{yearRange} · {targetCount}</Text>
</div>
</div>
@@ -111,9 +82,6 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
);
}
const englishSources = dataSources.filter(s => s.category === 'english');
const chineseSources = dataSources.filter(s => s.category === 'chinese');
return (
<div>
<div className="flex items-center gap-3 mb-4">
@@ -133,77 +101,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
/>
</Card>
<Card className="mb-4" size="small" title={<><GlobalOutlined className="mr-2" /></>}>
{loadingSources ? (
<Spin size="small" />
) : (
<>
<Text type="secondary" className="block mb-3 text-xs"></Text>
<div className="flex flex-col gap-2 mb-4">
{englishSources.map(ds => (
<Checkbox
key={ds.id}
checked={selectedIds.includes(ds.id)}
onChange={() => handleToggle(ds.id)}
>
<span className="font-medium">{ds.label}</span>
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
</Checkbox>
))}
</div>
<Divider className="!my-3" />
<Text type="secondary" className="block mb-3 text-xs"></Text>
<div className="flex flex-col gap-2">
{chineseSources.map(ds => (
<Checkbox
key={ds.id}
checked={selectedIds.includes(ds.id)}
onChange={() => handleToggle(ds.id)}
>
<span className="font-medium">{ds.label}</span>
<span className="text-gray-400 text-xs ml-2">{ds.domainScope}</span>
</Checkbox>
))}
</div>
</>
)}
</Card>
<Card className="mb-6" size="small" title="高级筛选">
<div className="flex gap-4">
<div className="flex-1">
<Text type="secondary" className="block mb-1 text-xs"></Text>
<Select
value={yearRange}
onChange={setYearRange}
className="w-full"
options={[
{ value: '不限', label: '不限' },
{ value: '近1年', label: '近1年' },
{ value: '近3年', label: '近3年' },
{ value: '近5年', label: '近5年' },
{ value: '近10年', label: '近10年' },
]}
/>
</div>
<div className="flex-1">
<Text type="secondary" className="block mb-1 text-xs"></Text>
<Select
value={targetCount}
onChange={setTargetCount}
className="w-full"
options={[
{ value: '全面检索', label: '全面检索' },
{ value: '约20篇', label: '约20篇' },
{ value: '约50篇', label: '约50篇' },
{ value: '约100篇', label: '约100篇' },
]}
/>
</div>
</div>
</Card>
{/* 数据源/年限/篇数暂时隐藏,默认 PubMed + 近5年 + 全面检索 */}
<Button
type="primary"
@@ -212,7 +110,7 @@ const SetupPanel: React.FC<SetupPanelProps> = ({
block
onClick={handleSubmit}
loading={loading}
disabled={!query.trim() || selectedIds.length === 0}
disabled={!query.trim()}
>
{loading ? LOADING_TEXTS[loadingTextIdx] : '生成检索需求书'}
</Button>

View File

@@ -47,7 +47,7 @@ const ASLModule = () => {
>
<Routes>
<Route path="" element={<ASLLayout />}>
<Route index element={<Navigate to="screening/title/settings" replace />} />
<Route index element={<Navigate to="research/deep" replace />} />
{/* 智能文献检索 V1.x保留兼容 */}
<Route path="research/search" element={<ResearchSearch />} />