feat(messages): 新增 PC 消息中心与订单群聊入口
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Button, ErrorBlock, Input, Skeleton, Toast } from 'antd-mobile'
|
||||
import { LeftOutline, SendOutline } from 'antd-mobile-icons'
|
||||
import { CloseOutline, LeftOutline, PictureOutline, SendOutline } from 'antd-mobile-icons'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
useConversation,
|
||||
} from '@/features/messages/api'
|
||||
import type { ChatMessage } from '@/features/messages/types'
|
||||
import { uploadFile } from '@/shared/api/files'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
|
||||
function messageTime(value: string) {
|
||||
@@ -67,7 +68,10 @@ export function MobileChatPage() {
|
||||
const conversation = useConversation(id, Boolean(token))
|
||||
const messagesQuery = useChatMessages(id, Boolean(token))
|
||||
const [content, setContent] = useState('')
|
||||
const [pendingImages, setPendingImages] = useState<{ url: string; name: string }[]>([])
|
||||
const [uploading, setUploading] = useState(false)
|
||||
const [sending, setSending] = useState(false)
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const lastMessageIdRef = useRef<number | null>(null)
|
||||
const lastRenderedIdRef = useRef<number | null>(null)
|
||||
@@ -99,13 +103,51 @@ export function MobileChatPage() {
|
||||
.catch(() => undefined)
|
||||
}, [id, lastMessage, queryClient])
|
||||
|
||||
const onPickImages = async (files: FileList | null) => {
|
||||
if (!files?.length) return
|
||||
const remaining = 9 - pendingImages.length
|
||||
if (remaining <= 0) {
|
||||
Toast.show({ content: '最多发送 9 张图片' })
|
||||
return
|
||||
}
|
||||
const picked = Array.from(files)
|
||||
.filter(file => file.type.startsWith('image/'))
|
||||
.slice(0, remaining)
|
||||
if (!picked.length) {
|
||||
Toast.show({ content: '请选择图片文件' })
|
||||
return
|
||||
}
|
||||
setUploading(true)
|
||||
try {
|
||||
const uploaded: { url: string; name: string }[] = []
|
||||
for (const file of picked) {
|
||||
const item = await uploadFile(file, 'chat')
|
||||
uploaded.push({ url: item.url, name: item.filename })
|
||||
}
|
||||
setPendingImages(prev => [...prev, ...uploaded])
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
icon: 'fail',
|
||||
content: error instanceof Error ? error.message : '图片上传失败',
|
||||
})
|
||||
} finally {
|
||||
setUploading(false)
|
||||
if (fileInputRef.current) fileInputRef.current.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const onSend = async () => {
|
||||
const text = content.trim()
|
||||
if (!id || !text || sending) return
|
||||
if (!id || (!text && pendingImages.length === 0) || sending || uploading) return
|
||||
setSending(true)
|
||||
try {
|
||||
await sendChatMessage(id, text)
|
||||
await sendChatMessage(
|
||||
id,
|
||||
text,
|
||||
pendingImages.map(image => image.url)
|
||||
)
|
||||
setContent('')
|
||||
setPendingImages([])
|
||||
await messagesQuery.refetch()
|
||||
await queryClient.invalidateQueries({ queryKey: ['chat-conversations'] })
|
||||
} catch (error) {
|
||||
@@ -229,17 +271,54 @@ export function MobileChatPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{pendingImages.length > 0 ? (
|
||||
<div className="mobile-chat__pending">
|
||||
{pendingImages.map(image => (
|
||||
<div className="mobile-chat__pending-item" key={image.url}>
|
||||
<img src={image.url} alt={image.name} />
|
||||
<button
|
||||
type="button"
|
||||
aria-label="移除图片"
|
||||
onClick={() =>
|
||||
setPendingImages(prev => prev.filter(item => item.url !== image.url))
|
||||
}
|
||||
>
|
||||
<CloseOutline />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<span className="mobile-chat__pending-count">{pendingImages.length}/9</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mobile-chat__composer">
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
multiple
|
||||
hidden
|
||||
onChange={event => void onPickImages(event.target.files)}
|
||||
/>
|
||||
<Button
|
||||
fill="none"
|
||||
className="mobile-chat__attach"
|
||||
loading={uploading}
|
||||
disabled={sending || pendingImages.length >= 9}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<PictureOutline />
|
||||
</Button>
|
||||
<Input
|
||||
value={content}
|
||||
placeholder="输入消息…"
|
||||
placeholder={pendingImages.length > 0 ? '补充图片说明…' : '输入消息…'}
|
||||
onChange={setContent}
|
||||
onEnterPress={() => void onSend()}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
loading={sending}
|
||||
disabled={!content.trim()}
|
||||
disabled={(!content.trim() && pendingImages.length === 0) || uploading}
|
||||
onClick={() => void onSend()}
|
||||
>
|
||||
<SendOutline />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Button, ErrorBlock, List, Skeleton, Tag } from 'antd-mobile'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { Button, ErrorBlock, List, Skeleton, Tag, Toast } from 'antd-mobile'
|
||||
import { LeftOutline } from 'antd-mobile-icons'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useHandoffRecords, useMyOrder } from '@/features/orders/api'
|
||||
import { fetchOrderConversation } from '@/features/messages/api'
|
||||
import { MobileOrderActionPanel } from '@/features/mobile/MobileOrderActionPanel'
|
||||
import {
|
||||
checkoutStatusMeta,
|
||||
@@ -42,6 +43,23 @@ export function MobileOrderDetailPage() {
|
||||
const handoffQuery = useHandoffRecords(id, Boolean(token))
|
||||
const order = query.data
|
||||
const handoffRecords = handoffQuery.data ?? []
|
||||
const [openingChat, setOpeningChat] = useState(false)
|
||||
|
||||
const openOrderChat = async () => {
|
||||
if (!order || openingChat) return
|
||||
setOpeningChat(true)
|
||||
try {
|
||||
const conversation = await fetchOrderConversation(order.id)
|
||||
navigate(`/m/messages/${conversation.id}`)
|
||||
} catch (error) {
|
||||
Toast.show({
|
||||
icon: 'fail',
|
||||
content: error instanceof Error ? error.message : '订单群聊暂时无法打开',
|
||||
})
|
||||
} finally {
|
||||
setOpeningChat(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!hydrated) {
|
||||
return (
|
||||
@@ -229,6 +247,15 @@ export function MobileOrderDetailPage() {
|
||||
) : null}
|
||||
|
||||
<div className="mobile-order-detail__actions">
|
||||
<Button
|
||||
block
|
||||
fill="outline"
|
||||
color="primary"
|
||||
loading={openingChat}
|
||||
onClick={() => void openOrderChat()}
|
||||
>
|
||||
进入订单群聊
|
||||
</Button>
|
||||
<MobileOrderActionPanel
|
||||
order={order}
|
||||
onChanged={async () => {
|
||||
|
||||
Reference in New Issue
Block a user