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

527 lines
14 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<!-- <div class="onlineConsultation" @click="consultation">-->
<!-- <div class="btn">-->
<!-- <img :src="customerService" alt="" class="customerService">-->
<!-- <span class="text">在线咨询</span>-->
<!-- </div>-->
<!-- </div>-->
<div class="chat" v-if="isChat" ref="dragDiv">
<div class="topDrag" @mousedown="onMouseDown">
<span class="chatTitle">海威 AI 助手</span>
<el-select
v-model="selectedModel"
size="mini"
class="modelSelect"
placeholder="模型"
@mousedown.native.stop>
<el-option label="DeepSeek" value="deepseek"></el-option>
<el-option label="Ollama" value="ollama"></el-option>
</el-select>
<i class="el-icon-close close" @click="isChat = false"></i>
</div>
<div class="content1" ref="chatContainer">
<template v-for="i in chatInfo">
<div v-if="i.type===0" class="message">
<div class="userInfo">{{ i.userName }} {{ formatTime(i.time) }}</div>
<div v-if="i.html" class="info" @click="handleClick" v-html="i.content"></div>
<div v-else class="info plain">{{ i.content }}</div>
</div>
<div v-if="i.type===1" class="message1">
<div class="userInfo">{{ i.userName }} {{ formatTime(i.time) }}</div>
<div class="info plain">{{ i.content }}</div>
</div>
<div v-if="i.type===2" class="message">
<div class="userInfo">{{ i.userName }} {{ formatTime(i.time) }}</div>
<div class="info">
<div>
请选择问题类型
</div>
<el-button size="mini" @click="busy(1)">设备相关</el-button>
<el-button size="mini" @click="busy(2)">软件相关</el-button>
<el-button size="mini" @click="busy(3)">测试</el-button>
</div>
<div>
</div>
</div>
</template>
</div>
<div class="chatBox">
<el-input
type="textarea"
placeholder="请输入内容"
class="no-border-textarea"
@keydown.enter.native.exact.prevent="sendChat"
v-model="textarea"
:disabled="isLoading">
</el-input>
<el-button type="primary" size="mini" class="chatBtn" @click="sendChat" :disabled="isLoading">
</el-button>
</div>
</div>
<div class="viewImg" v-if="imgUrl" @click="imgUrl = ''">
<img :src="imgUrl" alt="" class="img"/>
</div>
</div>
</template>
<script>
import customerService from '@/assets/icon/customerService.png'
import { aiChat } from '@/api/aiChat'
export default {
name: "Chat",
data() {
return {
imgUrl: '',
customerService,
textarea: '',
isChat: false,
isLoading: false,
selectedModel: 'deepseek',
isDown: false,
offsetX: 0,
offsetY: 0,
chatInfo: [
{
type: 0,
content: '您好!我是海威 AI 助手,可以回答关于海威物联产品、方案和服务的问题。',
time: new Date().getTime(),
userName: '海威物联',
},
]
}
},
mounted() {
// listHwWebDocument()
// setTimeout(() => {
// this.isChat = true;
// this.$nextTick(() => {
// this.scrollToBottom()
// })
// }, 5 * 1000)
},
methods: {
consultation() {
this.isChat = true;
this.$nextTick(() => {
this.scrollToBottom()
})
},
onMouseDown(e) {
this.isDown = true;
const rect = this.$refs.dragDiv.getBoundingClientRect();
this.offsetX = e.clientX - rect.left;
this.offsetY = e.clientY - rect.top;
document.addEventListener("mousemove", this.onMouseMove);
document.addEventListener("mouseup", this.onMouseUp);
},
onMouseMove(e) {
if (!this.isDown) return;
const dragDiv = this.$refs.dragDiv;
// 鼠标相对窗口的 left/top
let left = e.clientX - this.offsetX;
let top = e.clientY - this.offsetY;
// ✅ 边界限制 (保证不出窗口)
if (left < 0) left = 0;
if (top < 0) top = 0;
if (left > window.innerWidth - dragDiv.offsetWidth) {
left = window.innerWidth - dragDiv.offsetWidth;
}
if (top > window.innerHeight - dragDiv.offsetHeight) {
top = window.innerHeight - dragDiv.offsetHeight;
}
// ✅ 转换为 right/bottom
let right = window.innerWidth - left - dragDiv.offsetWidth;
let bottom = window.innerHeight - top - dragDiv.offsetHeight;
dragDiv.style.right = right + "px";
dragDiv.style.bottom = bottom + "px";
},
onMouseUp() {
this.isDown = false;
document.removeEventListener("mousemove", this.onMouseMove);
document.removeEventListener("mouseup", this.onMouseUp);
},
scrollToBottom() {
const container = this.$refs.chatContainer;
container.scrollTop = container.scrollHeight;
},
formatTime(date) {
const now = new Date();
const input = new Date(date);
const diffTime = now - input;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
const isToday = now.toDateString() === input.toDateString();
const yesterday = new Date();
yesterday.setDate(now.getDate() - 1);
const isYesterday = yesterday.toDateString() === input.toDateString();
const padZero = num => String(num).padStart(2, '0');
const timeStr = `${padZero(input.getHours())}:${padZero(input.getMinutes())}:${padZero(input.getSeconds())}`;
if (isToday) {
return `今天 ${timeStr}`;
} else if (isYesterday) {
return `昨天 ${timeStr}`;
} else {
return `${diffDays}天前`;
}
},
async sendChat(e) {
if (e && e.isComposing) {
return;
}
const question = this.textarea.trim()
if (question === '' || this.isLoading) {
return
}
this.chatInfo.push({
type: 1,
content: question,
time: new Date().getTime(),
userName: '我',
})
this.textarea = ''
this.isLoading = true
const loadingIndex = this.chatInfo.push({
type: 0,
content: '正在检索官网知识库并生成回答...',
time: new Date().getTime(),
userName: '海威物联',
}) - 1
this.$nextTick(() => {
this.scrollToBottom()
})
try {
// AI 输出一律按纯文本展示,避免模型返回 HTML 时进入 v-html 渲染链路。
const res = await aiChat({
question,
model: this.selectedModel
})
if (res && res.code === 200 && res.data) {
this.chatInfo.splice(loadingIndex, 1, {
type: 0,
content: res.data.answer || '官网知识库暂未检索到相关信息。',
time: new Date().getTime(),
userName: '海威物联',
})
} else {
this.chatInfo.splice(loadingIndex, 1, {
type: 0,
content: (res && res.msg) || 'AI 服务暂时不可用,请稍后重试。',
time: new Date().getTime(),
userName: '海威物联',
})
}
} catch (err) {
this.chatInfo.splice(loadingIndex, 1, {
type: 0,
content: '网络连接异常,请稍后重试。',
time: new Date().getTime(),
userName: '海威物联',
})
} finally {
this.isLoading = false
this.$nextTick(() => {
this.scrollToBottom()
})
}
},
busy(e) {
if (e === 1) {
this.chatInfo.push({
type: 0,
content: '请稍后',
time: new Date().getTime(),
userName: '海威物联',
})
setTimeout(() => {
this.chatInfo.push({
type: 0,
content: `<text>目前咨询人数过多如有疑问请致电xxxx,或</text> <a href="javascript:void(0)" data-action="addWechat">添加微信</a>`,
html: true,
time: new Date().getTime(),
userName: '海威物联',
})
this.$nextTick(() => {
this.scrollToBottom()
})
}, 1000)
}
if (e === 2) {
this.chatInfo.push({
type: 0,
content: '请稍后',
time: new Date().getTime(),
userName: '海威物联',
})
setTimeout(() => {
this.chatInfo.push({
type: 0,
content: '目前咨询人数过多如有疑问请致电xxxx',
time: new Date().getTime(),
userName: '海威物联',
})
this.$nextTick(() => {
this.scrollToBottom()
})
}, 1000)
}
if (e === 3) {
window.location.href = "weixin://dl/add";
}
this.$nextTick(() => {
this.scrollToBottom()
})
},
handleClick(e) {
const action = e.target.dataset.action
if (action && typeof this[action] === 'function') {
this[action](e.target.dataset.parmas) // 调用 Vue methods 里的函数
}
},
copyToClipboard(text) {
if (!navigator.clipboard) {
console.warn('浏览器不支持 Clipboard API使用 fallback 方法')
this.fallbackCopy(text)
return
}
navigator.clipboard.writeText(text)
},
fallbackCopy(text) {
const input = document.createElement('textarea')
input.value = text
document.body.appendChild(input)
input.select()
try {
document.execCommand('copy')
} catch (err) {
}
document.body.removeChild(input)
},
addWechat() {
window.location.href = "weixin://dl/add";
this.copyToClipboard('Yeshenge_')
this.chatInfo.push({
type: 0,
content: '已复制微信号,请打开微信添加好友,或扫描下方二维码添加好友',
time: new Date().getTime(),
userName: '海威物联',
})
let img = require('@/assets/image/wechatQRCode.png')
this.chatInfo.push({
type: 0,
content: `<img data-action="viewImg" data-parmas='${JSON.stringify({img: img})}' src="${img}" alt="" style="width: 100px;">`,
html: true,
time: new Date().getTime(),
userName: '海威物联',
})
this.$nextTick(() => {
this.scrollToBottom()
})
},
viewImg(e) {
let data = JSON.parse(e)
console.log(data.img)
this.imgUrl = data.img
}
}
}
</script>
<style scoped lang="less">
.onlineConsultation {
position: fixed;
right: 5vw;
bottom: 0;
width: 150px;
height: 50px;
background-color: #41B5EA;
border-radius: 5px;
cursor: pointer;
z-index: 99;
.customerService {
position: absolute;
bottom: 0;
left: 0;
width: 70px;
}
.text {
position: absolute;
bottom: 0;
left: 70px;
width: 80px;
line-height: 50px;
font-size: 16px;
color: #fff;
}
}
.chat {
z-index: 999;
position: fixed;
bottom: 5vw;
right: 5vw;
width: 450px;
height: 450px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
background-color: #fff;
overflow: hidden;
box-shadow: -4px 4px 8px rgba(0, 0, 0, 0.1),
4px 4px 8px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.1);
.topDrag {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 50px;
background-color: #41B5EA;
cursor: move;
.chatTitle {
position: absolute;
top: 50%;
left: 16px;
transform: translateY(-50%);
color: #fff;
font-size: 15px;
font-weight: 600;
}
.modelSelect {
position: absolute;
top: 50%;
right: 54px;
width: 104px;
transform: translateY(-50%);
cursor: default;
}
.close {
position: absolute;
top: 50%;
right: 20px;
transform: translateY(-50%);
color: #fff;
font-size: 20px;
cursor: pointer;
}
}
.content1 {
position: absolute;
top: 50px;
height: 300px;
width: 100%;
overflow: auto;
.message {
width: 80%;
margin: 10px 0 10px 15px;
text-align: left;
.userInfo {
font-size: 12px;
color: #0008
}
.info {
max-width: 80%;
display: inline-block;
background-color: #EDEFF5;
padding: 5px;
border-radius: 5px;
border-top-left-radius: 0;
}
.plain {
white-space: pre-wrap;
line-height: 1.6;
}
}
.message1 {
width: calc(100% - 20px);
margin: 10px 0 10px 5px;
text-align: right;
overflow: hidden;
.userInfo {
font-size: 12px;
color: #0008
}
.info {
text-align: left;
max-width: 80%;
float: right;
background-color: #EDEFF5;
padding: 5px;
border-radius: 5px 0px 5px 5px;
}
.plain {
white-space: pre-wrap;
line-height: 1.6;
}
}
}
.chatBox {
position: absolute;
top: 350px;
height: 88px;
width: 100%;
margin-top: 12px;
border-top: 2px solid #2222;
.no-border-textarea /deep/ .el-textarea__inner {
border: none;
box-shadow: none;
resize: none;
outline: none;
}
.chatBtn {
position: absolute;
bottom: 5px;
right: 10px;
}
}
}
.viewImg {
z-index: 9999;
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, .2);
.img {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40vw;
}
}
</style>