2025-10-27 23:13:08 +08:00

851 lines
20 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">
<!-- 顶部导航栏 -->
<view class="top_bar flex w-full" v-if="isMobile">
<view class="chat-header">
<view class="header-left" @click="goBack">
<i class="fas fa-arrow-left"></i>
</view>
<view class="header-title">聊天</view>
<view class="header-right" @click="goToChatDetail">
<i class="fas fa-ellipsis-v"></i>
</view>
</view>
</view>
<!-- 浏览器环境下的导航栏 -->
<view class="chat-header" v-else>
<view class="header-left" @click="goBack">
<i class="fas fa-arrow-left"></i>
</view>
<view class="header-title">聊天</view>
<view class="header-right" @click="goToChatDetail">
<i class="fas fa-ellipsis-v"></i>
</view>
</view>
<!-- 聊天消息列表 -->
<scroll-view
class="message-list"
scroll-y
:scroll-top="scrollTop"
:scroll-into-view="scrollIntoView"
>
<block v-for="(message, index) in messages" :key="index">
<view
:id="'message-' + index"
:class="[
'message-item',
message.type === 'sent' ? 'sent' : 'received',
]"
>
<!-- 头像 -->
<view class="avatar" @click="goToUserDetail">
<image
:src="
message.type === 'sent'
? '/static/imgs/default_avatar.png'
: '/static/imgs/default_avatar.png'
"
mode="aspectFill"
></image>
</view>
<!-- 消息内容 -->
<view class="message-content">
<view class="message-bubble">
<!-- 文本消息 -->
<text class="message-text">{{
parseEmoji(message.content)
}}</text>
</view>
<!-- 消息时间 -->
<view class="message-time" v-if="message.time">
{{ formatTime(message.time) }}
</view>
</view>
</view>
</block>
</scroll-view>
<!-- 输入区域 -->
<view class="input-area">
<!-- 表情选择器 -->
<view class="emoji-picker-container" v-if="showEmojiPicker">
<EmojiPicker
:visible="showEmojiPicker"
@select="onEmojiSelect"
@close="showEmojiPicker = false"
/>
</view>
<!-- 输入工具栏 -->
<view class="input-toolbar">
<!-- 左侧切换按钮 -->
<view class="left-switch">
<view
class="switch-btn"
@click="switchInputMode"
:class="{ active: inputMode === 'voice' }"
>
<i
:class="
inputMode === 'voice' ? 'fas fa-keyboard' : 'fas fa-microphone'
"
></i>
</view>
</view>
<!-- 中间输入区域 -->
<view class="input-wrapper">
<!-- 语音模式按住说话按钮 -->
<view
v-if="inputMode === 'voice'"
class="voice-input"
:class="{ recording: isRecording }"
@touchstart="startVoiceRecord"
@touchend="endVoiceRecord"
@touchcancel="cancelVoiceRecord"
>
<text class="voice-text">{{
isRecording ? "松开结束" : "按住说话"
}}</text>
<view v-if="isRecording" class="recording-indicator">
<view class="recording-dot"></view>
<view class="recording-dot"></view>
<view class="recording-dot"></view>
</view>
</view>
<!-- 文本模式输入框 -->
<view v-else class="text-input-container">
<textarea
v-model="inputMessage"
placeholder="输入消息..."
class="message-input"
:style="{ height: inputHeight + 'rpx' }"
:maxlength="500"
@confirm="sendMessage"
@focus="onInputFocus"
@blur="onInputBlur"
@input="onInputChange"
@linechange="onLineChange"
/>
</view>
</view>
<!-- 右侧操作区 -->
<view class="right-actions">
<!-- 文本模式表情按钮 -->
<view
v-if="inputMode === 'text'"
class="emoji-btn"
@click="toggleEmojiPicker"
:class="{ active: showEmojiPicker }"
>
<i class="fas fa-smile"></i>
</view>
<!-- 文本模式发送按钮有内容时显示 -->
<view
v-if="inputMode === 'text' && inputMessage.trim()"
class="send-btn"
@click="sendMessage"
>
发送
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import EmojiPicker from "../../src/components/EmojiPicker.vue";
import { parseEmoji } from "../../src/utils/emojiParser.js";
export default {
components: {
EmojiPicker,
},
data() {
return {
messages: [
{
type: "received",
content: "欢迎使用聊天功能!有什么可以帮您?",
time: new Date(Date.now() - 3000 * 60 * 1000), // 30分钟前
},
{
type: "received",
content: "您可以咨询产品信息、下单流程等相关问题。",
time: new Date(Date.now() - 29 * 60 * 1000), // 29分钟前
},
{
type: "sent",
content: "请问你们有哪些热门产品?",
time: new Date(Date.now() - 28 * 60 * 1000), // 28分钟前
},
{
type: "received",
content: "我们的热门产品有A、B、C三款您想了解哪一款",
time: new Date(Date.now() - 27 * 60 * 1000), // 27分钟前
},
{
type: "received",
content: "点击发送按钮或按回车键发送消息哦~",
time: new Date(Date.now() - 26 * 60 * 1000), // 26分钟前
},
],
inputMessage: "",
showEmojiPicker: false,
inputMode: "text", // 'text' 或 'voice'
isRecording: false,
recordingTimer: null,
inputHeight: 80, // 输入框高度默认1行
scrollTop: 0, // 消息列表滚动位置
scrollIntoView: "", // 滚动到指定元素
};
},
mounted() {
// 确保输入框初始高度为1行
this.inputHeight = 80;
},
computed: {
// 从全局数据获取设备信息
isMobile() {
return getApp().globalData.isMobile;
}
},
watch: {
// 监听消息数组变化,自动滚动到最新消息
messages: {
handler(newMessages, oldMessages) {
if (newMessages.length > (oldMessages ? oldMessages.length : 0)) {
// 有新消息时延迟滚动确保DOM更新完成
this.$nextTick(() => {
setTimeout(() => {
this.scrollToBottom();
}, 200);
});
}
},
deep: true,
immediate: false,
},
},
methods: {
parseEmoji(text) {
// 使用工具函数解析emoji
return parseEmoji(text);
},
sendMessage() {
if (this.inputMessage.trim() !== "") {
// 添加用户发送的消息
const newMessage = {
type: "sent",
content: this.inputMessage,
time: new Date(),
};
this.messages.push(newMessage);
// 强制更新视图
this.$forceUpdate();
// 清空输入框并重置高度
const message = this.inputMessage;
this.inputMessage = "";
this.inputHeight = 80; // 重置为1行高度
this.showEmojiPicker = false;
// 立即滚动到最新消息
this.$nextTick(() => {
this.scrollToBottom();
});
// 模拟回复
setTimeout(() => {
this.simulateReply(message);
}, 1000);
}
},
simulateReply(message) {
// 简单的回复逻辑
let reply = "感谢你的消息!";
if (message.includes("你好")) {
reply = "你好!很高兴见到你!";
} else if (message.includes("产品")) {
reply = "我们的产品非常棒,你可以查看我们的官网了解更多信息。";
}
const replyMessage = {
type: "received",
content: reply,
time: new Date(),
};
this.messages.push(replyMessage);
// 强制更新视图
this.$forceUpdate();
// 立即滚动到最新消息
this.$nextTick(() => {
this.scrollToBottom();
});
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
},
onEmojiSelect(emoji) {
this.inputMessage += emoji.unicode;
},
onInputFocus() {
this.showEmojiPicker = false;
},
onInputBlur() {
// 输入框失焦时的处理
},
onInputChange(e) {
// 输入内容变化时的处理
this.inputMessage = e.detail.value;
// 根据内容长度估算行数并调整高度
this.adjustInputHeight();
},
onLineChange(e) {
// 行数变化时调整高度限制最大10行
const lineCount = e.detail.lineCount;
const minHeight = 80; // 1行高度
const lineHeight = 40; // 每行高度
const maxLines = 10; // 最大10行
let newHeight = minHeight + (lineCount - 1) * lineHeight;
newHeight = Math.min(
Math.max(newHeight, minHeight),
minHeight + (maxLines - 1) * lineHeight
);
this.inputHeight = newHeight;
},
adjustInputHeight() {
// 根据输入内容调整高度
const content = this.inputMessage;
if (!content.trim()) {
// 如果内容为空设置为1行高度
this.inputHeight = 80;
return;
}
const lines = content.split("\n").length;
const minHeight = 80;
const lineHeight = 40;
const maxLines = 10;
let newHeight = minHeight + (lines - 1) * lineHeight;
newHeight = Math.min(
Math.max(newHeight, minHeight),
minHeight + (maxLines - 1) * lineHeight
);
this.inputHeight = newHeight;
},
// 切换输入模式
switchInputMode() {
if (this.inputMode === "text") {
this.inputMode = "voice";
this.showEmojiPicker = false;
} else {
this.inputMode = "text";
}
},
// 切换更多选项
toggleMoreOptions() {
// 这里可以添加更多选项的弹窗
},
// 语音录制相关
startVoiceRecord() {
this.isRecording = true;
// 这里可以添加实际的录音逻辑
// 例如调用 uni.getRecorderManager()
},
endVoiceRecord() {
this.isRecording = false;
// 这里可以添加录音结束的处理逻辑
// 例如发送语音消息
},
cancelVoiceRecord() {
this.isRecording = false;
// 这里可以添加取消录音的处理逻辑
},
scrollToBottom() {
// 滚动到消息列表底部
// 方法1使用scroll-top
this.scrollTop = 99999;
// 方法2使用scroll-into-view滚动到最后一个消息
if (this.messages.length > 0) {
const lastIndex = this.messages.length - 1;
this.scrollIntoView = "message-" + lastIndex;
// 重置scrollIntoView以允许重复滚动
setTimeout(() => {
this.scrollIntoView = "";
}, 100);
}
},
/**
* 返回
*/
goBack() {
uni.navigateBack();
},
/**
* 跳转到聊天详情
*/
goToChatDetail() {
uni.navigateTo({
url: "/pages/message/chatdetail",
});
},
/**
* 跳转到用户详情
*/
goToUserDetail() {
uni.navigateTo({
url: "/pages/message/userdetail",
});
},
/**
* 格式化时间
*/
formatTime(time) {
const date = new Date(time);
const now = new Date();
// 如果是今天,只显示时间
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
});
}
// 如果是昨天,显示"昨天"
const yesterday = new Date(now);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === yesterday.toDateString()) {
return (
"昨天 " +
date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
})
);
}
// 其他情况显示完整日期
return (
date.toLocaleDateString("zh-CN") +
" " +
date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
})
);
},
},
};
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--background);
}
/* 移动设备顶部状态栏 */
.top_bar {
background: var(--gradient-primary);
box-shadow: var(--shadow-lg);
z-index: 9999;
position: fixed;
top: 0;
left: 0;
right: 0;
height: calc(var(--status-bar-height) + 88rpx);
display: flex;
align-items: flex-end;
padding-top: var(--status-bar-height);
box-sizing: border-box;
}
/* 支持安全区域的设备 */
@supports (padding: max(0px)) {
.top_bar {
height: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
padding-top: calc(var(--status-bar-height) + env(safe-area-inset-top));
}
.top_bar + .message-list {
margin-top: calc(var(--status-bar-height) + 88rpx + env(safe-area-inset-top));
}
}
/* 顶部导航栏 */
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
height: 88rpx;
background-color: var(--surface);
border-bottom: 1rpx solid var(--border);
padding: 0 20rpx;
box-sizing: border-box;
box-shadow: var(--shadow);
}
/* 移动设备下的导航栏样式 */
.top_bar .chat-header {
background: transparent;
border-bottom: none;
box-shadow: none;
width: 100%;
height: 88rpx;
padding: 0 20rpx;
box-sizing: border-box;
}
.top_bar .header-title {
color: var(--white);
font-weight: 600;
font-size: 36rpx;
}
.top_bar .header-left i,
.top_bar .header-right i {
color: var(--white);
font-size: 40rpx;
}
.header-left,
.header-right {
width: 80rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.header-right:active {
background-color: var(--surface-hover);
border-radius: 8rpx;
}
/* 移动设备下的按钮悬停效果 */
.top_bar .header-left:active,
.top_bar .header-right:active {
background-color: rgba(255, 255, 255, 0.2);
border-radius: 8rpx;
}
.header-title {
font-size: 32rpx;
font-weight: 600;
color: var(--title-color);
}
.iconfont {
font-family: "iconfont" !important;
font-size: 36rpx;
font-style: normal;
color: var(--text-secondary);
}
/* 消息列表 */
.message-list {
flex: 1;
padding: 20rpx;
overflow-y: auto;
background-color: var(--background);
}
/* 移动设备下为消息列表添加顶部间距 */
.top_bar + .message-list {
margin-top: calc(var(--status-bar-height) + 88rpx);
}
.message-item {
display: flex;
margin-bottom: 40rpx;
}
.message-item.sent {
flex-direction: row-reverse;
}
/* 头像 */
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 10rpx;
overflow: hidden;
flex-shrink: 0;
margin: 0 20rpx;
}
.avatar image {
width: 100%;
height: 100%;
}
/* 消息内容 */
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
.message-item.sent .message-content {
align-items: flex-end;
}
.message-item.received .message-content {
align-items: flex-start;
}
/* 消息气泡 */
.message-bubble {
position: relative;
padding: 20rpx;
border-radius: 12rpx;
word-wrap: break-word;
word-break: break-all;
font-size: 28rpx;
line-height: 1.4;
max-width: 100%;
}
.message-item.sent .message-bubble {
background: var(--gradient-primary);
color: var(--white);
border-radius: 12rpx 2rpx 12rpx 12rpx;
}
.message-item.received .message-bubble {
background-color: var(--surface);
color: var(--text-color);
border-radius: 2rpx 12rpx 12rpx 12rpx;
box-shadow: var(--shadow);
}
/* 消息时间 */
.message-time {
font-size: 20rpx;
color: var(--text-muted);
margin-top: 10rpx;
}
/* 输入区域 */
.input-area {
background-color: var(--surface);
border-top: 1rpx solid var(--border);
box-shadow: var(--shadow-md);
}
/* 表情选择器容器 */
.emoji-picker-container {
border-bottom: 1rpx solid var(--border-light);
}
/* 输入工具栏 */
.input-toolbar {
display: flex;
align-items: flex-end;
padding: 20rpx 16rpx;
gap: 16rpx;
min-height: 100rpx;
}
/* 左侧切换按钮 */
.left-switch {
display: flex;
align-items: center;
}
.switch-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: transparent;
transition: all 0.2s ease;
}
.switch-btn.active {
background: var(--gradient-primary);
}
.switch-btn i {
font-size: 40rpx;
color: var(--text-secondary);
}
.switch-btn.active i {
color: var(--white);
}
/* 中间输入区域 */
.input-wrapper {
flex: 1;
position: relative;
}
/* 文本输入容器 */
.text-input-container {
background-color: var(--surface);
border-radius: 8rpx;
border: 1rpx solid var(--border);
overflow: hidden;
box-shadow: var(--shadow);
}
.message-input {
width: 100%;
height: 80rpx; /* 固定默认高度为1行 */
min-height: 80rpx;
max-height: 440rpx; /* 10行高度80rpx + 9 * 40rpx = 440rpx */
padding: 20rpx 24rpx;
border: none;
font-size: 32rpx;
line-height: 1.4;
background-color: transparent;
box-sizing: border-box;
resize: none;
word-wrap: break-word;
word-break: break-all;
overflow-y: auto; /* 超出10行时显示滚动条 */
}
.message-input:focus {
outline: none;
}
/* 语音输入 */
.voice-input {
width: 100%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--gray-lighter);
border-radius: 8rpx;
transition: all 0.2s ease;
position: relative;
border: 1rpx solid var(--border);
}
.voice-input:active,
.voice-input.recording {
background-color: var(--gray);
}
.voice-text {
font-size: 32rpx;
color: var(--text-secondary);
}
.recording-indicator {
position: absolute;
right: 20rpx;
display: flex;
gap: 8rpx;
}
.recording-dot {
width: 12rpx;
height: 12rpx;
background-color: #ff4444;
border-radius: 50%;
animation: recordingPulse 1s infinite;
}
.recording-dot:nth-child(2) {
animation-delay: 0.2s;
}
.recording-dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes recordingPulse {
0%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1.2);
}
}
/* 右侧操作区 */
.right-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.emoji-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: transparent;
transition: all 0.2s ease;
}
.emoji-btn.active {
background: var(--gradient-primary);
}
.emoji-btn:active {
background-color: var(--surface-hover);
}
.emoji-btn i {
font-size: 40rpx;
color: var(--text-secondary);
}
.emoji-btn.active i {
color: var(--white);
}
.send-btn {
padding: 16rpx 32rpx;
background: var(--gradient-primary);
color: var(--white);
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
transition: all 0.2s ease;
min-width: 120rpx;
text-align: center;
box-shadow: var(--shadow);
}
.send-btn:active {
background: var(--primary-dark);
transform: scale(0.98);
}
</style>