|
|
|
@ -29,7 +29,7 @@
|
|
|
|
<span v-else-if="reviewFinished" class="tab-status tab-status--done"></span>
|
|
|
|
<span v-else-if="reviewFinished" class="tab-status tab-status--done"></span>
|
|
|
|
</span>
|
|
|
|
</span>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
<div ref="reviewScrollRef" class="tab-scroll">
|
|
|
|
<div ref="reviewScrollRef" class="tab-scroll" @scroll.passive="onReviewScroll">
|
|
|
|
<ElTimeline class="review-timeline">
|
|
|
|
<ElTimeline class="review-timeline">
|
|
|
|
<ElTimelineItem
|
|
|
|
<ElTimelineItem
|
|
|
|
v-for="(log, idx) in reviewLogs"
|
|
|
|
v-for="(log, idx) in reviewLogs"
|
|
|
|
@ -49,6 +49,27 @@
|
|
|
|
<div v-if="log.detail" class="review-card-detail">
|
|
|
|
<div v-if="log.detail" class="review-card-detail">
|
|
|
|
{{ log.detail }}
|
|
|
|
{{ log.detail }}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 子 timeline:分段审查的调用链路 -->
|
|
|
|
|
|
|
|
<ElTimeline v-if="log.children && log.children.length" class="sub-timeline">
|
|
|
|
|
|
|
|
<ElTimelineItem
|
|
|
|
|
|
|
|
v-for="(child, cidx) in log.children"
|
|
|
|
|
|
|
|
:key="cidx"
|
|
|
|
|
|
|
|
:timestamp="child.time"
|
|
|
|
|
|
|
|
placement="top"
|
|
|
|
|
|
|
|
:type="reviewLevelType(child.level)"
|
|
|
|
|
|
|
|
:hollow="child.level === 'info'"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div class="review-sub-card" :class="`review-sub-card--${child.level}`">
|
|
|
|
|
|
|
|
<div class="review-sub-header">
|
|
|
|
|
|
|
|
<span class="review-sub-step">{{ child.title }}</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="child.detail" class="review-sub-detail">
|
|
|
|
|
|
|
|
{{ child.detail }}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ElTimelineItem>
|
|
|
|
|
|
|
|
</ElTimeline>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ElTimelineItem>
|
|
|
|
</ElTimelineItem>
|
|
|
|
</ElTimeline>
|
|
|
|
</ElTimeline>
|
|
|
|
@ -73,7 +94,7 @@
|
|
|
|
</ElButton>
|
|
|
|
</ElButton>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 滚动锚点:新增日志时通过 scrollIntoView 滚动至此 -->
|
|
|
|
<!-- 滚动锚点 -->
|
|
|
|
<div ref="scrollSentinelRef" />
|
|
|
|
<div ref="scrollSentinelRef" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ElTabPane>
|
|
|
|
</ElTabPane>
|
|
|
|
@ -160,8 +181,8 @@ watch(
|
|
|
|
(v) => {
|
|
|
|
(v) => {
|
|
|
|
if (v) {
|
|
|
|
if (v) {
|
|
|
|
activeTab.value = props.defaultTab || "operation";
|
|
|
|
activeTab.value = props.defaultTab || "operation";
|
|
|
|
|
|
|
|
isAutoScroll.value = true;
|
|
|
|
if (activeTab.value === "review" && props.startReview) {
|
|
|
|
if (activeTab.value === "review" && props.startReview) {
|
|
|
|
// 审查入口:每次确认后都重新启动渐进式审查日志输出,清空历史日志
|
|
|
|
|
|
|
|
restartReview();
|
|
|
|
restartReview();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
@ -182,10 +203,10 @@ watch(
|
|
|
|
|
|
|
|
|
|
|
|
watch(activeTab, (tab) => {
|
|
|
|
watch(activeTab, (tab) => {
|
|
|
|
if (tab === "review" && props.visible) {
|
|
|
|
if (tab === "review" && props.visible) {
|
|
|
|
// 仅审查入口(startReview=true)才启动审查流程
|
|
|
|
|
|
|
|
if (props.startReview) {
|
|
|
|
if (props.startReview) {
|
|
|
|
ensureReviewRunning();
|
|
|
|
ensureReviewRunning();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
isAutoScroll.value = true;
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@ -262,52 +283,109 @@ function logStatusText(t: ReviewLogActionType): string {
|
|
|
|
// ========== 审查日志(Mock 演示数据) ==========
|
|
|
|
// ========== 审查日志(Mock 演示数据) ==========
|
|
|
|
|
|
|
|
|
|
|
|
type ReviewLogLevel = "info" | "success" | "warning" | "error";
|
|
|
|
type ReviewLogLevel = "info" | "success" | "warning" | "error";
|
|
|
|
|
|
|
|
type ReviewStep = "vectorize" | "search" | "model_load" | "model_output" | "record";
|
|
|
|
|
|
|
|
|
|
|
|
interface MockReviewLog {
|
|
|
|
interface MockReviewLog {
|
|
|
|
phase: string;
|
|
|
|
phase: string;
|
|
|
|
title: string;
|
|
|
|
title: string;
|
|
|
|
detail?: string;
|
|
|
|
detail?: string;
|
|
|
|
level: ReviewLogLevel;
|
|
|
|
level: ReviewLogLevel;
|
|
|
|
}
|
|
|
|
/** 步骤类型,用于控制产出间隔 */
|
|
|
|
|
|
|
|
step?: ReviewStep;
|
|
|
|
|
|
|
|
/** 子步骤(分段审查时用子 timeline 展示调用链路) */
|
|
|
|
|
|
|
|
children?: MockReviewLog[];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 单个分段审查数据,用于构建完整调用链路子日志 */
|
|
|
|
|
|
|
|
interface SegData {
|
|
|
|
|
|
|
|
seg: string;
|
|
|
|
|
|
|
|
vectorize: string;
|
|
|
|
|
|
|
|
dify: string;
|
|
|
|
|
|
|
|
local?: string;
|
|
|
|
|
|
|
|
modelInput: string;
|
|
|
|
|
|
|
|
output: string;
|
|
|
|
|
|
|
|
outputLevel: ReviewLogLevel;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 将分段审查数据构建为一条父日志 + 5条子步骤日志 */
|
|
|
|
|
|
|
|
function buildSegLog(d: SegData): MockReviewLog {
|
|
|
|
|
|
|
|
const searchDetail = [
|
|
|
|
|
|
|
|
`Dify知识库:${d.dify}`,
|
|
|
|
|
|
|
|
d.local ? `本地记忆库:${d.local}` : "本地记忆库:无高相似分段",
|
|
|
|
|
|
|
|
].join("\n");
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
|
|
phase: "分段审查",
|
|
|
|
|
|
|
|
title: d.seg,
|
|
|
|
|
|
|
|
level: "info",
|
|
|
|
|
|
|
|
children: [
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "向量化编码", detail: d.vectorize, level: "info", step: "vectorize" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "知识库检索", detail: searchDetail, level: "info", step: "search" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "大模型加载", detail: d.modelInput, level: "info", step: "model_load" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "推理输出", detail: d.output, level: d.outputLevel, step: "model_output" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "记录落池", detail: "", level: "success", step: "record" },
|
|
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 9个分段审查数据(严格依据审查日志7.23.md) */
|
|
|
|
|
|
|
|
const segDataList: SegData[] = [
|
|
|
|
|
|
|
|
{ seg: "Seg01|第一章 项目概况", vectorize: "分段文本向量化编码,向量写入本地记忆RAG库生成唯一索引", dify: "检索标签「项目概况、公共机构能源托管」,召回《JS/T 301—2024》5.1条款、医院可研编制范本", local: "召回附件Att01规划批复,向量相似度0.88", modelInput: "Seg01原文 + 第一章审查规则(地址精确到门牌号)+ Dify规范条文 + Att01附件文本", output: "章节框架完整;仅标注街道无精确门牌号,违反业务规则;院区面积描述合规;可研地址与规划附件文字存在偏差", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg02|第二章 商业合作模式", vectorize: "分段向量化,写入本地记忆RAG库", dify: "检索能源托管合作、计费结算行业规范", local: "检索Seg01面积相关段落,相似度0.77", modelInput: "大模型加载本章规则、规范、跨段面积数据执行校验", output: "15年托管周期符合标准;缺少能源单价涨跌调整约定、保底用能条款", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg03|第三章 项目建设方案", vectorize: "向量编码存入本地记忆库", dify: "高效冷站、空气源热泵、光伏设计国标", local: "召回Att01规划图纸分段,相似度0.82", modelInput: "大模型校验负荷、装机、分期建设面积与规划文件一致性", output: "冷站EER、热泵参数满足一级能效;屋面光伏缺少荷载安全鉴定说明", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg04|第四章 项目运行和保障措施", vectorize: "分段向量化入库本地记忆库", dify: "检索运维、维保相关企业标准", modelInput: "大模型校验人员配置、维保周期、维保成本测算逻辑", output: "人工、维保测算逻辑合规;未明确空调末端故障责任免责条款", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg05|第五章 投资估算", vectorize: "向量写入本地记忆RAG库", dify: "高效机房、光伏造价企业标准", local: "召回Seg03建设面积分段,相似度0.78", modelInput: "大模型交叉核对单位造价、总投资与建设规模匹配度", output: "冷站造价与公司标准基本持平;管路、电缆主材材质明细缺失", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg06|第六章 经济可行性分析", vectorize: "分段向量化入库本地记忆库", dify: "检索能源项目经济评价规范", local: "召回Seg04运维、Seg05投资分段", modelInput: "大模型联动多段校验收入、成本、IRR、回收期测算", output: "全投资、资本金指标计算逻辑合规;缺少完整25年逐年明细表格", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg07|第七章 不确定性分析", vectorize: "向量存入本地记忆库", dify: "项目敏感性分析规范检索", local: "召回Seg05投资、Seg04成本分段", modelInput: "大模型校验三类敏感因素分析完整性", output: "初投资、维保、能效敏感性完整;缺失电价单独敏感性测算", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg08|第八章 风险分析与防控措施", vectorize: "分段向量化入库本地记忆库", dify: "能源项目风险编制规范检索", modelInput: "大模型逐条校验回款、技术、实施、运维、续签风险条款", output: "现有风险防控完整;未单独设置能源价格波动风险专项分析", outputLevel: "warning" },
|
|
|
|
|
|
|
|
{ seg: "Seg09|第九章 结论与建议", vectorize: "向量写入本地记忆RAG库", dify: "可研结论编制规范检索", local: "召回前8段问题记录向量", modelInput: "大模型结合全章节问题校验结论合理性", output: "投资可行结论表述标准;未针对识别缺陷补充专项整改建议", outputLevel: "warning" },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
/** 审查日志 Mock 数据 */
|
|
|
|
/** 审查日志 Mock 数据(结构化:独立日志 + 分段父日志含子步骤) */
|
|
|
|
const mockReviewLogs: MockReviewLog[] = [
|
|
|
|
const mockReviewData: MockReviewLog[] = [
|
|
|
|
// —— 基础资源加载 ——
|
|
|
|
// —— 基础资源加载 ——
|
|
|
|
{ phase: "基础资源加载", title: "系统初始化", detail: "加载Dify专业能源RAG知识库,库分类:国标库/暖通行标库/综合能源企业范本库,存量规范文档92份,向量模型bge-large-zh,语义检索阈值0.7", level: "info" },
|
|
|
|
{ phase: "基础资源加载", title: "系统初始化", detail: "加载Dify能源专业RAG知识库,分类:国标库/暖通行标/综合能源企业范本,存量规范92份,向量模型bge-large-zh,语义检索阈值0.7", level: "info" },
|
|
|
|
{ phase: "基础资源加载", title: "规则引擎加载", detail: "载入人工预设多级可研章节大纲模板、全章节业务审查规则共47条,各章节绑定对应知识库检索过滤标签", level: "info" },
|
|
|
|
{ phase: "基础资源加载", title: "规则引擎加载", detail: "载入人工预设9章多级大纲模板、全章节业务审查规则47条,每章节绑定专属知识库检索过滤标签", level: "info" },
|
|
|
|
{ phase: "基础资源加载", title: "本地记忆库初始化", detail: "创建本项目独立临时RAG向量库,清空历史项目向量索引,生成分段存储表,预留Seg/Att分段向量存储通道", level: "info" },
|
|
|
|
{ phase: "基础资源加载", title: "本地记忆库初始化", detail: "创建本项目独立临时RAG向量库,清空历史向量数据,生成分段索引存储表", level: "info" },
|
|
|
|
// —— 文件解析/分段拆分 ——
|
|
|
|
// —— 多模态文件解析 & 分段拆分 ——
|
|
|
|
{ phase: "文件解析", title: "MinerUR引擎启动", detail: "并行读取主可研docx、2份PDF附件,启动版面分层、文字/表格/图像/公式多模态识别", level: "info" },
|
|
|
|
{ phase: "文件解析", title: "MinerUR引擎启动", detail: "并行解析主DOCX可研、2份PDF附件,执行版面分层、文字/表格/图像/公式识别、关键数值提取", level: "info" },
|
|
|
|
{ phase: "文件解析", title: "多模态信息提取完成", detail: "识别制冷负荷、托管单价、总投资额、院区面积、合同年限等18类关键数值,提取造价、能耗、财务表格9张,过滤页眉、空白页冗余文本", level: "success" },
|
|
|
|
{ phase: "文件解析", title: "多模态解析完成", detail: "提取面积、托管单价、总投资、负荷、营收等核心数据,识别造价、财务、工期表格11张,过滤页眉空白冗余内容", level: "success" },
|
|
|
|
{ phase: "文件解析", title: "长文档拆分触发", detail: "可研正文按章节边界切割为Seg01~Seg13共13个独立文本分段;附件拆分为Att01(规划批复)、Att02(财务报表)", level: "info" },
|
|
|
|
{ phase: "文件解析", title: "长文档拆分", detail: "按9大章边界切割,生成Seg01~Seg09共9个独立分段(一章对应一个分段);附件拆分为Att01、Att02", level: "info" },
|
|
|
|
{ phase: "文件解析", title: "分段预处理完成", detail: "13个主分段、2个附件分段全部完成文本清洗,等待循环审查流程", level: "success" },
|
|
|
|
{ phase: "文件解析", title: "分段预处理完毕", detail: "9个主分段完成文本清洗,进入循环审查队列", level: "success" },
|
|
|
|
// —— 分段审查 Seg01~Seg13 ——
|
|
|
|
// —— 分段循环审查 Seg01~Seg09(父日志 + 子timeline展示调用链路) ——
|
|
|
|
{ phase: "分段审查", title: "Seg01|第一章 项目概述", detail: "章节框架完整;可研仅标注街道名称,未写明精确门牌号,违反业务规则;院区面积描述符合国标;可研地址与规划文件地址存在文字差异", level: "warning" },
|
|
|
|
...segDataList.map(d => buildSegLog(d)),
|
|
|
|
{ phase: "分段审查", title: "Seg02|第二章 业主资信与付款能力", detail: "营收数据与附件财务表数值匹配,无失信说明完整,缺少涉诉风险专项分析", level: "warning" },
|
|
|
|
// —— 全章节汇总 & 生成最终审查报告 ——
|
|
|
|
{ phase: "分段审查", title: "Seg03|第三章 建设必要性", detail: "政策引用合规,缺少区域同类项目示范效益分析", level: "warning" },
|
|
|
|
{ phase: "数据汇总", title: "汇总推理任务启动", detail: "本地大模型读取9个分段全部初审记录、全部Dify知识库检索原文、本地记忆跨段比对数据", level: "info" },
|
|
|
|
{ phase: "分段审查", title: "Seg04|第四章 编制依据", detail: "遗漏《GB50189公共建筑节能设计标准》,编制依据不全", level: "error" },
|
|
|
|
{ phase: "数据汇总", title: "综合归集分类", detail: "大模型统一归类所有问题:章节内容缺失、规范条文遗漏、跨段数据不一致、合同条款缺失四大类,每条绑定对应Seg分段ID、知识库来源、附件溯源", level: "info" },
|
|
|
|
{ phase: "分段审查", title: "Seg05|第五章 商业合作模式", detail: "15年托管周期符合规范,未约定用能单价上涨调整规则", level: "warning" },
|
|
|
|
{ phase: "数据汇总", title: "报告输出落地", detail: "导出标准化审查报告,每条问题附带检索来源、分段编号、校验时间戳", level: "success" },
|
|
|
|
{ phase: "分段审查", title: "Seg06|第六章 计费与结算方式", detail: "半年结算无专项政策依据,缺少保底用能约定", level: "warning" },
|
|
|
|
// —— 全流程技术调用统计 ——
|
|
|
|
{ phase: "分段审查", title: "Seg07|第七章 项目建设现状与需求", detail: "分期建设面积数据和规划文件完全匹配", level: "success" },
|
|
|
|
{ phase: "流程统计", title: "Dify专业知识库调用", detail: "9次(每章1次检索)", level: "info" },
|
|
|
|
{ phase: "分段审查", title: "Seg08|第八章 冷热源系统技术方案", detail: "冷站EER≥4.54指标符合行业标准", level: "success" },
|
|
|
|
{ phase: "流程统计", title: "本地记忆临时RAG库检索", detail: "7次(存在跨章/附件比对章节触发)", level: "info" },
|
|
|
|
{ phase: "分段审查", title: "Seg09|第九章 智慧能源系统方案", detail: "屋面荷载鉴定说明缺失", level: "warning" },
|
|
|
|
{ phase: "流程统计", title: "本地大模型推理", detail: "分段推理43次", level: "info" },
|
|
|
|
{ phase: "分段审查", title: "Seg10|第十章 投资估算", detail: "冷站造价与企业标准基本持平,未列明主材材质明细", level: "warning" },
|
|
|
|
{ phase: "流程统计", title: "MinerUR多模态解析总耗时", detail: "3分37秒", level: "success" },
|
|
|
|
{ phase: "分段审查", title: "Seg11|第十一章 运营成本测算", detail: "成本逐年递增测算逻辑合规", level: "success" },
|
|
|
|
{ phase: "流程统计", title: "全流程运行状态", detail: "全流程运行稳定,无算力不足、上下文溢出报错", level: "success" },
|
|
|
|
{ phase: "分段审查", title: "Seg12|第十二章 经济与敏感性分析", detail: "缺少能源单价变动敏感性测算", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg13|第十三章 风险与结论建议", detail: "缺少能源价格波动专项风险防控条款", level: "warning" },
|
|
|
|
|
|
|
|
// —— 数据汇总/报告生成 ——
|
|
|
|
|
|
|
|
{ phase: "数据汇总", title: "汇总任务触发", detail: "本地大模型读取结果池内13个主分段、2个附件全部审查记录、全部Dify知识库检索原文、本地记忆跨段比对数据", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "数据汇总", title: "综合推理运算", detail: "大模型对13个分段问题分类归集:章节缺失类、规范遗漏类、数据偏差类、条款缺失类,每条问题绑定对应分段ID、知识库来源、校验时间戳", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "数据汇总", title: "标准化审查报告落地", detail: "导出完整审查文档,每条问题附带Dify规范条文索引、本地记忆库匹配附件/分段溯源信息", level: "success" },
|
|
|
|
|
|
|
|
// —— 流程统计 ——
|
|
|
|
|
|
|
|
{ phase: "流程统计", title: "Dify专业知识库总调用", detail: "13次(13个分段各1次检索)", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "流程统计", title: "本地记忆临时RAG库总检索", detail: "9次(存在跨段/附件数值比对需求分段)", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "流程统计", title: "本地大模型推理次数", detail: "分段推理42次", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "流程统计", title: "MinerUR多模态解析总耗时", detail: "4分23秒", level: "success" },
|
|
|
|
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 播放队列:将结构化数据展开为扁平的播放序列 */
|
|
|
|
|
|
|
|
interface PlaybackItem {
|
|
|
|
|
|
|
|
isChild: boolean;
|
|
|
|
|
|
|
|
log: MockReviewLog;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const playbackQueue: PlaybackItem[] = [];
|
|
|
|
|
|
|
|
for (const item of mockReviewData) {
|
|
|
|
|
|
|
|
if (item.children && item.children.length > 0) {
|
|
|
|
|
|
|
|
playbackQueue.push({ isChild: false, log: { ...item, children: [] } });
|
|
|
|
|
|
|
|
for (const child of item.children) {
|
|
|
|
|
|
|
|
playbackQueue.push({ isChild: true, log: child });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
playbackQueue.push({ isChild: false, log: item });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface ReviewLogEntry extends MockReviewLog {
|
|
|
|
interface ReviewLogEntry extends MockReviewLog {
|
|
|
|
time: string;
|
|
|
|
time: string;
|
|
|
|
|
|
|
|
children?: ReviewLogEntry[];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const reviewLogs = ref<ReviewLogEntry[]>([]);
|
|
|
|
const reviewLogs = ref<ReviewLogEntry[]>([]);
|
|
|
|
@ -318,6 +396,9 @@ let reviewTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
const reviewScrollRef = ref<HTMLElement | null>(null);
|
|
|
|
const reviewScrollRef = ref<HTMLElement | null>(null);
|
|
|
|
const scrollSentinelRef = ref<HTMLElement | null>(null);
|
|
|
|
const scrollSentinelRef = ref<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 智能自动滚动:用户手动上滚时暂停,回到底部时恢复 */
|
|
|
|
|
|
|
|
const isAutoScroll = ref(true);
|
|
|
|
|
|
|
|
|
|
|
|
/** 任务切换时重置审查日志 */
|
|
|
|
/** 任务切换时重置审查日志 */
|
|
|
|
watch(
|
|
|
|
watch(
|
|
|
|
() => props.taskId,
|
|
|
|
() => props.taskId,
|
|
|
|
@ -327,6 +408,7 @@ watch(
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
reviewFinished.value = false;
|
|
|
|
reviewFinished.value = false;
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
|
|
|
|
isAutoScroll.value = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
@ -336,15 +418,22 @@ function nowTime(): string {
|
|
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 根据阶段返回不同的日志产出间隔(ms),模拟真实处理耗时 */
|
|
|
|
/** 根据步骤类型返回不同的日志产出间隔(ms),模拟真实处理耗时 */
|
|
|
|
function getIntervalForPhase(phase: string): number {
|
|
|
|
function getIntervalForLog(log: MockReviewLog): number {
|
|
|
|
switch (phase) {
|
|
|
|
switch (log.step) {
|
|
|
|
case "基础资源加载": return 3000 + Math.random() * 2000;
|
|
|
|
case "vectorize": return 1500 + Math.random() * 1500;
|
|
|
|
case "文件解析": return 4000 + Math.random() * 3000;
|
|
|
|
case "search": return 2500 + Math.random() * 2000;
|
|
|
|
case "分段审查": return 5000 + Math.random() * 4000;
|
|
|
|
case "model_load": return 2000 + Math.random() * 1500;
|
|
|
|
case "数据汇总": return 6000 + Math.random() * 4000;
|
|
|
|
case "model_output": return 3500 + Math.random() * 3000;
|
|
|
|
case "流程统计": return 3000 + Math.random() * 2000;
|
|
|
|
case "record": return 1000 + Math.random() * 1000;
|
|
|
|
default: return 4000 + Math.random() * 3000;
|
|
|
|
default:
|
|
|
|
|
|
|
|
switch (log.phase) {
|
|
|
|
|
|
|
|
case "基础资源加载": return 3000 + Math.random() * 2000;
|
|
|
|
|
|
|
|
case "文件解析": return 4000 + Math.random() * 3000;
|
|
|
|
|
|
|
|
case "数据汇总": return 6000 + Math.random() * 4000;
|
|
|
|
|
|
|
|
case "流程统计": return 3000 + Math.random() * 2000;
|
|
|
|
|
|
|
|
default: return 3000 + Math.random() * 2000;
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -355,17 +444,31 @@ function ensureReviewRunning() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleNextReviewLog() {
|
|
|
|
function scheduleNextReviewLog() {
|
|
|
|
if (reviewIndex.value >= mockReviewLogs.length) {
|
|
|
|
if (reviewIndex.value >= playbackQueue.length) {
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
reviewFinished.value = true;
|
|
|
|
reviewFinished.value = true;
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
reviewWaiting.value = true;
|
|
|
|
reviewWaiting.value = true;
|
|
|
|
const entry = mockReviewLogs[reviewIndex.value]!;
|
|
|
|
const item = playbackQueue[reviewIndex.value]!;
|
|
|
|
const interval = getIntervalForPhase(entry.phase);
|
|
|
|
const interval = getIntervalForLog(item.log);
|
|
|
|
reviewTimer = setTimeout(() => {
|
|
|
|
reviewTimer = setTimeout(() => {
|
|
|
|
reviewTimer = null;
|
|
|
|
reviewTimer = null;
|
|
|
|
reviewLogs.value.push({ ...entry, time: nowTime() });
|
|
|
|
if (item.isChild) {
|
|
|
|
|
|
|
|
// 追加到最后一条父日志的 children 中
|
|
|
|
|
|
|
|
const parent = reviewLogs.value[reviewLogs.value.length - 1];
|
|
|
|
|
|
|
|
if (parent && parent.children) {
|
|
|
|
|
|
|
|
const { children: _c, ...childRest } = item.log;
|
|
|
|
|
|
|
|
parent.children.push({ ...childRest, time: nowTime() });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
const { children: _children, ...rest } = item.log;
|
|
|
|
|
|
|
|
const entry: ReviewLogEntry = { ...rest, time: nowTime() };
|
|
|
|
|
|
|
|
if (item.log.children) {
|
|
|
|
|
|
|
|
entry.children = [];
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
reviewLogs.value.push(entry);
|
|
|
|
|
|
|
|
}
|
|
|
|
reviewIndex.value++;
|
|
|
|
reviewIndex.value++;
|
|
|
|
scheduleNextReviewLog();
|
|
|
|
scheduleNextReviewLog();
|
|
|
|
}, interval);
|
|
|
|
}, interval);
|
|
|
|
@ -384,10 +487,19 @@ function restartReview() {
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
reviewFinished.value = false;
|
|
|
|
reviewFinished.value = false;
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
|
|
|
|
isAutoScroll.value = true;
|
|
|
|
ensureReviewRunning();
|
|
|
|
ensureReviewRunning();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** 自动滚动到底部:利用 sentinel 元素 scrollIntoView 滚动,比 scrollTop 赋值更可靠 */
|
|
|
|
/** 用户滚动时判断是否在底部附近 */
|
|
|
|
|
|
|
|
function onReviewScroll() {
|
|
|
|
|
|
|
|
const el = reviewScrollRef.value;
|
|
|
|
|
|
|
|
if (!el) return;
|
|
|
|
|
|
|
|
const threshold = 50;
|
|
|
|
|
|
|
|
const isAtBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold;
|
|
|
|
|
|
|
|
isAutoScroll.value = isAtBottom;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToBottom() {
|
|
|
|
function scrollToBottom() {
|
|
|
|
const sentinel = scrollSentinelRef.value;
|
|
|
|
const sentinel = scrollSentinelRef.value;
|
|
|
|
if (sentinel) {
|
|
|
|
if (sentinel) {
|
|
|
|
@ -396,15 +508,23 @@ function scrollToBottom() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToBottomDelayed() {
|
|
|
|
function scrollToBottomDelayed() {
|
|
|
|
|
|
|
|
if (!isAutoScroll.value) return;
|
|
|
|
nextTick(() => {
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
if (!isAutoScroll.value) return;
|
|
|
|
scrollToBottom();
|
|
|
|
scrollToBottom();
|
|
|
|
// Element Plus 组件内部渲染 + 卡片动画 0.35s,延迟重试确保到位
|
|
|
|
setTimeout(() => {
|
|
|
|
setTimeout(scrollToBottom, 400);
|
|
|
|
if (isAutoScroll.value) scrollToBottom();
|
|
|
|
|
|
|
|
}, 400);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 监听日志总数变化(父日志 + 子日志)触发自动滚动 */
|
|
|
|
|
|
|
|
const totalLogCount = computed(() => {
|
|
|
|
|
|
|
|
return reviewLogs.value.reduce((sum, log) => sum + 1 + (log.children?.length ?? 0), 0);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
watch(
|
|
|
|
() => reviewLogs.value.length,
|
|
|
|
() => totalLogCount.value,
|
|
|
|
() => {
|
|
|
|
() => {
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -565,6 +685,73 @@ function phaseTagType(phase: string): "primary" | "success" | "warning" | "dange
|
|
|
|
background: #fff;
|
|
|
|
background: #fff;
|
|
|
|
padding: 6px 8px;
|
|
|
|
padding: 6px 8px;
|
|
|
|
border-radius: 4px;
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
white-space: pre-line;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 子 timeline:分段审查调用链路 ===== */
|
|
|
|
|
|
|
|
.sub-timeline {
|
|
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
|
|
padding-left: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-timeline-item__timestamp) {
|
|
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
|
|
color: #c0c4cc;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-timeline-item__node--normal) {
|
|
|
|
|
|
|
|
left: -1px;
|
|
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-timeline-item__tail) {
|
|
|
|
|
|
|
|
left: 3px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-timeline-item:last-child .el-timeline-item__tail) {
|
|
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-sub-card {
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
animation: review-card-in 0.3s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&--success {
|
|
|
|
|
|
|
|
border-color: #dcfce7;
|
|
|
|
|
|
|
|
background: #f0fdf4;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
&--warning {
|
|
|
|
|
|
|
|
border-color: #fef3c7;
|
|
|
|
|
|
|
|
background: #fffbeb;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
&--error {
|
|
|
|
|
|
|
|
border-color: #fee2e2;
|
|
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-sub-header {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-sub-step {
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-sub-detail {
|
|
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
|
|
white-space: pre-line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 等待新日志加载动画 */
|
|
|
|
/* 等待新日志加载动画 */
|
|
|
|
|