新增订单群聊
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import {
|
||||
fetchChat,
|
||||
fetchChatMessages,
|
||||
markChatRead,
|
||||
sendChatMessage,
|
||||
type ChatConversation,
|
||||
type ChatMessage,
|
||||
} from '@/api/chats'
|
||||
import { formatDateMinute } from '@/utils/time'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const conversation = ref<ChatConversation | null>(null)
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const loading = ref(false)
|
||||
const sending = ref(false)
|
||||
const content = ref('')
|
||||
const listRef = ref<HTMLElement | null>(null)
|
||||
let timer: number | undefined
|
||||
|
||||
const conversationID = computed(() => Number(route.params.id || 0))
|
||||
const memberText = computed(() => {
|
||||
const participants = conversation.value?.participants || []
|
||||
if (participants.length === 0) return '订单群聊'
|
||||
return participants.map(item => roleLabel(item.role)).join(' · ')
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAll()
|
||||
timer = window.setInterval(() => {
|
||||
loadMessages(false)
|
||||
}, 5000)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (timer) window.clearInterval(timer)
|
||||
})
|
||||
|
||||
async function loadAll() {
|
||||
if (!conversationID.value) return
|
||||
loading.value = true
|
||||
try {
|
||||
const [chat] = await Promise.all([
|
||||
fetchChat(conversationID.value),
|
||||
loadMessages(false),
|
||||
])
|
||||
conversation.value = chat
|
||||
await markChatRead(conversationID.value)
|
||||
} catch {
|
||||
showToast({ message: '加载会话失败', icon: 'cross' })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMessages(scrollToBottom = true) {
|
||||
if (!conversationID.value) return
|
||||
const res = await fetchChatMessages(conversationID.value, 1, 100)
|
||||
messages.value = res.items
|
||||
if (scrollToBottom) {
|
||||
await nextTick()
|
||||
scrollBottom()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSend() {
|
||||
const text = content.value.trim()
|
||||
if (!text || sending.value) return
|
||||
sending.value = true
|
||||
try {
|
||||
const message = await sendChatMessage(conversationID.value, text)
|
||||
content.value = ''
|
||||
messages.value = [...messages.value, message]
|
||||
await markChatRead(conversationID.value)
|
||||
await nextTick()
|
||||
scrollBottom()
|
||||
} catch {
|
||||
showToast({ message: '发送失败', icon: 'cross' })
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function scrollBottom() {
|
||||
const el = listRef.value
|
||||
if (!el) return
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
|
||||
function roleLabel(role: string) {
|
||||
const map: Record<string, string> = {
|
||||
renter: '租客',
|
||||
owner: '号主',
|
||||
support: '客服',
|
||||
system: '系统',
|
||||
}
|
||||
return map[role] || '成员'
|
||||
}
|
||||
|
||||
function senderLabel(message: ChatMessage) {
|
||||
if (message.sender_type === 'system') return '系统'
|
||||
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="mobile-chat">
|
||||
<header class="chat-header">
|
||||
<button class="icon-btn" type="button" @click="router.back()">
|
||||
<van-icon name="arrow-left" :size="20" />
|
||||
</button>
|
||||
<div class="chat-title">
|
||||
<h1>{{ conversation?.title || '订单群聊' }}</h1>
|
||||
<p>{{ memberText }}</p>
|
||||
</div>
|
||||
<button class="icon-btn" type="button" @click="conversation && router.push(`/m/orders/${conversation.order_id}`)">
|
||||
<van-icon name="orders-o" :size="20" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section ref="listRef" class="message-list" :class="{ loading }">
|
||||
<van-loading v-if="loading && messages.length === 0" class="loading-state" />
|
||||
<div
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
class="message-row"
|
||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
||||
>
|
||||
<template v-if="item.sender_type === 'system'">
|
||||
<span class="system-message">{{ item.content }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="avatar">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
||||
<div class="bubble-wrap">
|
||||
<span class="sender-name">{{ senderLabel(item) }}</span>
|
||||
<div class="bubble">{{ item.content }}</div>
|
||||
<span class="message-time">{{ formatDateMinute(item.created_at) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="composer">
|
||||
<van-field
|
||||
v-model="content"
|
||||
class="composer-input"
|
||||
type="textarea"
|
||||
autosize
|
||||
:maxlength="1000"
|
||||
rows="1"
|
||||
placeholder="发送消息"
|
||||
@keydown.enter.prevent="handleSend"
|
||||
/>
|
||||
<button class="send-btn" type="button" :disabled="!content.trim() || sending" @click="handleSend">
|
||||
<van-icon name="guide-o" :size="20" />
|
||||
</button>
|
||||
</footer>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mobile-chat {
|
||||
display: grid;
|
||||
grid-template-rows: 56px minmax(0, 1fr) auto;
|
||||
height: 100dvh;
|
||||
background: #f3f6fa;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) 44px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #e7ecf2;
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-title h1 {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-title p {
|
||||
margin: 3px 0 0;
|
||||
overflow: hidden;
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 14px 12px 18px;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: block;
|
||||
margin: 70px auto;
|
||||
}
|
||||
|
||||
.message-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.message-row.self {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.message-row.system {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: grid;
|
||||
flex: none;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #1477ff;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.message-row.self .avatar {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.bubble-wrap {
|
||||
display: flex;
|
||||
max-width: min(76vw, 330px);
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-row.self .bubble-wrap {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.sender-name {
|
||||
margin-bottom: 4px;
|
||||
color: #8a94a6;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 100%;
|
||||
padding: 9px 11px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
word-break: break-word;
|
||||
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
.message-row.self .bubble {
|
||||
background: #dff5eb;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
margin-top: 4px;
|
||||
color: #a1a8b4;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
max-width: 82%;
|
||||
padding: 5px 9px;
|
||||
border-radius: 8px;
|
||||
background: #e6ebf2;
|
||||
color: #6b7280;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.composer {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 42px;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
padding: 8px 10px calc(8px + env(safe-area-inset-bottom));
|
||||
border-top: 1px solid #e7ecf2;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.composer-input {
|
||||
border: 1px solid #d9e0e8;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
display: grid;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: #1477ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
background: #c8d1dd;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user