317 lines
6.7 KiB
Vue
317 lines
6.7 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { showToast } from 'vant'
|
|
import MobileBottomNav from '@/components/MobileBottomNav.vue'
|
|
import { fetchChats, type ChatConversation } from '@/api/chats'
|
|
import { formatDateMinute } from '@/utils/time'
|
|
|
|
const router = useRouter()
|
|
const loading = ref(false)
|
|
const refreshing = ref(false)
|
|
const finished = ref(false)
|
|
const conversations = ref<ChatConversation[]>([])
|
|
const page = ref(1)
|
|
const pageSize = 20
|
|
const total = ref(0)
|
|
|
|
onMounted(() => {
|
|
onRefresh()
|
|
})
|
|
|
|
async function loadChats(isRefresh = false) {
|
|
if (isRefresh) {
|
|
page.value = 1
|
|
finished.value = false
|
|
}
|
|
loading.value = true
|
|
try {
|
|
const res = await fetchChats(page.value, pageSize)
|
|
conversations.value = isRefresh ? res.items : [...conversations.value, ...res.items]
|
|
total.value = res.total
|
|
if (conversations.value.length >= res.total || res.items.length === 0) {
|
|
finished.value = true
|
|
} else {
|
|
page.value += 1
|
|
}
|
|
} catch {
|
|
showToast({ message: '获取会话失败', icon: 'cross' })
|
|
finished.value = true
|
|
} finally {
|
|
loading.value = false
|
|
refreshing.value = false
|
|
}
|
|
}
|
|
|
|
function onRefresh() {
|
|
refreshing.value = true
|
|
loadChats(true)
|
|
}
|
|
|
|
function onLoad() {
|
|
if (loading.value || finished.value) return
|
|
loadChats(false)
|
|
}
|
|
|
|
function openConversation(item: ChatConversation) {
|
|
router.push(`/m/chats/${item.id}`)
|
|
}
|
|
|
|
function roleLabel(role: string) {
|
|
const map: Record<string, string> = {
|
|
renter: '租客',
|
|
owner: '号主',
|
|
support: '客服',
|
|
}
|
|
return map[role] || '成员'
|
|
}
|
|
|
|
function previewText(item: ChatConversation) {
|
|
return item.last_message_preview || '订单群聊已创建'
|
|
}
|
|
|
|
const unreadTotal = computed(() => conversations.value.reduce((sum, item) => sum + item.unread_count, 0))
|
|
</script>
|
|
|
|
<template>
|
|
<main class="mobile-messages">
|
|
<header class="page-header">
|
|
<button class="back-btn" type="button" @click="router.back()">
|
|
<van-icon name="arrow-left" :size="20" />
|
|
</button>
|
|
<h1>消息</h1>
|
|
<span class="header-count">{{ unreadTotal > 0 ? `${unreadTotal} 未读` : '' }}</span>
|
|
</header>
|
|
|
|
<van-pull-refresh v-model="refreshing" class="scroll-container" @refresh="onRefresh">
|
|
<van-list
|
|
v-model:loading="loading"
|
|
:finished="finished"
|
|
finished-text="没有更多会话了"
|
|
:immediate-check="false"
|
|
@load="onLoad"
|
|
>
|
|
<van-empty
|
|
v-if="!loading && conversations.length === 0"
|
|
description="暂无订单群聊"
|
|
class="empty-state"
|
|
/>
|
|
|
|
<div v-else class="conversation-list">
|
|
<button
|
|
v-for="item in conversations"
|
|
:key="item.id"
|
|
class="conversation-item"
|
|
type="button"
|
|
@click="openConversation(item)"
|
|
>
|
|
<div class="avatar-stack">
|
|
<span class="avatar main">{{ roleLabel(item.role).slice(0, 1) }}</span>
|
|
<span class="avatar support">客</span>
|
|
</div>
|
|
<div class="conversation-main">
|
|
<div class="conversation-title-row">
|
|
<h2>{{ item.title }}</h2>
|
|
<span class="conversation-time">{{ formatDateMinute(item.last_message_at || item.created_at) }}</span>
|
|
</div>
|
|
<div class="conversation-meta">
|
|
<span class="role-chip">{{ roleLabel(item.role) }}</span>
|
|
<span>订单 #{{ item.order_id }}</span>
|
|
</div>
|
|
<p>{{ previewText(item) }}</p>
|
|
</div>
|
|
<span v-if="item.unread_count > 0" class="unread-badge">
|
|
{{ item.unread_count > 99 ? '99+' : item.unread_count }}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</van-list>
|
|
</van-pull-refresh>
|
|
|
|
<MobileBottomNav />
|
|
</main>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.mobile-messages {
|
|
min-height: 100dvh;
|
|
background: #f5f7fb;
|
|
padding-bottom: calc(62px + env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.page-header {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
display: grid;
|
|
grid-template-columns: 44px 1fr 72px;
|
|
align-items: center;
|
|
height: 48px;
|
|
padding: 0 8px;
|
|
background: rgba(255, 255, 255, 0.96);
|
|
border-bottom: 1px solid #edf0f5;
|
|
backdrop-filter: blur(10px);
|
|
}
|
|
|
|
.page-header h1 {
|
|
margin: 0;
|
|
color: #111827;
|
|
font-size: 17px;
|
|
font-weight: 800;
|
|
text-align: center;
|
|
}
|
|
|
|
.back-btn {
|
|
display: grid;
|
|
width: 40px;
|
|
height: 40px;
|
|
place-items: center;
|
|
border: 0;
|
|
background: transparent;
|
|
color: #374151;
|
|
}
|
|
|
|
.header-count {
|
|
color: #ef4444;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
text-align: right;
|
|
}
|
|
|
|
.scroll-container {
|
|
min-height: calc(100dvh - 48px - 62px - env(safe-area-inset-bottom));
|
|
}
|
|
|
|
.empty-state {
|
|
padding-top: 90px;
|
|
}
|
|
|
|
.conversation-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
padding: 12px;
|
|
}
|
|
|
|
.conversation-item {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-columns: 52px minmax(0, 1fr);
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 1px solid #e8edf3;
|
|
border-radius: 8px;
|
|
background: #fff;
|
|
text-align: left;
|
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.conversation-item:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.avatar-stack {
|
|
position: relative;
|
|
width: 48px;
|
|
height: 48px;
|
|
}
|
|
|
|
.avatar {
|
|
display: grid;
|
|
place-items: center;
|
|
border-radius: 50%;
|
|
color: #fff;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.avatar.main {
|
|
width: 44px;
|
|
height: 44px;
|
|
background: #1477ff;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.avatar.support {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
width: 22px;
|
|
height: 22px;
|
|
border: 2px solid #fff;
|
|
background: #10b981;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.conversation-main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.conversation-title-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.conversation-title-row h2 {
|
|
flex: 1;
|
|
min-width: 0;
|
|
margin: 0;
|
|
overflow: hidden;
|
|
color: #111827;
|
|
font-size: 15px;
|
|
font-weight: 800;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.conversation-time {
|
|
flex: none;
|
|
color: #9ca3af;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.conversation-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
margin-top: 5px;
|
|
color: #6b7280;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.role-chip {
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
background: #eef6ff;
|
|
color: #1477ff;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.conversation-main p {
|
|
margin: 7px 0 0;
|
|
overflow: hidden;
|
|
color: #4b5563;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.unread-badge {
|
|
position: absolute;
|
|
right: 10px;
|
|
bottom: 10px;
|
|
min-width: 18px;
|
|
height: 18px;
|
|
padding: 0 5px;
|
|
border-radius: 9px;
|
|
background: #ef4444;
|
|
color: #fff;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
line-height: 18px;
|
|
text-align: center;
|
|
}
|
|
</style>
|