Merge remote-tracking branch 'yudao/develop'

pull/125/head
puhui999 2024-11-18 17:32:38 +08:00
commit 7017561242
4 changed files with 105 additions and 63 deletions

View File

@ -30,11 +30,11 @@
支持 Spring Boot、Spring Cloud 两种架构: 支持 Spring Boot、Spring Cloud 两种架构:
① Spring Boot 单体架构:<https://github.com/YunaiV/ruoyi-vue-pro> ① Spring Boot 单体架构:<https://doc.iocoder.cn>
![架构图](/.image/common/ruoyi-vue-pro-architecture.png) ![架构图](/.image/common/ruoyi-vue-pro-architecture.png)
② Spring Cloud 微服务架构:<https://github.com/YunaiV/yudao-cloud> ② Spring Cloud 微服务架构:<https://cloud.iocoder.cn>
![架构图](/.image/common/yudao-cloud-architecture.png) ![架构图](/.image/common/yudao-cloud-architecture.png)

View File

@ -1,20 +1,35 @@
<template> <template>
<!-- 聊天虚拟列表 --> <!-- 聊天虚拟列表 -->
<z-paging ref="pagingRef" v-model="messageList" use-chat-record-mode use-virtual-list <z-paging
cell-height-mode="dynamic" default-page-size="20" :auto-clean-list-when-reload="false" ref="pagingRef"
safe-area-inset-bottom bottom-bg-color="#f8f8f8" :back-to-top-style="backToTopStyle" v-model="messageList"
:auto-show-back-to-top="showNewMessageTip" @backToTopClick="onBackToTopClick" use-chat-record-mode
@scrolltoupper="onScrollToUpper" @query="queryList"> use-virtual-list
cell-height-mode="dynamic"
default-page-size="20"
:auto-clean-list-when-reload="false"
safe-area-inset-bottom
bottom-bg-color="#f8f8f8"
:back-to-top-style="backToTopStyle"
:auto-show-back-to-top="showNewMessageTip"
@backToTopClick="onBackToTopClick"
@scrolltoupper="onScrollToUpper"
@query="queryList"
>
<template #top> <template #top>
<!-- 撑一下顶部导航 --> <!-- 撑一下顶部导航 -->
<view :style="{ height: sys_navBar + 'px' }"></view> <view :style="{ height: sys_navBar + 'px' }"></view>
</template> </template>
<!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 --> <!-- style="transform: scaleY(-1)"必须写否则会导致列表倒置 -->
<!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view --> <!-- 注意不要直接在chat-item组件标签上设置style因为在微信小程序中是无效的请包一层view -->
<template #cell="{item,index}"> <template #cell="{ item, index }">
<view style="transform: scaleY(-1)"> <view style="transform: scaleY(-1)">
<!-- 消息渲染 --> <!-- 消息渲染 -->
<MessageListItem :message="item" :message-index="index" :message-list="messageList"></MessageListItem> <MessageListItem
:message="item"
:message-index="index"
:message-list="messageList"
></MessageListItem>
</view> </view>
</template> </template>
<!-- 底部聊天输入框 --> <!-- 底部聊天输入框 -->
@ -41,13 +56,13 @@
const showNewMessageTip = ref(false); // const showNewMessageTip = ref(false); //
const refreshMessage = ref(false); // const refreshMessage = ref(false); //
const backToTopStyle = reactive({ const backToTopStyle = reactive({
'width': '100px', width: '100px',
'background-color': '#fff', 'background-color': '#fff',
'border-radius': '30px', 'border-radius': '30px',
'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)', 'box-shadow': '0 2px 4px rgba(0, 0, 0, 0.1)',
'display': 'flex', display: 'flex',
'justifyContent': 'center', justifyContent: 'center',
'alignItems': 'center', alignItems: 'center',
}); // }); //
const queryParams = reactive({ const queryParams = reactive({
no: 1, // no: 1, //
@ -106,6 +121,7 @@
onScrollToUpper(); onScrollToUpper();
} }
}; };
/** 滚动到最新消息 */ /** 滚动到最新消息 */
const onBackToTopClick = (event) => { const onBackToTopClick = (event) => {
event(false); // event(false); //

View File

@ -1,17 +1,35 @@
<template> <template>
<s-layout class="chat-wrap" :title="!isReconnecting ? '连接客服成功' : '会话重连中'" navbar="inner"> <s-layout
class="chat-wrap"
:title="!isReconnecting ? '连接客服成功' : '会话重连中'"
navbar="inner"
>
<!-- 覆盖头部导航栏背景颜色 --> <!-- 覆盖头部导航栏背景颜色 -->
<div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div> <div class="page-bg" :style="{ height: sys_navBar + 'px' }"></div>
<!-- 聊天区域 --> <!-- 聊天区域 -->
<MessageList ref="messageListRef"> <MessageList ref="messageListRef">
<template #bottom> <template #bottom>
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input> <message-input
v-model="chat.msg"
@on-tools="onTools"
@send-message="onSendMessage"
></message-input>
</template> </template>
</MessageList> </MessageList>
<!-- 聊天工具 --> <!-- 聊天工具 -->
<tools-popup :show-tools="chat.showTools" :tools-mode="chat.toolsMode" @close="handleToolsClose" <tools-popup
@on-emoji="onEmoji" @image-select="onSelect" @on-show-select="onShowSelect"> :show-tools="chat.showTools"
<message-input v-model="chat.msg" @on-tools="onTools" @send-message="onSendMessage"></message-input> :tools-mode="chat.toolsMode"
@close="handleToolsClose"
@on-emoji="onEmoji"
@image-select="onSelect"
@on-show-select="onShowSelect"
>
<message-input
v-model="chat.msg"
@on-tools="onTools"
@send-message="onSendMessage"
></message-input>
</tools-popup> </tools-popup>
<!-- 商品订单选择 --> <!-- 商品订单选择 -->
<SelectPopup <SelectPopup
@ -30,7 +48,10 @@
import ToolsPopup from '@/pages/chat/components/toolsPopup.vue'; import ToolsPopup from '@/pages/chat/components/toolsPopup.vue';
import MessageInput from '@/pages/chat/components/messageInput.vue'; import MessageInput from '@/pages/chat/components/messageInput.vue';
import SelectPopup from '@/pages/chat/components/select-popup.vue'; import SelectPopup from '@/pages/chat/components/select-popup.vue';
import { KeFuMessageContentTypeEnum, WebSocketMessageTypeConstants } from '@/pages/chat/util/constants'; import {
KeFuMessageContentTypeEnum,
WebSocketMessageTypeConstants,
} from '@/pages/chat/util/constants';
import FileApi from '@/sheep/api/infra/file'; import FileApi from '@/sheep/api/infra/file';
import KeFuApi from '@/sheep/api/promotion/kefu'; import KeFuApi from '@/sheep/api/promotion/kefu';
import { useWebSocket } from '@/sheep/hooks/useWebSocket'; import { useWebSocket } from '@/sheep/hooks/useWebSocket';
@ -105,7 +126,7 @@
const res = await FileApi.uploadFile(data.tempFiles[0].path); const res = await FileApi.uploadFile(data.tempFiles[0].path);
msg = { msg = {
contentType: KeFuMessageContentTypeEnum.IMAGE, contentType: KeFuMessageContentTypeEnum.IMAGE,
content: JSON.stringify({picUrl: res.data}), content: JSON.stringify({ picUrl: res.data }),
}; };
break; break;
case 'goods': case 'goods':
@ -135,8 +156,7 @@
//======================= end ======================= //======================= end =======================
const { options } = useWebSocket({ const { options } = useWebSocket({
// //
onConnected: async () => { onConnected: async () => {},
},
// //
onMessage: async (data) => { onMessage: async (data) => {
const type = data.type; const type = data.type;
@ -161,7 +181,6 @@
<style scoped lang="scss"> <style scoped lang="scss">
.chat-wrap { .chat-wrap {
.page-bg { .page-bg {
width: 100%; width: 100%;
position: absolute; position: absolute;

View File

@ -1,4 +1,4 @@
import dayjs from "dayjs"; import dayjs from 'dayjs';
/** /**
* 将一个整数转换为分数保留两位小数 * 将一个整数转换为分数保留两位小数
@ -6,10 +6,10 @@ import dayjs from "dayjs";
* @return {number} 分数 * @return {number} 分数
*/ */
export const formatToFraction = (num) => { export const formatToFraction = (num) => {
if (typeof num === 'undefined') return 0 if (typeof num === 'undefined') return 0;
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num const parsedNumber = typeof num === 'string' ? parseFloat(num) : num;
return parseFloat((parsedNumber / 100).toFixed(2)) return parseFloat((parsedNumber / 100).toFixed(2));
} };
/** /**
* 将一个数转换为 1.00 这样 * 将一个数转换为 1.00 这样
@ -19,26 +19,26 @@ export const formatToFraction = (num) => {
* @return {string} 分数 * @return {string} 分数
*/ */
export const floatToFixed2 = (num) => { export const floatToFixed2 = (num) => {
let str = '0.00' let str = '0.00';
if (typeof num === 'undefined') { if (typeof num === 'undefined') {
return str return str;
} }
const f = formatToFraction(num) const f = formatToFraction(num);
const decimalPart = f.toString().split('.')[1] const decimalPart = f.toString().split('.')[1];
const len = decimalPart ? decimalPart.length : 0 const len = decimalPart ? decimalPart.length : 0;
switch (len) { switch (len) {
case 0: case 0:
str = f.toString() + '.00' str = f.toString() + '.00';
break break;
case 1: case 1:
str = f.toString() + '.0' str = f.toString() + '.0';
break break;
case 2: case 2:
str = f.toString() str = f.toString();
break break;
} }
return str return str;
} };
/** /**
* 将一个分数转换为整数 * 将一个分数转换为整数
@ -47,11 +47,11 @@ export const floatToFixed2 = (num) => {
* @return {number} 整数 * @return {number} 整数
*/ */
export const convertToInteger = (num) => { export const convertToInteger = (num) => {
if (typeof num === 'undefined') return 0 if (typeof num === 'undefined') return 0;
const parsedNumber = typeof num === 'string' ? parseFloat(num) : num const parsedNumber = typeof num === 'string' ? parseFloat(num) : num;
// TODO 分转元后还有小数则四舍五入 // TODO 分转元后还有小数则四舍五入
return Math.round(parsedNumber * 100) return Math.round(parsedNumber * 100);
} };
/** /**
* 时间日期转换 * 时间日期转换
@ -64,16 +64,16 @@ export const convertToInteger = (num) => {
* @description format 季度 + 星期 + 几周"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ" * @description format 季度 + 星期 + 几周"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
* @returns {string} 返回拼接后的时间字符串 * @returns {string} 返回拼接后的时间字符串
*/ */
export function formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') { export function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
// 日期不存在,则返回空 // 日期不存在,则返回空
if (!date) { if (!date) {
return '' return '';
} }
// 日期存在,则进行格式化 // 日期存在,则进行格式化
if (format === undefined) { if (format === undefined) {
format = 'YYYY-MM-DD HH:mm:ss' format = 'YYYY-MM-DD HH:mm:ss';
} }
return dayjs(date).format(format) return dayjs(date).format(format);
} }
/** /**
@ -85,16 +85,22 @@ export function formatDate(date, format= 'YYYY-MM-DD HH:mm:ss') {
* @param {*} children 孩子节点字段 默认 'children' * @param {*} children 孩子节点字段 默认 'children'
* @param {*} rootId 根Id 默认 0 * @param {*} rootId 根Id 默认 0
*/ */
export function handleTree(data, id = 'id', parentId = 'parentId', children = 'children', rootId = 0) { export function handleTree(
data,
id = 'id',
parentId = 'parentId',
children = 'children',
rootId = 0,
) {
// 对源数据深度克隆 // 对源数据深度克隆
const cloneData = JSON.parse(JSON.stringify(data)) const cloneData = JSON.parse(JSON.stringify(data));
// 循环所有项 // 循环所有项
const treeData = cloneData.filter(father => { const treeData = cloneData.filter((father) => {
let branchArr = cloneData.filter(child => { let branchArr = cloneData.filter((child) => {
//返回每一项的子级数组 //返回每一项的子级数组
return father[id] === child[parentId] return father[id] === child[parentId];
}); });
branchArr.length > 0 ? father.children = branchArr : ''; branchArr.length > 0 ? (father.children = branchArr) : '';
//返回第一层 //返回第一层
return father[parentId] === rootId; return father[parentId] === rootId;
}); });
@ -120,17 +126,18 @@ export function resetPagination(pagination) {
* @param source 源对象 * @param source 源对象
*/ */
export const copyValueToTarget = (target, source) => { export const copyValueToTarget = (target, source) => {
const newObj = Object.assign({}, target, source) const newObj = Object.assign({}, target, source);
// 删除多余属性 // 删除多余属性
Object.keys(newObj).forEach((key) => { Object.keys(newObj).forEach((key) => {
// 如果不是target中的属性则删除 // 如果不是target中的属性则删除
if (Object.keys(target).indexOf(key) === -1) { if (Object.keys(target).indexOf(key) === -1) {
delete newObj[key] delete newObj[key];
} }
}) });
// 更新目标对象值 // 更新目标对象值
Object.assign(target, newObj) Object.assign(target, newObj);
} };
/** /**
* 解析 JSON 字符串 * 解析 JSON 字符串
* *
@ -138,9 +145,9 @@ export const copyValueToTarget = (target, source) => {
*/ */
export function jsonParse(str) { export function jsonParse(str) {
try { try {
return JSON.parse(str) return JSON.parse(str);
} catch (e) { } catch (e) {
console.error(`str[${str}] 不是一个 JSON 字符串`) console.error(`str[${str}] 不是一个 JSON 字符串`);
return '' return '';
} }
} }