dangdang пре 1 недеља
родитељ
комит
196f9ffa57
3 измењених фајлова са 180 додато и 25 уклоњено
  1. 98 10
      src/views/ChatTts.vue
  2. 67 15
      src/views/ResultView.vue
  3. 15 0
      src/views/utils/api.js

+ 98 - 10
src/views/ChatTts.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { ref, computed, onMounted, onBeforeUnmount } from "vue";
+import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from "vue";
 import BG from "./components/BG.vue";
 import BottomArea from "./components/BottomArea.vue";
 import ChatList from "./components/ChatList.vue";
@@ -9,7 +9,7 @@ import axios from "axios";
 import { fetchEventSource } from "@microsoft/fetch-event-source";
 import { useRoute, useRouter } from "vue-router";
 import delay from "delay";
-import { fetchTaskStatus } from "./utils/api";
+import { fetchTaskEnd, fetchTaskStatus } from "./utils/api";
 
 const route = useRoute();
 const router = useRouter();
@@ -19,6 +19,11 @@ const sessionId = ref("");
 const taskStatus = ref(false)
 const loading = ref(true);  //初始化进入的加载状态
 
+const lastMessageTime = ref(Date.now()); // 上次发送消息的时间
+let inactivityTimer = null;  // 定时器
+let warned = false; // 3 分钟提醒是否已触发
+let ended = false;  // 5 分钟结束是否已触发
+
 const currentRate = ref(0);
 const currentRound = computed(() => {
   const filteredMessages = chatList.value.filter(
@@ -27,6 +32,63 @@ const currentRound = computed(() => {
   );
   return Math.floor(filteredMessages.length / 2);
 });
+
+
+function startInactivityTimer() {
+  console.log("开始计时");
+  // clearInterval(inactivityTimer);
+  inactivityTimer = setInterval(async () => {
+    const diff = (Date.now() - lastMessageTime.value) / 1000; // 单位: 秒
+    console.log(diff,"开始计时1");
+    //diff >= 180 && diff < 300
+    if (diff >= 30 && diff < 60 && !warned) {
+      console.log("开始计时3");
+      // 超过 3 分钟没消息 -> 提示
+      console.log("超过3分钟但小于5分钟,发送提示消息");
+      const text = "还有事儿吗?没事儿就挂了~";
+      pushTimeoutMessage(text);
+      // const text = "还有事儿吗?没事儿就挂了~";
+      // updateChatList(text, false);
+      // queue.add(async () => await splitMessage(text));
+      warned = true;
+    }
+    // console.log("开始计时2");
+    if (diff >= 60 && !ended) {
+      ended = true;               // 🔑 立即标记已结束
+      clearInterval(inactivityTimer); // 🔑 停止定时器
+      inactivityTimer = null;
+      try {
+        const res = await fetchTaskEnd(conversationId.value); // 🔑 只调用一次
+        if (res.code === 200) {
+          taskStatus.value = true;
+          console.log("[Timer] 会话结束接口调用成功");
+        }
+      } catch (err) {
+        console.error("[Timer] 会话结束接口出错:", err);
+      }
+      // const res = await fetchTaskEnd(conversationId.value);
+      // if (res.code === 200) {
+      //   taskStatus.value = true
+      // }
+      // ended = true;
+      // clearInterval(inactivityTimer);
+    }
+  }, 1000);
+}
+
+function resetInactivityTimer() {
+  // 停掉旧的
+  if (inactivityTimer) {
+    clearInterval(inactivityTimer);
+    inactivityTimer = null;
+  }
+  // 重新开始
+  warned = false;
+  ended = false;
+  lastMessageTime.value = Date.now();
+  startInactivityTimer();
+}
+
 // 当前进度百分比整数部分
 const rate = computed(() => {
   return Math.floor((currentRound.value / round.value) * 100);
@@ -57,7 +119,19 @@ const updateChatList = async (message = "", loading = true) => {
     }
   }, 50);
 };
+// 追加超时提示消息,不覆盖最后一条AI消息
+const pushTimeoutMessage = (text) => {
+  chatList.value.push({
+    id: Date.now(),
+    type: "gpt",      // 标识AI消息
+    text: text,
+    loading: false,   // 超时提示不需要流式显示
+    timestamp: new Date(),
+  });
 
+  localStorage.setItem("chatHistory", JSON.stringify(chatList.value));
+  queue.add(async () => await splitMessage(text)); // 可继续TTS播报
+};
 // 创建一个队列实例,设置并发数为 1
 const queue = new PQueue({ concurrency: 3 });
 const queue1 = new PQueue({ concurrency: 1 });
@@ -86,6 +160,7 @@ const splitMessage = async (str) => {
 const handleStartRecord = async () => {
   queue.clear();
   queue1.clear();
+  resetInactivityTimer(); 
 };
 
 const generateSessionId = () => {
@@ -118,6 +193,7 @@ const handleStopRecord = (message) => {
   ];
   sessionId.value = generateSessionId();
   chatGpt(message);
+  // lastMessageTime.value = Date.now(); // 用户发送消息,计时器重置
 };
 // let audioQueue = [];  // 音频队列,保存按请求顺序的音频数据
 // let playingQueue = []; // 用来记录当前正在播放的音频序号
@@ -348,6 +424,11 @@ const playAudio = (audioData) => {
 // };
 
 const chatGpt = async (userMessage) => {
+  // 重置状态
+  lastMessageTime.value = Date.now();
+  warned = false;
+  ended = false;
+
   const downloadUrl = "/openapi-prd/ai/intelligent-tutoring/task/dialogue";
   i = 0;
   queue2.add(() => delay(3000));
@@ -499,12 +580,17 @@ const handleTaskStatus = async () => {
 
 // 在组件挂载时调用
 onMounted(() => {
+   startInactivityTimer();
+  // timer = setInterval(checkTimeout, 1000); // 每秒检查一次
   loadChatHistory();
   setTimeout(() => {
     loading.value = false;
   }, 3000);
 });
 
+onUnmounted(() => {
+  clearInterval(inactivityTimer); // 组件卸载时清除定时器
+})
 </script>
 
 <template>
@@ -514,12 +600,12 @@ onMounted(() => {
       <van-circle :stroke-width="80" size="70px" v-model:current-rate="currentRate" :rate="rate" :text="text" />
     </div>
     <div class="tooltip">
-        <p>车辆信息:24年10月起租金额14万,租期36月,已还9期</p>
-        <p>提前结清:121,021.62元,预期利益损失:6,535.05元</p>
-        <p>全额打款:137,362.73元,两者差额:16,341.11元  </p>
-        <p>下一期租金:5,068.99元(本金3,421元,利息1,647.99元) </p>
-        <p>违约金规则:1-12期6%,13-36期5%(两段式)</p>
-        <p>租金减免测算:最高8,500元 </p>
+      <p>车辆信息:24年10月起租金额14万,租期36月,已还9期</p>
+      <p>提前结清:121,021.62元,预期利益损失:6,535.05元</p>
+      <p>全额打款:137,362.73元,两者差额:16,341.11元 </p>
+      <p>下一期租金:5,068.99元(本金3,421元,利息1,647.99元) </p>
+      <p>违约金规则:1-12期6%,13-36期5%(两段式)</p>
+      <p>租金减免测算:最高8,500元 </p>
     </div>
     <BG />
     <div class="loading" v-show="loading">
@@ -567,8 +653,10 @@ onMounted(() => {
   width: 85%;
   position: fixed;
   top: 150px;
-  left: 50%;       /* 左边到视口宽度的一半 */
-  transform: translateX(-50%); /* 再左移自身宽度的一半,实现居中 */
+  left: 50%;
+  /* 左边到视口宽度的一半 */
+  transform: translateX(-50%);
+  /* 再左移自身宽度的一半,实现居中 */
   z-index: 9999;
   font-size: 12px;
   padding: 8px 12px;

+ 67 - 15
src/views/ResultView.vue

@@ -11,7 +11,7 @@ import {
 import NavBarPage from "../components/NavBarPage.vue";
 import randarPage from "./result/randarPage.vue";
 import { useRoute } from "vue-router";
-import { fetchTaskScore } from "./utils/api";
+import { fetchScoreLevel, fetchTaskScore } from "./utils/api";
 import { showToast } from 'vant';
 
 const route = useRoute();
@@ -21,20 +21,31 @@ const data = ref(null);
 const scoresList = ref(null);
 const defectsList = ref(null);
 const taskId = computed(() => route.query.conversationId); // 会话ID "2c64eb8b69be432ca0bb9ae55bc78def"
-const randarlist = ref([0, 0, 0, 0, 0,0]);
+const randarlist = ref([0, 0, 0, 0, 0, 0]);
 const loading = ref(false);
+const grade = ref(null);
 
 const nameMapping = {
   settle_reason: "提前结清原因(10分)",
   speech_retention: "话术挽留客户(20分)",
   strategic_retention: "策略挽留客户(30分)",
   other_retentions: "其他挽留客户(10分)",
-  call_ratio:"通话比例(10分)",
-  persistence:"坚持力度(20分)",
+  call_ratio: "通话比例(10分)",
+  persistence: "坚持力度(20分)",
   penalty_points: "扣分项",
   // defects: "评分详解"
 };
 
+// 计算属性:根据评分等级返回对应的样式类
+const gradeClass = computed(() => {
+  if (!grade.value) return ''
+
+  // 假设不合格的等级是 '不合格' 或 'D',其他都是合格
+  const isUnqualified = grade.value === '不合格'
+
+  return isUnqualified ? 'badge-danger' : 'badge-warning'
+})
+
 const handleResult = async () => {
   let res;
   let shouldContinue = true; // 控制是否继续调用接口的标志
@@ -71,9 +82,10 @@ const handleResult = async () => {
       return acc;
     }, {});
     defectsList.value = res.body.defects;
-    console.log(defectsList.value, '000000000');
     const obj = extractScores(scoresList.value);
     randarlist.value = Object.values(obj);
+    // 获取评分等级
+    getScoreLevel();
   }
 };
 
@@ -89,6 +101,19 @@ const extractScores = (data) => {
   });
 };
 
+const getScoreLevel = async () => {
+  const data = {
+    score: score.value, // 假设 score 是一个数字
+    sceneType: "QIRONG_JIEQING_1",
+  }
+  const levelRes = await fetchScoreLevel(data);
+  if (levelRes.code === 200) {
+    grade.value = levelRes.body;
+  } else {
+    showToast(levelRes.message || "获取评分等级失败");
+  }
+}
+
 onMounted(() => {
   handleResult();
   window.addEventListener("popstate", function (event) {
@@ -109,7 +134,8 @@ onBeforeUnmount(() => {
     <NavBarPage title="考试" type="home" />
     <div style="height: 46px"></div>
     <div class="score">
-      <span>最终得分</span>
+      <span v-if="grade" :class="['badge', gradeClass]">{{ grade }}</span>
+      <span class="score-title">最终得分</span>
       <p class="score-num">
         <span style="margin-right: 0.2rem;">{{ score }}</span>分
       </p>
@@ -120,7 +146,7 @@ onBeforeUnmount(() => {
         <randarPage :randarList="randarlist" />
       </div>
       <div class="loading" v-show="loading">
-       <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
+        <van-loading color="#0094ff" size="26px" vertical>加载中...</van-loading>
       </div>
       <div class="standard">
         <div class="standard-title">
@@ -137,15 +163,15 @@ onBeforeUnmount(() => {
             <span>{{ item.reason }}</span>
           </p>
         </div>
-        <!-- <div class="standard-content" style="margin-top: 1.5rem;">
+        <div class="standard-content" style="margin-top: 1.5rem;">
           <div class="standard-title">
-          <img src="../assets/robot.jpg" alt="" />
-          <span>评分详解</span>
-        </div>
+            <img src="../assets/robot.jpg" alt="" />
+            <span>评分详解</span>
+          </div>
           <div class="standard-content" v-for="(item, index) in defectsList" :key="index">
             <div class="standard-subtitle">
-            <span class="pro">轮次{{item.rounds}}:</span>
-          </div>
+              <span class="pro">轮次{{ item.rounds }}:</span>
+            </div>
             <p>
               <span>原句:</span>
               <span>{{ item.original_sentence }}</span>
@@ -159,7 +185,7 @@ onBeforeUnmount(() => {
               <span>{{ item.example }}</span>
             </p>
           </div>
-        </div> -->
+        </div>
       </div>
 
     </div>
@@ -194,7 +220,7 @@ body {
   align-items: center;
   justify-content: center;
 
-  span {
+  .score-title {
     display: flex;
     width: 5.06rem;
     height: 2.88rem;
@@ -222,6 +248,32 @@ body {
       font-family: Times New Roman-regular;
     }
   }
+
+  .badge {
+    position: absolute;
+    /* 在 .score 内部悬浮 */
+    left: 0.5rem;
+    top: 0.5rem;
+    // background: #f5222d;
+    // color: #fff;
+    font-size: 1rem;
+    padding: 0.2rem 0.4rem;
+    border-radius: 0.3rem;
+    white-space: nowrap;
+  }
+
+  /* 不合格:红色 */
+  .badge-danger {
+    color: #fff;
+    background: #f5222d;
+
+  }
+
+  /* 合格:绿色 */
+  .badge-warning {
+      color: #fff;
+  background-color: #28a745;
+  }
 }
 
 .content {

+ 15 - 0
src/views/utils/api.js

@@ -23,3 +23,18 @@ export async function fetchTaskStatus(conversationId) {
     method:"get",
   });
 }
+
+// 结束任务
+export async function fetchTaskEnd(conversationId) {
+  return request(`/openapi-prd/ai/intelligent-tutoring/task/end?conversationId=${conversationId}`,{
+    method:"get",
+  });
+}
+
+// 获取评分等级
+export async function fetchScoreLevel(data) {
+  return request(`/openapi-prd/ai/intelligent-tutoring/task/scoreLevel`,{
+    method:"post",
+    data
+  });
+}