JinShan_uniapp/pages/chat/chatPage.vue
2025-07-22 16:32:04 +08:00

578 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="chat-container">
<!-- 聊天消息区域 -->
<scroll-view scroll-y :scroll-top="scrollTop" class="chat-messages" :scroll-with-animation="true"
@scroll="onScroll">
<view class="message-time" v-if="showDate">今天 {{ getTime() }}</view>
<!-- 消息列表 -->
<view v-for="(msg, index) in messages" :key="index"
:class="['message', msg.sender === 'me' ? 'sent' : 'received']">
<image v-if="msg.sender !== 'me'" :src="currentChat.avatar" class="avatar"></image>
<view :class="['message-bubble', msg.sender === 'me' ? 'me' : '']">
<text class="message-text" v-if="msg.content">{{ msg.content }}</text>
<!-- 对方正在输入提示 -->
<view class="typing-indicator" v-if="msg.isTyping">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
<text class="typing-text">对方正在输入...</text>
</view>
<!-- <text class="message-status">{{ msg.time }}</text> -->
</view>
<image v-if="msg.sender === 'me'" :src="myAvatar" class="avatar"></image>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input">
<view class="input-container">
<!-- <uni-icons type="plus" size="28" color="#666" class="action-icon"></uni-icons> -->
<input class="input-field" placeholder="输入消息..." v-model="inputMsg" @confirm="sendMessage"
@input="onInput" :focus="isFocus" />
<!-- <uni-icons type="mic" size="28" color="#666" class="action-icon"></uni-icons>
<uni-icons type="image" size="28" color="#666" class="action-icon"></uni-icons> -->
</view>
<button class="send-btn" :class="{active: inputMsg.trim() !== ''}" @click="sendMessage">
<uni-icons type="paperplane" size="24" color="#fff"></uni-icons>
</button>
</view>
</view>
</template>
<script>
import uniIcons from '@dcloudio/uni-ui/lib/uni-icons/uni-icons.vue'
import {IMAGE_BASE_URL,BASE_URL} from '@/utils/config';
export default {
components: {
uniIcons
},
data() {
return {
inputMsg: '',
scrollTop: 0,
isTyping: false,
isFocus: false,
showDate: true,
myAvatar: require('../../static/imgs/index/nav.png'),
currentChat: {
name: '张婷婷',
avatar: require('../../static/imgs/ai/chuandaguwen.png'),
status: '在线'
},
messages: [],
currentResponse: null, // 用于存储当前正在接收的响应
currentResponseId: null, // 用于标识当前响应
abortController: null, // 用于取消请求
isStreaming: false, // 标记是否正在接收流
userId: null,
serviceUrl:'',
apiKey:'',
aiId:null,
aiName:''
}
},
onLoad(options) {
this.serviceUrl=options.serviceUrl
this.apiKey=options.apiKey
this.aiId=options.id
this.currentChat.avatar=options.icon
uni.setNavigationBarTitle({
title: options.name
})
},
mounted() {
try {
const userInfo = wx.getStorageSync('userInfo')
if (userInfo === undefined || userInfo === null||userInfo=='') {
const defaultValue=this.generateMiniProgramUUID()
wx.setStorageSync('userId', defaultValue)
this.userId=defaultValue
this.myAvatar=require('../../static/imgs/index/nav.png')
}else{
this.userId=userInfo.id
this.myAvatar=userInfo.avatar||require('../../static/imgs/index/nav.png')
}
} catch (e) {
const defaultValue=this.generateMiniProgramUUID()
wx.setStorageSync('userId', defaultValue)
this.userId=defaultValue
}
},
methods: {
generateMiniProgramUUID() {
const buffer = new Uint8Array(16);
wx.getRandomValues(buffer);
buffer[6] = (buffer[6] & 0x0f) | 0x40; // Version 4
buffer[8] = (buffer[8] & 0x3f) | 0x80; // Variant 10
return Array.from(buffer)
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.replace(/(\w{8})(\w{4})(\w{4})(\w{4})(\w{12})/, '$1-$2-$3-$4-$5');
},
async sendMessage() {
if (!this.inputMsg.trim() || this.isStreaming) return;
// 添加用户消息
const userMsg = {
sender: 'me',
content: this.inputMsg,
time: this.getTime()
};
this.messages.push(userMsg);
const userInput = this.inputMsg;
this.inputMsg = '';
this.scrollToBottom();
// 创建AI回复的消息对象
const aiMsg = {
sender: 'other',
content: '',
time: this.getTime(),
isTyping: true,
id: Date.now() // 唯一标识符
};
this.messages.push(aiMsg);
this.isTyping = true;
this.isStreaming = true;
try {
// 创建AbortController以便取消请求
// this.abortController = new AbortController();
const response = await this.streamResponse(userInput, aiMsg.id);
console.log('Stream completed:', response);
} catch (error) {
console.error('Stream error:', error);
if (error.name !== 'AbortError') {
this.updateAiMessage(aiMsg.id, '\n\n抱歉请求过程中出现错误: ' + error.message);
}
} finally {
this.isTyping = false;
this.isStreaming = false;
// this.abortController = null;
}
},
async streamResponse(inputParams, messageId) {
return new Promise((resolve, reject) => {
// 使用uni.request进行流式请求
uni.request({
url: this.serviceUrl,
method: 'POST',
data: {
'query':inputParams,
"inputs": {
"content": inputParams
},
"response_mode": "blocking", //blockingstreaming
"user":this.userId
},
header: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
// signal: this.abortController?.signal, // 支持取消请求
// enableChunked: true, // 启用分块传输
success: (res) => {
// console.log(res)
// 处理流式数据
if (res.data) {
console.log(res.data)
console.log(res.data.data)
try {
let result=null
if(this.aiId==3){
const result = res.data.data.outputs.text;
// 提取实际内容(移除<think>标签)
const content = result.replace(/<think>[\s\S]*?<\/think>\n?/g, '');
this.updateAiMessage(messageId, content);
this.isStreaming = false
this.isTyping = false;
}else{
const result = res.data.answer;
// 提取实际内容(移除<think>标签)
const content = result.replace(/<think>[\s\S]*?<\/think>\n?/g, '');
this.updateAiMessage(messageId, content);
this.isStreaming = false
this.isTyping = false;
}
} catch (e) {
console.error('Parse error:', e);
}
}
},
fail: (err) => {
reject(new Error(err.errMsg));
},
complete: () => {
// 请求完成
}
});
});
},
// 更新消息内容
async updateAiMessage(id, newContent) {
const msg = this.messages.find(m => m.id === id);
if (msg) {
// 格式化JSON内容为更易读的形式
if (newContent.includes('{') && newContent.includes('}')) {
try {
const jsonObj = JSON.parse(newContent);
msg.content = await this.formatJsonToReadableText(jsonObj);
msg.isTyping = false
} catch (e) {
// 如果不是完整JSON直接显示
msg.content = newContent;
}
} else {
msg.content = newContent;
msg.isTyping = false
}
this.scrollToBottom();
}
},
// 将JSON格式化为易读的文本
formatJsonToReadableText(jsonObj) {
let result = '';
// 处理穿搭部分
if (jsonObj.outfit) {
result += '👕 穿搭建议:\n';
result += `上衣: ${jsonObj.outfit.top.style} (${jsonObj.outfit.top.color})\n`;
result += `下装: ${jsonObj.outfit.bottom.style} (${jsonObj.outfit.bottom.color})\n`;
result += `鞋子: ${jsonObj.outfit.shoes.style} (${jsonObj.outfit.shoes.color})\n`;
if (jsonObj.outfit.accessories && jsonObj.outfit.accessories.length > 0) {
result += '配饰:\n';
jsonObj.outfit.accessories.forEach(acc => {
result += `- ${acc.type}: ${acc.style} (${acc.color})\n`;
});
}
}
// 处理化妆建议
if (jsonObj.makeup_suggestion) {
result += '\n💄 化妆建议:\n';
result += `风格: ${jsonObj.makeup_suggestion.style}\n`;
result += `描述: ${jsonObj.makeup_suggestion.description}\n`;
if (jsonObj.makeup_suggestion.products && jsonObj.makeup_suggestion.products.length > 0) {
result += '推荐产品:\n';
jsonObj.makeup_suggestion.products.forEach(product => {
result += `- ${product.name}: ${product.detail_description}\n`;
});
}
}
// 处理造型建议
if (jsonObj.styling_tips) {
result += '\n💡 造型小贴士:\n';
result += `色彩搭配: ${jsonObj.styling_tips.color_coordination}\n`;
if (jsonObj.styling_tips.detail_enhancements && jsonObj.styling_tips.detail_enhancements.length > 0) {
result += '细节提升:\n';
jsonObj.styling_tips.detail_enhancements.forEach(tip => {
result += `- ${tip}\n`;
});
}
if (jsonObj.styling_tips.occasion_adaptation) {
result += `场合适配: ${jsonObj.styling_tips.occasion_adaptation}\n`;
}
}
return result;
},
// 取消请求
cancelStream() {
if (this.requestTask) {
this.requestTask.abort();
this.isTyping = false;
this.isStreaming = false;
this.requestTask = null;
}
},
receiveMessage() {
this.isTyping = true;
this.scrollToBottom();
setTimeout(() => {
const replies = [
"明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释明白了,谢谢您的解释",
"这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"请问价格方面是怎样的?这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"我稍后会仔细查看这些信息这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用",
"好的,我会考虑购买专业版这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用这个功能听起来很实用"
];
const newMsg = {
sender: 'other',
content: replies[Math.floor(Math.random() * replies.length)],
time: this.getTime()
};
this.messages.push(newMsg);
this.isTyping = false;
this.scrollToBottom();
}, 2000);
},
scrollToBottom() {
setTimeout(() => {
this.scrollTop = Math.floor(Math.random() * 1000) + 10000;
}, 100);
},
onScroll(e) {
// 可以在这里实现加载历史消息
},
onInput() {
// 模拟对方正在输入
if (this.inputMsg.trim() && !this.isTyping) {
this.isTyping = true;
setTimeout(() => {
this.isTyping = false;
}, 2000);
}
},
getTime() {
const now = new Date();
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
},
goBack() {
uni.navigateBack();
}
}
}
</script>
<style>
/* 通用样式 */
* {
box-sizing: border-box;
}
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f0f0f0;
}
/* 顶部导航栏 */
.chat-header {
background: linear-gradient(135deg, #3498db, #1a5276);
padding: 10px 15px;
color: white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
margin-left: 15px;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
}
.user-details {
margin-left: 10px;
display: flex;
flex-direction: column;
}
.user-name {
font-size: 18px;
font-weight: bold;
}
.user-status {
font-size: 12px;
opacity: 0.9;
}
/* 消息区域 */
.chat-messages {
flex: 1;
overflow-y: auto;
/* padding: 15px; */
background-color: rgb(229, 221, 213);
/* background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" opacity="0.05"><path d="M20,20 C30,15 40,25 50,20 C60,15 70,25 80,20" stroke="black" fill="none"/></svg>'); */
}
.message-time {
text-align: center;
color: #7f8c8d;
font-size: 12px;
margin: 10px 0;
}
.message {
display: flex;
margin-bottom: 15px;
}
.sent {
justify-content: flex-end;
}
.received {
justify-content: flex-start;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin: 0 8px;
/* align-self: flex-end; */
}
.message-bubble {
max-width: 70%;
background: white;
border-radius: 18px;
padding: 10px 15px;
position: relative;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.me {
background: #dcf8c6;
border-bottom-right-radius: 4px;
}
.message-text {
font-size: 16px;
line-height: 1.4;
}
.message-status {
font-size: 11px;
color: #7f8c8d;
text-align: right;
margin-top: 3px;
}
/* 输入区域 */
.chat-input {
background: #f0f0f0;
padding: 10px 15px;
display: flex;
align-items: center;
border-top: 1px solid #ddd;
}
.input-container {
flex: 1;
background: white;
border-radius: 25px;
display: flex;
align-items: center;
padding: 5px 15px;
margin-right: 10px;
}
.input-field {
flex: 1;
border: none;
outline: none;
padding: 8px 0;
font-size: 16px;
background: transparent;
}
.action-icon {
margin: 0 5px;
}
.send-btn {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #2196f3;
display: flex;
justify-content: center;
align-items: center;
border: none;
transition: all 0.2s;
}
.send-btn.active {
background-color: #3498db;
transform: scale(1.05);
}
/* 对方正在输入提示 */
.typing-indicator {
display: flex;
align-items: center;
margin: 10px 0 20px 10px;
opacity: 0.7;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #7f8c8d;
margin: 0 3px;
animation: typing 1.4s infinite ease-in-out;
}
.dot:nth-child(1) {
animation-delay: 0s;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
.typing-text {
font-size: 13px;
margin-left: 8px;
color: #555;
}
@keyframes typing {
0%,
60%,
100% {
transform: translateY(0);
}
30% {
transform: translateY(-5px);
}
}
</style>