refactor(layout): move safe-area logic to HeaderBar with fallback to scaffold

This commit is contained in:
Clover You
2026-02-25 15:59:12 +08:00
committed by Dawn
parent 90a9150ac4
commit 9d3c37b22d
6 changed files with 424 additions and 388 deletions

View File

@@ -6,8 +6,6 @@
'bg-cover bg-center bg-no-repeat': props.backgroundImage 'bg-cover bg-center bg-no-repeat': props.backgroundImage
}" }"
:style="mergedStyle"> :style="mergedStyle">
<!-- 顶部安全区域 -->
<div :class="[{ 'safe-area-top': safeAreaTop }, props.topSafeAreaClass]" />
<!-- 内容区域 --> <!-- 内容区域 -->
<div class="flex-1 min-h-0"> <div class="flex-1 min-h-0">
@@ -234,9 +232,6 @@ useMitt.on(WsResponseMessageType.RECEIVE_MESSAGE, async (data: MessageType) => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.safe-area-top {
padding-top: var(--safe-area-inset-top);
}
.safe-area-bottom { .safe-area-bottom {
padding-bottom: var(--safe-area-inset-bottom); padding-bottom: var(--safe-area-inset-bottom);
} }

View File

@@ -3,7 +3,7 @@
<img v-if="bgmURL" :src="bgmURL" class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" /> <img v-if="bgmURL" :src="bgmURL" class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<!-- 页面容器 --> <!-- 页面容器 -->
<div class="flex w-full items-start flex-col flex-1 min-h-0 z-1"> <div class="flex w-full items-start flex-col flex-1 min-h-0 z-1">
<div class="w-full"> <div class="w-full" :class="{ 'pt-[var(--safe-area-inset-top)]': safeAreaRef }">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<!-- 消息内容区 --> <!-- 消息内容区 -->
@@ -19,11 +19,31 @@
<script setup lang="ts"> <script setup lang="ts">
import bgImg from '@/assets/mobile/chat-home/background.webp' import bgImg from '@/assets/mobile/chat-home/background.webp'
const { showFooter = true, background = true } = defineProps<{ showFooter?: boolean; background?: string | boolean }>() const {
showFooter = true,
background = true,
safeArea = true
} = defineProps<{
showFooter?: boolean
background?: string | boolean
safeArea?: boolean
}>()
const bgmURL = computed(() => { const bgmURL = computed(() => {
return typeof background === 'boolean' && background ? bgImg : background return typeof background === 'boolean' && background ? bgImg : background
}) })
const safeAreaRef = useSafeArea(() => safeArea)
function useSafeArea(getter: () => boolean) {
const safeArea = ref(true)
// 移除脚手架提供的安全区域样式,交由组件自己控制
const removeSafeArea = () => {
safeArea.value = false
}
provide('removeSafeArea', removeSafeArea)
return computed(() => safeArea.value && getter())
}
</script> </script>
<style lang="scss"></style> <style lang="scss"></style>

View File

@@ -1,5 +1,6 @@
<template> <template>
<div class="w-full h-[56px] grid grid-cols-[100px_1fr_100px] z-2 bg-background text-foreground"> <div
class="w-full h-[56px] grid grid-cols-[100px_1fr_100px] z-2 bg-background text-foreground pt-[var(--safe-area-inset-top)]">
<div @click="handleBack" class="w-full h-full flex items-center"> <div @click="handleBack" class="w-full h-full flex items-center">
<svg class="iconpark-icon w-24px h-24px ms-16px p-5px"> <svg class="iconpark-icon w-24px h-24px ms-16px p-5px">
<use href="#fanhui" class="text-foreground"></use> <use href="#fanhui" class="text-foreground"></use>
@@ -51,6 +52,10 @@ const props = withDefaults(defineProps<HeaderBarProps>(), {
border: false border: false
}) })
// 移除脚手架提供的安全区域样式,交由组件自己控制
const removeScaffoldSaveArea = inject<() => void>('removeSafeArea', () => undefined)
removeScaffoldSaveArea()
const emits = defineEmits<(e: 'roomNameClick', payload: HeaderBarProps) => void>() const emits = defineEmits<(e: 'roomNameClick', payload: HeaderBarProps) => void>()
const handleRoomNameClick = () => { const handleRoomNameClick = () => {

View File

@@ -1,162 +1,165 @@
<template> <template>
<div class="flex flex-col overflow-auto h-full relative"> <AutoFixHeightPage>
<img <template #container>
src="@/assets/mobile/chat-home/background.webp" <div class="flex flex-col overflow-auto h-full relative">
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" /> <!-- 页面蒙板 -->
<!-- 页面蒙板 --> <div
<div v-if="showMask"
v-if="showMask" @touchend="maskHandler.close"
@touchend="maskHandler.close" @click="maskHandler.close"
@click="maskHandler.close" class="fixed inset-0 bg-black/20 backdrop-blur-sm z-[999] transition-all duration-3000 ease-in-out opacity-100"></div>
class="fixed inset-0 bg-black/20 backdrop-blur-sm z-[999] transition-all duration-3000 ease-in-out opacity-100"></div>
<!-- 导航条 --> <!-- 导航条 -->
<NavBar> <NavBar>
<template #center> <template #center>
<n-text>{{ t('mobile_contact.title') }}</n-text> <n-text>{{ t('mobile_contact.title') }}</n-text>
</template> </template>
<template #right> <template #right>
<n-dropdown <n-dropdown
@on-clickoutside="addIconHandler.clickOutside" @on-clickoutside="addIconHandler.clickOutside"
@select="addIconHandler.select" @select="addIconHandler.select"
trigger="click" trigger="click"
:show-arrow="true" :show-arrow="true"
:options="uiViewsData.addOptions"> :options="uiViewsData.addOptions">
<n-button round strong secondary @click="addIconHandler.open"> <n-button round strong secondary @click="addIconHandler.open">
<template #icon> <template #icon>
<n-icon> <n-icon>
<svg><use href="#plus"></use></svg> <svg><use href="#plus"></use></svg>
</n-icon> </n-icon>
</template>
</n-button>
</n-dropdown>
</template>
</NavBar>
<!-- 输入框 -->
<div class="px-16px mt-2 mb-12px z-1">
<n-input
id="search"
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="t('mobile_contact.input.search')">
<template #prefix>
<svg class="w-12px h-12px"><use href="#search"></use></svg>
</template>
</n-input>
</div>
<n-card
:segmented="{ content: true, footer: 'soft' }"
:bordered="false"
class="custom-rounded flex-1"
header-class="py-15px! px-16px! text-14px!"
:title="t('mobile_contact.my_chat')">
<template #header-extra>
<div @click="toMessage" class="h-full flex items-center justify-end">
<span
v-if="contactUnreadCount > 0"
class="px-4px py-4px rounded-999px bg-#c14053 text-white text-12px font-600 min-w-20px text-center">
{{ contactUnreadCount > 99 ? '99+' : contactUnreadCount }}
</span>
</div>
<div @click="toMessage" class="h-full flex justify-end items-center">
<img src="@/assets/mobile/friend/right-arrow.webp" class="block h-20px dark:invert" alt="" />
</div>
</template>
<n-tabs type="segment" animated class="mt-4px p-[4px_10px_0px_8px]">
<n-tab-pane name="1" :tab="t('mobile_contact.tab.contacts')">
<n-collapse :display-directive="'show'" accordion :default-expanded-names="['1']">
<ContextMenu @contextmenu="showMenu($event)" @select="handleSelect($event.label)" :menu="menuList">
<n-collapse-item :title="t('mobile_contact.friend.title')" name="1">
<template #header-extra>
<span class="text-(10px #707070)">{{ onlineCount }}/{{ contactStore.contactsList.length }}</span>
</template> </template>
<n-scrollbar style="max-height: calc(100vh - (340px + var(--safe-area-inset-top)))"> </n-button>
<!-- 用户框 多套一层div来移除默认的右键事件然后覆盖掉因为margin空隙而导致右键可用 --> </n-dropdown>
<div @contextmenu.stop="$event.preventDefault()"> </template>
<n-flex </NavBar>
:size="10"
@click="handleClick(item.uid, RoomTypeEnum.SINGLE)" <!-- 输入框 -->
:class="{ active: activeItem === item.uid }" <div class="px-16px mt-2 mb-12px z-1">
<n-input
id="search"
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="t('mobile_contact.input.search')">
<template #prefix>
<svg class="w-12px h-12px"><use href="#search"></use></svg>
</template>
</n-input>
</div>
<n-card
:segmented="{ content: true, footer: 'soft' }"
:bordered="false"
class="custom-rounded flex-1"
header-class="py-15px! px-16px! text-14px!"
:title="t('mobile_contact.my_chat')">
<template #header-extra>
<div @click="toMessage" class="h-full flex items-center justify-end">
<span
v-if="contactUnreadCount > 0"
class="px-4px py-4px rounded-999px bg-#c14053 text-white text-12px font-600 min-w-20px text-center">
{{ contactUnreadCount > 99 ? '99+' : contactUnreadCount }}
</span>
</div>
<div @click="toMessage" class="h-full flex justify-end items-center">
<img src="@/assets/mobile/friend/right-arrow.webp" class="block h-20px dark:invert" alt="" />
</div>
</template>
<n-tabs type="segment" animated class="mt-4px p-[4px_10px_0px_8px]">
<n-tab-pane name="1" :tab="t('mobile_contact.tab.contacts')">
<n-collapse :display-directive="'show'" accordion :default-expanded-names="['1']">
<ContextMenu @contextmenu="showMenu($event)" @select="handleSelect($event.label)" :menu="menuList">
<n-collapse-item :title="t('mobile_contact.friend.title')" name="1">
<template #header-extra>
<span class="text-(10px #707070)">{{ onlineCount }}/{{ contactStore.contactsList.length }}</span>
</template>
<n-scrollbar style="max-height: calc(100vh - (340px + var(--safe-area-inset-top)))">
<!-- 用户框 多套一层div来移除默认的右键事件然后覆盖掉因为margin空隙而导致右键可用 -->
<div @contextmenu.stop="$event.preventDefault()">
<n-flex
:size="10"
@click="handleClick(item.uid, RoomTypeEnum.SINGLE)"
:class="{ active: activeItem === item.uid }"
class="item-box w-full h-75px mb-5px"
v-for="item in sortedContacts"
:key="item.uid">
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-avatar
round
style="border: 1px solid var(--avatar-border-color)"
:size="44"
class="grayscale"
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
:src="AvatarUtils.getAvatarUrl(groupStore.getUserInfo(item.uid)?.avatar!)"
fallback-src="/logo.png" />
<n-flex vertical justify="space-between" class="h-fit flex-1 truncate">
<span class="text-14px leading-tight flex-1 truncate">
{{ groupStore.getUserInfo(item.uid)?.name }}
</span>
<div class="text leading-tight text-12px flex-y-center gap-4px flex-1 truncate">
[
<template v-if="isBotUser(item.uid)">助手</template>
<template v-else-if="getUserState(item.uid)">
<img class="size-12px rounded-50%" :src="getUserState(item.uid)?.url" alt="" />
{{ getUserState(item.uid)?.title }}
</template>
<template v-else>
<n-badge
:color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'"
dot />
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
</template>
]
</div>
</n-flex>
</n-flex>
</n-flex>
</div>
</n-scrollbar>
</n-collapse-item>
</ContextMenu>
</n-collapse>
</n-tab-pane>
<n-tab-pane name="2" :tab="t('mobile_contact.tab.group')">
<n-collapse :display-directive="'show'" accordion :default-expanded-names="['1']">
<n-collapse-item :title="t('mobile_contact.group.title')" name="1">
<template #header-extra>
<span class="text-(10px #707070)">{{ groupChatList.length }}</span>
</template>
<n-scrollbar style="max-height: calc(100vh - (340px + var(--safe-area-inset-top)))">
<div
@click="handleClick(item.roomId, RoomTypeEnum.GROUP)"
:class="{ active: activeItem === item.roomId }"
class="item-box w-full h-75px mb-5px" class="item-box w-full h-75px mb-5px"
v-for="item in sortedContacts" v-for="item in groupChatList"
:key="item.uid"> :key="item.roomId">
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate"> <n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-avatar <n-avatar
round round
style="border: 1px solid var(--avatar-border-color)" style="border: 1px solid var(--avatar-border-color)"
bordered
:size="44" :size="44"
class="grayscale" :src="AvatarUtils.getAvatarUrl(item.avatar)"
:class="{ 'grayscale-0': item.activeStatus === OnlineEnum.ONLINE }"
:src="AvatarUtils.getAvatarUrl(groupStore.getUserInfo(item.uid)?.avatar!)"
fallback-src="/logo.png" /> fallback-src="/logo.png" />
<n-flex vertical justify="space-between" class="h-fit flex-1 truncate"> <span class="text-14px leading-tight flex-1 truncate">{{ item.remark || item.groupName }}</span>
<span class="text-14px leading-tight flex-1 truncate">
{{ groupStore.getUserInfo(item.uid)?.name }}
</span>
<div class="text leading-tight text-12px flex-y-center gap-4px flex-1 truncate">
[
<template v-if="isBotUser(item.uid)">助手</template>
<template v-else-if="getUserState(item.uid)">
<img class="size-12px rounded-50%" :src="getUserState(item.uid)?.url" alt="" />
{{ getUserState(item.uid)?.title }}
</template>
<template v-else>
<n-badge :color="item.activeStatus === OnlineEnum.ONLINE ? '#1ab292' : '#909090'" dot />
{{ item.activeStatus === OnlineEnum.ONLINE ? '在线' : '离线' }}
</template>
]
</div>
</n-flex>
</n-flex> </n-flex>
</n-flex> </div>
</div> </n-scrollbar>
</n-scrollbar> </n-collapse-item>
</n-collapse-item> </n-collapse>
</ContextMenu> </n-tab-pane>
</n-collapse> </n-tabs>
</n-tab-pane> </n-card>
<n-tab-pane name="2" :tab="t('mobile_contact.tab.group')"> </div>
<n-collapse :display-directive="'show'" accordion :default-expanded-names="['1']"> </template>
<n-collapse-item :title="t('mobile_contact.group.title')" name="1"> </AutoFixHeightPage>
<template #header-extra>
<span class="text-(10px #707070)">{{ groupChatList.length }}</span>
</template>
<n-scrollbar style="max-height: calc(100vh - (340px + var(--safe-area-inset-top)))">
<div
@click="handleClick(item.roomId, RoomTypeEnum.GROUP)"
:class="{ active: activeItem === item.roomId }"
class="item-box w-full h-75px mb-5px"
v-for="item in groupChatList"
:key="item.roomId">
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
<n-avatar
round
style="border: 1px solid var(--avatar-border-color)"
bordered
:size="44"
:src="AvatarUtils.getAvatarUrl(item.avatar)"
fallback-src="/logo.png" />
<span class="text-14px leading-tight flex-1 truncate">{{ item.remark || item.groupName }}</span>
</n-flex>
</div>
</n-scrollbar>
</n-collapse-item>
</n-collapse>
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template> </template>
<style scoped> <style scoped>
.custom-rounded { .custom-rounded {

View File

@@ -1,207 +1,215 @@
<template> <template>
<div class="flex flex-col overflow-auto h-full relative"> <AutoFixHeightPage>
<img <template #container>
src="@/assets/mobile/chat-home/background.webp" <div class="flex flex-col overflow-auto h-full relative">
class="absolute fixed top-0 l-0 w-full h-full z-0 dark:opacity-20" /> <!-- 页面蒙板 -->
<div
v-if="showMask"
@touchend="maskHandler.close"
@mouseup="maskHandler.close"
:class="[
longPressState.longPressActive
? ''
: 'bg-black/20 backdrop-blur-sm transition-all duration-3000 ease-in-out opacity-100'
]"
class="fixed inset-0 z-[999]"></div>
<!-- 页面蒙板 --> <NavBar>
<div <template #left>
v-if="showMask" <n-flex @click="toSimpleBio" align="center" :size="6" class="w-full">
@touchend="maskHandler.close" <n-avatar
@mouseup="maskHandler.close" :size="38"
:class="[ :src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar ? userStore.userInfo.avatar : '/logoD.png')"
longPressState.longPressActive fallback-src="/logo.png"
? '' round />
: 'bg-black/20 backdrop-blur-sm transition-all duration-3000 ease-in-out opacity-100'
]"
class="fixed inset-0 z-[999]"></div>
<NavBar> <n-flex vertical justify="center" :size="6">
<template #left> <p
<n-flex @click="toSimpleBio" align="center" :size="6" class="w-full"> style="
<n-avatar font-weight: bold !important;
:size="38" font-family:
:src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar ? userStore.userInfo.avatar : '/logoD.png')" system-ui,
fallback-src="/logo.png" -apple-system,
round /> sans-serif;
"
<n-flex vertical justify="center" :size="6"> class="text-(16px [--text-color])">
<p {{ userStore.userInfo?.name ? userStore.userInfo.name : t('mobile_home.noname') }}
style=" </p>
font-weight: bold !important; <p class="text-(10px [--text-color])">
font-family: {{
system-ui, userStore.userInfo?.uid
-apple-system, ? groupStore.getUserInfo(userStore.userInfo!.uid)?.locPlace || t('mobile_home.china')
sans-serif; : t('mobile_home.china')
" }}
class="text-(16px [--text-color])"> </p>
{{ userStore.userInfo?.name ? userStore.userInfo.name : t('mobile_home.noname') }} </n-flex>
</p> </n-flex>
<p class="text-(10px [--text-color])">
{{
userStore.userInfo?.uid
? groupStore.getUserInfo(userStore.userInfo!.uid)?.locPlace || t('mobile_home.china')
: t('mobile_home.china')
}}
</p>
</n-flex>
</n-flex>
</template>
<template #right>
<n-dropdown
@on-clickoutside="addIconHandler.clickOutside"
@select="addIconHandler.select"
trigger="click"
:show-arrow="true"
:options="uiViewsData.addOptions">
<n-button round strong secondary @click="addIconHandler.open">
<template #icon>
<n-icon>
<svg><use href="#plus"></use></svg>
</n-icon>
</template>
</n-button>
<!-- <svg @click="addIconHandler.open" class="size-22px p-5px rounded-8px">
<use href="#plus"></use>
</svg> -->
</n-dropdown>
</template>
</NavBar>
<div class="px-16px mt-5px">
<div class="py-5px shrink-0">
<n-input
id="search"
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="t('mobile_home.input.search')"
@focus="lockScroll"
@blur="unlockScroll">
<template #prefix>
<svg class="w-12px h-12px">
<use href="#search"></use>
</svg>
</template> </template>
</n-input>
</div>
<n-divider class="m-0! p-0! mt-10px!" />
</div>
<van-pull-refresh <template #right>
class="flex-1" <n-dropdown
:pull-distance="100" @on-clickoutside="addIconHandler.clickOutside"
:disabled="!isEnablePullRefresh" @select="addIconHandler.select"
v-model="loading" trigger="click"
@refresh="onRefresh"> :show-arrow="true"
<div class="flex flex-col h-full"> :options="uiViewsData.addOptions">
<div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0" @scroll="onScroll" ref="scrollContainer"> <n-button round strong secondary @click="addIconHandler.open">
<van-swipe-cell <template #icon>
@open="handleSwipeOpen" <n-icon>
@close="handleSwipeClose" <svg><use href="#plus"></use></svg>
v-for="(item, idx) in sessionList" </n-icon>
v-on-long-press="[(e: PointerEvent) => handleLongPress(e, item), longPressOption]" </template>
:key="`${item.id}-${idx}`" </n-button>
class="text-black" <!-- <svg @click="addIconHandler.open" class="size-22px p-5px rounded-8px">
:class="item.top ? 'w-full bg-#64A29C18' : ''"> <use href="#plus"></use>
<!-- 长按项 --> </svg> -->
<div </n-dropdown>
@click.stop="intoRoom(item)" </template>
class="grid grid-cols-[2.2rem_1fr_max-content] items-start px-4 py-3 gap-1"> </NavBar>
<div class="flex-shrink-0">
<n-badge
:offset="[-6, 6]"
:color="item.muteNotification === NotificationTypeEnum.NOT_DISTURB ? 'grey' : '#c14053'"
:value="item.unreadCount"
:max="99">
<n-avatar :size="52" :src="AvatarUtils.getAvatarUrl(item.avatar)" fallback-src="/logo.png" round />
</n-badge>
</div>
<!-- 中间两行内容 -->
<div class="truncate pl-7 flex pt-5px gap-10px leading-tight flex-col">
<n-text class="text-16px font-bold flex-1 truncate">{{ item.name }}</n-text>
<div class="text-13px text-gray-600 dark:text-gray-400 truncate">
{{ item.text }}
</div>
</div>
<!-- 时间靠顶 --> <div class="px-16px mt-5px">
<div class="text-12px pt-9px text-right flex flex-col gap-1 items-end justify-center"> <div class="py-5px shrink-0">
<div class="flex items-center gap-1"> <n-input
<span v-if="item.hotFlag === IsAllUserEnum.Yes"> id="search"
<svg class="size-22px select-none outline-none cursor-pointer color-#13987f"> class="rounded-6px w-full relative text-12px"
<use href="#auth"></use> :maxlength="20"
</svg> clearable
</span> spellCheck="false"
<span class="text-gray-600 whitespace-nowrap"> autoComplete="off"
{{ formatTimestamp(item?.activeTime) }} autoCorrect="off"
</span> autoCapitalize="off"
</div> :placeholder="t('mobile_home.input.search')"
<div v-if="item.muteNotification === NotificationTypeEnum.NOT_DISTURB"> @focus="lockScroll"
<svg class="size-14px z-100 color-gray-500/90"> @blur="unlockScroll">
<use href="#close-remind"></use> <template #prefix>
</svg> <svg class="w-12px h-12px">
<use href="#search"></use>
</svg>
</template>
</n-input>
</div>
<n-divider class="m-0! p-0! mt-10px!" />
</div>
<van-pull-refresh
class="flex-1"
:pull-distance="100"
:disabled="!isEnablePullRefresh"
v-model="loading"
@refresh="onRefresh">
<div class="flex flex-col h-full">
<div class="flex-1 overflow-y-auto overflow-x-hidden min-h-0" @scroll="onScroll" ref="scrollContainer">
<van-swipe-cell
@open="handleSwipeOpen"
@close="handleSwipeClose"
v-for="(item, idx) in sessionList"
v-on-long-press="[(e: PointerEvent) => handleLongPress(e, item), longPressOption]"
:key="`${item.id}-${idx}`"
class="text-black"
:class="item.top ? 'w-full bg-#64A29C18' : ''">
<!-- 长按项 -->
<div
@click.stop="intoRoom(item)"
class="grid grid-cols-[2.2rem_1fr_max-content] items-start px-4 py-3 gap-1">
<div class="flex-shrink-0">
<n-badge
:offset="[-6, 6]"
:color="item.muteNotification === NotificationTypeEnum.NOT_DISTURB ? 'grey' : '#c14053'"
:value="item.unreadCount"
:max="99">
<n-avatar
:size="52"
:src="AvatarUtils.getAvatarUrl(item.avatar)"
fallback-src="/logo.png"
round />
</n-badge>
</div>
<!-- 中间两行内容 -->
<div class="truncate pl-7 flex pt-5px gap-10px leading-tight flex-col">
<n-text class="text-16px font-bold flex-1 truncate">{{ item.name }}</n-text>
<div class="text-13px text-gray-600 dark:text-gray-400 truncate">
{{ item.text }}
</div>
</div>
<!-- 时间靠顶 -->
<div class="text-12px pt-9px text-right flex flex-col gap-1 items-end justify-center">
<div class="flex items-center gap-1">
<span v-if="item.hotFlag === IsAllUserEnum.Yes">
<svg class="size-22px select-none outline-none cursor-pointer color-#13987f">
<use href="#auth"></use>
</svg>
</span>
<span class="text-gray-600 whitespace-nowrap">
{{ formatTimestamp(item?.activeTime) }}
</span>
</div>
<div v-if="item.muteNotification === NotificationTypeEnum.NOT_DISTURB">
<svg class="size-14px z-100 color-gray-500/90">
<use href="#close-remind"></use>
</svg>
</div>
</div>
</div> </div>
<template #right>
<div class="flex w-auto flex-wrap h-full">
<div
class="h-full text-14px w-80px bg-#13987f text-white flex items-center justify-center"
@click="handleToggleTop(item)">
{{ item.top ? t('mobile_home.chat.unpin') : t('mobile_home.chat.pintop') }}
</div>
<div
:class="(item?.unreadCount ?? 0) > 0 ? 'bg-#909090' : 'bg-#fbb160'"
class="h-full text-14px w-80px text-white flex items-center justify-center"
@click="handleToggleReadStatus((item?.unreadCount ?? 0) > 0, item)">
{{
(item?.unreadCount ?? 0) > 0
? t('mobile_home.chat.mark_as_read')
: t('mobile_home.chat.mark_as_unread')
}}
</div>
<div
class="h-full text-14px w-80px bg-#d5304f text-white flex items-center justify-center"
@click="handleDelete(item)">
{{ t('mobile_home.chat.delete') }}
</div>
</div>
</template>
</van-swipe-cell>
</div>
</div>
</van-pull-refresh>
<teleport to="body">
<div
v-if="longPressState.showLongPressMenu"
:style="{ top: longPressState.longPressMenuTop + 'px' }"
class="fixed gap-10px z-999 left-1/2 transform -translate-x-1/2">
<div class="flex justify-between p-18px text-16px gap-22px rounded-16px bg-#4e4e4e whitespace-nowrap">
<div class="text-white" @click="handleDelete(currentLongPressItem)">
{{ t('mobile_home.menu.delete') }}
</div>
<div class="text-white" @click="handleToggleTop(currentLongPressItem)">
{{ currentLongPressItem?.top ? t('mobile_home.menu.unpin') : t('mobile_home.menu.pintop') }}
</div>
<div class="text-white" @click="handleToggleReadStatus((currentLongPressItem?.unreadCount ?? 0) > 0)">
{{
(currentLongPressItem?.unreadCount ?? 0) > 0
? t('mobile_home.menu.read')
: t('mobile_home.menu.unread')
}}
</div> </div>
</div> </div>
<template #right> <div class="flex w-full justify-center h-15px">
<div class="flex w-auto flex-wrap h-full"> <svg width="34" height="13" viewBox="0 0 35 13">
<div <path d="M0 0 L35 0 L17.5 13 Z" fill="#4e4e4e" />
class="h-full text-14px w-80px bg-#13987f text-white flex items-center justify-center" </svg>
@click="handleToggleTop(item)"> </div>
{{ item.top ? t('mobile_home.chat.unpin') : t('mobile_home.chat.pintop') }}
</div>
<div
:class="(item?.unreadCount ?? 0) > 0 ? 'bg-#909090' : 'bg-#fbb160'"
class="h-full text-14px w-80px text-white flex items-center justify-center"
@click="handleToggleReadStatus((item?.unreadCount ?? 0) > 0, item)">
{{
(item?.unreadCount ?? 0) > 0
? t('mobile_home.chat.mark_as_read')
: t('mobile_home.chat.mark_as_unread')
}}
</div>
<div
class="h-full text-14px w-80px bg-#d5304f text-white flex items-center justify-center"
@click="handleDelete(item)">
{{ t('mobile_home.chat.delete') }}
</div>
</div>
</template>
</van-swipe-cell>
</div>
</div>
</van-pull-refresh>
<teleport to="body">
<div
v-if="longPressState.showLongPressMenu"
:style="{ top: longPressState.longPressMenuTop + 'px' }"
class="fixed gap-10px z-999 left-1/2 transform -translate-x-1/2">
<div class="flex justify-between p-18px text-16px gap-22px rounded-16px bg-#4e4e4e whitespace-nowrap">
<div class="text-white" @click="handleDelete(currentLongPressItem)">{{ t('mobile_home.menu.delete') }}</div>
<div class="text-white" @click="handleToggleTop(currentLongPressItem)">
{{ currentLongPressItem?.top ? t('mobile_home.menu.unpin') : t('mobile_home.menu.pintop') }}
</div> </div>
<div class="text-white" @click="handleToggleReadStatus((currentLongPressItem?.unreadCount ?? 0) > 0)"> </teleport>
{{
(currentLongPressItem?.unreadCount ?? 0) > 0 ? t('mobile_home.menu.read') : t('mobile_home.menu.unread')
}}
</div>
</div>
<div class="flex w-full justify-center h-15px">
<svg width="34" height="13" viewBox="0 0 35 13">
<path d="M0 0 L35 0 L17.5 13 Z" fill="#4e4e4e" />
</svg>
</div>
</div> </div>
</teleport> </template>
</div> </AutoFixHeightPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,49 +1,54 @@
<template> <template>
<div class="flex flex-col overflow-auto h-full"> <AutoFixHeightPage :show-footer="false">
<!-- 设置区 --> <template #container>
<Settings /> <div class="flex flex-col overflow-auto h-full">
<!-- 设置区 -->
<Settings />
<PersonalInfo :is-show="isShow"></PersonalInfo> <PersonalInfo :is-show="isShow"></PersonalInfo>
<div class="relative top-0 flex-1 flex"> <!-- FIX: 内容消失问题 待确定 -->
<div ref="measureRef" class="h-full w-full absolute top-0 z-0"></div> <div class="relative top-0 flex-1 flex">
<!-- 动态内容 --> <div ref="measureRef" class="h-full w-full absolute top-0 z-0"></div>
<div <!-- 动态内容 -->
ref="scrollContainer" <div
:style="{ height: tabHeight + 'px' }" ref="scrollContainer"
class="z-1 overflow-hidden mt-2 absolute z-3 w-full">
<n-card class="custom-rounded" content-class="flex flex-col gap-4 z-1 p-10px mt-4 p-15px!">
<n-scrollbar
ref="scrollbarRef"
:style="{ height: tabHeight + 'px' }" :style="{ height: tabHeight + 'px' }"
:content-style="{ overflowX: 'hidden' }" class="z-1 overflow-hidden mt-2 absolute z-3 w-full">
class="overflow-x-hidden" <n-card class="custom-rounded" content-class="flex flex-col gap-4 z-1 p-10px mt-4 p-15px!">
@scroll="handleScroll"> <n-scrollbar
<!-- 动态内容区域 --> ref="scrollbarRef"
<div class="py-12px"> :style="{ height: tabHeight + 'px' }"
<DynamicList :content-style="{ overflowX: 'hidden' }"
mode="mobile" class="overflow-x-hidden"
@preview-image="previewImage" @scroll="handleScroll">
@video-play="handleVideoPlay" <!-- 动态内容区域 -->
@load-more="loadMore" <div class="py-12px">
@item-click="handleItemClick" /> <DynamicList
</div> mode="mobile"
</n-scrollbar> @preview-image="previewImage"
</n-card> @video-play="handleVideoPlay"
</div> @load-more="loadMore"
</div> @item-click="handleItemClick" />
</div>
</n-scrollbar>
</n-card>
</div>
</div>
<div <div
@click="toPublishCommunity" @click="toPublishCommunity"
class="w-52px h-52px rounded-full absolute bottom-120px right-20px z-3 flex items-center justify-center bg-[linear-gradient(145deg,#ACD7DA,#13987F)] shadow-[0_4px_12px_rgba(0,0,0,0.25),0_0_12px_rgba(172,215,218,0.8)]"> class="w-52px h-52px rounded-full absolute bottom-120px right-20px z-3 flex items-center justify-center bg-[linear-gradient(145deg,#ACD7DA,#13987F)] shadow-[0_4px_12px_rgba(0,0,0,0.25),0_0_12px_rgba(172,215,218,0.8)]">
<div class="relative w-20px h-20px"> <div class="relative w-20px h-20px">
<!-- 竖线 --> <!-- 竖线 -->
<div class="absolute left-1/2 top-0 h-full w-2px bg-white -translate-x-1/2"></div> <div class="absolute left-1/2 top-0 h-full w-2px bg-white -translate-x-1/2"></div>
<!-- 横线 --> <!-- 横线 -->
<div class="absolute top-1/2 left-0 w-full h-2px bg-white -translate-y-1/2"></div> <div class="absolute top-1/2 left-0 w-full h-2px bg-white -translate-y-1/2"></div>
</div>
</div>
</div> </div>
</div> </template>
</div> </AutoFixHeightPage>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import PersonalInfo from '#/components/my/PersonalInfo.vue' import PersonalInfo from '#/components/my/PersonalInfo.vue'