Browse Source

添加常见问题功能

master
[yxf] 1 month ago
parent
commit
21dcda569c
  1. 29
      im-platform/src/main/java/com/bx/implatform/controller/AutoReplyController.java
  2. 24
      im-platform/src/main/java/com/bx/implatform/entity/AutoReply.java
  3. 9
      im-platform/src/main/java/com/bx/implatform/mapper/AutoReplyMapper.java
  4. 8
      im-platform/src/main/java/com/bx/implatform/service/AutoReplyService.java
  5. 49
      im-platform/src/main/java/com/bx/implatform/service/impl/AutoReplyServiceImpl.java
  6. 16
      im-platform/src/main/java/com/bx/implatform/vo/AutoReplyVO.java
  7. 232
      im-uniapp/pages/chat/chat-box.vue
  8. 1
      im-web/src/main.js

29
im-platform/src/main/java/com/bx/implatform/controller/AutoReplyController.java

@ -0,0 +1,29 @@
package com.bx.implatform.controller;
import com.bx.implatform.result.Result;
import com.bx.implatform.result.ResultUtils;
import com.bx.implatform.service.AutoReplyService;
import com.bx.implatform.vo.AutoReplyVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "常见问题")
@RestController
@RequestMapping("/auto/reply")
@RequiredArgsConstructor
public class AutoReplyController {
private final AutoReplyService AutoReplyService;
@GetMapping("/list")
@Operation(summary = "获取常见问题列表", description = "前端聊天页调用")
public Result<List<AutoReplyVO>> getList(@RequestParam Long userId) {
return ResultUtils.success(AutoReplyService.getList(userId));
}
}

24
im-platform/src/main/java/com/bx/implatform/entity/AutoReply.java

@ -0,0 +1,24 @@
package com.bx.implatform.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("im_auto_reply")
public class AutoReply {
@TableId(type = IdType.AUTO)
private Long id;
private String uniqueToken;
private String replyName;
private Integer replyType;
private String replyTitle;
private String replyContent;
private String remark;
private LocalDateTime createdTime;
private LocalDateTime updatedTime;
private Long creatorId;
private Long updaterId;
}

9
im-platform/src/main/java/com/bx/implatform/mapper/AutoReplyMapper.java

@ -0,0 +1,9 @@
package com.bx.implatform.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bx.implatform.entity.AutoReply;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AutoReplyMapper extends BaseMapper<AutoReply> {
}

8
im-platform/src/main/java/com/bx/implatform/service/AutoReplyService.java

@ -0,0 +1,8 @@
package com.bx.implatform.service;
import com.bx.implatform.vo.AutoReplyVO;
import java.util.List;
public interface AutoReplyService {
List<AutoReplyVO> getList(Long userId);
}

49
im-platform/src/main/java/com/bx/implatform/service/impl/AutoReplyServiceImpl.java

@ -0,0 +1,49 @@
package com.bx.implatform.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bx.implatform.entity.AutoReply;
import com.bx.implatform.entity.User;
import com.bx.implatform.mapper.AutoReplyMapper;
import com.bx.implatform.mapper.UserMapper;
import com.bx.implatform.service.AutoReplyService;
import com.bx.implatform.vo.AutoReplyVO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class AutoReplyServiceImpl extends ServiceImpl<AutoReplyMapper, AutoReply> implements AutoReplyService {
private final UserMapper userMapper;
@Override
public List<AutoReplyVO> getList(Long userId) {
// 1. 根据前端传的 userId 查询用户
User user = userMapper.selectById(userId);
if (user == null) {
return List.of();
}
// 2. 拿到用户的 uniqueToken
String uniqueToken = user.getUniqueToken();
// 3. 匹配常见问题
LambdaQueryWrapper<AutoReply> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(AutoReply::getUniqueToken, uniqueToken);
wrapper.orderByAsc(AutoReply::getCreatedTime);
List<AutoReply> list = this.list(wrapper);
// 4. 转VO返回
return list.stream().map(item -> {
AutoReplyVO vo = new AutoReplyVO();
BeanUtils.copyProperties(item, vo);
return vo;
}).collect(Collectors.toList());
}
}

16
im-platform/src/main/java/com/bx/implatform/vo/AutoReplyVO.java

@ -0,0 +1,16 @@
package com.bx.implatform.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class AutoReplyVO {
private Long id;
private String uniqueToken;
private String replyName;
private Integer replyType; // 0文本 1图片
private String replyTitle;
private String replyContent;
private String remark;
private LocalDateTime createdTime;
}

232
im-uniapp/pages/chat/chat-box.vue

@ -3,6 +3,16 @@
<nav-bar>{{ title }}</nav-bar> <nav-bar>{{ title }}</nav-bar>
<view class="chat-main-box" :style="{height: chatMainHeight+'px'}"> <view class="chat-main-box" :style="{height: chatMainHeight+'px'}">
<view class="chat-message" @click="switchChatTabBox('none')"> <view class="chat-message" @click="switchChatTabBox('none')">
<!-- 固定在聊天区顶部的常见问题提示条 -->
<view v-if="showAutoQuestionTip" class="question-tip-fixed">
<view class="tip-title">请选择您想咨询的问题</view>
<view class="question-list">
<view class="question-item" v-for="(q, i) in commonQuestions" :key="i"
@click="sendQuestionMessage(q)">
{{ q }}
</view>
</view>
</view>
<scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop" <scroll-view class="scroll-box" scroll-y="true" upper-threshold="200" @scrolltoupper="onScrollToTop"
@scroll="onScroll" :scroll-into-view="'chat-item-' + scrollMsgIdx" :scroll-top="scrollTop"> @scroll="onScroll" :scroll-into-view="'chat-item-' + scrollMsgIdx" :scroll-top="scrollTop">
<view v-if="chat" class="chat-wrap"> <view v-if="chat" class="chat-wrap">
@ -126,43 +136,109 @@ import UNI_APP from '@/.env.js';
export default { export default {
data() { data() {
return { return {
// chat: {},
userInfo: {}, userInfo: {},
group: {}, group: {},
groupMembers: [], groupMembers: [],
isReceipt: false, // showAutoQuestionTip: true,
scrollMsgIdx: 0, // commonQuestions: [], //
autoReplyList: [], //
isReceipt: false,
scrollMsgIdx: 0,
chatTabBox: 'none', chatTabBox: 'none',
currentTargetId: null, // currentTargetId: null,
currentChatType: 'PRIVATE', currentChatType: 'PRIVATE',
isLoading: true, // _activeChatIdx: 0,
defaultTitle: '加载中...', //
// activeChatIdx: 0,
_activeChatIdx: 0, //
showRecord: false, showRecord: false,
chatMainHeight: 800, // chatMainHeight: 800,
keyboardHeight: 290, // keyboardHeight: 290,
screenHeight: 1000, // screenHeight: 1000,
windowHeight: 1000, // windowHeight: 1000,
initHeight: 1000, // h5 initHeight: 1000,
atUserIds: [], atUserIds: [],
showMinIdx: 0, // showMinIdx showMinIdx: 0,
reqQueue: [], // reqQueue: [],
isSending: false, // isSending: false,
isShowKeyBoard: false, // isShowKeyBoard: false,
editorCtx: null, // editorCtx: null,
isEmpty: true, // isEmpty: true,
isFocus: false, // isFocus: false,
isReadOnly: false, // isReadOnly: false,
playingAudio: null, // playingAudio: null,
isInBottom: true, // isInBottom: true,
newMessageSize: 0, // newMessageSize: 0,
scrollTop: 0, // ios h5 scrollTop: 0,
scrollViewHeight: 0, // scrollViewHeight: 0,
maxTmpId: 0 // id maxTmpId: 0
} }
}, },
methods: { methods: {
loadCommonQuestions(userId) {
this.$http({
url: "/auto/reply/list?userId=" + userId,
method: 'get',
}).then(res => {
// res.data
let list = res || [];
this.autoReplyList = list;
this.commonQuestions = list.map(item => item.replyTitle);
}).catch(() => {
// this.commonQuestions = ["", "退", "", "", ""];
});
},
sendQuestionMessage(replyTitle) {
let autoReply = this.autoReplyList.find(item => item.replyTitle === replyTitle);
if (!autoReply) return;
//
if (this.isBanned) {
this.showBannedTip();
return;
}
let msgInfo = {
tmpId: this.generateId(),
receipt: this.isReceipt
};
// ============== ==============
if (autoReply.replyType === 0) {
//
msgInfo.type = this.$enums.MESSAGE_TYPE.TEXT;
msgInfo.content = autoReply.replyContent;
} else if (autoReply.replyType === 1) {
//
msgInfo.type = this.$enums.MESSAGE_TYPE.IMAGE;
msgInfo.content = JSON.stringify({
originUrl: autoReply.replyContent,
thumbUrl: autoReply.replyContent
});
} else {
return;
}
this.fillTargetId(msgInfo, this.chat.targetId);
const chat = this.chat;
if (!chat) return;
let tmpMessage = this.buildTmpMessage(msgInfo);
this.chatStore.insertMessage(tmpMessage, chat);
this.moveChatToTop();
this.sendMessageRequest(msgInfo).then((m) => {
tmpMessage = JSON.parse(JSON.stringify(tmpMessage));
tmpMessage.id = m.id;
tmpMessage.status = m.status;
this.chatStore.updateMessage(tmpMessage, chat);
this.scrollToBottom();
this.isReceipt = false;
}).catch(() => {
tmpMessage = JSON.parse(JSON.stringify(tmpMessage));
tmpMessage.status = this.$enums.MESSAGE_STATUS.FAILED;
this.chatStore.updateMessage(tmpMessage, chat);
});
},
onRecorderInput() { onRecorderInput() {
this.showRecord = true; this.showRecord = true;
this.switchChatTabBox('none'); this.switchChatTabBox('none');
@ -1038,7 +1114,6 @@ export default {
}); });
}, },
generateId() { generateId() {
// id
const id = String(new Date().getTime()) + String(Math.floor(Math.random() * 1000)); const id = String(new Date().getTime()) + String(Math.floor(Math.random() * 1000));
// id // id
if (this.maxTmpId > id) { if (this.maxTmpId > id) {
@ -1050,20 +1125,14 @@ export default {
}, },
computed: { computed: {
title() { title() {
// if (!this.chat) return "";
if (this.isLoading) { let title = this.chat.showName;
return this.defaultTitle; if (this.isGroup) {
} let size = this.groupMembers.filter(m => !m.quit).length;
if (!this.chat) { title += `(${size})`;
return ""; }
} return title;
let title = this.chat.showName; },
if (this.isGroup) {
let size = this.groupMembers.filter(m => !m.quit).length;
title += `(${size})`;
}
return title;
},
chat() { chat() {
if (!this.currentTargetId) return null; if (!this.currentTargetId) return null;
return this.chatStore.chats.find(c => return this.chatStore.chats.find(c =>
@ -1076,37 +1145,13 @@ export default {
get() { return this._activeChatIdx || 0; }, get() { return this._activeChatIdx || 0; },
set(val) { this._activeChatIdx = val; } set(val) { this._activeChatIdx = val; }
}, },
// activeChatIdx: {
// get() {
// return this._activeChatIdx || 0;
// },
// set(val) {
// this._activeChatIdx = val;
// }
// },
mine() { mine() {
return this.userStore.userInfo; return this.userStore.userInfo;
}, },
friend() { friend() {
return this.friendStore.findFriend(this.userInfo.id); return this.friendStore.findFriend(this.userInfo.id);
}, },
title() { messageAction() {
if (!this.chat) {
return "";
}
let title = this.chat.showName;
if (this.isGroup) {
let size = this.groupMembers.filter(m => !m.quit).length;
title += `(${size})`;
}
return title;
},
// messageAction() {
// return `/message/${this.chat.type.toLowerCase()}/send`;
// },
messageAction() {
if (!this.chat) return ''; if (!this.chat) return '';
return `/message/${this.chat.type.toLowerCase()}/send`; return `/message/${this.chat.type.toLowerCase()}/send`;
}, },
@ -1171,8 +1216,6 @@ export default {
deep: true, deep: true,
immediate: true immediate: true
}, },
//
'chat.messages.length': function(newLength, oldLength) { 'chat.messages.length': function(newLength, oldLength) {
if (newLength > oldLength && this.isInBottom) { if (newLength > oldLength && this.isInBottom) {
this.$nextTick(() => { this.$nextTick(() => {
@ -1180,8 +1223,6 @@ export default {
}); });
} }
}, },
// messageSize watch
messageSize: function(newSize, oldSize) { messageSize: function(newSize, oldSize) {
if (newSize > oldSize && oldSize > 0) { if (newSize > oldSize && oldSize > 0) {
let lastMessage = this.chat.messages[newSize - 1]; let lastMessage = this.chat.messages[newSize - 1];
@ -1218,12 +1259,10 @@ export default {
let targetId = null; let targetId = null;
let type = 'PRIVATE'; let type = 'PRIVATE';
//
if (options.targetId) { if (options.targetId) {
targetId = Number(options.targetId); targetId = Number(options.targetId);
type = options.type || 'PRIVATE'; type = options.type || 'PRIVATE';
} else { } else {
//
if (this.friendStore.friends.length === 0) await this.friendStore.loadFriend(); if (this.friendStore.friends.length === 0) await this.friendStore.loadFriend();
const first = this.friendStore.friends[0]; const first = this.friendStore.friends[0];
if (!first) return uni.showToast({ title: '暂无好友', icon: 'none' }); if (!first) return uni.showToast({ title: '暂无好友', icon: 'none' });
@ -1233,7 +1272,6 @@ export default {
await this.chatStore.loadChat(); await this.chatStore.loadChat();
await new Promise(r => setTimeout(r, 300)); await new Promise(r => setTimeout(r, 300));
//
let chat = this.chatStore.chats.find(c => c.type === type && c.targetId === targetId); let chat = this.chatStore.chats.find(c => c.type === type && c.targetId === targetId);
if (!chat) { if (!chat) {
const friend = this.friendStore.findFriend(targetId) || this.friendStore.friends[0]; const friend = this.friendStore.findFriend(targetId) || this.friendStore.friends[0];
@ -1246,7 +1284,7 @@ export default {
}; };
this.chatStore.chats.unshift(chat); this.chatStore.chats.unshift(chat);
} }
this.currentTargetId = targetId; this.currentTargetId = targetId;
this.currentChatType = type; this.currentChatType = type;
@ -1261,7 +1299,10 @@ export default {
this.loadFriend(targetId); this.loadFriend(targetId);
this.loadReaded(targetId); this.loadReaded(targetId);
//
this.loadCommonQuestions(targetId);
this.listenKeyBoard(); this.listenKeyBoard();
this.windowHeight = uni.getSystemInfoSync().windowHeight; this.windowHeight = uni.getSystemInfoSync().windowHeight;
this.screenHeight = uni.getSystemInfoSync().screenHeight; this.screenHeight = uni.getSystemInfoSync().screenHeight;
@ -1329,9 +1370,46 @@ export default {
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-color: white; background-color: white;
.scroll-box { .scroll-box {
height: 100%; height: 100%;
padding-top: 120rpx; /* 给常见问题条留出空间 */
box-sizing: border-box;
}
//
.question-tip-fixed {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: 10;
background-color: #ffffff;
padding: 12rpx 20rpx;
// border-bottom: 1rpx solid #f0f0f0;
.tip-title {
font-size: 26rpx;
color: #999;
text-align: center;
margin-bottom: 8rpx;
}
.question-list {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
justify-content: center;
}
.question-item {
background-color: #f5f7fa;
border-radius: 30rpx;
padding: 8rpx 16rpx;
font-size: 26rpx;
color: #333;
border: 1rpx solid #eaeaea;
}
.question-item:active {
background-color: #e6e6e6;
}
} }
.scroll-to-bottom { .scroll-to-bottom {

1
im-web/src/main.js

@ -36,7 +36,6 @@ Vue.prototype.$str = str; // 字符串相关
Vue.prototype.$elm = element; // 元素操作 Vue.prototype.$elm = element; // 元素操作
Vue.prototype.$enums = enums; // 枚举 Vue.prototype.$enums = enums; // 枚举
Vue.prototype.$eventBus = new Vue(); // 全局事件 Vue.prototype.$eventBus = new Vue(); // 全局事件
Vue.config.productionTip = false; Vue.config.productionTip = false;
new Vue({ new Vue({

Loading…
Cancel
Save