优化了消息数量,筛选和顶部部分

This commit is contained in:
yml2213
2026-06-13 19:03:29 +08:00
parent 3b4cd76e42
commit 9d7184ecde
12 changed files with 223 additions and 12 deletions
+45 -3
View File
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { useRoute, RouterLink } from 'vue-router'
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
const route = useRoute()
const { unreadCount, unreadLabel } = useChatUnreadCount(route)
function isNavActive(path: string) {
if (path === '/m') return route.path === '/m'
@@ -16,7 +18,10 @@ function isNavActive(path: string) {
<span>首页</span>
</RouterLink>
<RouterLink to="/m/messages" class="nav-item" :class="{ active: isNavActive('/m/messages') }">
<van-icon name="chat-o" :size="22" />
<span class="nav-icon-wrap">
<van-icon name="chat-o" :size="22" />
<em v-if="unreadCount > 0" class="nav-badge">{{ unreadLabel }}</em>
</span>
<span>消息</span>
</RouterLink>
<RouterLink to="/m/seller/listings/create" class="nav-item nav-publish">
@@ -39,16 +44,26 @@ function isNavActive(path: string) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
bottom: -1px;
z-index: 100;
display: flex;
background: #fff;
border-top: 1px solid #eee;
padding-bottom: env(safe-area-inset-bottom);
height: calc(50px + env(safe-area-inset-bottom));
height: calc(52px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.bottom-nav::after {
position: absolute;
right: 0;
bottom: -18px;
left: 0;
height: 18px;
background: #fff;
content: '';
}
.nav-item {
flex: 1;
display: flex;
@@ -69,6 +84,33 @@ function isNavActive(path: string) {
font-weight: 600;
}
.nav-icon-wrap {
position: relative;
display: grid;
width: 24px;
height: 24px;
place-items: center;
}
.nav-badge {
position: absolute;
top: -5px;
right: -10px;
min-width: 16px;
height: 16px;
padding: 0 4px;
box-sizing: border-box;
border: 2px solid #fff;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-style: normal;
font-weight: 800;
line-height: 12px;
text-align: center;
}
.publish-pill {
width: 36px;
height: 26px;
+10
View File
@@ -52,6 +52,16 @@ export async function fetchChats(page = 1, pageSize = 20) {
return data.data
}
export async function fetchUnreadChatCount() {
const { data } = await apiClient.get<ApiResponse<{ unread_count: number }>>(
'/chats/unread-count',
{
silent: true,
}
)
return data.data.unread_count
}
export async function fetchChat(id: number) {
const { data } = await apiClient.get<ApiResponse<ChatConversation>>(`/chats/${id}`)
return data.data
@@ -0,0 +1,77 @@
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import type { RouteLocationNormalizedLoaded } from 'vue-router'
import { fetchUnreadChatCount } from '@/features/chats/api/chats'
import { useSessionStore } from '@/stores/session'
export function useChatUnreadCount(route: RouteLocationNormalizedLoaded) {
const session = useSessionStore()
const unreadCount = ref(0)
let timer: number | null = null
let requestID = 0
const unreadLabel = computed(() => (unreadCount.value > 99 ? '99+' : String(unreadCount.value)))
function stopPolling() {
if (timer) {
window.clearInterval(timer)
timer = null
}
}
async function loadUnreadCount() {
if (!session.isLoggedIn) {
unreadCount.value = 0
return
}
const currentID = ++requestID
try {
const count = await fetchUnreadChatCount()
if (currentID === requestID) {
unreadCount.value = count
}
} catch {
// 底部导航不弹错误提示,保持上一次未读数即可。
}
}
function startPolling() {
stopPolling()
void loadUnreadCount()
timer = window.setInterval(loadUnreadCount, 30_000)
}
onMounted(() => {
if (session.isLoggedIn) {
startPolling()
}
})
onBeforeUnmount(stopPolling)
watch(
() => session.isLoggedIn,
loggedIn => {
if (loggedIn) {
startPolling()
return
}
stopPolling()
unreadCount.value = 0
}
)
watch(
() => route.fullPath,
() => {
void loadUnreadCount()
}
)
return {
unreadCount,
unreadLabel,
refreshUnreadCount: loadUnreadCount,
}
}
@@ -33,6 +33,7 @@ const emit = defineEmits<{
'update:show': [value: boolean]
'update:selectedFilters': [value: SelectedFilters]
'update:rangeFilters': [value: RangeFilters]
reset: []
}>()
const activeCount = computed(() => {
@@ -105,8 +106,7 @@ function isRangePresetActive(sectionKey: string, min: string, max: string) {
}
function resetFilters() {
emit('update:selectedFilters', {})
emit('update:rangeFilters', {})
emit('reset')
}
function scrollToSection(sectionKey: string) {
@@ -3,15 +3,12 @@
max-width: 430px;
min-height: 100vh;
margin: 0 auto;
padding-bottom: 68px;
padding-bottom: calc(72px + env(safe-area-inset-bottom));
background: #f6f8fb;
color: #17233d;
}
.mobile-hero {
position: sticky;
top: 0;
z-index: 20;
padding: 12px 12px 10px;
background: #fff;
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.06);
@@ -825,6 +825,7 @@ syncMobileHomeQuery()
v-model:range-filters="rangeFilters"
:sections="filterSections"
:range-presets="rangePresets"
@reset="clearFilters"
/>
<button
@@ -1,7 +1,9 @@
<script setup lang="ts">
import { useRoute, RouterLink } from 'vue-router'
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
const route = useRoute()
const { unreadCount, unreadLabel } = useChatUnreadCount(route)
function isNavActive(path: string) {
if (path === '/m') return route.path === '/m'
@@ -16,7 +18,10 @@ function isNavActive(path: string) {
<span>首页</span>
</RouterLink>
<RouterLink to="/m/messages" class="nav-item" :class="{ active: isNavActive('/m/messages') }">
<van-icon name="chat-o" :size="22" />
<span class="nav-icon-wrap">
<van-icon name="chat-o" :size="22" />
<em v-if="unreadCount > 0" class="nav-badge">{{ unreadLabel }}</em>
</span>
<span>消息</span>
</RouterLink>
<RouterLink to="/m/seller/listings/create" class="nav-item nav-publish">
@@ -39,16 +44,26 @@ function isNavActive(path: string) {
position: fixed;
left: 0;
right: 0;
bottom: 0;
bottom: -1px;
z-index: 100;
display: flex;
background: #fff;
border-top: 1px solid #eee;
padding-bottom: env(safe-area-inset-bottom);
height: calc(50px + env(safe-area-inset-bottom));
height: calc(52px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.bottom-nav::after {
position: absolute;
right: 0;
bottom: -18px;
left: 0;
height: 18px;
background: #fff;
content: '';
}
.nav-item {
flex: 1;
display: flex;
@@ -69,6 +84,33 @@ function isNavActive(path: string) {
font-weight: 600;
}
.nav-icon-wrap {
position: relative;
display: grid;
width: 24px;
height: 24px;
place-items: center;
}
.nav-badge {
position: absolute;
top: -5px;
right: -10px;
min-width: 16px;
height: 16px;
padding: 0 4px;
box-sizing: border-box;
border: 2px solid #fff;
border-radius: 999px;
background: #ef4444;
color: #fff;
font-size: 10px;
font-style: normal;
font-weight: 800;
line-height: 12px;
text-align: center;
}
.publish-pill {
width: 36px;
height: 26px;