|
|
|
@ -1,9 +1,9 @@
|
|
|
|
<template>
|
|
|
|
<template>
|
|
|
|
<ElDrawer
|
|
|
|
<ElDrawer
|
|
|
|
:model-value="visible"
|
|
|
|
:model-value="visible"
|
|
|
|
title="操作日志"
|
|
|
|
title="任务日志"
|
|
|
|
direction="rtl"
|
|
|
|
direction="rtl"
|
|
|
|
size="460px"
|
|
|
|
size="620px"
|
|
|
|
append-to-body
|
|
|
|
append-to-body
|
|
|
|
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
|
|
|
@update:model-value="(v: boolean) => emit('update:visible', v)"
|
|
|
|
>
|
|
|
|
>
|
|
|
|
@ -18,51 +18,113 @@
|
|
|
|
</ElDescriptions>
|
|
|
|
</ElDescriptions>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 操作记录 -->
|
|
|
|
<!-- 日志 Tab 切换 -->
|
|
|
|
<div class="log-section">
|
|
|
|
<ElTabs v-model="activeTab" class="log-tabs">
|
|
|
|
<h4 class="section-title">操作记录</h4>
|
|
|
|
<!-- ============ 审查日志 ============ -->
|
|
|
|
<ElTimeline v-loading="loading" class="log-timeline">
|
|
|
|
<ElTabPane name="review">
|
|
|
|
<ElTimelineItem
|
|
|
|
<template #label>
|
|
|
|
v-for="log in logList"
|
|
|
|
<span class="tab-label">
|
|
|
|
:key="log.id"
|
|
|
|
审查日志
|
|
|
|
:timestamp="formatDateTime(log.created_time)"
|
|
|
|
<span v-if="reviewWaiting" class="tab-status tab-status--running"></span>
|
|
|
|
placement="top"
|
|
|
|
<span v-else-if="reviewFinished" class="tab-status tab-status--done"></span>
|
|
|
|
:type="logType(log.action_type)"
|
|
|
|
</span>
|
|
|
|
:hollow="log.action_type === 'start_review'"
|
|
|
|
</template>
|
|
|
|
>
|
|
|
|
<div ref="reviewScrollRef" class="tab-scroll">
|
|
|
|
<div class="log-card">
|
|
|
|
<ElTimeline class="review-timeline">
|
|
|
|
<div class="log-header">
|
|
|
|
<ElTimelineItem
|
|
|
|
<span class="log-action">{{ log.action_name }}</span>
|
|
|
|
v-for="(log, idx) in reviewLogs"
|
|
|
|
<ElTag
|
|
|
|
:key="idx"
|
|
|
|
size="small"
|
|
|
|
:timestamp="log.time"
|
|
|
|
:type="logTagType(log.action_type)"
|
|
|
|
placement="top"
|
|
|
|
effect="light"
|
|
|
|
:type="reviewLevelType(log.level)"
|
|
|
|
>
|
|
|
|
:hollow="log.level === 'info'"
|
|
|
|
{{ logStatusText(log.action_type) }}
|
|
|
|
>
|
|
|
|
</ElTag>
|
|
|
|
<div class="review-card" :class="`review-card--${log.level}`">
|
|
|
|
</div>
|
|
|
|
<div class="review-card-header">
|
|
|
|
<div class="log-operator">
|
|
|
|
<ElTag size="small" :type="phaseTagType(log.phase)" effect="light">
|
|
|
|
<ElIcon><User /></ElIcon>
|
|
|
|
{{ log.phase }}
|
|
|
|
{{ log.operator_name || "系统" }}
|
|
|
|
</ElTag>
|
|
|
|
</div>
|
|
|
|
<span class="review-card-title">{{ log.title }}</span>
|
|
|
|
<div v-if="log.description" class="log-desc">
|
|
|
|
</div>
|
|
|
|
{{ log.description }}
|
|
|
|
<div v-if="log.detail" class="review-card-detail">
|
|
|
|
</div>
|
|
|
|
{{ log.detail }}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ElTimelineItem>
|
|
|
|
|
|
|
|
</ElTimeline>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 无审查日志时的空状态 -->
|
|
|
|
|
|
|
|
<div v-if="reviewLogs.length === 0 && !reviewWaiting && !reviewFinished" class="empty-logs">
|
|
|
|
|
|
|
|
暂无审查日志
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ElTimelineItem>
|
|
|
|
|
|
|
|
<div v-if="!loading && logList.length === 0" class="empty-logs">
|
|
|
|
<!-- 等待新日志加载动画 -->
|
|
|
|
暂无操作记录
|
|
|
|
<div v-if="reviewWaiting" class="review-loading">
|
|
|
|
|
|
|
|
<ElIcon class="is-loading" :size="16"><Loading /></ElIcon>
|
|
|
|
|
|
|
|
<span class="loading-text">正在审查,等待日志…</span>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 审查完成 -->
|
|
|
|
|
|
|
|
<div v-if="reviewFinished" class="review-finished">
|
|
|
|
|
|
|
|
<ElIcon color="#67c23a" :size="16"><CircleCheck /></ElIcon>
|
|
|
|
|
|
|
|
<span>审查完成</span>
|
|
|
|
|
|
|
|
<ElButton link type="primary" :icon="RefreshRight" @click="restartReview">
|
|
|
|
|
|
|
|
重新审查
|
|
|
|
|
|
|
|
</ElButton>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 滚动锚点:新增日志时通过 scrollIntoView 滚动至此 -->
|
|
|
|
|
|
|
|
<div ref="scrollSentinelRef" />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ElTimeline>
|
|
|
|
</ElTabPane>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ============ 操作日志 ============ -->
|
|
|
|
|
|
|
|
<ElTabPane label="操作日志" name="operation">
|
|
|
|
|
|
|
|
<div class="tab-scroll">
|
|
|
|
|
|
|
|
<ElTimeline v-loading="loading" class="log-timeline">
|
|
|
|
|
|
|
|
<ElTimelineItem
|
|
|
|
|
|
|
|
v-for="log in logList"
|
|
|
|
|
|
|
|
:key="log.id"
|
|
|
|
|
|
|
|
:timestamp="formatDateTime(log.created_time)"
|
|
|
|
|
|
|
|
placement="top"
|
|
|
|
|
|
|
|
:type="logType(log.action_type)"
|
|
|
|
|
|
|
|
:hollow="log.action_type === 'start_review'"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<div class="log-card">
|
|
|
|
|
|
|
|
<div class="log-header">
|
|
|
|
|
|
|
|
<span class="log-action">{{ log.action_name }}</span>
|
|
|
|
|
|
|
|
<ElTag
|
|
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
|
|
:type="logTagType(log.action_type)"
|
|
|
|
|
|
|
|
effect="light"
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{{ logStatusText(log.action_type) }}
|
|
|
|
|
|
|
|
</ElTag>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="log-operator">
|
|
|
|
|
|
|
|
<ElIcon><User /></ElIcon>
|
|
|
|
|
|
|
|
{{ log.operator_name || "系统" }}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="log.description" class="log-desc">
|
|
|
|
|
|
|
|
{{ log.description }}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ElTimelineItem>
|
|
|
|
|
|
|
|
<div v-if="!loading && logList.length === 0" class="empty-logs">
|
|
|
|
|
|
|
|
暂无操作记录
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ElTimeline>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</ElTabPane>
|
|
|
|
|
|
|
|
</ElTabs>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
</ElDrawer>
|
|
|
|
</ElDrawer>
|
|
|
|
</template>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
<script setup lang="ts">
|
|
|
|
import { ref, watch, computed } from "vue";
|
|
|
|
import { ref, watch, computed, nextTick, onUnmounted } from "vue";
|
|
|
|
import { useRoute } from "vue-router";
|
|
|
|
import { useRoute } from "vue-router";
|
|
|
|
import { User } from "@element-plus/icons-vue";
|
|
|
|
import { User, Loading, CircleCheck, RefreshRight } from "@element-plus/icons-vue";
|
|
|
|
import { ReviewTaskAPI } from "@/api/module_report/review";
|
|
|
|
import { ReviewTaskAPI } from "@/api/module_report/review";
|
|
|
|
import type { ReviewTaskItem, ReviewLogItem, ReviewLogActionType } from "@/api/module_report/review";
|
|
|
|
import type { ReviewTaskItem, ReviewLogItem, ReviewLogActionType } from "@/api/module_report/review";
|
|
|
|
import type { ReportType } from "@/api/module_report/report";
|
|
|
|
import type { ReportType } from "@/api/module_report/report";
|
|
|
|
@ -73,6 +135,9 @@ const props = defineProps<{
|
|
|
|
visible: boolean;
|
|
|
|
visible: boolean;
|
|
|
|
taskId?: number | null;
|
|
|
|
taskId?: number | null;
|
|
|
|
task?: ReviewTaskItem | null;
|
|
|
|
task?: ReviewTaskItem | null;
|
|
|
|
|
|
|
|
defaultTab?: "review" | "operation";
|
|
|
|
|
|
|
|
/** 是否在展示审查日志时自动启动审查流程(审查入口为 true,日志入口为 false) */
|
|
|
|
|
|
|
|
startReview?: boolean;
|
|
|
|
}>();
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const route = useRoute();
|
|
|
|
const route = useRoute();
|
|
|
|
@ -87,6 +152,45 @@ const emit = defineEmits<{
|
|
|
|
"update:visible": [v: boolean];
|
|
|
|
"update:visible": [v: boolean];
|
|
|
|
}>();
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== Tab 状态 ==========
|
|
|
|
|
|
|
|
const activeTab = ref<"review" | "operation">(props.defaultTab || "operation");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
() => props.visible,
|
|
|
|
|
|
|
|
(v) => {
|
|
|
|
|
|
|
|
if (v) {
|
|
|
|
|
|
|
|
activeTab.value = props.defaultTab || "operation";
|
|
|
|
|
|
|
|
if (activeTab.value === "review" && props.startReview) {
|
|
|
|
|
|
|
|
// 审查入口:每次确认后都重新启动渐进式审查日志输出,清空历史日志
|
|
|
|
|
|
|
|
restartReview();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
stopReviewTimer();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
() => props.defaultTab,
|
|
|
|
|
|
|
|
(tab) => {
|
|
|
|
|
|
|
|
if (tab && props.visible) {
|
|
|
|
|
|
|
|
activeTab.value = tab;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(activeTab, (tab) => {
|
|
|
|
|
|
|
|
if (tab === "review" && props.visible) {
|
|
|
|
|
|
|
|
// 仅审查入口(startReview=true)才启动审查流程
|
|
|
|
|
|
|
|
if (props.startReview) {
|
|
|
|
|
|
|
|
ensureReviewRunning();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 操作日志 ==========
|
|
|
|
const loading = ref(false);
|
|
|
|
const loading = ref(false);
|
|
|
|
const logList = ref<ReviewLogItem[]>([]);
|
|
|
|
const logList = ref<ReviewLogItem[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
@ -154,24 +258,341 @@ function logStatusText(t: ReviewLogActionType): string {
|
|
|
|
default: return t;
|
|
|
|
default: return t;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ========== 审查日志(Mock 演示数据) ==========
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type ReviewLogLevel = "info" | "success" | "warning" | "error";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface MockReviewLog {
|
|
|
|
|
|
|
|
phase: string;
|
|
|
|
|
|
|
|
title: string;
|
|
|
|
|
|
|
|
detail?: string;
|
|
|
|
|
|
|
|
level: ReviewLogLevel;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 审查日志 Mock 数据 */
|
|
|
|
|
|
|
|
const mockReviewLogs: MockReviewLog[] = [
|
|
|
|
|
|
|
|
// —— 基础资源加载 ——
|
|
|
|
|
|
|
|
{ phase: "基础资源加载", title: "系统初始化", detail: "加载Dify专业能源RAG知识库,库分类:国标库/暖通行标库/综合能源企业范本库,存量规范文档92份,向量模型bge-large-zh,语义检索阈值0.7", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "基础资源加载", title: "规则引擎加载", detail: "载入人工预设多级可研章节大纲模板、全章节业务审查规则共47条,各章节绑定对应知识库检索过滤标签", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "基础资源加载", title: "本地记忆库初始化", detail: "创建本项目独立临时RAG向量库,清空历史项目向量索引,生成分段存储表,预留Seg/Att分段向量存储通道", level: "info" },
|
|
|
|
|
|
|
|
// —— 文件解析/分段拆分 ——
|
|
|
|
|
|
|
|
{ phase: "文件解析", title: "MinerUR引擎启动", detail: "并行读取主可研docx、2份PDF附件,启动版面分层、文字/表格/图像/公式多模态识别", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "文件解析", title: "多模态信息提取完成", detail: "识别制冷负荷、托管单价、总投资额、院区面积、合同年限等18类关键数值,提取造价、能耗、财务表格9张,过滤页眉、空白页冗余文本", level: "success" },
|
|
|
|
|
|
|
|
{ phase: "文件解析", title: "长文档拆分触发", detail: "可研正文按章节边界切割为Seg01~Seg13共13个独立文本分段;附件拆分为Att01(规划批复)、Att02(财务报表)", level: "info" },
|
|
|
|
|
|
|
|
{ phase: "文件解析", title: "分段预处理完成", detail: "13个主分段、2个附件分段全部完成文本清洗,等待循环审查流程", level: "success" },
|
|
|
|
|
|
|
|
// —— 分段审查 Seg01~Seg13 ——
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg01|第一章 项目概述", detail: "章节框架完整;可研仅标注街道名称,未写明精确门牌号,违反业务规则;院区面积描述符合国标;可研地址与规划文件地址存在文字差异", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg02|第二章 业主资信与付款能力", detail: "营收数据与附件财务表数值匹配,无失信说明完整,缺少涉诉风险专项分析", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg03|第三章 建设必要性", detail: "政策引用合规,缺少区域同类项目示范效益分析", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg04|第四章 编制依据", detail: "遗漏《GB50189公共建筑节能设计标准》,编制依据不全", level: "error" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg05|第五章 商业合作模式", detail: "15年托管周期符合规范,未约定用能单价上涨调整规则", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg06|第六章 计费与结算方式", detail: "半年结算无专项政策依据,缺少保底用能约定", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg07|第七章 项目建设现状与需求", detail: "分期建设面积数据和规划文件完全匹配", level: "success" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg08|第八章 冷热源系统技术方案", detail: "冷站EER≥4.54指标符合行业标准", level: "success" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg09|第九章 智慧能源系统方案", detail: "屋面荷载鉴定说明缺失", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg10|第十章 投资估算", detail: "冷站造价与企业标准基本持平,未列明主材材质明细", level: "warning" },
|
|
|
|
|
|
|
|
{ phase: "分段审查", title: "Seg11|第十一章 运营成本测算", 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 ReviewLogEntry extends MockReviewLog {
|
|
|
|
|
|
|
|
time: string;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const reviewLogs = ref<ReviewLogEntry[]>([]);
|
|
|
|
|
|
|
|
const reviewWaiting = ref(false);
|
|
|
|
|
|
|
|
const reviewFinished = ref(false);
|
|
|
|
|
|
|
|
const reviewIndex = ref(0);
|
|
|
|
|
|
|
|
let reviewTimer: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
const reviewScrollRef = ref<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
const scrollSentinelRef = ref<HTMLElement | null>(null);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 任务切换时重置审查日志 */
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
() => props.taskId,
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
stopReviewTimer();
|
|
|
|
|
|
|
|
reviewLogs.value = [];
|
|
|
|
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
|
|
|
|
reviewFinished.value = false;
|
|
|
|
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function nowTime(): string {
|
|
|
|
|
|
|
|
const d = new Date();
|
|
|
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, "0");
|
|
|
|
|
|
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 根据阶段返回不同的日志产出间隔(ms),模拟真实处理耗时 */
|
|
|
|
|
|
|
|
function getIntervalForPhase(phase: string): number {
|
|
|
|
|
|
|
|
switch (phase) {
|
|
|
|
|
|
|
|
case "基础资源加载": return 3000 + Math.random() * 2000;
|
|
|
|
|
|
|
|
case "文件解析": return 4000 + Math.random() * 3000;
|
|
|
|
|
|
|
|
case "分段审查": return 5000 + Math.random() * 4000;
|
|
|
|
|
|
|
|
case "数据汇总": return 6000 + Math.random() * 4000;
|
|
|
|
|
|
|
|
case "流程统计": return 3000 + Math.random() * 2000;
|
|
|
|
|
|
|
|
default: return 4000 + Math.random() * 3000;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function ensureReviewRunning() {
|
|
|
|
|
|
|
|
if (reviewFinished.value) return;
|
|
|
|
|
|
|
|
if (reviewTimer !== null) return;
|
|
|
|
|
|
|
|
scheduleNextReviewLog();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scheduleNextReviewLog() {
|
|
|
|
|
|
|
|
if (reviewIndex.value >= mockReviewLogs.length) {
|
|
|
|
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
|
|
|
|
reviewFinished.value = true;
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
reviewWaiting.value = true;
|
|
|
|
|
|
|
|
const entry = mockReviewLogs[reviewIndex.value]!;
|
|
|
|
|
|
|
|
const interval = getIntervalForPhase(entry.phase);
|
|
|
|
|
|
|
|
reviewTimer = setTimeout(() => {
|
|
|
|
|
|
|
|
reviewTimer = null;
|
|
|
|
|
|
|
|
reviewLogs.value.push({ ...entry, time: nowTime() });
|
|
|
|
|
|
|
|
reviewIndex.value++;
|
|
|
|
|
|
|
|
scheduleNextReviewLog();
|
|
|
|
|
|
|
|
}, interval);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function stopReviewTimer() {
|
|
|
|
|
|
|
|
if (reviewTimer !== null) {
|
|
|
|
|
|
|
|
clearTimeout(reviewTimer);
|
|
|
|
|
|
|
|
reviewTimer = null;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function restartReview() {
|
|
|
|
|
|
|
|
stopReviewTimer();
|
|
|
|
|
|
|
|
reviewLogs.value = [];
|
|
|
|
|
|
|
|
reviewIndex.value = 0;
|
|
|
|
|
|
|
|
reviewFinished.value = false;
|
|
|
|
|
|
|
|
reviewWaiting.value = false;
|
|
|
|
|
|
|
|
ensureReviewRunning();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** 自动滚动到底部:利用 sentinel 元素 scrollIntoView 滚动,比 scrollTop 赋值更可靠 */
|
|
|
|
|
|
|
|
function scrollToBottom() {
|
|
|
|
|
|
|
|
const sentinel = scrollSentinelRef.value;
|
|
|
|
|
|
|
|
if (sentinel) {
|
|
|
|
|
|
|
|
sentinel.scrollIntoView({ behavior: 'instant', block: 'nearest' });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function scrollToBottomDelayed() {
|
|
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
|
|
scrollToBottom();
|
|
|
|
|
|
|
|
// Element Plus 组件内部渲染 + 卡片动画 0.35s,延迟重试确保到位
|
|
|
|
|
|
|
|
setTimeout(scrollToBottom, 400);
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
|
|
() => reviewLogs.value.length,
|
|
|
|
|
|
|
|
() => {
|
|
|
|
|
|
|
|
scrollToBottomDelayed();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
|
|
stopReviewTimer();
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function reviewLevelType(level: ReviewLogLevel): "primary" | "success" | "warning" | "danger" {
|
|
|
|
|
|
|
|
switch (level) {
|
|
|
|
|
|
|
|
case "info": return "primary";
|
|
|
|
|
|
|
|
case "success": return "success";
|
|
|
|
|
|
|
|
case "warning": return "warning";
|
|
|
|
|
|
|
|
case "error": return "danger";
|
|
|
|
|
|
|
|
default: return "primary";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function phaseTagType(phase: string): "primary" | "success" | "warning" | "danger" | "info" {
|
|
|
|
|
|
|
|
switch (phase) {
|
|
|
|
|
|
|
|
case "基础资源加载": return "primary";
|
|
|
|
|
|
|
|
case "文件解析": return "success";
|
|
|
|
|
|
|
|
case "分段审查": return "warning";
|
|
|
|
|
|
|
|
case "数据汇总": return "info";
|
|
|
|
|
|
|
|
case "流程统计": return "info";
|
|
|
|
|
|
|
|
default: return "info";
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
|
|
/* 抽屉 body 改为 flex 布局,使 Tab 内容区可独立滚动 */
|
|
|
|
|
|
|
|
:deep(.el-drawer__body) {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.task-summary {
|
|
|
|
.task-summary {
|
|
|
|
margin-bottom: 16px;
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
padding: 16px 20px 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* Tab 区域 */
|
|
|
|
|
|
|
|
.log-tabs {
|
|
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
padding: 0 20px 16px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-tabs__header) {
|
|
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-tabs__content) {
|
|
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-tab-pane) {
|
|
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.tab-label {
|
|
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.tab-status {
|
|
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
|
|
width: 7px;
|
|
|
|
|
|
|
|
height: 7px;
|
|
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&--running {
|
|
|
|
|
|
|
|
background: #e6a23c;
|
|
|
|
|
|
|
|
animation: tab-pulse 1.5s infinite;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&--done {
|
|
|
|
|
|
|
|
background: #67c23a;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes tab-pulse {
|
|
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
|
|
50% { opacity: 0.3; }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.log-section {
|
|
|
|
.tab-scroll {
|
|
|
|
.section-title {
|
|
|
|
height: 100%;
|
|
|
|
margin: 0 0 12px;
|
|
|
|
overflow-y: auto;
|
|
|
|
font-size: 14px;
|
|
|
|
padding: 12px 4px 0 0;
|
|
|
|
font-weight: 600;
|
|
|
|
}
|
|
|
|
color: #303133;
|
|
|
|
|
|
|
|
padding-left: 8px;
|
|
|
|
/* ===== 审查日志卡片 ===== */
|
|
|
|
border-left: 3px solid #409eff;
|
|
|
|
.review-timeline {
|
|
|
|
|
|
|
|
padding-left: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-card {
|
|
|
|
|
|
|
|
background: #f9fafb;
|
|
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
|
|
border: 1px solid #f0f0f0;
|
|
|
|
|
|
|
|
animation: review-card-in 0.35s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&--success {
|
|
|
|
|
|
|
|
background: #f0fdf4;
|
|
|
|
|
|
|
|
border-color: #dcfce7;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
&--warning {
|
|
|
|
|
|
|
|
background: #fffbeb;
|
|
|
|
|
|
|
|
border-color: #fef3c7;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
&--error {
|
|
|
|
|
|
|
|
background: #fef2f2;
|
|
|
|
|
|
|
|
border-color: #fee2e2;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes review-card-in {
|
|
|
|
|
|
|
|
from {
|
|
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
|
|
transform: translateY(8px);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
to {
|
|
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-card-header {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-card-title {
|
|
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
color: #303133;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.review-card-detail {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #606266;
|
|
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 等待新日志加载动画 */
|
|
|
|
|
|
|
|
.review-loading {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
|
|
padding: 14px 0 8px 28px;
|
|
|
|
|
|
|
|
color: #409eff;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.loading-text {
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
color: #909399;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* 审查完成 */
|
|
|
|
|
|
|
|
.review-finished {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
|
|
padding: 12px 0 8px 28px;
|
|
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
color: #67c23a;
|
|
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/* ===== 操作日志卡片(保持原有样式) ===== */
|
|
|
|
.log-card {
|
|
|
|
.log-card {
|
|
|
|
background: #f9fafb;
|
|
|
|
background: #f9fafb;
|
|
|
|
border-radius: 6px;
|
|
|
|
border-radius: 6px;
|
|
|
|
|