refactor: kefu

This commit is contained in:
xingyu4j
2025-11-06 15:04:27 +08:00
parent 103f633344
commit af60802840
4 changed files with 238 additions and 236 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 { Layout, message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
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>
<Layout.Content class="absolute left-0 top-0 m-4 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"
</Layout.Content> />
<!-- 会话详情选中会话的消息列表 -->
<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 { Avatar, Badge, Layout, message } from 'ant-design-vue'; import { Avatar, message } from 'ant-design-vue';
import { import {
deleteConversation, deleteConversation,
@@ -158,38 +158,36 @@ onBeforeUnmount(() => {
</script> </script>
<template> <template>
<Layout.Sider <div
class="bg-card relative flex h-full flex-col justify-between overflow-hidden p-4" class="bg-background flex flex-shrink-0 flex-col border-r border-gray-200 p-4"
width="260px"
> >
<div class="m-4 border-b border-gray-200 pb-2 font-bold"> <div class="flex h-12 w-full flex-row items-center justify-between">
会话记录({{ kefuStore.getConversationList.length }}) <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-14 cursor-pointer items-center px-3" :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-12 w-12 items-center justify-center"> >
<!-- 头像 + 未读 --> <div
<Badge :max="99" :value="item.adminUnreadMessageCount"> class="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200"
>
<Avatar :src="item.userAvatar" alt="avatar" /> <Avatar :src="item.userAvatar" alt="avatar" />
</Badge> </div>
</div> <div class="ml-2 text-sm font-semibold">
<div class="ml-3 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-sm text-gray-500">
{{ lastMessageTimeMap.get(item.id) ?? '计算中' }}
</span>
</div> </div>
<!-- 最后聊天内容 --> <!-- 最后聊天内容 -->
<div <div
@@ -201,7 +199,13 @@ onBeforeUnmount(() => {
" "
class="line-clamp-1 flex items-center text-sm text-gray-500" 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>
@@ -242,5 +246,5 @@ onBeforeUnmount(() => {
取消 取消
</li> </li>
</ul> </ul>
</Layout.Sider> </div>
</template> </template>

View File

@@ -1,18 +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 { Empty, message } from 'ant-design-vue';
import { Empty, Layout, message } from 'ant-design-vue';
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';
@@ -93,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 的导入报错;需要看下;
@@ -139,18 +140,16 @@ async function getUserData() {
</script> </script>
<template> <template>
<Layout <div class="bg-background relative">
class="bg-card relative w-72 after:absolute after:left-0 after:top-0 after:h-full after:w-[1px] after:scale-x-[0.3] after:bg-gray-200 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-['']"
<Layout.Header
class="!bg-card relative flex 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-['']"
> >
<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('会员信息')"
> >
会员信息 会员信息
@@ -160,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('最近浏览')"
> >
最近浏览 最近浏览
@@ -170,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>
</Layout.Header> </div>
<Layout.Content class="relative m-0 h-full w-full p-2"> <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">
@@ -205,7 +204,6 @@ async function getUserData() {
<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"
> >
<!-- 最近浏览 --> <!-- 最近浏览 -->
@@ -221,6 +219,6 @@ async function getUserData() {
</div> </div>
</template> </template>
<Empty v-else description="请选择左侧的一个会话后开始" class="mt-[20%]" /> <Empty v-else description="请选择左侧的一个会话后开始" class="mt-[20%]" />
</Layout.Content> </div>
</Layout> </div>
</template> </template>

View File

@@ -1,27 +1,17 @@
<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 { Avatar, Empty, Image, notification, Textarea } from 'ant-design-vue';
import {
Avatar,
Empty,
Image,
Layout,
notification,
Textarea,
} from 'ant-design-vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime'; import relativeTime from 'dayjs/plugin/relativeTime';
@@ -206,7 +196,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;
@@ -228,23 +220,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;
@@ -264,164 +259,163 @@ function showTime(item: MallKefuMessageApi.Message, index: number) {
</script> </script>
<template> <template>
<Layout <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"
> >
<Layout.Header <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>
</Layout.Header>
<Layout.Content class="relative m-0 h-full w-full 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 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-5 flex items-center justify-center">
<!-- 日期 -->
<div
v-if="
item.contentType !== KeFuMessageContentTypeEnum.SYSTEM &&
showTime(item, index)
"
class="w-fit rounded-lg bg-black/10 px-2 text-xs text-white"
>
{{ formatDate(item.createTime) }}
</div>
<!-- 系统消息 -->
<div
v-if="item.contentType === KeFuMessageContentTypeEnum.SYSTEM"
class="w-fit rounded-lg bg-black/10 px-2 text-xs 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-5 flex w-full"
> >
<Avatar <div class="mb-5 flex items-center justify-center">
v-if="item.senderType === UserTypeEnum.MEMBER" <!-- 日期 -->
:src="conversation.userAvatar" <div
alt="avatar" v-if="
class="size-8" 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%] p-1 font-medium text-gray-500 transition-all duration-200 hover:scale-105': >
KeFuMessageContentTypeEnum.TEXT === item.contentType, {{ formatDate(item.createTime) }}
'm-1 break-words rounded-lg bg-gray-100': </div>
KeFuMessageContentTypeEnum.TEXT === item.contentType && <!-- 系统消息 -->
item.senderType === UserTypeEnum.MEMBER, <div
'm-1 break-words rounded-lg bg-blue-50': 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="h-full w-full text-justify" class="mb-5 flex w-full"
></div> >
</template> <Avatar
</MessageItem> v-if="item.senderType === UserTypeEnum.MEMBER"
<!-- 图片消息 --> :src="conversation.userAvatar"
<MessageItem :message="item"> alt="avatar"
<Image class="size-8"
v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType" />
:initial-index="0" <div
:preview-src-list="[ :class="{
getMessageContent(item).picUrl || item.content, 'w-auto max-w-[50%] p-1 font-medium text-gray-500 transition-all duration-200 hover:scale-105':
]" KeFuMessageContentTypeEnum.TEXT === item.contentType,
:src="getMessageContent(item).picUrl || item.content" 'm-1 break-words rounded-lg bg-gray-100':
class="mx-2 !w-52" KeFuMessageContentTypeEnum.TEXT === item.contentType &&
fit="contain" item.senderType === UserTypeEnum.MEMBER,
preview-teleported 'm-1 break-words rounded-lg bg-blue-50':
/> KeFuMessageContentTypeEnum.TEXT === item.contentType &&
</MessageItem> item.senderType === UserTypeEnum.ADMIN,
<!-- 商品消息 --> }"
<MessageItem :message="item"> >
<ProductItem <!-- 文本消息 -->
v-if="KeFuMessageContentTypeEnum.PRODUCT === item.contentType" <MessageItem :message="item">
:pic-url="getMessageContent(item).picUrl" <template
:price="getMessageContent(item).price" v-if="KeFuMessageContentTypeEnum.TEXT === item.contentType"
:sales-count="getMessageContent(item).salesCount" >
:spu-id="getMessageContent(item).spuId" <div
:stock="getMessageContent(item).stock" v-dompurify-html="
:title="getMessageContent(item).spuName" replaceEmoji(
class="mx-2 max-w-80" getMessageContent(item).text || item.content,
/> )
</MessageItem> "
<!-- 订单消息 --> class="h-full w-full text-justify"
<MessageItem :message="item"> ></div>
<OrderItem </template>
v-if="KeFuMessageContentTypeEnum.ORDER === item.contentType" </MessageItem>
:message="item" <!-- 图片消息 -->
class="mx-2 max-w-full" <MessageItem :message="item">
/> <Image
</MessageItem> v-if="KeFuMessageContentTypeEnum.IMAGE === item.contentType"
:initial-index="0"
:preview-src-list="[
getMessageContent(item).picUrl || item.content,
]"
:src="getMessageContent(item).picUrl || item.content"
class="mx-2 !w-52"
fit="contain"
preview-teleported
/>
</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>
<Avatar
v-if="item.senderType === UserTypeEnum.ADMIN"
:src="item.senderAvatar"
alt="avatar"
/>
</div> </div>
<Avatar
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="bg-card absolute bottom-9 right-9 z-10 flex cursor-pointer items-center rounded-full p-2.5 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-1" icon="lucide:arrow-down-from-line" /> <IconifyIcon class="ml-1" icon="lucide:arrow-down-from-line" />
</div> </div>
</Layout.Content> <div class="flex flex-col">
<Layout.Footer class="!bg-card m-0 flex flex-col p-0"> <div class="border-border -mt-3 flex flex-col rounded-xl border">
<div class="border-border flex flex-col rounded-xl border p-2"> <div class="flex h-10 w-full items-center">
<div class="flex h-11 w-full items-center"> <EmojiSelectPopover @select-emoji="handleEmojiSelect" />
<EmojiSelectPopover @select-emoji="handleEmojiSelect" /> <PictureSelectUpload
<PictureSelectUpload class="ml-4 mt-1 cursor-pointer"
class="ml-4 mt-1 cursor-pointer" @send-picture="handleSendPicture"
@send-picture="handleSendPicture" />
</div>
<Textarea
v-model:value="message"
:rows="4"
class="border-none p-2"
placeholder="输入消息Enter发送Shift+Enter换行"
@press-enter="handleSendMessage"
/> />
</div> </div>
<Textarea
v-model:value="message"
:rows="6"
class="border-none"
placeholder="输入消息Enter发送Shift+Enter换行"
@press-enter="handleSendMessage"
/>
</div> </div>
</Layout.Footer> </div>
</Layout> </div>
<Layout v-else class="bg-card relative w-[calc(100vw-300px-260px)]"> <div v-else class="bg-background relative">
<Layout.Content> <Empty description="请选择左侧的一个会话后开始" class="mt-[20%]" />
<Empty description="请选择左侧的一个会话后开始" class="mt-[20%]" /> </div>
</Layout.Content>
</Layout>
</template> </template>