refactor: ele kefu

This commit is contained in:
xingyu4j
2025-11-06 16:27:18 +08:00
parent 36addb1380
commit c405e464a0
8 changed files with 277 additions and 272 deletions

View File

@@ -7,7 +7,7 @@ import { Page } from '@vben/common-ui';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { useWebSocket } from '@vueuse/core'; import { useWebSocket } from '@vueuse/core';
import { ElContainer, ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { useMallKefuStore } from '#/store/mall/kefu'; import { useMallKefuStore } from '#/store/mall/kefu';
@@ -104,13 +104,19 @@ onBeforeUnmount(() => {
<template> <template>
<Page auto-content-height> <Page auto-content-height>
<ElContainer class="absolute left-0 top-0 flex h-full w-full flex-1"> <div class="flex h-full antialiased">
<!-- 会话列表 --> <div class="flex h-full w-full flex-row overflow-x-hidden">
<ConversationList ref="conversationListRef" @change="handleChange" /> <!-- 会话列表 -->
<!-- 会话详情选中会话的消息列表 --> <ConversationList
<MessageList ref="messageListRef" /> class="w-1/6"
<!-- 会员信息选中会话的会员信息 --> ref="conversationListRef"
<MemberInfo ref="memberInfoRef" /> @change="handleChange"
</ElContainer> />
<!-- 会话详情选中会话的消息列表 -->
<MessageList class="w-4/6" ref="messageListRef" />
<!-- 会员信息选中会话的会员信息 -->
<MemberInfo class="w-1/6" ref="memberInfoRef" />
</div>
</div>
</Page> </Page>
</template> </template>

View File

@@ -7,7 +7,7 @@ import { confirm } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatPast, jsonParse } from '@vben/utils'; import { formatPast, jsonParse } from '@vben/utils';
import { ElAside, ElAvatar, ElBadge, ElMessage } from 'element-plus'; import { ElAvatar, ElMessage } from 'element-plus';
import { import {
deleteConversation, deleteConversation,
@@ -158,39 +158,36 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<ElAside class="!bg-card pt-[5px]" width="260px"> <div
<div class="my-[10px] font-bold text-[#999]"> class="bg-background flex flex-shrink-0 flex-col border-r border-gray-200 p-4"
会话记录({{ kefuStore.getConversationList.length }}) >
<div class="flex h-12 w-full flex-row items-center justify-between">
<span class="text-lg font-bold">会话记录</span>
<span
class="flex h-4 w-4 items-center justify-center rounded-full bg-indigo-100 text-indigo-600"
>
{{ kefuStore.getConversationList.length }}
</span>
</div> </div>
<div <div class="mt-2 flex flex-col">
v-for="item in kefuStore.getConversationList" <div class="-mx-2 mt-4 flex h-48 flex-col space-y-1 overflow-y-auto">
:key="item.id" <button
:class="{ v-for="(item, index) in kefuStore.getConversationList"
'bg-gray-500/50': item.id === activeConversationId, :key="index"
}" class="flex flex-row items-center rounded-xl p-2 hover:bg-gray-100"
class="flex h-[60px] cursor-pointer items-center px-[10px]" :class="{
@click="openRightMessage(item)" 'bg-gray-500/50': item.id === activeConversationId,
@contextmenu.prevent="rightClick($event as PointerEvent, item)" }"
> @click="openRightMessage(item)"
<div class="flex w-full items-center justify-center"> @contextmenu.prevent="rightClick($event as PointerEvent, item)"
<div class="flex h-[50px] w-[50px] items-center justify-center"> >
<!-- 头像 + 未读 --> <div
<ElBadge class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"
:hidden="item.adminUnreadMessageCount === 0"
:max="99"
:value="item.adminUnreadMessageCount"
> >
<ElAvatar :src="item.userAvatar" alt="avatar" /> <ElAvatar :src="item.userAvatar" alt="avatar" class="size-8" />
</ElBadge> </div>
</div> <div class="ml-2 text-sm font-semibold">
<div class="ml-[10px] w-full"> {{ item.userNickname || 'null' }}
<div class="flex w-full items-center justify-between">
<span class="line-clamp-1 min-w-0 max-w-[60%]">{{
item.userNickname || 'null'
}}</span>
<span class="text-[13px] text-[#999]">
{{ lastMessageTimeMap.get(item.id) ?? '计算中' }}
</span>
</div> </div>
<!-- 最后聊天内容 --> <!-- 最后聊天内容 -->
<div <div
@@ -200,9 +197,15 @@ onBeforeUnmount(() => {
item.lastMessageContent, item.lastMessageContent,
) )
" "
class="line-clamp-1 flex items-center text-[13px] text-[#999]" class="line-clamp-1 flex items-center text-sm text-gray-500"
></div> ></div>
</div> <div
v-if="item.adminUnreadMessageCount > 0"
class="ml-auto flex h-4 w-4 items-center justify-center rounded bg-red-500 text-xs leading-none text-white"
>
{{ item.adminUnreadMessageCount }}
</div>
</button>
</div> </div>
</div> </div>
@@ -210,14 +213,14 @@ onBeforeUnmount(() => {
<ul <ul
v-show="showRightMenu" v-show="showRightMenu"
:style="rightMenuStyle" :style="rightMenuStyle"
class="absolute z-[1999] m-0 w-[130px] list-none rounded-xl bg-[hsl(var(--background))] p-[5px] shadow-md" class="bg-background absolute z-[9999] m-0 w-32 list-none rounded-xl p-1 shadow-md"
> >
<li <li
v-show="!rightClickConversation.adminPinned" v-show="!rightClickConversation.adminPinned"
class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50" class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50"
@click.stop="updateConversationPinnedFn(true)" @click.stop="updateConversationPinnedFn(true)"
> >
<IconifyIcon class="mr-[5px]" icon="ep:top" /> <IconifyIcon class="mr-1" icon="lucide:arrow-up-to-line" />
置顶会话 置顶会话
</li> </li>
<li <li
@@ -225,23 +228,23 @@ onBeforeUnmount(() => {
class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50" class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50"
@click.stop="updateConversationPinnedFn(false)" @click.stop="updateConversationPinnedFn(false)"
> >
<IconifyIcon class="mr-[5px]" icon="ep:bottom" /> <IconifyIcon class="mr-1" icon="lucide:arrow-down-from-line" />
取消置顶 取消置顶
</li> </li>
<li <li
class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50" class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50"
@click.stop="deleteConversationFn" @click.stop="deleteConversationFn"
> >
<IconifyIcon class="mr-[5px]" color="red" icon="ep:delete" /> <IconifyIcon class="mr-1" color="red" icon="lucide:trash-2" />
删除会话 删除会话
</li> </li>
<li <li
class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50" class="flex cursor-pointer items-center rounded-xl px-4 py-2 transition-colors hover:bg-gray-500/50"
@click.stop="closeRightMenu" @click.stop="closeRightMenu"
> >
<IconifyIcon class="mr-[5px]" color="red" icon="ep:close" /> <IconifyIcon class="mr-1" color="red" icon="lucide:x" />
取消 取消
</li> </li>
</ul> </ul>
</ElAside> </div>
</template> </template>

View File

@@ -1,25 +1,15 @@
<!-- 右侧信息会员信息 + 最近浏览 + 交易订单 --> <!-- 右侧信息会员信息 + 最近浏览 + 交易订单 -->
<script lang="ts" setup> <script lang="ts" setup>
import type { UseScrollReturn } from '@vueuse/core';
import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation'; import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation';
import type { MemberUserApi } from '#/api/member/user'; import type { MemberUserApi } from '#/api/member/user';
import type { PayWalletApi } from '#/api/pay/wallet/balance'; import type { PayWalletApi } from '#/api/pay/wallet/balance';
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref, toRefs, watch } from 'vue';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { vScroll } from '@vueuse/components'; import { useScroll } from '@vueuse/core';
import { useDebounceFn } from '@vueuse/core'; import { ElEmpty, ElMessage } from 'element-plus';
import {
ElCard,
ElContainer,
ElEmpty,
ElHeader,
ElMain,
ElMessage,
} from 'element-plus';
import { getUser } from '#/api/member/user'; import { getUser } from '#/api/member/user';
import { getWallet } from '#/api/pay/wallet/balance'; import { getWallet } from '#/api/pay/wallet/balance';
@@ -29,7 +19,7 @@ import BasicInfo from '#/views/member/user/detail/modules/basic-info.vue';
import OrderBrowsingHistory from './order-browsing-history.vue'; import OrderBrowsingHistory from './order-browsing-history.vue';
import ProductBrowsingHistory from './product-browsing-history.vue'; import ProductBrowsingHistory from './product-browsing-history.vue';
const activeTab = ref<'交易订单' | '会员信息' | '最近浏览'>('会员信息'); const activeTab = ref<string>('会员信息');
const tabActivation = computed(() => (tab: string) => activeTab.value === tab); const tabActivation = computed(() => (tab: string) => activeTab.value === tab);
@@ -100,12 +90,16 @@ defineExpose({ initHistory });
/** 处理消息列表滚动事件(debounce 限流) */ /** 处理消息列表滚动事件(debounce 限流) */
const scrollbarRef = ref<InstanceType<any>>(); const scrollbarRef = ref<InstanceType<any>>();
const handleScroll = useDebounceFn(async (state: UseScrollReturn) => { const { arrivedState } = useScroll(scrollbarRef);
const { arrivedState } = state; const { bottom } = toRefs(arrivedState);
if (arrivedState.bottom) { watch(
await loadMore(); () => bottom.value,
} async (newVal) => {
}, 200); if (newVal) {
await loadMore();
}
},
);
/** 查询用户钱包信息 */ /** 查询用户钱包信息 */
// TODO @jaweidea 的导入报错;需要看下; // TODO @jaweidea 的导入报错;需要看下;
@@ -146,18 +140,16 @@ async function getUserData() {
</script> </script>
<template> <template>
<ElContainer <div class="bg-background relative">
class="relative w-[300px] bg-[var(--background)] after:absolute after:left-0 after:top-0 after:h-full after:w-[1px] after:scale-x-[0.3] after:bg-[var(--el-border-color)] after:content-['']" <div
> class="relative flex h-12 items-center justify-around before:absolute before:bottom-0 before:left-0 before:h-[1px] before:w-full before:scale-y-[0.3] before:bg-gray-200 before:content-['']"
<ElHeader
class="relative flex items-center justify-around bg-[var(--background)] before:absolute before:bottom-0 before:left-0 before:h-[1px] before:w-full before:scale-y-[0.3] before:bg-[var(--el-border-color)] before:content-['']"
> >
<div <div
:class="{ :class="{
'before:border-b-2 before:border-gray-500/50': 'before:border-b-2 before:border-gray-500/50':
tabActivation('会员信息'), tabActivation('会员信息'),
}" }"
class="relative flex h-full w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50" class="relative flex w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50"
@click="handleClick('会员信息')" @click="handleClick('会员信息')"
> >
会员信息 会员信息
@@ -167,7 +159,7 @@ async function getUserData() {
'before:border-b-2 before:border-gray-500/50': 'before:border-b-2 before:border-gray-500/50':
tabActivation('最近浏览'), tabActivation('最近浏览'),
}" }"
class="relative flex h-full w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50" class="relative flex w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50"
@click="handleClick('最近浏览')" @click="handleClick('最近浏览')"
> >
最近浏览 最近浏览
@@ -177,18 +169,18 @@ async function getUserData() {
'before:border-b-2 before:border-gray-500/50': 'before:border-b-2 before:border-gray-500/50':
tabActivation('交易订单'), tabActivation('交易订单'),
}" }"
class="relative flex h-full w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50" class="relative flex w-full cursor-pointer items-center justify-center before:pointer-events-none before:absolute before:inset-0 before:content-[''] hover:before:border-b-2 hover:before:border-gray-500/50"
@click="handleClick('交易订单')" @click="handleClick('交易订单')"
> >
交易订单 交易订单
</div> </div>
</ElHeader> </div>
<ElMain class="relative m-0 h-full w-full p-[10px]"> <div class="relative m-0 w-full p-2">
<template v-if="!isEmpty(conversation)"> <template v-if="!isEmpty(conversation)">
<div <div
v-loading="loading" v-loading="loading"
v-if="activeTab === '会员信息'" v-if="activeTab === '会员信息'"
class="relative h-full overflow-y-auto overflow-x-hidden" class="relative overflow-y-auto overflow-x-hidden"
> >
<!-- 基本信息 --> <!-- 基本信息 -->
<BasicInfo :user="user" mode="kefu"> <BasicInfo :user="user" mode="kefu">
@@ -197,17 +189,21 @@ async function getUserData() {
</template> </template>
</BasicInfo> </BasicInfo>
<!-- 账户信息 --> <!-- 账户信息 -->
<ElCard class="mt-10px h-full" shadow="never"> <AccountInfo
:column="1"
:user="user"
:wallet="wallet"
mode="kefu"
class="mt-2"
>
<template #title> <template #title>
<span class="text-sm font-bold">账户信息</span> <span class="text-sm font-bold">账户信息</span>
</template> </template>
<AccountInfo :column="1" :user="user" :wallet="wallet" /> </AccountInfo>
</ElCard>
</div> </div>
<div <div
v-show="activeTab !== '会员信息'" v-show="activeTab !== '会员信息'"
ref="scrollbarRef" ref="scrollbarRef"
v-scroll="handleScroll"
class="relative h-full overflow-y-auto overflow-x-hidden" class="relative h-full overflow-y-auto overflow-x-hidden"
> >
<!-- 最近浏览 --> <!-- 最近浏览 -->
@@ -227,6 +223,6 @@ async function getUserData() {
description="请选择左侧的一个会话后开始" description="请选择左侧的一个会话后开始"
class="mt-[20%]" class="mt-[20%]"
/> />
</ElMain> </div>
</ElContainer> </div>
</template> </template>

View File

@@ -53,6 +53,6 @@ defineExpose({ getHistoryList, loadMore });
:spu-id="item.spuId" :spu-id="item.spuId"
:stock="item.stock" :stock="item.stock"
:title="item.spuName" :title="item.spuName"
class="mb-10px" class="mb-2.5"
/> />
</template> </template>

View File

@@ -1,30 +1,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { UseScrollReturn } from '@vueuse/core';
import type { Emoji } from './tools/emoji'; import type { Emoji } from './tools/emoji';
import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation'; import type { MallKefuConversationApi } from '#/api/mall/promotion/kefu/conversation';
import type { MallKefuMessageApi } from '#/api/mall/promotion/kefu/message'; import type { MallKefuMessageApi } from '#/api/mall/promotion/kefu/message';
import { computed, reactive, ref, unref } from 'vue'; import { computed, reactive, ref, toRefs, unref, watch } from 'vue';
import { UserTypeEnum } from '@vben/constants'; import { UserTypeEnum } from '@vben/constants';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { formatDate, isEmpty, jsonParse } from '@vben/utils'; import { formatDate, isEmpty, jsonParse } from '@vben/utils';
import { vScroll } from '@vueuse/components'; import { useScroll } from '@vueuse/core';
import { useDebounceFn, useScroll } from '@vueuse/core';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
import { import {
ElAvatar, ElAvatar,
ElContainer,
ElEmpty, ElEmpty,
ElFooter,
ElHeader,
ElImage, ElImage,
ElInput, ElInput,
ElMain,
ElNotification, ElNotification,
} from 'element-plus'; } from 'element-plus';
@@ -209,7 +202,9 @@ async function sendMessage(msg: MallKefuMessageApi.MessageSend) {
/** 滚动到底部 */ /** 滚动到底部 */
const scrollbarRef = ref<HTMLElement | null>(null); const scrollbarRef = ref<HTMLElement | null>(null);
const { y } = useScroll(scrollbarRef); const { y, arrivedState } = useScroll(scrollbarRef);
const { bottom } = toRefs(arrivedState);
async function scrollToBottom() { async function scrollToBottom() {
if (!scrollbarRef.value) return; if (!scrollbarRef.value) return;
@@ -231,23 +226,26 @@ async function handleToNewMessage() {
await scrollToBottom(); await scrollToBottom();
} }
/** 处理消息列表滚动事件(debounce 限流) */ watch(
const handleScroll = useDebounceFn((state: UseScrollReturn) => { () => bottom.value,
const { arrivedState } = state; (newVal) => {
if (skipGetMessageList.value) { if (newVal) {
return; if (skipGetMessageList.value) {
} return;
// 滚动到底部了 }
if (arrivedState.bottom) { // 滚动到底部了
loadHistory.value = false; if (newVal) {
refreshMessageList(); loadHistory.value = false;
} refreshMessageList();
}
// 触顶自动加载下一页数据 // 触顶自动加载下一页数据
if (arrivedState.top) { if (arrivedState.top) {
handleOldMessage(); handleOldMessage();
} }
}, 200); }
},
);
/** 加载历史消息 */ /** 加载历史消息 */
async function handleOldMessage() { async function handleOldMessage() {
loadHistory.value = true; loadHistory.value = true;
@@ -258,7 +256,7 @@ async function handleOldMessage() {
function showTime(item: MallKefuMessageApi.Message, index: number) { function showTime(item: MallKefuMessageApi.Message, index: number) {
if (unref(messageList.value)[index + 1]) { if (unref(messageList.value)[index + 1]) {
const dateString = dayjs( const dateString = dayjs(
unref(messageList.value)[index + 1].createTime, unref(messageList.value)[index + 1]!.createTime,
).fromNow(); ).fromNow();
return dateString !== dayjs(unref(item).createTime).fromNow(); return dateString !== dayjs(unref(item).createTime).fromNow();
} }
@@ -267,158 +265,160 @@ function showTime(item: MallKefuMessageApi.Message, index: number) {
</script> </script>
<template> <template>
<ElContainer <div
v-if="showMessageList()" v-if="showMessageList()"
class="bg-card relative w-[calc(100%-300px-260px)]" class="bg-background flex h-full flex-auto flex-col p-4"
> >
<ElHeader <div class="flex h-full flex-auto flex-shrink-0 flex-col">
class="!bg-card border-border flex items-center justify-between border-b" <div class="flex h-12 w-full flex-row items-center justify-between">
> <span class="text-lg font-bold">{{ conversation.userNickname }}</span>
<div class="text-lg font-bold">{{ conversation.userNickname }}</div> </div>
</ElHeader>
<ElMain class="relative m-0 p-0">
<div <div
ref="scrollbarRef" ref="scrollbarRef"
class="absolute inset-0 m-0 overflow-y-auto overflow-x-hidden p-0" class="mb-4 flex h-full flex-col overflow-x-auto rounded-lg bg-gray-100 p-2"
v-scroll="handleScroll"
> >
<!-- <div v-if="refreshContent" ref="innerRef" class="w-full px-[10px] absolute inset-0 m-0 overflow-x-hidden p-0 overflow-y-auto"> --> <div class="flex flex-col">
<!-- 消息列表 --> <!-- 消息列表 -->
<div
v-for="(item, index) in getMessageList0"
:key="item.id"
class="w-full"
>
<div class="mb-[20px] flex items-center justify-center">
<!-- 日期 -->
<div
v-if="
item.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
showTime(item, index)
"
class="w-fit rounded-lg bg-black/10 px-[5px] text-[10px] text-white"
>
{{ formatDate(item.createTime) }}
</div>
<!-- 系统消息 -->
<div
v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
class="w-fit rounded-lg bg-black/10 px-[5px] text-[10px] text-white"
>
{{ item.content }}
</div>
</div>
<div <div
:class="[ v-for="(item, index) in getMessageList0"
item.senderType === UserTypeEnum.MEMBER :key="item.id"
? 'justify-start' class="w-full"
: item.senderType === UserTypeEnum.ADMIN
? 'justify-end'
: '',
]"
class="mb-[20px] flex w-full"
> >
<ElAvatar <div class="mb-5 flex items-center justify-center">
v-if="item.senderType === UserTypeEnum.MEMBER" <!-- 日期 -->
:src="conversation.userAvatar" <div
alt="avatar" v-if="
class="h-[60px] w-[60px]" item.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
/> showTime(item, index)
<div "
:class="{ class="w-fit rounded-lg bg-black/10 px-2 text-xs"
'w-auto max-w-[50%] px-[10px] py-[5px] font-medium text-[#414141] transition-all duration-200 hover:scale-105': >
KeFuMessageContentTypeEnum.TEXT === item.contentType, {{ formatDate(item.createTime) }}
'ml-[10px] mt-[3px] rounded-bl-[10px] rounded-br-[10px] rounded-tr-[10px] bg-white': </div>
KeFuMessageContentTypeEnum.TEXT === item.contentType && <!-- 系统消息 -->
item.senderType === UserTypeEnum.MEMBER, <div
'mr-[10px] mt-[3px] rounded-bl-[10px] rounded-br-[10px] rounded-tl-[10px] bg-[rgb(206_223_255)]': v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
KeFuMessageContentTypeEnum.TEXT === item.contentType && class="w-fit rounded-lg bg-black/10 px-2 text-xs"
item.senderType === UserTypeEnum.ADMIN, >
}" {{ item.content }}
> </div>
<!-- 文本消息 --> </div>
<MessageItem :message="item"> <div
<template :class="[
v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType" item.senderType === UserTypeEnum.MEMBER
> ? 'justify-start'
<div : item.senderType === UserTypeEnum.ADMIN
v-dompurify-html=" ? 'justify-end'
replaceEmoji(getMessageContent(item).text || item.content) : '',
" ]"
class="line-height-normal h-1/1 w-full text-justify" class="mb-5 flex w-full"
></div> >
</template> <ElAvatar
</MessageItem> v-if="item.senderType === UserTypeEnum.MEMBER"
<!-- 图片消息 --> :src="conversation.userAvatar"
<MessageItem :message="item"> alt="avatar"
<ElImage class="size-8"
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType" />
:src="getMessageContent(item).picUrl || item.content" <div
class="mx-[10px] !w-[200px]" :class="{
fit="cover" 'w-auto max-w-[50%] p-1 font-medium text-gray-500 transition-all duration-200 hover:scale-105':
/> KeFuMessageContentTypeEnum.TEXT === item.contentType,
</MessageItem> 'm-1 break-words rounded-lg bg-gray-100':
<!-- 商品消息 --> KeFuMessageContentTypeEnum.TEXT === item.contentType &&
<MessageItem :message="item"> item.senderType === UserTypeEnum.MEMBER,
<ProductItem 'm-1 break-words rounded-lg bg-blue-50':
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType" KeFuMessageContentTypeEnum.TEXT === item.contentType &&
:pic-url="getMessageContent(item).picUrl" item.senderType === UserTypeEnum.ADMIN,
:price="getMessageContent(item).price" }"
:sales-count="getMessageContent(item).salesCount" >
:spu-id="getMessageContent(item).spuId" <!-- 文本消息 -->
:stock="getMessageContent(item).stock" <MessageItem :message="item">
:title="getMessageContent(item).spuName" <template
class="mx-[10px] max-w-[300px]" v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType"
/> >
</MessageItem> <div
<!-- 订单消息 --> v-dompurify-html="
<MessageItem :message="item"> replaceEmoji(
<OrderItem getMessageContent(item).text || item.content,
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType" )
:message="item" "
class="mx-[10px] max-w-full" class="h-full w-full text-justify"
/> ></div>
</MessageItem> </template>
</MessageItem>
<!-- 图片消息 -->
<MessageItem :message="item">
<ElImage
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
:initial-index="0"
:src="getMessageContent(item).picUrl || item.content"
class="mx-2 !w-52"
fit="cover"
/>
</MessageItem>
<!-- 商品消息 -->
<MessageItem :message="item">
<ProductItem
v-if="
KeFuMessageContentTypeEnum.PRODUCT === item.contentType
"
:pic-url="getMessageContent(item).picUrl"
:price="getMessageContent(item).price"
:sales-count="getMessageContent(item).salesCount"
:spu-id="getMessageContent(item).spuId"
:stock="getMessageContent(item).stock"
:title="getMessageContent(item).spuName"
class="mx-2 max-w-80"
/>
</MessageItem>
<!-- 订单消息 -->
<MessageItem :message="item">
<OrderItem
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType"
:message="item"
class="mx-2 max-w-full"
/>
</MessageItem>
</div>
<ElAvatar
v-if="item.senderType === UserTypeEnum.ADMIN"
:src="item.senderAvatar"
alt="avatar"
/>
</div> </div>
<ElAvatar
v-if="item.senderType === UserTypeEnum.ADMIN"
:src="item.senderAvatar"
alt="avatar"
/>
</div> </div>
</div> </div>
<!-- </div> -->
</div> </div>
<div <div
v-show="showNewMessageTip" v-show="showNewMessageTip"
class="absolute bottom-[35px] right-[35px] z-10 flex cursor-pointer items-center rounded-[30px] bg-[var(--background)] p-[10px] text-xs shadow-md" class="absolute bottom-9 right-9 z-10 flex cursor-pointer items-center rounded-lg p-2 text-xs shadow-md"
@click="handleToNewMessage" @click="handleToNewMessage"
> >
<span>有新消息</span> <span>有新消息</span>
<IconifyIcon class="ml-5px" icon="ep:bottom" /> <IconifyIcon class="ml-1" icon="lucide:arrow-down-from-line" />
</div> </div>
</ElMain> <div class="flex flex-col">
<ElFooter class="!bg-card m-0 flex !h-auto flex-col p-0"> <div class="border-border -mt-3 flex flex-col rounded-xl border">
<div class="flex h-[44px] w-full items-center"> <div class="flex h-10 w-full items-center">
<EmojiSelectPopover @select-emoji="handleEmojiSelect" /> <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
<PictureSelectUpload <PictureSelectUpload
class="ml-[15px] mt-[3px] cursor-pointer" class="ml-4 mt-1 cursor-pointer"
@send-picture="handleSendPicture" @send-picture="handleSendPicture"
/> />
</div>
<ElInput
type="textarea"
v-model="message"
:rows="4"
class="border-none p-2"
placeholder="输入消息Enter发送Shift+Enter换行"
@keyup.enter="handleSendMessage"
/>
</div>
</div> </div>
<ElInput </div>
type="textarea" </div>
v-model="message" <div v-else class="bg-background relative">
:rows="6" <ElEmpty description="请选择左侧的一个会话后开始" class="mt-[20%]" />
placeholder="输入消息Enter发送Shift+Enter换行" </div>
style="border-style: none"
@keyup.enter="handleSendMessage"
/>
</ElFooter>
</ElContainer>
<ElContainer v-else class="bg-card relative w-[calc(100%-300px-260px)]">
<ElMain>
<ElEmpty description="请选择左侧的一个会话后开始" class="mt-[20%]" />
</ElMain>
</ElContainer>
</template> </template>

View File

@@ -34,19 +34,19 @@ function openDetail(id: number) {
*/ */
function formatOrderColor(order: any) { function formatOrderColor(order: any) {
if (order.status === 0) { if (order.status === 0) {
return 'text-[#999]'; return 'text-gray-500';
} }
if ( if (
order.status === 10 || order.status === 10 ||
order.status === 20 || order.status === 20 ||
(order.status === 30 && !order.commentStatus) (order.status === 30 && !order.commentStatus)
) { ) {
return 'text-[#faad14]'; return 'text-orange-500';
} }
if (order.status === 30 && order.commentStatus) { if (order.status === 30 && order.commentStatus) {
return 'text-[#52c41a]'; return 'text-green-500';
} }
return 'text-[#ff3000]'; return 'text-red-500';
} }
/** /**
@@ -81,10 +81,10 @@ function formatOrderStatus(order: any) {
<div <div
v-if="isObject(getMessageContent)" v-if="isObject(getMessageContent)"
:key="getMessageContent.id" :key="getMessageContent.id"
class="mb-[10px] rounded-[10px] bg-gray-500/30 p-[10px]" class="mb-2.5 rounded-md bg-gray-500/30 p-2.5"
> >
<div class="flex h-[28px] items-center justify-between px-[5px] font-bold"> <div class="flex h-7 items-center justify-between px-1.5 font-bold">
<div class="text-[13px]"> <div class="text-sm">
订单号 订单号
<span <span
class="cursor-pointer hover:text-[var(--left-menu-bg-active-color)] hover:underline" class="cursor-pointer hover:text-[var(--left-menu-bg-active-color)] hover:underline"
@@ -95,7 +95,7 @@ function formatOrderStatus(order: any) {
</div> </div>
<div <div
:class="formatOrderColor(getMessageContent)" :class="formatOrderColor(getMessageContent)"
class="text-[13px] font-bold" class="text-sm font-bold"
> >
{{ formatOrderStatus(getMessageContent) }} {{ formatOrderStatus(getMessageContent) }}
</div> </div>
@@ -103,7 +103,7 @@ function formatOrderStatus(order: any) {
<div <div
v-for="item in getMessageContent.items" v-for="item in getMessageContent.items"
:key="item.id" :key="item.id"
class="border-b" class="border-b border-gray-200"
> >
<ProductItem <ProductItem
:num="item.count" :num="item.count"
@@ -116,12 +116,12 @@ function formatOrderStatus(order: any) {
:title="item.spuName" :title="item.spuName"
/> />
</div> </div>
<div class="flex justify-end pr-[5px] pt-[10px] font-bold"> <div class="flex justify-end pr-1.5 pt-2.5 font-bold">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-[13px] leading-normal"> <div class="text-sm leading-normal">
共 {{ getMessageContent?.productCount }} 件商品,总金额: 共 {{ getMessageContent?.productCount }} 件商品,总金额:
</div> </div>
<div class="font-[OPPOSANS] text-[13px] leading-normal"> <div class="text-sm font-medium leading-normal">
{{ fenToYuan(getMessageContent?.payPrice) }} {{ fenToYuan(getMessageContent?.payPrice) }}
</div> </div>
</div> </div>

View File

@@ -42,11 +42,11 @@ function openDetail(spuId: number) {
<template> <template>
<div <div
class="mb-[10px] flex w-full cursor-pointer items-center rounded-lg bg-gray-500/30 p-[10px]" class="mb-2.5 flex w-full cursor-pointer items-center rounded-lg bg-gray-500/30 p-2.5"
@click.stop="openDetail(spuId)" @click.stop="openDetail(spuId)"
> >
<!-- 左侧商品图片--> <!-- 左侧商品图片-->
<div class="mr-6 w-[70px]"> <div class="mr-2 w-16">
<ElImage <ElImage
:initial-index="0" :initial-index="0"
:preview-src-list="[picUrl]" :preview-src-list="[picUrl]"
@@ -65,8 +65,8 @@ function openDetail(spuId: number) {
<span>销量: {{ salesCount || 0 }}</span> <span>销量: {{ salesCount || 0 }}</span>
</div> </div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-[#ff3000]">{{ fenToYuan(price) }}</span> <span class="text-red-500">{{ fenToYuan(price) }}</span>
<ElButton size="small" text type="primary">详情</ElButton> <ElButton size="small" type="text">详情</ElButton>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -58,17 +58,17 @@ export interface Emoji {
url: string; url: string;
} }
export const useEmoji = () => { export function useEmoji() {
const emojiPathList = ref<any[]>([]); const emojiPathList = ref<any[]>([]);
/** 加载本地图片 */ /** 加载本地图片 */
const initStaticEmoji = async () => { async function initStaticEmoji() {
const pathList = import.meta.glob('../../asserts/*.{png,jpg,jpeg,svg}'); const pathList = import.meta.glob('../../asserts/*.{png,jpg,jpeg,svg}');
for (const path in pathList) { for (const path in pathList) {
const imageModule: any = await pathList[path]?.(); const imageModule: any = await pathList[path]?.();
emojiPathList.value.push({ path, src: imageModule.default }); emojiPathList.value.push({ path, src: imageModule.default });
} }
}; }
/** 初始化 */ /** 初始化 */
onMounted(async () => { onMounted(async () => {
@@ -83,7 +83,7 @@ export const useEmoji = () => {
* @return 替换后的文本 * @return 替换后的文本
* @param content 消息内容 * @param content 消息内容
*/ */
const replaceEmoji = (content: string) => { function replaceEmoji(content: string) {
let newData = content; let newData = content;
if (typeof newData !== 'object') { if (typeof newData !== 'object') {
const reg = /\[(.+?)\]/g; // [] 中括号 const reg = /\[(.+?)\]/g; // [] 中括号
@@ -99,7 +99,7 @@ export const useEmoji = () => {
} }
} }
return newData; return newData;
}; }
/** 获得所有表情 */ /** 获得所有表情 */
function getEmojiList(): Emoji[] { function getEmojiList(): Emoji[] {
@@ -123,4 +123,4 @@ export const useEmoji = () => {
} }
return { replaceEmoji, getEmojiList }; return { replaceEmoji, getEmojiList };
}; }