|
|
@@ -0,0 +1,423 @@
|
|
|
+<template>
|
|
|
+ <div class="chat-shell">
|
|
|
+ <!-- 顶部 -->
|
|
|
+ <header class="chat-topbar">
|
|
|
+ <div class="topbar-left">
|
|
|
+ <span class="topbar-dot"></span>
|
|
|
+ <span class="topbar-title">AI 对话记录</span>
|
|
|
+ </div>
|
|
|
+ <div class="topbar-right">
|
|
|
+ <span class="topbar-badge" v-if="aiLogList.length">{{ aiLogList.length }} 轮对话</span>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- 空状态 -->
|
|
|
+ <div v-if="aiLogList.length === 0 && !loading" class="chat-void">
|
|
|
+ <div class="void-icon">
|
|
|
+ <i class="el-icon-chat-line-square"></i>
|
|
|
+ </div>
|
|
|
+ <p class="void-text">暂无聊天记录</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 消息流 -->
|
|
|
+ <div v-loading="loading" class="chat-stream">
|
|
|
+ <div v-for="(log, index) in aiLogList" :key="index" class="turn">
|
|
|
+
|
|
|
+ <!-- 用户侧 -->
|
|
|
+ <div class="msg msg--user">
|
|
|
+ <div class="msg-body">
|
|
|
+ <div class="bubble bubble--user">
|
|
|
+ <p class="bubble-text">{{ log.queryContent || '-' }}</p>
|
|
|
+ </div>
|
|
|
+ <div class="msg-tag">
|
|
|
+ <span class="tag-label">用户</span>
|
|
|
+ <span class="tag-dot">·</span>
|
|
|
+ <span class="tag-time">{{ formatTime(log.createTime) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- AI 侧 -->
|
|
|
+ <div class="msg msg--ai">
|
|
|
+ <div class="msg-body">
|
|
|
+ <div class="msg-head">
|
|
|
+ <div class="head-left">
|
|
|
+ <span class="head-model" v-if="log.modelName">
|
|
|
+ <i class="el-icon-cpu"></i>{{ log.modelName }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <span class="head-time" v-if="log.createTime">{{ formatTime(log.createTime) }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 思考折叠 -->
|
|
|
+ <div v-if="log.reasoningText" class="cogitation">
|
|
|
+ <button class="cogitation-trigger" @click="toggleReasoning(index)">
|
|
|
+ <span class="cogitation-chevron" :class="{ 'is-open': isReasoningOpen(index) }">
|
|
|
+ <i class="el-icon-arrow-right"></i>
|
|
|
+ </span>
|
|
|
+ <span class="cogitation-label">思考过程</span>
|
|
|
+ <span class="cogitation-hint">{{ isReasoningOpen(index) ? '收起' : '展开' }}</span>
|
|
|
+ </button>
|
|
|
+ <div v-show="isReasoningOpen(index)" class="cogitation-body">
|
|
|
+ {{ log.reasoningText }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 模型回答 -->
|
|
|
+ <div class="bubble bubble--ai">
|
|
|
+ <p class="bubble-text">{{ log.aiContent || '-' }}</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 元信息 -->
|
|
|
+ <div class="msg-foot">
|
|
|
+ <span>耗时 {{ formatElapsed(log.costTime) }}</span>
|
|
|
+ <template v-if="log.totalTokens">
|
|
|
+ <span class="foot-sep">·</span>
|
|
|
+ <span>{{ log.totalTokens }} tokens</span>
|
|
|
+ </template>
|
|
|
+ <template v-if="log.inputTokens != null">
|
|
|
+ <span class="foot-sep">·</span>
|
|
|
+ <span>输入 {{ log.inputTokens }}</span>
|
|
|
+ <span class="foot-sep">·</span>
|
|
|
+ <span>输出 {{ log.outputTokens }}</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+
|
|
|
+import { reportAiLogByReportId } from '@/api/his/testReport'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: "testReportAiLog",
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ aiLogList: [],
|
|
|
+ loading: false,
|
|
|
+ reasoningOpen: {},
|
|
|
+ };
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ getAiLog(reportId) {
|
|
|
+ this.aiLogList = [];
|
|
|
+ this.reasoningOpen = {};
|
|
|
+ this.loading = true;
|
|
|
+ reportAiLogByReportId({ reportId: reportId }).then(response => {
|
|
|
+ this.aiLogList = response.rows || response.data || [];
|
|
|
+ this.loading = false;
|
|
|
+ });
|
|
|
+ },
|
|
|
+ toggleReasoning(index) {
|
|
|
+ this.$set(this.reasoningOpen, index, !this.reasoningOpen[index]);
|
|
|
+ },
|
|
|
+ isReasoningOpen(index) {
|
|
|
+ return !!this.reasoningOpen[index];
|
|
|
+ },
|
|
|
+ formatTime(time) {
|
|
|
+ if (!time) return '-';
|
|
|
+ const d = new Date(time);
|
|
|
+ if (isNaN(d.getTime())) return time;
|
|
|
+ const pad = n => String(n).padStart(2, '0');
|
|
|
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
|
+ },
|
|
|
+ formatElapsed(seconds) {
|
|
|
+ if (seconds == null) return '-';
|
|
|
+ const s = Number(seconds);
|
|
|
+ if (s < 60) return s.toFixed(2) + 's';
|
|
|
+ const minutes = Math.floor(s / 60);
|
|
|
+ const remainSeconds = (s % 60).toFixed(0);
|
|
|
+ return minutes + 'm ' + remainSeconds + 's';
|
|
|
+ },
|
|
|
+ expandNestedJSON(node) {
|
|
|
+ if (typeof node === 'string') {
|
|
|
+ const s = node.trim();
|
|
|
+ if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) {
|
|
|
+ try {
|
|
|
+ return this.expandNestedJSON(JSON.parse(s));
|
|
|
+ } catch {
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return node;
|
|
|
+ }
|
|
|
+ if (Array.isArray(node)) {
|
|
|
+ return node.map(item => this.expandNestedJSON(item));
|
|
|
+ }
|
|
|
+ if (node !== null && typeof node === 'object') {
|
|
|
+ const result = {};
|
|
|
+ for (const [key, value] of Object.entries(node)) {
|
|
|
+ result[key] = this.expandNestedJSON(value);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ return node;
|
|
|
+ },
|
|
|
+ },
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+/* ============================================
|
|
|
+ 颜色体系对齐项目 Element UI 主题
|
|
|
+ 主色 #1890ff / 绿色 #30b08f / 白底灰边
|
|
|
+ ============================================ */
|
|
|
+
|
|
|
+.chat-shell {
|
|
|
+ --ink: #303133;
|
|
|
+ --ink-soft: #606266;
|
|
|
+ --ink-faint: #c0c4cc;
|
|
|
+ --surface: #fff;
|
|
|
+ --surface-ai: #fafbfc;
|
|
|
+ --surface-card: #fff;
|
|
|
+ --accent: #1890ff;
|
|
|
+ --accent-soft: #c6e2ff;
|
|
|
+ --user-bubble: #30b08f;
|
|
|
+ --border: #e6ebf5;
|
|
|
+ --radius-sm: 6px;
|
|
|
+ --radius-md: 14px;
|
|
|
+ --radius-lg: 20px;
|
|
|
+
|
|
|
+ background: var(--surface);
|
|
|
+ min-height: 100%;
|
|
|
+ padding-bottom: 48px;
|
|
|
+ font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, -apple-system, sans-serif;
|
|
|
+ color: var(--ink);
|
|
|
+ -webkit-font-smoothing: antialiased;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 顶栏 ---- */
|
|
|
+.chat-topbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ padding: 16px 28px;
|
|
|
+ background: var(--surface-card);
|
|
|
+ border-bottom: 1px solid var(--border);
|
|
|
+ position: sticky;
|
|
|
+ top: 0;
|
|
|
+ z-index: 10;
|
|
|
+ backdrop-filter: blur(8px);
|
|
|
+}
|
|
|
+.topbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+}
|
|
|
+.topbar-dot {
|
|
|
+ width: 8px;
|
|
|
+ height: 8px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: var(--accent);
|
|
|
+ box-shadow: 0 0 0 3px var(--accent-soft);
|
|
|
+}
|
|
|
+.topbar-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 600;
|
|
|
+ letter-spacing: 0.03em;
|
|
|
+ color: var(--ink);
|
|
|
+}
|
|
|
+.topbar-badge {
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--ink-soft);
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 4px 12px;
|
|
|
+ border-radius: 100px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 空状态 ---- */
|
|
|
+.chat-void {
|
|
|
+ text-align: center;
|
|
|
+ padding: 100px 0;
|
|
|
+}
|
|
|
+.void-icon {
|
|
|
+ font-size: 40px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.void-text {
|
|
|
+ font-size: 14px;
|
|
|
+ color: var(--ink-soft);
|
|
|
+ margin: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 消息流 ---- */
|
|
|
+.chat-stream {
|
|
|
+ max-width: 780px;
|
|
|
+ margin: 0 auto;
|
|
|
+ padding: 28px 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 一轮对话 ---- */
|
|
|
+.turn {
|
|
|
+ margin-bottom: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 消息行 ---- */
|
|
|
+.msg {
|
|
|
+ display: flex;
|
|
|
+}
|
|
|
+.msg--user {
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+.msg--ai {
|
|
|
+ background: var(--surface-ai);
|
|
|
+ border-radius: var(--radius-lg);
|
|
|
+ padding: 18px 22px;
|
|
|
+ border: 1px solid var(--border);
|
|
|
+}
|
|
|
+
|
|
|
+.msg-body {
|
|
|
+ max-width: 76%;
|
|
|
+}
|
|
|
+.msg--ai .msg-body {
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 气泡 ---- */
|
|
|
+.bubble {
|
|
|
+ line-height: 1.7;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+.bubble--user {
|
|
|
+ background: var(--user-bubble);
|
|
|
+ color: #fff;
|
|
|
+ padding: 12px 18px;
|
|
|
+ border-radius: var(--radius-lg);
|
|
|
+ border-bottom-right-radius: var(--radius-sm);
|
|
|
+}
|
|
|
+.bubble--ai {
|
|
|
+ color: var(--ink);
|
|
|
+ padding: 0;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+.bubble-text {
|
|
|
+ margin: 0;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+.bubble-json {
|
|
|
+ margin: 0;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+ font-family: "SF Mono", "Cascadia Code", "Fira Code", "Consolas", "Monaco", monospace;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.55;
|
|
|
+ opacity: 0.92;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 用户标签(气泡外) ---- */
|
|
|
+.msg-tag {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: flex-end;
|
|
|
+ gap: 3px;
|
|
|
+ margin-top: 6px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+}
|
|
|
+.tag-dot {
|
|
|
+ opacity: 0.4;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- AI 头部 ---- */
|
|
|
+.msg-head {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+.head-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+.head-model {
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: var(--ink-soft);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 4px;
|
|
|
+ letter-spacing: 0.02em;
|
|
|
+}
|
|
|
+.head-time {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- 思考过程 ---- */
|
|
|
+.cogitation {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ border: 1px solid rgba(24, 144, 255, 0.12);
|
|
|
+ border-radius: var(--radius-md);
|
|
|
+ background: rgba(24, 144, 255, 0.03);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+.cogitation-trigger {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border: none;
|
|
|
+ background: none;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--ink-soft);
|
|
|
+ font-family: inherit;
|
|
|
+ transition: background 0.2s;
|
|
|
+}
|
|
|
+.cogitation-trigger:hover {
|
|
|
+ background: rgba(24, 144, 255, 0.05);
|
|
|
+}
|
|
|
+.cogitation-chevron {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ margin-right: 8px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: var(--accent);
|
|
|
+ transition: transform 0.25s ease;
|
|
|
+}
|
|
|
+.cogitation-chevron.is-open {
|
|
|
+ transform: rotate(90deg);
|
|
|
+}
|
|
|
+.cogitation-label {
|
|
|
+ flex: 1;
|
|
|
+ text-align: left;
|
|
|
+ font-weight: 500;
|
|
|
+ color: var(--accent);
|
|
|
+}
|
|
|
+.cogitation-hint {
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+}
|
|
|
+.cogitation-body {
|
|
|
+ padding: 0 14px 14px 38px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: var(--ink-soft);
|
|
|
+ line-height: 1.65;
|
|
|
+ white-space: pre-wrap;
|
|
|
+}
|
|
|
+
|
|
|
+/* ---- AI 底部元信息 ---- */
|
|
|
+.msg-foot {
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 11px;
|
|
|
+ color: var(--ink-faint);
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+.foot-sep {
|
|
|
+ opacity: 0.35;
|
|
|
+ margin: 0 3px;
|
|
|
+}
|
|
|
+</style>
|