You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

459 lines
10 KiB

4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
4 weeks ago
  1. <template>
  2. <view class="main">
  3. <view class="top" :style="{ height: iSMT + 'px' }"></view>
  4. <!-- 头部导航 -->
  5. <view class="header">
  6. <view class="back-icon">
  7. <image @click="onBack" src="/static/customer-service-platform/cs-platform-back.png"
  8. class="header-icon-image"></image>
  9. </view>
  10. <view class="title">{{ headerTitle }}</view>
  11. <view class="notification-icon">
  12. <image src="/static/customer-service-platform/message.png" class="header-icon-image"></image>
  13. </view>
  14. </view>
  15. <scroll-view scroll-y class="content-container" :scroll-into-view="scrollIntoView">
  16. <view class="content-header">
  17. <view class="content-header-area">
  18. <view class="logo">
  19. <image mode="aspectFit" src="/static/customer-service-platform/ellipse-dc-img.png"></image>
  20. </view>
  21. <view class="greeting">
  22. <text class="greet-title">我能为你做点什么</text>
  23. <text class="greet-sub">DeepChart随时为您提供服务</text>
  24. </view>
  25. </view>
  26. </view>
  27. <!-- 卡片部分 -->
  28. <view class="card">
  29. <!-- 问题头部-->
  30. <view class="question-header">
  31. <view class="question-row">
  32. <image class="question-avatar" src="/static/customer-service-platform/robot-head.png"
  33. mode="aspectFill"></image>
  34. <view class="question-title">{{ questionTitle }}</view>
  35. </view>
  36. </view>
  37. <!-- 卡片内容区-->
  38. <view class="card-body">
  39. <image class="card-logo" src="/static/customer-service-platform/ellipse-dc-img.png"
  40. mode="aspectFit"></image>
  41. <view class="card-text">
  42. <rich-text :nodes="renderMarkdown(answerContent)"></rich-text>
  43. <view id="answer-end" style="width:1px;height:1px;"></view>
  44. <!-- <text class="card-paragraph">
  45. {{answerContent}}
  46. </text> -->
  47. </view>
  48. </view>
  49. </view>
  50. <view class="login-row" v-if="showLoginRegister">
  51. <button class="login-btn" @click="toLogin">登录</button>
  52. <button class="register-btn" @click="toRegistration">注册</button>
  53. </view>
  54. </scroll-view>
  55. </view>
  56. </template>
  57. <script>
  58. import {
  59. getAnswerIdApi,
  60. getAnswerStatusApi,
  61. getAnswerContentApi
  62. } from "../../api/customerServicePlatform/customerServicePlatform";
  63. import marked from "marked"; // 引入 marked 库
  64. export default {
  65. data() {
  66. return {
  67. headerTitle: '智能客服中台',
  68. iSMT: 0,
  69. questionTitle: '',
  70. answerContent: '正在思考...',
  71. showLoginRegister: false,
  72. // 轮询句柄
  73. pollInterval: null,
  74. pollTimeout: null,
  75. scrollIntoView: ''
  76. };
  77. },
  78. mounted() {
  79. this.iSMT = uni.getSystemInfoSync().statusBarHeight || 0;
  80. this.getAnswerContent()
  81. },
  82. onUnload() {
  83. clearInterval(this.pollInterval);
  84. clearTimeout(this.pollTimeout);
  85. clearInterval(this.interval);
  86. },
  87. onLoad(options) {
  88. if (options.question) {
  89. this.questionTitle = decodeURIComponent(options.question);
  90. if (this.questionTitle.includes("如何注册")) {
  91. this.showLoginRegister = true
  92. } else {
  93. this.showLoginRegister = false
  94. }
  95. }
  96. },
  97. methods: {
  98. scrollToBottom() {
  99. // 使用 $nextTick 保证 rich-text 的渲染/DOM 更新完成
  100. this.$nextTick(() => {
  101. // 先设置目标 id,scroll-view 会滚动使其可见
  102. this.scrollIntoView = 'answer-end';
  103. // 清空值以便下次再次触发
  104. setTimeout(() => {
  105. this.scrollIntoView = '';
  106. }, 100);
  107. });
  108. },
  109. renderMarkdown(content) {
  110. const renderer = new marked.Renderer();
  111. // renderer.heading = function (text, level) {
  112. // return `<p>${text}</p>`;
  113. // };
  114. // 设置 marked 选项
  115. marked.setOptions({
  116. renderer: renderer,
  117. highlight: null, // 如果需要代码高亮,可以设置适当的函数
  118. langPrefix: "language-",
  119. pedantic: false,
  120. gfm: true,
  121. breaks: false,
  122. sanitize: false,
  123. smartLists: true,
  124. smartypants: false,
  125. xhtml: false,
  126. });
  127. if (!content) return "";
  128. let renderedContent = marked.parse(content);
  129. renderedContent = renderedContent.replace(/\*/g, '');
  130. return renderedContent;
  131. },
  132. async getAnswerContent() {
  133. let conversationId = '';
  134. let chatId = '';
  135. //尝试获取本地缓存
  136. try {
  137. const cache = uni.getStorageSync('conversationId');
  138. const chatIdCache = uni.getStorageSync('chatId');
  139. if (cache) conversationId = cache;
  140. if (chatIdCache) chatId = chatIdCache
  141. } catch (e) {
  142. conversationId = '';
  143. }
  144. try {
  145. const res = await getAnswerIdApi({
  146. question: this.questionTitle,
  147. conversationId: conversationId,
  148. chatId: chatId
  149. })
  150. console.log(res)
  151. if (res.code == 200 && res.data.chatId) {
  152. const resConversationId = res.data.conversationId;
  153. const resChatId = res.data.chatId;
  154. uni.setStorageSync('conversationId', resConversationId);
  155. uni.setStorageSync('chatId', resChatId);
  156. let pollCount = 0;
  157. const maxPoll = 10;
  158. this.pollInterval && clearInterval(this.pollInterval);
  159. this.pollTimeout && clearTimeout(this.pollTimeout);
  160. // === 轮询函数 ===
  161. const checkStatus = async () => {
  162. pollCount++;
  163. const pollRes = await getAnswerStatusApi({
  164. conversationId: resConversationId,
  165. chatId: resChatId
  166. });
  167. console.log("pollRes =>", pollRes);
  168. const status = pollRes?.data;
  169. //轮询结束,获取回答
  170. if (status === "completed") {
  171. clearInterval(this.pollInterval);
  172. clearTimeout(this.pollTimeout);
  173. //获取最终答案
  174. const answerRes = await getAnswerContentApi({
  175. conversationId: resConversationId,
  176. chatId: resChatId
  177. });
  178. console.log("answerRes =>", answerRes);
  179. const answer = answerRes?.data;
  180. //打印机效果
  181. if (answer) {
  182. // 逐字输出
  183. let currentIndex = 0;
  184. const answerLength = answer.length;
  185. this.interval && clearInterval(this.interval);
  186. this.interval = setInterval(() => {
  187. this.answerContent = answer.slice(0, currentIndex);
  188. currentIndex++;
  189. this.scrollToBottom();
  190. if (currentIndex > answerLength) {
  191. clearInterval(this.interval);
  192. this.scrollToBottom();
  193. }
  194. }, Math.floor(Math.random() * (150 - 30 + 1)) + 30);
  195. } else {
  196. this.answerContent = "获取回答失败,请重试";
  197. }
  198. return;
  199. }
  200. //超过10秒就回答失败
  201. if (pollCount >= maxPoll) {
  202. clearInterval(this.pollInterval);
  203. clearTimeout(this.pollTimeout);
  204. this.answerContent = "获取回答失败,请重试";
  205. return;
  206. }
  207. };
  208. // 每 2 秒轮询
  209. this.pollInterval = setInterval(checkStatus, 2000);
  210. // 超时兜底 10s
  211. this.pollTimeout = setTimeout(() => {
  212. clearInterval(this.pollInterval);
  213. this.answerContent = "获取回答失败,请重试";
  214. }, 20000);
  215. } else {
  216. this.answerContent = '获取回答失败,请重试';
  217. }
  218. } catch {
  219. this.pollInterval && clearInterval(this.pollInterval);
  220. this.pollTimeout && clearTimeout(this.pollTimeout);
  221. this.interval && clearInterval(this.interval);
  222. this.answerContent = '获取回答失败,请重试';
  223. }
  224. },
  225. sleepTime() {
  226. const ms = Math.floor(Math.random() * (300 - 30 + 1)) + 30;
  227. return ms;
  228. },
  229. toRegistration() {
  230. uni.redirectTo({
  231. url: "/pages/start/Registration/Registration",
  232. });
  233. },
  234. toLogin() {
  235. uni.redirectTo({
  236. url: "/pages/start/login/login",
  237. });
  238. },
  239. onBack() {
  240. if (typeof uni !== 'undefined') uni.navigateBack();
  241. }
  242. }
  243. };
  244. </script>
  245. <style scoped>
  246. .main {
  247. display: flex;
  248. flex-direction: column;
  249. height: 100vh;
  250. background-color: #ffffff;
  251. }
  252. .header {
  253. display: flex;
  254. justify-content: space-between;
  255. align-items: center;
  256. padding: 20rpx 30rpx;
  257. background-color: #ffffff;
  258. }
  259. .title {
  260. color: #000000;
  261. text-align: center;
  262. font-size: 32rpx;
  263. font-weight: 400;
  264. }
  265. .back-icon,
  266. .notification-icon {
  267. width: 40rpx;
  268. display: flex;
  269. align-items: center;
  270. justify-content: center;
  271. }
  272. .header-icon-image {
  273. width: 40rpx;
  274. height: 40rpx;
  275. object-fit: contain;
  276. }
  277. .content-container {
  278. padding: 20rpx;
  279. width: 100%;
  280. box-sizing: border-box;
  281. overflow-x: hidden;
  282. }
  283. .content-header {
  284. display: flex;
  285. align-items: center;
  286. justify-content: center;
  287. gap: 24rpx;
  288. padding: 0 60rpx;
  289. width: 100%;
  290. box-sizing: border-box;
  291. height: 188rpx;
  292. }
  293. .content-header-area {
  294. display: flex;
  295. gap: 20rpx;
  296. }
  297. .logo {
  298. width: 120rpx;
  299. height: 120rpx;
  300. display: flex;
  301. align-items: center;
  302. justify-content: center;
  303. flex: 0 0 112rpx;
  304. }
  305. .greeting {
  306. display: flex;
  307. flex-direction: column;
  308. justify-content: center;
  309. flex: 1 1 auto;
  310. }
  311. .greet-title {
  312. color: #000;
  313. font-size: 40rpx;
  314. font-style: normal;
  315. font-weight: 500;
  316. line-height: normal;
  317. margin: 0;
  318. overflow: hidden;
  319. text-overflow: ellipsis;
  320. white-space: nowrap;
  321. }
  322. .greet-sub {
  323. color: #838383;
  324. font-size: 28rpx;
  325. font-style: normal;
  326. font-weight: 400;
  327. line-height: normal;
  328. margin-top: 12rpx;
  329. overflow: hidden;
  330. text-overflow: ellipsis;
  331. white-space: nowrap;
  332. }
  333. .card {
  334. width: 90%;
  335. margin: 0 auto 20rpx;
  336. padding: 28rpx;
  337. box-sizing: border-box;
  338. border-radius: 16rpx;
  339. border: 4rpx solid #FF7C99;
  340. background: #fff;
  341. }
  342. /* 问题头部 */
  343. .question-header {
  344. width: 100%;
  345. margin-bottom: 48rpx;
  346. }
  347. .question-row {
  348. display: flex;
  349. align-items: center;
  350. }
  351. .question-avatar {
  352. width: 52rpx;
  353. height: 52rpx;
  354. border-radius: 999rpx;
  355. margin-right: 20rpx;
  356. flex-shrink: 0;
  357. }
  358. .question-title {
  359. color: #000000;
  360. font-size: 34rpx;
  361. }
  362. /* 卡片内部布局 */
  363. .card-body {
  364. display: flex;
  365. gap: 20rpx;
  366. align-items: flex-start;
  367. }
  368. .card-logo {
  369. width: 52rpx;
  370. height: 52rpx;
  371. flex: 0 0 52rpx;
  372. border-radius: 8rpx;
  373. }
  374. .card-text {
  375. flex: 1 1 auto;
  376. }
  377. .card-paragraph {
  378. display: block;
  379. color: #000000;
  380. font-size: 28rpx;
  381. margin-bottom: 14rpx;
  382. font-style: normal;
  383. font-weight: 500;
  384. }
  385. .login-row {
  386. display: flex;
  387. justify-content: center;
  388. align-items: center;
  389. width: 100%;
  390. margin-top: 100rpx;
  391. }
  392. .login-btn {
  393. width: 260rpx;
  394. height: 100rpx;
  395. border-radius: 50rpx;
  396. background: #F3F3F3;
  397. color: #000000;
  398. display: flex;
  399. justify-content: center;
  400. align-items: center;
  401. font-size: 28rpx;
  402. margin-right: 20rpx;
  403. }
  404. .register-btn {
  405. width: 260rpx;
  406. height: 100rpx;
  407. border-radius: 60rpx;
  408. background: #000;
  409. color: #ffffff;
  410. display: flex;
  411. justify-content: center;
  412. align-items: center;
  413. font-size: 28rpx;
  414. }
  415. </style>