feat: 解析自定义消息

fix/0524_ch
向文可 3 years ago
parent 78f371960a
commit 2a59913e69

@ -1,4 +1,4 @@
VITE_BASE_URL=/api
#VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
VITE_SOCKET_URL=ws://192.168.10.93:8090/ws
VITE_SOCKET_URL=wss://k8s-horse-gateway.mashibing.cn/ws
#VITE_SOCKET_URL=ws://192.168.10.93:8090/ws
VITE_REQUEST_TIMEOUT=5000

@ -6,3 +6,30 @@ export const login = (params) => {
params,
});
};
export const searchService = (id) => {
return request({
url: '/im/admin/waiter/findAllWaiter/' + id,
method: 'get',
});
};
export const searchSession = (params) => {
return request({
url: '/im/admin/waiter/findWaiterSessions',
method: 'get',
params,
});
};
export const searchMessage = (params) => {
return request({
url: '/im/admin/waiter/findSessionMessage',
method: 'get',
params,
});
};
export const searchSummary = (params) => {
return request({
url: '/im/admin/waiter/waiterStatistics',
method: 'get',
params,
});
};

@ -22,8 +22,6 @@
import LayoutMain from './components/main.vue';
import LayoutMenu from './components/menu.vue';
import LayoutTabs from './components/tabs.vue';
const store = useStore();
store.dispatch('chat/connect');
</script>
<style lang="less" scoped>

@ -14,10 +14,19 @@ export default [
name: 'ChatSession',
component: () => import('@/views/chat/index.vue'),
meta: {
title: '会话列表',
title: '聊天会话',
icon: 'wechat-2-fill',
},
},
{
path: 'management',
name: 'CustomerServiceManagement',
component: () => import('@/views/chat/management.vue'),
meta: {
title: '客服管理',
icon: 'chat-smile-2-fill',
},
},
],
},
];

@ -36,12 +36,67 @@ const getters = {
dayjs(new Date(timestamp)).format('MM-DD HH:mm:ss');
};
},
parseContent: () => {
parseText: () => {
return (payload) => {
try {
payload = JSON.parse(payload);
if ('text' in payload) {
payload = payload.text;
} else if ('linkJump' in payload) {
payload = '[超链接]';
} else if ('orderNo' in payload) {
payload = '[订单信息]';
} else if ('productImageUrl' in payload) {
payload = '[商品信息]';
} else {
payload = '[未知消息]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
parseImage: () => {
return (payload) => {
try {
payload = JSON.parse(payload);
if ('url' in payload) {
payload = payload.url;
} else {
payload = '[未知图片]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
parseVideo: () => {
return (payload) => {
try {
payload = JSON.parse(payload);
if ('url' in payload) {
payload = payload.url;
} else {
payload = '[未知视频]';
}
} catch (e) {
payload = '[解析异常]';
}
return payload;
};
},
parseContent: () => {
return (payload) => {
try {
payload = JSON.parse(payload);
if ('linkJump' in payload) {
payload.type = 'link';
} else if ('orderNo' in payload) {
payload.type = 'order';
} else if ('productImageUrl' in payload) {
payload.type = 'product';
} else {
payload = '[未知消息]';
}

@ -0,0 +1,91 @@
import * as api from '@/api/chat/index.js';
import { ElMessage, ElMessageBox } from '@/plugins/element-plus';
const state = () => ({
code: 'CustomerServiceManagement',
condition: {},
list: [],
total: 0,
opts: {
init: false,
customerServiceType: [
{
label: '售前',
value: 1,
},
{
label: '售后',
value: 2,
},
{
label: '发货',
value: 3,
},
],
},
});
const getters = {};
const mutations = {
setCode: (state, data) => (state.code = data),
setCondition: (state, data) => (state.condition = data),
setList: (state, data) => (state.list = data),
setTotal: (state, data) => (state.total = data),
setOpts: (state, data) => (state.opts = data),
};
const actions = {
search: async ({ commit }) => {
let res = await api.searchService(1);
commit('setList', res || []);
if (!res) {
ElMessage.error('查询失败');
}
return res;
},
load: async ({ state, commit }) => {
commit('setOpts', {
init: true,
...state.opts,
});
},
detail: async (context, id) => {
let res = await api.detail(id);
if (!res) {
ElMessage.error('加载详情失败');
}
return res;
},
save: async ({ dispatch }, data) => {
let save = data.id ? api.update : api.create;
let res = await save(data);
if (res) {
ElMessage.success('保存成功');
dispatch('search');
} else {
ElMessage.error('保存失败');
}
return res;
},
remove: async ({ dispatch }, idList) => {
if (!idList.length) {
ElMessage.warning('请选择要删除的数据');
} else {
try {
await ElMessageBox.confirm('数据删除后无法恢复,确定要删除吗?', '危险操作');
let res = await api.remove(idList.join(','));
if (res) {
ElMessage.success('删除成功');
dispatch('search');
} else {
ElMessage.error('删除失败');
}
} catch (e) {
console.info('取消删除', e);
}
}
},
};
export default {
state,
getters,
mutations,
actions,
};

@ -42,7 +42,7 @@
</div>
<div class="row">
<div class="session-content">
{{ store.getters['chat/parseContent'](item.lastMessage.payload) }}
{{ store.getters['chat/parseText'](item.lastMessage.payload) }}
</div>
</div>
</div>
@ -136,6 +136,7 @@
import MessageItem from './message.vue';
const { proxy } = getCurrentInstance();
const store = useStore();
store.dispatch('chat/connect');
store.dispatch('chat/querySession');
const opts = computed(() => store.state.chat.opts);

@ -0,0 +1,100 @@
<template>
<div class="list-container">
<table-list
v-loading="loading"
:code="code"
:config="config"
:data="list"
:operation="['search']"
:reset="handleReset"
:total="total"
@search="handleSearch"
/>
</div>
</template>
<script setup lang="jsx">
import ElButton from '@/components/extra/ElButton.vue';
import ElSwitch from '@/components/extra/ElSwitch.vue';
const router = useRouter();
const store = useStore();
const loading = ref(false);
const code = computed(() => store.state.customerService.code);
const list = computed(() => store.state.customerService.list);
const total = computed(() => store.state.customerService.total);
const opts = computed(() => store.state.customerService.opts);
if (!unref(opts).init) {
store.dispatch('customerService/load');
}
const state = reactive({
condition: {},
});
watch(
() => state.condition,
(value) => {
store.commit('customerService/setCondition', _.cloneDeep(value));
},
{ immediate: true, deep: true }
);
const handleReset = () => {
state.condition = {};
};
const handleSearch = async () => {
loading.value = true;
await store.dispatch('customerService/search');
loading.value = false;
};
onActivated(handleSearch);
const handleHistory = async (row) => {
router.push({
name: 'ChatHistory',
params: {
id: row.id,
},
});
};
const config = reactive({
//
columns: [
{
label: '账号',
prop: 'account',
minWidth: 160,
},
{
label: '账号名',
prop: 'nickname',
minWidth: 160,
},
{
label: '客服昵称',
prop: 'waiterNickname',
minWidth: 160,
},
{
label: '是否启用',
slots: {
default: ({ row }) => <ElSwitch v-model={row.isEnable} />,
},
width: 120,
},
{
label: '操作',
fixed: 'right',
slots: {
default: ({ row }) => (
<div>
<ElButton type="text" onClick={() => handleHistory([row])}>
会话记录
</ElButton>
</div>
),
},
width: 120,
},
],
});
</script>
<style lang="less" scoped></style>

@ -15,66 +15,80 @@
{{ props.message.fromNickname }}
</div>
<template v-if="messageType[props.message.type] === 'custom'">
<el-card
v-if="store.getters['chat/parseContent'](props.message.payload).type === 'product'"
class="shadow"
>
<a v-if="content.type === 'link'" class="content shadow" :href="content.linkJump">
{{ content.content }}
</a>
<el-card v-if="content.type === 'product'" class="shadow">
<template #header>
<div class="flex">
<div class="left">商品编号{{ product.id }}</div>
<div class="left">商品编号{{ content.id }}</div>
<el-button type="text">复制</el-button>
</div>
</template>
<div class="flex">
<el-image :alt="product.name" height="64px" :src="product.mainPicture" width="64px" />
<el-image :alt="content.name" height="64px" :src="content.productImageUrl" width="64px" />
<div class="right">
<div class="name">{{ product.name }}</div>
<div class="price">{{ product.startingPrice }}</div>
<div class="name">{{ content.name }}</div>
<div class="price">{{ content.startingPrice }}</div>
</div>
</div>
<div class="footer">
<el-button>商品规格属性</el-button>
<el-button @click="handleProduct(content.id)"></el-button>
</div>
</el-card>
<el-card
v-if="store.getters['chat/parseContent'](props.message.payload).type === 'order'"
class="shadow"
>
<el-card v-if="content.type === 'order'" class="shadow">
<template #header>
<div class="flex">
<div class="status">{{ order.orderStatusDesc }}</div>
<div class="service">售后中</div>
<div class="status">{{ content.orderStatusDesc }}</div>
<div class="service"></div>
</div>
<div class="flex">
<div class="no">订单编号{{ order.orderNo }}</div>
<el-button type="text">复制</el-button>
<div class="no">订单编号{{ content.orderNo }}</div>
<el-button type="text" @click="$copy(content.orderNo)"></el-button>
</div>
</template>
<div class="flex">
<el-image
:alt="order.product.name"
:alt="content.productName"
height="64px"
:src="order.product.mainPicture"
:src="content.productImageUrl"
width="64px"
/>
<div class="right">
<div class="flex">
<div class="name">{{ order.product.name }}</div>
<div class="price">{{ order.product.startingPrice }}</div>
<div class="name">{{ content.productName }}</div>
<div class="price">{{ content.payAmount }}</div>
</div>
<div class="flex">
<div class="sku">{{ order.product.sku || '默认规格' }}</div>
<div class="count">x{{ order.count || 1 }}</div>
<div class="sku">{{ content.skuDescribe || '默认规格' }}</div>
<div class="count">x{{ content.quantity || 1 }}</div>
</div>
</div>
</div>
<div class="footer">
<el-button>查看详情</el-button>
<el-button @click="handleOrder(content.orderId)"></el-button>
</div>
</el-card>
</template>
<div v-else class="content" :class="{ shadow: messageType[props.message.type] === 'text' }">
{{ store.getters['chat/parseContent'](props.message.payload) }}
<div v-else-if="messageType[props.message.type] === 'image'" class="content shadow">
<el-image
alt="[图片消息]"
:src="store.getters['chat/parseImage'](props.message.payload)"
style="max-width: 240px"
/>
</div>
<div v-else-if="messageType[props.message.type] === 'video'" class="shadow">
<video controls width="240">
<source :src="store.getters['chat/parseVideo'](props.message.payload)" type="video/mp4" />
<object
:data="store.getters['chat/parseVideo'](props.message.payload)"
height="240"
width="320"
></object>
</video>
</div>
<div v-else class="content shadow">
{{ store.getters['chat/parseText'](props.message.payload) }}
</div>
</div>
<div v-if="!['revoke', 'notify'].includes(messageType[props.message.type])" class="time">
@ -84,31 +98,7 @@
</template>
<script setup>
/**
* 消息体结构
{
"code": 200,
"content": {
"createTimeStamp": "消息的创建时间戳",
"fromId": "消息发送人id",
"id": "消息id",
"payload": "{'text':'你好我是客服小王'}",
"traceId": "uuid",
"traceType": 23,
"sessionId": "消息所属会话id",
"messageIndex": "会话中消息的顺序",
"type": "消息的类型"
},
"session": {
"fromAvatar": "消息所属会话的对面人头像",
"fromId": "消息所属会话的对面人id",
"fromNickname": "消息所属会话的对面人昵称",
"id": "消息id",
"sysId": "系统id",
"type": "消息类型"
}
}
*/
const router = useRouter();
const store = useStore();
const props = defineProps({
session: {
@ -121,12 +111,24 @@
},
});
const messageType = computed(() => store.state.chat.messageType);
const product = computed(() => {
return props.message.type === 'product' ? JSON.parse(props.message.content) : {};
const content = computed(() => store.getters['chat/parseContent'](props.message.payload));
const handleProduct = (id) => {
router.push({
name: 'UpdateProduct',
params: {
id,
step: 2,
},
});
const order = computed(() => {
return props.message.type === 'order' ? JSON.parse(props.message.content) : {};
};
const handleOrder = (id) => {
router.push({
name: 'OrderDetail',
params: {
id,
},
});
};
</script>
<style lang="less" scoped>

@ -23,8 +23,8 @@ export default (configEnv) => {
// target: 'http://192.168.10.109:8090/', // 显雨
// target: 'http://192.168.10.5:4500', // 高玉
// target: 'http://192.168.10.67:8090', // 罗战
target: 'http://192.168.10.93:8090', // 周渺
// target: 'https://k8s-horse-gateway.mashibing.cn/', // 测试地址
// target: 'http://192.168.10.93:8090', // 周渺
target: 'https://k8s-horse-gateway.mashibing.cn/', // 测试地址
// target: 'https://you-gateway.mashibing.com', // 生产环境
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),

Loading…
Cancel
Save