Преглед на файлове

红德堂-V1.2 测一测结果对接豆包

Long преди 12 часа
родител
ревизия
54d6b12a39

+ 19 - 1
src/api/his/testReport.js

@@ -50,4 +50,22 @@ export function exportTestReport(query) {
     method: 'get',
     params: query
   })
-}
+}
+
+// 查询测试报告AI日志列表
+export function reportAiLogByReportId(query) {
+  return request({
+    url: '/his/testReport/reportAiLogByReportId',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询测试报告Token总计
+export function getTestReportTokenTotal(query) {
+  return request({
+    url: '/his/testReport/tokenTotal',
+    method: 'get',
+    params: query
+  })
+}

+ 423 - 0
src/views/components/his/testReportAiLog.vue

@@ -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>

+ 42 - 3
src/views/his/testReport/index.vue

@@ -28,6 +28,13 @@
         <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
       </el-form-item>
     </el-form>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="3" style="line-height: 28px;">
+        <span style="color: #606266; font-size: 13px;">
+          消耗Token合计:<strong style="color: #409EFF;">{{ tokenTotal }}</strong>
+        </span>
+      </el-col>
+    </el-row>
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
         <el-button
@@ -56,7 +63,7 @@
       <el-table-column label="分数" align="center" prop="score" />
       <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column label="提交时间" align="center" prop="createTime" width="150px" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150px">
         <template slot-scope="scope">
 
           <el-button
@@ -65,6 +72,12 @@
               @click="handledetails(scope.row)"
           >详情
           </el-button>
+          <el-button
+              size="mini"
+              type="text"
+              @click="handleChatRecord(scope.row)"
+          >聊天记录
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -99,20 +112,31 @@
              :title="show.title" :visible.sync="show.open">
          <testReportDetails  ref="Details" />
        </el-drawer>
+    <el-drawer
+      :with-header="false"
+      size="40%"
+      :visible.sync="aiLogDrawer.open">
+      <testReportAiLog ref="AiLog" />
+    </el-drawer>
   </div>
 </template>
 
 <script>
-import { listTestReport, getTestReport, delTestReport, addTestReport, updateTestReport, exportTestReport } from "@/api/his/testReport";
+import { listTestReport, getTestReport, delTestReport, addTestReport, updateTestReport, exportTestReport, getTestReportTokenTotal } from "@/api/his/testReport";
 import testReportDetails from '../../components/his/testReportDetails.vue';
+import testReportAiLog from '../../components/his/testReportAiLog.vue';
 export default {
   name: "TestReport",
-  components: { testReportDetails },
+  components: { testReportDetails, testReportAiLog },
   data() {
     return {
       show:{
               open:false,
             },
+      aiLogDrawer: {
+        open: false,
+      },
+      tokenTotal: 0,
       // 遮罩层
       loading: true,
       // 导出遮罩层
@@ -176,6 +200,15 @@ export default {
         this.total = response.total;
         this.loading = false;
       });
+      this.fetchTokenTotal();
+    },
+    /** 查询Token总计 */
+    fetchTokenTotal() {
+      getTestReportTokenTotal(this.queryParams).then(response => {
+        this.tokenTotal = response.data || response.total || 0;
+      }).catch(() => {
+        this.tokenTotal = 0;
+      });
     },
     // 取消按钮
     cancel() {
@@ -244,6 +277,12 @@ export default {
                  this.$refs.Details.getDetails(row.reportId);
             }, 1);
         },
+    handleChatRecord(row) {
+      this.aiLogDrawer.open = true;
+      setTimeout(() => {
+        this.$refs.AiLog.getAiLog(row.reportId);
+      }, 1);
+    },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate(valid => {

+ 1 - 1
src/views/his/testTemp/index.vue

@@ -100,7 +100,7 @@
    <!--   <el-table-column label="备注" align="center" prop="remark" /> -->
        <el-table-column label="创建时间" align="center" prop="createTime" width="160"/>
         <el-table-column label="更新时间" align="center" prop="updateTime" width="160"/>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px">
         <template slot-scope="scope">
           <el-button
             size="mini"

+ 6 - 0
src/views/system/config/config.vue

@@ -2274,6 +2274,10 @@
           <el-button type="primary" @click="submitForm34">提  交</el-button>
         </div>
       </el-tab-pane>
+
+      <el-tab-pane label="测一测配置" name="test.config">
+        <TestConfig v-if="activeName === 'test.config'"/>
+      </el-tab-pane>
     </el-tabs>
 
 
@@ -2305,10 +2309,12 @@ import { getCitys } from '@/api/store/city'
 import { listCompany } from '@/api/company/company'
 import { getStoreProductColumns } from '@/api/hisStore/storeProduct'
 import { getStoreColumns } from '@/api/hisStore/store'
+import TestConfig from '@/views/system/config/testConfig.vue'
 
 export default {
   name: 'Config',
   components: {
+    TestConfig,
     companyMenuConfig,
     Material, productAttrValueSelect, productDeliveryGiftValueSelect,
     IntegralConfig,

+ 84 - 0
src/views/system/config/testConfig.vue

@@ -0,0 +1,84 @@
+<template>
+  <div class="app-container">
+    <el-form ref="form" :model="form" :rules="rules" label-width="180px" v-loading="loading">
+      <el-card class="box-card" style="margin-bottom: 20px;">
+        <div slot="header" class="clearfix">
+          <span>测一测AI配置</span>
+        </div>
+        <div class="text">
+          <el-form-item label="启用AI优化" prop="enable">
+            <el-switch v-model="form.enable" active-text="开启" inactive-text="关闭"></el-switch>
+          </el-form-item>
+          <el-form-item v-if="form.enable" label="FastGPT API地址" prop="url">
+            <el-input v-model="form.url" placeholder="请输入API地址"></el-input>
+          </el-form-item>
+          <el-form-item v-if="form.enable" label="AppKey" prop="appKey">
+            <el-input v-model="form.appKey" placeholder="请输入AppKey" show-password></el-input>
+          </el-form-item>
+        </div>
+      </el-card>
+
+      <div style="display: flex; justify-content: flex-end;">
+        <el-button type="primary" :disabled="saveLoading" :loading="saveLoading" @click="submitForm">提  交</el-button>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script>
+import { getConfigByKey, updateConfigByKey } from '@/api/system/config'
+
+export default {
+  name: 'testConfig',
+  data() {
+    return {
+      loading: true,
+      saveLoading: false,
+      rules: {
+        url: [
+          { required: true, message: '请输入FastGPT API地址', trigger: 'blur' }
+        ],
+        appKey: [
+          { required: true, message: '请输入AppKey', trigger: 'blur' }
+        ]
+      },
+      form: {
+        enable: false,
+        url: '',
+        appKey: '',
+      }
+    }
+  },
+  created() {
+    this.getConfigByKey("test.config");
+  },
+  methods: {
+    getConfigByKey(key){
+      getConfigByKey(key).then(response => {
+        this.configId = response.data.configId;
+        this.configKey = response.data.configKey;
+        this.form = JSON.parse(response.data.configValue);
+        this.loading = false
+      });
+    },
+    submitForm(){
+      this.$refs.form.validate(valid => {
+        if (!valid) return;
+        this.saveLoading = true
+        const param={configId:this.configId,configValue:JSON.stringify(this.form)}
+        updateConfigByKey(param).then(response => {
+        const {code} = response
+        if (code === 200) {
+          this.msgSuccess("修改成功");
+        }
+          this.saveLoading = false
+        }).catch(() => this.saveLoading = false);
+      });
+    },
+  },
+}
+</script>
+
+<style scoped>
+
+</style>