Merge remote-tracking branch 'yudao/develop'
commit
7017561242
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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); // 禁用默认操作
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue