| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654 |
- <template>
- <view class="chat-container">
- <!-- 聊天消息列表 -->
- <scroll-view
- class="message-list"
- scroll-y
- :scroll-top="scrollTop"
- :scroll-with-animation="true"
- @scroll="onScroll"
- >
- <view class="message-wrapper">
- <!-- 日期分隔符 -->
- <view class="date-divider" v-if="messageList.length > 0">
- <text class="date-text">{{ getCurrentDate() }}</text>
- </view>
- <!-- 消息列表 -->
- <view
- class="message-item"
- :class="{ 'message-right': message.type === 'user', 'message-left': message.type === 'expert' }"
- v-for="(message, index) in messageList"
- :key="index"
- >
- <!-- 专家消息(左侧) -->
- <template v-if="message.type === 'expert'">
- <view class="avatar-container">
- <image class="message-avatar" :src="expertInfo.avatar" mode="aspectFill"></image>
- </view>
- <view class="message-content-wrapper">
- <view class="message-bubble expert-bubble">
- <!-- 文本消息 -->
- <text class="message-text" v-if="message.contentType === 'text'">{{ message.content }}</text>
-
- <!-- 图片消息 -->
- <image
- v-if="message.contentType === 'image'"
- class="message-image"
- :src="message.content"
- mode="aspectFit"
- @click="previewImage(message.content)"
- ></image>
- </view>
- <view class="message-time">{{ formatTime(message.timestamp) }}</view>
- </view>
- </template>
- <!-- 用户消息(右侧) -->
- <template v-else>
- <view class="message-content-wrapper user-content">
- <view class="message-bubble user-bubble">
- <!-- 文本消息 -->
- <text class="message-text" v-if="message.contentType === 'text'">{{ message.content }}</text>
-
- <!-- 图片消息 -->
- <image
- v-if="message.contentType === 'image'"
- class="message-image"
- :src="message.content"
- mode="aspectFit"
- @click="previewImage(message.content)"
- ></image>
- </view>
- <view class="message-time user-time">{{ formatTime(message.timestamp) }}</view>
- </view>
- <view class="avatar-container">
- <image class="message-avatar" src="/static/icons/user icon.png" mode="aspectFill"></image>
- </view>
- </template>
- </view>
- <!-- 打字中状态 -->
- <view class="typing-indicator" v-if="isTyping">
- <view class="avatar-container">
- <image class="message-avatar" :src="expertInfo.avatar" mode="aspectFill"></image>
- </view>
- <view class="typing-bubble">
- <view class="typing-dots">
- <view class="dot"></view>
- <view class="dot"></view>
- <view class="dot"></view>
- </view>
- </view>
- </view>
- </view>
- </scroll-view>
- <!-- 输入区域 -->
- <view class="input-area">
- <!-- 图片预览 -->
- <view class="image-preview" v-if="selectedImages.length > 0">
- <view class="preview-list">
- <view
- class="preview-item"
- v-for="(image, index) in selectedImages"
- :key="index"
- >
- <image class="preview-image" :src="image" mode="aspectFill"></image>
- <view class="remove-btn" @click="removeImage(index)">
- <text class="remove-icon">×</text>
- </view>
- </view>
- </view>
- </view>
- <!-- 输入工具栏 -->
- <view class="input-toolbar">
- <!-- 添加图片按钮 -->
- <view class="tool-btn" @click="chooseImage">
- <image class="tool-icon" src="/static/icons/camera.png" mode="aspectFit"></image>
- </view>
- <!-- 输入框 -->
- <view class="input-wrapper">
- <textarea
- class="message-input"
- v-model="inputMessage"
- placeholder="请输入您的问题..."
- placeholder-style="color: #999;"
- :auto-height="true"
- :maxlength="500"
- @focus="onInputFocus"
- @blur="onInputBlur"
- ></textarea>
- </view>
- <!-- 发送按钮 -->
- <view
- class="send-btn"
- :class="{ active: canSend }"
- @click="sendMessage"
- >
- <text class="send-text">发送</text>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script>
- export default {
- data() {
- return {
- expertId: '',
- expertName: '',
- inputMessage: '',
- selectedImages: [],
- scrollTop: 0,
- isTyping: false,
-
- expertInfo: {
- id: '',
- name: '',
- avatar: '/static/icons/professor.png'
- },
-
- // 消息列表
- messageList: [
- {
- id: 1,
- type: 'expert',
- contentType: 'text',
- content: '您好!我是张教授,很高兴为您提供农业技术咨询服务。请问您遇到了什么问题?',
- timestamp: Date.now() - 3600000
- },
- {
- id: 2,
- type: 'user',
- contentType: 'text',
- content: '教授您好,我想咨询一下水稻种植的相关问题',
- timestamp: Date.now() - 3500000
- },
- {
- id: 3,
- type: 'expert',
- contentType: 'text',
- content: '好的,请您详细描述一下具体是什么问题?比如是关于播种、施肥、病虫害防治还是其他方面的?',
- timestamp: Date.now() - 3400000
- }
- ]
- }
- },
-
- computed: {
- canSend() {
- return this.inputMessage.trim().length > 0 || this.selectedImages.length > 0;
- }
- },
-
- onLoad(options) {
- if (options.expertId) {
- this.expertId = options.expertId;
- this.expertName = decodeURIComponent(options.expertName || '专家');
- this.loadExpertInfo();
- }
-
- // 设置页面标题
- uni.setNavigationBarTitle({
- title: this.expertName
- });
-
- // 滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- },
-
- methods: {
- // 加载专家信息
- loadExpertInfo() {
- this.expertInfo = {
- id: this.expertId,
- name: this.expertName,
- avatar: '/static/icons/professor.png'
- };
- },
-
- // 发送消息
- sendMessage() {
- if (!this.canSend) return;
-
- // 发送文本消息
- if (this.inputMessage.trim()) {
- const textMessage = {
- id: Date.now(),
- type: 'user',
- contentType: 'text',
- content: this.inputMessage.trim(),
- timestamp: Date.now()
- };
- this.messageList.push(textMessage);
- this.inputMessage = '';
- }
-
- // 发送图片消息
- if (this.selectedImages.length > 0) {
- this.selectedImages.forEach(image => {
- const imageMessage = {
- id: Date.now() + Math.random(),
- type: 'user',
- contentType: 'image',
- content: image,
- timestamp: Date.now()
- };
- this.messageList.push(imageMessage);
- });
- this.selectedImages = [];
- }
-
- // 滚动到底部
- this.$nextTick(() => {
- this.scrollToBottom();
- });
-
- // 模拟专家回复
- this.simulateExpertReply();
- },
-
- // 模拟专家回复
- simulateExpertReply() {
- this.isTyping = true;
-
- setTimeout(() => {
- this.isTyping = false;
-
- const replies = [
- '根据您的描述,我建议您可以从以下几个方面来处理...',
- '这个问题比较常见,通常是由于以下原因造成的...',
- '从您提供的信息来看,建议您先检查一下土壤情况...',
- '我理解您的担心,这种情况下最好的解决方案是...'
- ];
-
- const randomReply = replies[Math.floor(Math.random() * replies.length)];
-
- const expertMessage = {
- id: Date.now(),
- type: 'expert',
- contentType: 'text',
- content: randomReply,
- timestamp: Date.now()
- };
-
- this.messageList.push(expertMessage);
-
- this.$nextTick(() => {
- this.scrollToBottom();
- });
- }, 2000);
- },
-
- // 选择图片
- chooseImage() {
- const maxImages = 3 - this.selectedImages.length;
- if (maxImages <= 0) {
- uni.showToast({
- title: '最多可选择3张图片',
- icon: 'none'
- });
- return;
- }
-
- uni.chooseImage({
- count: maxImages,
- sizeType: ['compressed'],
- sourceType: ['album', 'camera'],
- success: (res) => {
- this.selectedImages = this.selectedImages.concat(res.tempFilePaths);
- }
- });
- },
-
- // 移除图片
- removeImage(index) {
- this.selectedImages.splice(index, 1);
- },
-
- // 预览图片
- previewImage(current) {
- // 收集所有图片消息的URL
- const imageUrls = this.messageList
- .filter(msg => msg.contentType === 'image')
- .map(msg => msg.content);
-
- uni.previewImage({
- current: current,
- urls: imageUrls
- });
- },
-
- // 滚动到底部
- scrollToBottom() {
- const query = uni.createSelectorQuery().in(this);
- query.select('.message-wrapper').boundingClientRect();
- query.exec((res) => {
- if (res[0]) {
- this.scrollTop = res[0].height;
- }
- });
- },
-
- // 滚动事件
- onScroll(e) {
- // 可以在这里处理滚动相关逻辑
- },
-
- // 输入框聚焦
- onInputFocus() {
- setTimeout(() => {
- this.scrollToBottom();
- }, 300);
- },
-
- // 输入框失焦
- onInputBlur() {
- // 可以在这里处理失焦逻辑
- },
-
- // 格式化时间
- formatTime(timestamp) {
- const date = new Date(timestamp);
- const now = new Date();
- const diff = now - date;
-
- if (diff < 60000) { // 1分钟内
- return '刚刚';
- } else if (diff < 3600000) { // 1小时内
- return `${Math.floor(diff / 60000)}分钟前`;
- } else if (date.toDateString() === now.toDateString()) { // 今天
- return date.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit' });
- } else {
- return date.toLocaleDateString('zh-CN', { month: '2-digit', day: '2-digit' });
- }
- },
-
- // 获取当前日期
- getCurrentDate() {
- return new Date().toLocaleDateString('zh-CN', {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- });
- }
- }
- }
- </script>
- <style lang="scss">
- .chat-container {
- height: 100vh;
- display: flex;
- flex-direction: column;
- background-color: #f5f5f5;
- }
- // 消息列表
- .message-list {
- flex: 1;
- padding: 20rpx 0;
- }
- .message-wrapper {
- padding: 0 20rpx;
- }
- // 日期分隔符
- .date-divider {
- text-align: center;
- margin: 20rpx 0;
- }
- .date-text {
- padding: 8rpx 20rpx;
- background-color: rgba(0, 0, 0, 0.1);
- color: #666;
- font-size: 22rpx;
- border-radius: 16rpx;
- }
- // 消息项
- .message-item {
- display: flex;
- margin-bottom: 20rpx;
-
- &.message-left {
- justify-content: flex-start;
- }
-
- &.message-right {
- justify-content: flex-end;
- }
- }
- .avatar-container {
- flex-shrink: 0;
- margin: 0 16rpx;
- }
- .message-avatar {
- width: 80rpx;
- height: 80rpx;
- border-radius: 40rpx;
- border: 2rpx solid #f0f0f0;
- }
- // 消息内容
- .message-content-wrapper {
- max-width: 500rpx;
-
- &.user-content {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- }
- }
- .message-bubble {
- padding: 16rpx 20rpx;
- border-radius: 20rpx;
- margin-bottom: 8rpx;
- word-wrap: break-word;
-
- &.expert-bubble {
- background-color: #fff;
- border-top-left-radius: 8rpx;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
- }
-
- &.user-bubble {
- background-color: #4CAF50;
- border-top-right-radius: 8rpx;
-
- .message-text {
- color: #fff;
- }
- }
- }
- .message-text {
- font-size: 28rpx;
- line-height: 1.4;
- color: #333;
- }
- .message-image {
- max-width: 300rpx;
- max-height: 300rpx;
- border-radius: 12rpx;
- }
- .message-time {
- font-size: 22rpx;
- color: #999;
-
- &.user-time {
- text-align: right;
- }
- }
- // 打字中指示器
- .typing-indicator {
- display: flex;
- align-items: center;
- margin-bottom: 20rpx;
- }
- .typing-bubble {
- background-color: #fff;
- padding: 16rpx 20rpx;
- border-radius: 20rpx;
- border-top-left-radius: 8rpx;
- margin-left: 16rpx;
- box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
- }
- .typing-dots {
- display: flex;
- gap: 6rpx;
- }
- .dot {
- width: 8rpx;
- height: 8rpx;
- background-color: #999;
- border-radius: 50%;
- animation: typing 1.4s infinite;
-
- &:nth-child(2) {
- animation-delay: 0.2s;
- }
-
- &:nth-child(3) {
- animation-delay: 0.4s;
- }
- }
- @keyframes typing {
- 0%, 60%, 100% {
- transform: translateY(0);
- opacity: 0.5;
- }
- 30% {
- transform: translateY(-10rpx);
- opacity: 1;
- }
- }
- // 输入区域
- .input-area {
- background-color: #fff;
- border-top: 1rpx solid #e5e5e5;
- padding-bottom: env(safe-area-inset-bottom);
- }
- // 图片预览
- .image-preview {
- padding: 20rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .preview-list {
- display: flex;
- gap: 16rpx;
- }
- .preview-item {
- position: relative;
- width: 120rpx;
- height: 120rpx;
- }
- .preview-image {
- width: 100%;
- height: 100%;
- border-radius: 12rpx;
- }
- .remove-btn {
- position: absolute;
- top: -8rpx;
- right: -8rpx;
- width: 32rpx;
- height: 32rpx;
- background-color: #ff4757;
- border-radius: 16rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .remove-icon {
- color: #fff;
- font-size: 20rpx;
- font-weight: bold;
- }
- // 输入工具栏
- .input-toolbar {
- display: flex;
- align-items: flex-end;
- padding: 20rpx;
- gap: 16rpx;
- }
- .tool-btn {
- width: 60rpx;
- height: 60rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #f5f5f5;
- border-radius: 30rpx;
- }
- .tool-icon {
- width: 32rpx;
- height: 32rpx;
- }
- .input-wrapper {
- flex: 1;
- background-color: #f8f8f8;
- border-radius: 24rpx;
- padding: 16rpx 20rpx;
- min-height: 48rpx;
- max-height: 200rpx;
- }
- .message-input {
- width: 100%;
- font-size: 28rpx;
- line-height: 1.4;
- min-height: 48rpx;
- }
- .send-btn {
- padding: 16rpx 24rpx;
- background-color: #e0e0e0;
- border-radius: 24rpx;
- transition: background-color 0.2s ease;
-
- &.active {
- background-color: #4CAF50;
-
- .send-text {
- color: #fff;
- }
- }
- }
- .send-text {
- font-size: 28rpx;
- color: #999;
- font-weight: bold;
- }
- </style>
|