From bcfb90a4c76696344a2d26c958aaab5e4b98462a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=98=B1=E8=BE=BE?= Date: Wed, 11 Jun 2025 11:44:19 +0800 Subject: [PATCH] =?UTF-8?q?feat(router):=20=E6=96=B0=E5=A2=9E=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D=E5=8A=A9=E6=89=8B=E9=A1=B5=E9=9D=A2=E8=B7=AF=E7=94=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在路由配置中添加了 '/customer' 路由,对应客服助手页面 - 新增了客服助手页面的组件结构 - 重构了 AI智能助手页面,提取了聊天组件 - 优化了首页布局,增加了导航功能 --- src/router/generatedRouter/index.js | 12 +- src/views/AI/components/chat.vue | 506 ++++++++++++++++++++++++ src/views/AI/components/message.vue | 9 +- src/views/AI/components/sticky.vue | 11 + src/views/AI/components/treasureBox.vue | 1 + src/views/AI/index.vue | 475 ++-------------------- src/views/app/Home.vue | 101 ++++- src/views/customer/index.vue | 154 ++++++++ 8 files changed, 795 insertions(+), 474 deletions(-) create mode 100644 src/views/AI/components/chat.vue create mode 100644 src/views/customer/index.vue diff --git a/src/router/generatedRouter/index.js b/src/router/generatedRouter/index.js index 12371e7..4284d5e 100644 --- a/src/router/generatedRouter/index.js +++ b/src/router/generatedRouter/index.js @@ -4,7 +4,15 @@ export default [ name: 'chatPage', component: () => import('@/views/AI/index.vue'), meta: { - title: '智能助手' - } + title: '智能助手', + }, + }, + { + path: '/customer', + name: 'customer', + component: () => import('@/views/customer/index.vue'), + meta: { + title: '客服助手', + }, }, ] diff --git a/src/views/AI/components/chat.vue b/src/views/AI/components/chat.vue new file mode 100644 index 0000000..afa225c --- /dev/null +++ b/src/views/AI/components/chat.vue @@ -0,0 +1,506 @@ + + + + diff --git a/src/views/AI/components/message.vue b/src/views/AI/components/message.vue index 0a5752e..9050069 100644 --- a/src/views/AI/components/message.vue +++ b/src/views/AI/components/message.vue @@ -7,7 +7,7 @@
- + @@ -17,11 +17,12 @@

-

+

+
此内容由AI生成
@@ -127,8 +128,8 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); // 使用rgba定义颜色,透 p { background-color: $primary-color; color: #fff; - padding: 10px; border-radius: 5px; + padding: 10px; //max-width: 70%; //max-width: 80%; } @@ -146,6 +147,7 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); // 使用rgba定义颜色,透 max-width: calc(80% + 20px); display: flex; flex-wrap: wrap; + line-height: 20px; .bot-avatar { margin-left: 10px; @@ -169,6 +171,7 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); // 使用rgba定义颜色,透 p { background-color: #fff; + padding: 10px 10px 2px 10px; color: $primary-color; } diff --git a/src/views/AI/components/sticky.vue b/src/views/AI/components/sticky.vue index fd674f4..e2d70c4 100644 --- a/src/views/AI/components/sticky.vue +++ b/src/views/AI/components/sticky.vue @@ -97,6 +97,14 @@ export default { value: '1', // icon: 'more-o', }, + { + text: '关闭产品询问', + value: '2', + }, + { + text: '产品比对', + value: '3', + }, ], // } @@ -112,6 +120,9 @@ export default { }) this.$emit('update:messageList', this.messagesList) break + case '2': + this.$emit('setProductName', '') + break } this.value2 = '' diff --git a/src/views/AI/components/treasureBox.vue b/src/views/AI/components/treasureBox.vue index 7d2551e..4120d34 100644 --- a/src/views/AI/components/treasureBox.vue +++ b/src/views/AI/components/treasureBox.vue @@ -18,6 +18,7 @@ +
此内容由AI生成
diff --git a/src/views/AI/index.vue b/src/views/AI/index.vue index 2283556..27a4ea1 100644 --- a/src/views/AI/index.vue +++ b/src/views/AI/index.vue @@ -28,57 +28,16 @@ -
- -
- -
- - - -
- + @@ -88,7 +47,7 @@ import messageComponent from './components/message.vue' import SvgIcon from '@/components/svg-icon/index.vue' import HotProducts from '@/views/AI/components/HotProducts.vue' import sticky from '@/views/AI/components/sticky.vue' -import { chat, chatProduct, audioToText } from '@/api/generatedApi' +import chatMessage from '@/views/AI/components/chat.vue' export default { components: { @@ -96,85 +55,37 @@ export default { [Icon.name]: Icon, messageComponent, HotProducts, + chatMessage, sticky, }, data() { return { hotList: [], productName: '', - answerMap: '', - timer: null, - answerIndex: 0, conversationId: '', - currentMessage: null, messageStatus: 'stop', - isThink: false, - newMessage: '', - messages: [], + isThink: null, + messages: [ + { + type: 'bot', + text: '欢迎使用AI智能助手,请输入问题开始对话或输入关键词 "xxx工具箱","产品对比"。', + }, + ], isSearching: false, isDeep: false, autoScrollEnabled: true, scrollPosition: 0, - // 录音相关状态 - isRecording: false, - mediaRecorder: null, - audioChunks: [], - isRecognizing: false, - // 语音模式开关 - isVoiceMode: false, } }, methods: { + getIsThink(e) { + this.isThink = e + }, getHotProducts(e) { console.log(e) this.hotList = e }, - deepInternet() { - this.isDeep = !this.isDeep - }, - searchInternet() { - this.isSearching = !this.isSearching - }, - startNewConversation() { - this.messages = [] - this.conversationId = '' - this.productName = '' - }, - hasTreasureBox() { - chatProduct({ query: this.newMessage }) - .then((res) => { - if (res) { - this.messageStatus = 'stop' - this.messages.push({ type: 'box', text: this.newMessage, detail: res.content }) - this.newMessage = '' - } - }) - .catch(() => { - this.messageStatus = 'stop' - }) - }, - sendMessage() { - if (this.messageStatus === 'send') return - if (this.newMessage.trim() === '') return - this.messages.push({ type: 'user', text: this.newMessage }) - this.messageStatus = 'send' - if (this.newMessage.includes('工具箱')) { - this.hasTreasureBox() - return - } - this.autoScrollEnabled = true - this.axiosGetAiChat() - }, - throttle(fn, delay = 50) { - let lastCall = 0 - return (...args) => { - const now = Date.now() - if (now - lastCall >= delay) { - lastCall = now - fn.apply(this, args) - } - } - }, + scrollToTop() { const messageArea = this.$refs.messageArea if (messageArea) { @@ -201,166 +112,6 @@ export default { this.autoScrollEnabled = isAtBottom this.scrollPosition = messageArea.scrollTop }, - async startRecording() { - if (this.messageStatus === 'send') return - if (this.isRecognizing) return - try { - const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) - this.mediaRecorder = new MediaRecorder(stream) - this.audioChunks = [] - this.mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - this.audioChunks.push(event.data) - } - } - this.mediaRecorder.onstop = () => { - this.handleStopRecording() - } - this.mediaRecorder.start() - this.isRecording = true - } catch (err) { - alert('无法访问麦克风,请检查权限') - console.error(err) - } - }, - stopRecording() { - if (this.mediaRecorder && this.isRecording) { - this.mediaRecorder.stop() - this.isRecording = false - } - }, - async handleStopRecording() { - this.isRecognizing = true - const blob = new Blob(this.audioChunks, { type: 'audio/webm' }) - try { - const text = await this.callVoiceRecognitionAPI(blob) - if (text) { - this.newMessage = text - this.messageStatus = 'stop' - // this.sendMessage() - } - } catch (error) { - console.error('语音识别失败:', error) - this.newMessage = '' - } finally { - this.isRecognizing = false - } - }, - callVoiceRecognitionAPI(blob) { - return new Promise((resolve, reject) => { - const formData = new FormData() - formData.append('file', blob) - formData.append('appType', 'haslBigHelper') - formData.append('user', 'chenyuda') - audioToText(formData) - .then((res) => { - if (res) { - resolve(res.content) - } - }) - .catch((err) => { - reject(err) - }) - }) - }, - axiosGetAiChat() { - const abortController = new AbortController() - this.currentMessage = JSON.parse( - JSON.stringify({ - type: 'bot', - text: '', - isThink: this.isDeep, - showThink: true, - think: '', - isLike: false, - isDisLike: false, - }), - ) - this.messages.push(this.currentMessage) - const params = { - query: this.newMessage, - isDeep: this.isDeep ? 1 : 0, - isOnline: this.isSearching ? 1 : 0, - user: 'chenyuda', - conversationId: this.conversationId, - productName: this.productName, - } - fetch(chat(), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - signal: abortController.signal, - body: JSON.stringify(params), - }) - .then(async (res) => { - this.newMessage = '' - await this.processStreamResponse(res) - }) - .catch((err) => { - this.messageStatus = 'stop' - }) - }, - async processStreamResponse(response) { - if (!response.ok) throw new Error(`HTTP错误: ${response.status}`) - if (!response.body) { - console.error('响应体不存在:', response) - return - } - const reader = response.body.getReader() - let buffer = '' - while (true) { - try { - const { done, value } = await reader.read() - if (done) break - buffer += new TextDecoder().decode(value) - const lines = buffer.split('\n') - lines.slice(0, -1).forEach((line) => { - const parsed = this.parseStreamLine(line) - if (parsed) this.updateMessageContent(parsed) - }) - buffer = lines[lines.length - 1] || '' - } catch (error) { - console.error('读取流数据时发生错误:', error) - break - } - } - }, - parseStreamLine(line) { - try { - const cleanLine = line.replace(/^data:\s*/, '') - if (!cleanLine) return null - const data = JSON.parse(cleanLine) - // console.log(data) - if (data.answer) { - this.answerMap += data.answer - } - this.updateConversationState(data) - return data - } catch (error) { - console.error('流数据解析失败:', error) - return null - } - }, - updateConversationState(data) { - this.conversationId = data.conversation_id || conversationId.value - if (data.answer && data.answer.indexOf('') !== -1) { - this.isThink = true - } - - if (data.answer && data.answer.indexOf('') !== -1) { - this.isThink = false - } - }, - updateMessageContent({ answer, event }) { - if (event === 'message_end') { - this.messageStatus = 'stop' - } - - // console.log(answer) - // console.log(this.currentMessage) - if (!this.currentMessage || !answer) return - const mode = this.isThink ? 'think' : 'text' - this.currentMessage[mode] += answer - }, }, watch: { messages: { @@ -382,10 +133,10 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); display: flex; flex-direction: column; height: 100vh; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + //-webkit-user-select: none; + //-moz-user-select: none; + //-ms-user-select: none; + //user-select: none; .chat-main { flex: 1; @@ -410,177 +161,5 @@ $primary-trans-color: rgba(135, 162, 208, 0.5); } } } - .isVoiceModeText { - display: flex; - textarea { - flex: 1; - max-height: 80px; - resize: none; - background: #fff; - outline: none; - overflow-y: auto; - padding: 10px; - border: none; - } - } - .section { - font-size: 13px; - display: flex; - align-items: center; - padding: 10px 10px 0 10px; - background-color: #fff; - gap: 10px; - - button { - padding: 4px 8px; - border: none; - background-color: $primary-trans-color; - color: #fff; - font-weight: 600; - border-radius: 20px; - cursor: pointer; - } - - .active { - background-color: $primary-color; - color: #fff; - } - } - - .chat-footer { - display: flex; - align-items: center; - padding: 10px 10px 20px 10px; - //padding-bottom: constant(safe-area-inset-bottom); - //padding-bottom: env(safe-area-inset-bottom); - background-color: #fff; - - .input-wrapper { - flex: 1; - margin-right: 10px; - - input { - width: 100%; - padding: 10px; - border: none; - background: #f5f5f5; - border-radius: 5px; - font-size: 14px; - } - - .voice-hint-container { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 10px; - background-color: #f5f5f5; - border-radius: 5px; - color: #888; - font-size: 14px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - &:active { - background-color: #eaeaea; - } - - .waveform { - display: flex; - align-items: flex-end; - align-items: center; - //height: 30px; - //width: 60px; - gap: 2px; - &.disabled { - cursor: not-allowed; - background: red; - } - &.active .bar { - animation: wave-animation 1s infinite ease-in-out; - } - - .bar { - width: 4px; - background-color: $primary-color; - margin: 0 1px; - - &:nth-child(1) { - height: 10px; - animation-delay: 0s; - } - &:nth-child(2) { - height: 16px; - animation-delay: 0.1s; - } - &:nth-child(3) { - height: 12px; - animation-delay: 0.2s; - } - &:nth-child(4) { - height: 18px; - animation-delay: 0.3s; - } - &:nth-child(5) { - height: 14px; - animation-delay: 0.4s; - } - } - } - - .hint-text { - margin-top: 4px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - } - } - } - - .mic-button { - display: flex; - align-items: center; - justify-content: center; - flex-direction: column; - border: none; - background: transparent; - color: $primary-color; - cursor: pointer; - font-size: 12px; - margin-left: 5px; - - &:active { - color: #e6454a; /* 按下变红 */ - } - } - - button { - border: none; - outline: none; - color: $primary-text-color; - border-radius: 5px; - background: transparent; - font-weight: 600; - cursor: pointer; - } - - .disabled { - pointer-events: none; - opacity: 0.5; - } - } -} - -@keyframes wave-animation { - 0%, - 100% { - transform: scaleY(1); - } - 50% { - transform: scaleY(1.5); - } } diff --git a/src/views/app/Home.vue b/src/views/app/Home.vue index f4bbcbc..28edba2 100644 --- a/src/views/app/Home.vue +++ b/src/views/app/Home.vue @@ -1,38 +1,97 @@ - - - + - +.home-container { + padding: 10px; +} +.my-swipe { + background: $primary-color; + border-radius: 5px; + .item { + display: flex; + justify-content: space-around; + align-items: center; + justify-items: center; + text-align: center; + div { + flex: 1; + padding: 10px 5px; + text-align: center; + .icon-contact { + padding: 5px; + border-radius: 50%; + overflow: hidden; + margin: 0 auto; + background: $primary-trans-color; + text-align: center; + width: 35px; + height: 35px; + display: flex; + //justify-items: center; + align-items: center; + justify-items: center; + .icon { + flex: 1; + width: 25px; + height: 25px; + } + } + .nav-title { + font-size: 12px; + font-weight: 600; + color: #fff; + } + } + } +} + diff --git a/src/views/customer/index.vue b/src/views/customer/index.vue new file mode 100644 index 0000000..36b051b --- /dev/null +++ b/src/views/customer/index.vue @@ -0,0 +1,154 @@ + + + + +