优化咨询客服

This commit is contained in:
yml2213
2026-05-29 10:15:21 +08:00
parent 2966b59c90
commit 2346f2e064
16 changed files with 202 additions and 32 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type ChatConversation struct { type ChatConversation struct {
ID uint64 `gorm:"primaryKey" json:"id"` ID uint64 `gorm:"primaryKey" json:"id"`
OrderID uint64 `gorm:"not null;uniqueIndex" json:"order_id"` OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"`
Type string `gorm:"size:32;not null;default:'order_group'" json:"type"` Type string `gorm:"size:32;not null;default:'order_group'" json:"type"`
Title string `gorm:"size:128;not null" json:"title"` Title string `gorm:"size:128;not null" json:"title"`
Status string `gorm:"size:32;not null;default:'active'" json:"status"` Status string `gorm:"size:32;not null;default:'active'" json:"status"`
+1 -1
View File
@@ -9,7 +9,7 @@ type Principal struct {
type ConversationDTO struct { type ConversationDTO struct {
ID uint64 `json:"id"` ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"` OrderID *uint64 `json:"order_id"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Status string `json:"status"` Status string `json:"status"`
+14
View File
@@ -81,6 +81,20 @@ func (h *Handler) OrderConversation(c *gin.Context) {
response.OK(c, item) response.OK(c, item)
} }
func (h *Handler) EnsureSupportConversation(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
item, err := h.service.EnsureSupportConversation(userID)
if err != nil {
writeChatError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) Messages(c *gin.Context) { func (h *Handler) Messages(c *gin.Context) {
userID, ok := currentUserID(c) userID, ok := currentUserID(c)
if !ok { if !ok {
+85 -2
View File
@@ -43,7 +43,7 @@ func EnsureOrderConversation(tx *gorm.DB, order model.RentalOrder) (*model.ChatC
now := time.Now() now := time.Now()
conversation := model.ChatConversation{ conversation := model.ChatConversation{
OrderID: order.ID, OrderID: &order.ID,
Type: "order_group", Type: "order_group",
Title: orderConversationTitle(order), Title: orderConversationTitle(order),
Status: "active", Status: "active",
@@ -182,6 +182,89 @@ func (r *Repository) FindOrderConversation(userID uint64, orderID uint64) (*Conv
return &dto, nil return &dto, nil
} }
func (r *Repository) EnsureSupportConversation(userID uint64) (*ConversationDTO, error) {
var conversationID uint64
err := r.db.Transaction(func(tx *gorm.DB) error {
var existing model.ChatConversation
err := tx.Table("chat_conversations AS c").
Select("c.*").
Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id").
Where("c.type = ? AND cp.participant_type = ? AND cp.participant_id = ?", "general_support", "user", userID).
Order("c.id ASC").
First(&existing).Error
if err == nil {
conversationID = existing.ID
return nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
now := time.Now()
conversation := model.ChatConversation{
Type: "general_support",
Title: "平台客服",
Status: "active",
}
if err := tx.Create(&conversation).Error; err != nil {
return err
}
participants := []model.ChatParticipant{
{
ConversationID: conversation.ID,
ParticipantType: "user",
ParticipantID: userID,
Role: "customer",
JoinedAt: now,
},
}
if supportID := defaultSupportAdminID(tx); supportID > 0 {
participants = append(participants, model.ChatParticipant{
ConversationID: conversation.ID,
ParticipantType: "admin",
ParticipantID: supportID,
Role: "support",
JoinedAt: now,
})
}
for _, participant := range participants {
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&participant).Error; err != nil {
return err
}
}
message := model.ChatMessage{
ConversationID: conversation.ID,
SenderType: "system",
SenderRole: "system",
ContentType: "system",
Content: "您好,客服会尽快回复,请直接描述您遇到的问题。",
AttachmentURLS: emptyJSONList(),
}
if err := tx.Create(&message).Error; err != nil {
return err
}
conversation.LastMessageID = &message.ID
conversation.LastMessagePreview = truncatePreview(message.Content)
conversation.LastMessageAt = &message.CreatedAt
if err := tx.Save(&conversation).Error; err != nil {
return err
}
conversationID = conversation.ID
return nil
})
if err != nil {
return nil, err
}
item, err := r.FindConversation(Principal{Type: "user", ID: userID}, conversationID)
if err != nil {
return nil, err
}
r.NotifyNewConversation(conversationID)
return item, nil
}
func (r *Repository) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { func (r *Repository) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
page, pageSize = normalizePagination(page, pageSize) page, pageSize = normalizePagination(page, pageSize)
if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil { if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil {
@@ -473,7 +556,7 @@ func (r *Repository) adminNames(ids []uint64) (map[uint64]string, error) {
type conversationRow struct { type conversationRow struct {
ID uint64 ID uint64
OrderID uint64 OrderID *uint64
Type string Type string
Title string Title string
Status string Status string
+10
View File
@@ -41,6 +41,16 @@ func (s *Service) FindOrderConversation(userID uint64, orderID uint64) (*Convers
return s.repo.FindOrderConversation(userID, orderID) return s.repo.FindOrderConversation(userID, orderID)
} }
func (s *Service) EnsureSupportConversation(userID uint64) (*ConversationDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if userID == 0 {
return nil, ErrPermissionDenied
}
return s.repo.EnsureSupportConversation(userID)
}
func (s *Service) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { func (s *Service) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil { if s.repo == nil {
return nil, ErrDependencyUnavailable return nil, ErrDependencyUnavailable
+1
View File
@@ -247,6 +247,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
if chatHubHandler != nil { if chatHubHandler != nil {
chatRoutes.GET("/events", chatHubHandler.UserEvents) chatRoutes.GET("/events", chatHubHandler.UserEvents)
} }
chatRoutes.POST("/support", chatHandler.EnsureSupportConversation)
chatRoutes.GET("", chatHandler.List) chatRoutes.GET("", chatHandler.List)
chatRoutes.GET("/:id", chatHandler.Detail) chatRoutes.GET("/:id", chatHandler.Detail)
chatRoutes.GET("/:id/messages", chatHandler.Messages) chatRoutes.GET("/:id/messages", chatHandler.Messages)
+1 -1
View File
@@ -315,7 +315,7 @@ CREATE TABLE system_configs (
CREATE TABLE chat_conversations ( CREATE TABLE chat_conversations (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT UNSIGNED NOT NULL, order_id BIGINT UNSIGNED NULL,
type VARCHAR(32) NOT NULL DEFAULT 'order_group', type VARCHAR(32) NOT NULL DEFAULT 'order_group',
title VARCHAR(128) NOT NULL, title VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'active', status VARCHAR(32) NOT NULL DEFAULT 'active',
@@ -0,0 +1,2 @@
ALTER TABLE chat_conversations
MODIFY order_id BIGINT UNSIGNED NULL;
+9 -4
View File
@@ -6,7 +6,7 @@ export interface ChatParticipant {
conversation_id: number conversation_id: number
participant_type: 'user' | 'admin' participant_type: 'user' | 'admin'
participant_id: number participant_id: number
role: 'renter' | 'owner' | 'support' role: 'renter' | 'owner' | 'support' | 'customer'
remark: string remark: string
display_name: string display_name: string
avatar_url: string avatar_url: string
@@ -16,11 +16,11 @@ export interface ChatParticipant {
export interface ChatConversation { export interface ChatConversation {
id: number id: number
order_id: number order_id: number | null
type: string type: string
title: string title: string
status: string status: string
role: 'renter' | 'owner' | 'support' role: 'renter' | 'owner' | 'support' | 'customer'
participants?: ChatParticipant[] participants?: ChatParticipant[]
last_message_id?: number last_message_id?: number
last_message_preview: string last_message_preview: string
@@ -35,7 +35,7 @@ export interface ChatMessage {
conversation_id: number conversation_id: number
sender_type: 'user' | 'admin' | 'system' sender_type: 'user' | 'admin' | 'system'
sender_id: number sender_id: number
sender_role: 'renter' | 'owner' | 'support' | 'system' sender_role: 'renter' | 'owner' | 'support' | 'customer' | 'system'
sender_name: string sender_name: string
sender_avatar: string sender_avatar: string
is_self: boolean is_self: boolean
@@ -62,6 +62,11 @@ export async function fetchOrderChat(orderId: number) {
return data.data return data.data
} }
export async function ensureSupportChat() {
const { data } = await apiClient.post<ApiResponse<ChatConversation>>('/chats/support')
return data.data
}
export async function fetchChatMessages(id: number, page = 1, pageSize = 100) { export async function fetchChatMessages(id: number, page = 1, pageSize = 100) {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatMessage>>>(`/chats/${id}/messages`, { const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatMessage>>>(`/chats/${id}/messages`, {
params: { page, page_size: pageSize }, params: { page, page_size: pageSize },
+24 -3
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { import {
ArrowDown, ArrowDown,
ChatDotRound, ChatDotRound,
@@ -13,6 +14,7 @@ import {
UserFilled, UserFilled,
Wallet, Wallet,
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import { ensureSupportChat } from "@/api/chats";
import { useSessionStore } from "@/stores/session"; import { useSessionStore } from "@/stores/session";
const session = useSessionStore(); const session = useSessionStore();
@@ -26,12 +28,30 @@ const navItems = [
]; ];
const showUserDropdown = ref(false); const showUserDropdown = ref(false);
const supportLoading = ref(false);
function handleLogout() { function handleLogout() {
session.logout(); session.logout();
showUserDropdown.value = false; showUserDropdown.value = false;
router.replace("/"); router.replace("/");
} }
async function handleSupportClick() {
if (!session.isLoggedIn) {
router.push({ path: "/login", query: { redirect: router.currentRoute.value.fullPath } });
return;
}
if (supportLoading.value) return;
supportLoading.value = true;
try {
const chat = await ensureSupportChat();
router.push(`/messages/${chat.id}`);
} catch {
ElMessage.error("联系客服失败,请稍后重试");
} finally {
supportLoading.value = false;
}
}
</script> </script>
<template> <template>
@@ -63,10 +83,10 @@ function handleLogout() {
</div> </div>
<div class="pc-user-actions"> <div class="pc-user-actions">
<RouterLink class="pc-icon-link service-link" to="/notifications" title="联系客服"> <button class="pc-icon-link service-link" type="button" title="联系客服" @click="handleSupportClick">
<el-icon><Service /></el-icon> <el-icon><Service /></el-icon>
<span>客服</span> <span>{{ supportLoading ? "接入中" : "客服" }}</span>
</RouterLink> </button>
<RouterLink class="pc-post-link" :to="session.isLoggedIn ? '/seller/listings/create' : '/login'"> <RouterLink class="pc-post-link" :to="session.isLoggedIn ? '/seller/listings/create' : '/login'">
<el-icon><CirclePlus /></el-icon> <el-icon><CirclePlus /></el-icon>
@@ -310,6 +330,7 @@ function handleLogout() {
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
text-decoration: none; text-decoration: none;
cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
} }
+3 -2
View File
@@ -28,7 +28,7 @@ const listRef = ref<HTMLElement | null>(null)
const conversationID = computed(() => Number(route.params.id || 0)) const conversationID = computed(() => Number(route.params.id || 0))
const memberText = computed(() => { const memberText = computed(() => {
const participants = conversation.value?.participants || [] const participants = conversation.value?.participants || []
if (participants.length === 0) return '订单群聊' if (participants.length === 0) return conversation.value?.type === 'general_support' ? '平台客服' : '订单群聊'
return participants.map(item => roleLabel(item.role)).join(' · ') return participants.map(item => roleLabel(item.role)).join(' · ')
}) })
@@ -138,6 +138,7 @@ function roleLabel(role: string) {
renter: '租客', renter: '租客',
owner: '号主', owner: '号主',
support: '客服', support: '客服',
customer: '咨询',
system: '系统', system: '系统',
} }
return map[role] || '成员' return map[role] || '成员'
@@ -166,7 +167,7 @@ function handleKeydown(e: KeyboardEvent) {
<span>返回</span> <span>返回</span>
</button> </button>
<div class="chat-title"> <div class="chat-title">
<h2>{{ conversation?.title || '订单群聊' }}</h2> <h2>{{ conversation?.title || '客服会话' }}</h2>
<p>{{ memberText }}</p> <p>{{ memberText }}</p>
</div> </div>
<el-button <el-button
+5 -5
View File
@@ -51,12 +51,12 @@ function openConversation(item: ChatConversation) {
} }
function roleLabel(role: string) { function roleLabel(role: string) {
const map: Record<string, string> = { renter: '租客', owner: '号主', support: '客服' } const map: Record<string, string> = { renter: '租客', owner: '号主', support: '客服', customer: '咨询' }
return map[role] || '成员' return map[role] || '成员'
} }
function previewText(item: ChatConversation) { function previewText(item: ChatConversation) {
return item.last_message_preview || '订单群聊已创建' return item.last_message_preview || (item.type === 'general_support' ? '客服会话已创建' : '订单群聊已创建')
} }
</script> </script>
@@ -66,7 +66,7 @@ function previewText(item: ChatConversation) {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Messages</p> <p class="eyebrow">Messages</p>
<h1>消息</h1> <h1>消息</h1>
<p>{{ unreadTotal > 0 ? `${unreadTotal} 条未读` : '查看订单群聊消息,与租客、号主和客服沟通。' }}</p> <p>{{ unreadTotal > 0 ? `${unreadTotal} 条未读` : '查看订单群聊和平台客服消息。' }}</p>
</div> </div>
<div class="header-actions"> <div class="header-actions">
<el-button :icon="Refresh" :loading="loading" @click="loadChats(true)">刷新</el-button> <el-button :icon="Refresh" :loading="loading" @click="loadChats(true)">刷新</el-button>
@@ -77,7 +77,7 @@ function previewText(item: ChatConversation) {
<div v-if="loading && conversations.length === 0" class="message-loading" v-loading="loading" /> <div v-if="loading && conversations.length === 0" class="message-loading" v-loading="loading" />
<div v-else-if="!loading && conversations.length === 0" class="empty-panel"> <div v-else-if="!loading && conversations.length === 0" class="empty-panel">
<el-empty description="暂无订单群聊"> <el-empty description="暂无会话">
<el-button type="primary" :icon="Tickets" @click="router.push('/orders')">查看订单</el-button> <el-button type="primary" :icon="Tickets" @click="router.push('/orders')">查看订单</el-button>
</el-empty> </el-empty>
</div> </div>
@@ -101,7 +101,7 @@ function previewText(item: ChatConversation) {
</div> </div>
<div class="conversation-meta"> <div class="conversation-meta">
<span class="role-chip">{{ roleLabel(item.role) }}</span> <span class="role-chip">{{ roleLabel(item.role) }}</span>
<span class="order-id">订单 #{{ item.order_id }}</span> <span class="order-id">{{ item.order_id ? `订单 #${item.order_id}` : '平台客服' }}</span>
</div> </div>
<p class="conversation-preview">{{ previewText(item) }}</p> <p class="conversation-preview">{{ previewText(item) }}</p>
</div> </div>
+5 -4
View File
@@ -200,6 +200,7 @@ function roleLabel(role: string) {
renter: '租客', renter: '租客',
owner: '号主', owner: '号主',
support: '客服', support: '客服',
customer: '咨询',
system: '系统', system: '系统',
} }
return map[role] || '成员' return map[role] || '成员'
@@ -227,8 +228,8 @@ function getSupportName(item: ChatConversation) {
<section class="admin-page"> <section class="admin-page">
<div class="page-head"> <div class="page-head">
<div> <div>
<h1>客服群聊</h1> <h1>客服会话</h1>
<p>处理订单三方沟通</p> <p>处理订单三方沟通和平台咨询</p>
</div> </div>
<div class="head-right"> <div class="head-right">
<el-button @click="quickReplyVisible = true">快捷回复管理</el-button> <el-button @click="quickReplyVisible = true">快捷回复管理</el-button>
@@ -257,7 +258,7 @@ function getSupportName(item: ChatConversation) {
<strong>{{ getConversationTitle(item) }}</strong> <strong>{{ getConversationTitle(item) }}</strong>
<span>{{ formatDateMinute(item.last_message_at || item.created_at) }}</span> <span>{{ formatDateMinute(item.last_message_at || item.created_at) }}</span>
</div> </div>
<p>{{ item.last_message_preview || '订单群聊已创建' }}</p> <p>{{ item.last_message_preview || (item.type === 'general_support' ? '客服会话已创建' : '订单群聊已创建') }}</p>
<div class="row-meta"> <div class="row-meta">
<span class="support-name">{{ getSupportName(item) }}</span> <span class="support-name">{{ getSupportName(item) }}</span>
<em v-if="item.unread_count > 0">{{ item.unread_count }}</em> <em v-if="item.unread_count > 0">{{ item.unread_count }}</em>
@@ -288,7 +289,7 @@ function getSupportName(item: ChatConversation) {
</div> </div>
<div class="head-actions"> <div class="head-actions">
<el-button size="small" @click="transferVisible = true">转接</el-button> <el-button size="small" @click="transferVisible = true">转接</el-button>
<RouterLink :to="`/admin/orders/${active.order_id}`"> <RouterLink v-if="active.order_id" :to="`/admin/orders/${active.order_id}`">
<el-button size="small">查看订单</el-button> <el-button size="small">查看订单</el-button>
</RouterLink> </RouterLink>
</div> </div>
+11 -3
View File
@@ -27,7 +27,7 @@ const listRef = ref<HTMLElement | null>(null)
const conversationID = computed(() => Number(route.params.id || 0)) const conversationID = computed(() => Number(route.params.id || 0))
const memberText = computed(() => { const memberText = computed(() => {
const participants = conversation.value?.participants || [] const participants = conversation.value?.participants || []
if (participants.length === 0) return '订单群聊' if (participants.length === 0) return conversation.value?.type === 'general_support' ? '平台客服' : '订单群聊'
return participants.map(item => roleLabel(item.role)).join(' · ') return participants.map(item => roleLabel(item.role)).join(' · ')
}) })
@@ -123,6 +123,7 @@ function roleLabel(role: string) {
renter: '租客', renter: '租客',
owner: '号主', owner: '号主',
support: '客服', support: '客服',
customer: '咨询',
system: '系统', system: '系统',
} }
return map[role] || '成员' return map[role] || '成员'
@@ -141,12 +142,13 @@ function senderLabel(message: ChatMessage) {
<van-icon name="arrow-left" :size="20" /> <van-icon name="arrow-left" :size="20" />
</button> </button>
<div class="chat-title"> <div class="chat-title">
<h1>{{ conversation?.title || '订单群聊' }}</h1> <h1>{{ conversation?.title || '客服会话' }}</h1>
<p>{{ memberText }}</p> <p>{{ memberText }}</p>
</div> </div>
<button class="icon-btn" type="button" @click="conversation && router.push(`/m/orders/${conversation.order_id}`)"> <button v-if="conversation?.order_id" class="icon-btn" type="button" @click="router.push(`/m/orders/${conversation.order_id}`)">
<van-icon name="orders-o" :size="20" /> <van-icon name="orders-o" :size="20" />
</button> </button>
<span v-else class="icon-placeholder"></span>
</header> </header>
<section ref="listRef" class="message-list" :class="{ loading }"> <section ref="listRef" class="message-list" :class="{ loading }">
@@ -216,6 +218,12 @@ function senderLabel(message: ChatMessage) {
color: #374151; color: #374151;
} }
.icon-placeholder {
display: block;
width: 44px;
height: 44px;
}
.chat-title { .chat-title {
min-width: 0; min-width: 0;
text-align: center; text-align: center;
+26 -3
View File
@@ -1,9 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from "vue"; import { computed, onMounted, ref } from "vue";
import { RouterLink } from "vue-router"; import { RouterLink, useRouter } from "vue-router";
import { showToast } from "vant"; import { showToast } from "vant";
import MobileBottomNav from "@/components/MobileBottomNav.vue"; import MobileBottomNav from "@/components/MobileBottomNav.vue";
import { ensureSupportChat } from "@/api/chats";
import { import {
emptyListingPublishOptions, emptyListingPublishOptions,
type ListingPublishOptions, type ListingPublishOptions,
@@ -36,7 +37,10 @@ import {
readAssetNumber, readAssetNumber,
readAssetString, readAssetString,
} from "@/utils/listingDisplay"; } from "@/utils/listingDisplay";
import { useSessionStore } from "@/stores/session";
const router = useRouter();
const session = useSessionStore();
const loading = ref(false); const loading = ref(false);
const loadFailed = ref(false); const loadFailed = ref(false);
const listings = ref<Listing[]>([]); const listings = ref<Listing[]>([]);
@@ -50,6 +54,7 @@ const refreshing = ref(false);
const searchValue = ref(""); const searchValue = ref("");
const announcements = ref<string[]>(defaultHomeAnnouncements); const announcements = ref<string[]>(defaultHomeAnnouncements);
const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners); const bannerSlides = ref<HomeBannerSlide[]>(defaultHomeBanners);
const supportLoading = ref(false);
const sortOptions = [ const sortOptions = [
{ key: "comprehensive", label: "综合排序" }, { key: "comprehensive", label: "综合排序" },
@@ -81,6 +86,23 @@ const activeSortLabel = computed(
"综合排序" "综合排序"
); );
async function handleSupportClick() {
if (!session.isLoggedIn) {
router.push({ path: "/m/login", query: { redirect: router.currentRoute.value.fullPath } });
return;
}
if (supportLoading.value) return;
supportLoading.value = true;
try {
const chat = await ensureSupportChat();
router.push(`/m/chats/${chat.id}`);
} catch {
showToast({ message: "联系客服失败,请稍后重试", icon: "cross" });
} finally {
supportLoading.value = false;
}
}
const serverFilterOptions = computed(() => const serverFilterOptions = computed(() =>
uniqueOptions( uniqueOptions(
publishOptions.value.server_options publishOptions.value.server_options
@@ -327,7 +349,9 @@ function uniqueOptions(values: string[]) {
placeholder="搜区服 / 段位" placeholder="搜区服 / 段位"
class="home-search" class="home-search"
/> />
<button class="mobile-service" type="button">客服</button> <button class="mobile-service" type="button" :disabled="supportLoading" @click="handleSupportClick">
{{ supportLoading ? "接入中" : "客服" }}
</button>
</div> </div>
<!-- 防骗提示卡片不用 van-notice-bar --> <!-- 防骗提示卡片不用 van-notice-bar -->
@@ -498,4 +522,3 @@ function uniqueOptions(values: string[]) {
</template> </template>
<style scoped src="./MobileHomeView.css"></style> <style scoped src="./MobileHomeView.css"></style>
@@ -62,12 +62,13 @@ function roleLabel(role: string) {
renter: '租客', renter: '租客',
owner: '号主', owner: '号主',
support: '客服', support: '客服',
customer: '咨询',
} }
return map[role] || '成员' return map[role] || '成员'
} }
function previewText(item: ChatConversation) { function previewText(item: ChatConversation) {
return item.last_message_preview || '订单群聊已创建' return item.last_message_preview || (item.type === 'general_support' ? '客服会话已创建' : '订单群聊已创建')
} }
const unreadTotal = computed(() => conversations.value.reduce((sum, item) => sum + item.unread_count, 0)) const unreadTotal = computed(() => conversations.value.reduce((sum, item) => sum + item.unread_count, 0))
@@ -93,7 +94,7 @@ const unreadTotal = computed(() => conversations.value.reduce((sum, item) => sum
> >
<van-empty <van-empty
v-if="!loading && conversations.length === 0" v-if="!loading && conversations.length === 0"
description="暂无订单群聊" description="暂无会话"
class="empty-state" class="empty-state"
/> />
@@ -116,7 +117,7 @@ const unreadTotal = computed(() => conversations.value.reduce((sum, item) => sum
</div> </div>
<div class="conversation-meta"> <div class="conversation-meta">
<span class="role-chip">{{ roleLabel(item.role) }}</span> <span class="role-chip">{{ roleLabel(item.role) }}</span>
<span>订单 #{{ item.order_id }}</span> <span>{{ item.order_id ? `订单 #${item.order_id}` : '平台客服' }}</span>
</div> </div>
<p>{{ previewText(item) }}</p> <p>{{ previewText(item) }}</p>
</div> </div>