支持客服入口按业务场景分流到客服分组
联系客服支持 scene(general/mohong),按用户与场景复用会话;摸大红入口走摸大红分组选人。
This commit is contained in:
@@ -11,6 +11,7 @@ type ChatConversation struct {
|
|||||||
OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"`
|
OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"`
|
||||||
ListingID *uint64 `gorm:"index" json:"listing_id"`
|
ListingID *uint64 `gorm:"index" json:"listing_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"`
|
||||||
|
SupportScene string `gorm:"column:support_scene;size:32;not null;default:''" json:"support_scene"`
|
||||||
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"`
|
||||||
LastMessageID *uint64 `json:"last_message_id"`
|
LastMessageID *uint64 `json:"last_message_id"`
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ func (r *Repository) FindConversation(ctx context.Context, principal Principal,
|
|||||||
OrderID: conversation.OrderID,
|
OrderID: conversation.OrderID,
|
||||||
ListingID: conversation.ListingID,
|
ListingID: conversation.ListingID,
|
||||||
Type: conversation.Type,
|
Type: conversation.Type,
|
||||||
|
SupportScene: conversation.SupportScene,
|
||||||
Title: conversation.Title,
|
Title: conversation.Title,
|
||||||
Status: conversation.Status,
|
Status: conversation.Status,
|
||||||
Role: "admin", // 管理员角色
|
Role: "admin", // 管理员角色
|
||||||
@@ -131,20 +132,43 @@ func (r *Repository) FindOrderConversation(ctx context.Context, userID uint64, o
|
|||||||
dto := row.toDTO(participants)
|
dto := row.toDTO(participants)
|
||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint64) (*ConversationDTO, error) {
|
func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint64, scene string) (*ConversationDTO, error) {
|
||||||
|
scene, title, groupCode, welcome := resolveSupportScene(scene)
|
||||||
var conversationID uint64
|
var conversationID uint64
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var existing model.ChatConversation
|
var existing model.ChatConversation
|
||||||
err := tx.Table("chat_conversations AS c").
|
err := tx.Table("chat_conversations AS c").
|
||||||
Select("c.*").
|
Select("c.*").
|
||||||
Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id").
|
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).
|
Where(
|
||||||
|
"c.type = ? AND c.support_scene = ? AND cp.participant_type = ? AND cp.participant_id = ?",
|
||||||
|
ConversationTypeGeneralSupport, scene, "user", userID,
|
||||||
|
).
|
||||||
Order("c.id ASC").
|
Order("c.id ASC").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Find(&existing).Error
|
Find(&existing).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// 兼容旧数据:scene=general 时也匹配未回填 support_scene 的历史会话
|
||||||
|
if existing.ID == 0 && scene == SupportSceneGeneral {
|
||||||
|
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 (c.support_scene = '' OR c.support_scene IS NULL) AND cp.participant_type = ? AND cp.participant_id = ?",
|
||||||
|
ConversationTypeGeneralSupport, "user", userID,
|
||||||
|
).
|
||||||
|
Order("c.id ASC").
|
||||||
|
Limit(1).
|
||||||
|
Find(&existing).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if existing.ID > 0 && existing.SupportScene == "" {
|
||||||
|
_ = tx.Model(&existing).Update("support_scene", SupportSceneGeneral).Error
|
||||||
|
}
|
||||||
|
}
|
||||||
if existing.ID > 0 {
|
if existing.ID > 0 {
|
||||||
conversationID = existing.ID
|
conversationID = existing.ID
|
||||||
return nil
|
return nil
|
||||||
@@ -152,9 +176,10 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
|||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
conversation := model.ChatConversation{
|
conversation := model.ChatConversation{
|
||||||
Type: "general_support",
|
Type: ConversationTypeGeneralSupport,
|
||||||
Title: "平台客服",
|
SupportScene: scene,
|
||||||
Status: "active",
|
Title: title,
|
||||||
|
Status: "active",
|
||||||
}
|
}
|
||||||
if err := tx.Create(&conversation).Error; err != nil {
|
if err := tx.Create(&conversation).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -169,7 +194,7 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
|||||||
JoinedAt: now,
|
JoinedAt: now,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if supportID := defaultSupportAdminID(tx); supportID > 0 {
|
if supportID := pickSupportAdminForScene(tx, groupCode); supportID > 0 {
|
||||||
participants = append(participants, model.ChatParticipant{
|
participants = append(participants, model.ChatParticipant{
|
||||||
ConversationID: conversation.ID,
|
ConversationID: conversation.ID,
|
||||||
ParticipantType: "admin",
|
ParticipantType: "admin",
|
||||||
@@ -189,7 +214,7 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
|||||||
SenderType: "system",
|
SenderType: "system",
|
||||||
SenderRole: "system",
|
SenderRole: "system",
|
||||||
ContentType: "system",
|
ContentType: "system",
|
||||||
Content: "您好,客服会尽快回复,请直接描述您遇到的问题。",
|
Content: welcome,
|
||||||
AttachmentURLS: emptyJSONList(),
|
AttachmentURLS: emptyJSONList(),
|
||||||
}
|
}
|
||||||
if err := tx.Create(&message).Error; err != nil {
|
if err := tx.Create(&message).Error; err != nil {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type ConversationDTO struct {
|
|||||||
LatestHandoffStatus string `json:"latest_handoff_status,omitempty"`
|
LatestHandoffStatus string `json:"latest_handoff_status,omitempty"`
|
||||||
LatestRefundStatus string `json:"latest_refund_status,omitempty"`
|
LatestRefundStatus string `json:"latest_refund_status,omitempty"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
SupportScene string `json:"support_scene,omitempty"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
@@ -32,6 +33,12 @@ type ConversationDTO struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureSupportRequest 创建/复用客服会话。
|
||||||
|
type EnsureSupportRequest struct {
|
||||||
|
// Scene 业务场景:general(默认)/ mohong(摸大红)
|
||||||
|
Scene string `json:"scene"`
|
||||||
|
}
|
||||||
|
|
||||||
type ParticipantDTO struct {
|
type ParticipantDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
ConversationID uint64 `json:"conversation_id"`
|
ConversationID uint64 `json:"conversation_id"`
|
||||||
|
|||||||
@@ -62,7 +62,10 @@ func (h *Handler) EnsureSupportConversation(c *gin.Context) {
|
|||||||
response.Unauthorized(c, "缺少用户上下文")
|
response.Unauthorized(c, "缺少用户上下文")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item, err := h.service.EnsureSupportConversation(c.Request.Context(), userID)
|
var req EnsureSupportRequest
|
||||||
|
// scene 可选;空 body 仍按平台客服处理
|
||||||
|
_ = c.ShouldBindJSON(&req)
|
||||||
|
item, err := h.service.EnsureSupportConversation(c.Request.Context(), userID, req.Scene)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeChatError(c, err)
|
writeChatError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type conversationRow struct {
|
|||||||
LatestHandoffStatus string
|
LatestHandoffStatus string
|
||||||
LatestRefundStatus string
|
LatestRefundStatus string
|
||||||
Type string
|
Type string
|
||||||
|
SupportScene string
|
||||||
Title string
|
Title string
|
||||||
Status string
|
Status string
|
||||||
Role string
|
Role string
|
||||||
@@ -37,7 +38,7 @@ type conversationRow struct {
|
|||||||
|
|
||||||
func (r *Repository) conversationQuery(ctx context.Context, principal Principal) *gorm.DB {
|
func (r *Repository) conversationQuery(ctx context.Context, principal Principal) *gorm.DB {
|
||||||
return r.db.WithContext(ctx).Table("chat_conversations AS c").
|
return r.db.WithContext(ctx).Table("chat_conversations AS c").
|
||||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.title, c.status, c.last_message_id,
|
Select(`c.id, c.order_id, c.listing_id, c.type, c.support_scene, c.title, c.status, c.last_message_id,
|
||||||
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at, cp.role,
|
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at, cp.role,
|
||||||
(
|
(
|
||||||
SELECT COUNT(1)
|
SELECT COUNT(1)
|
||||||
@@ -300,6 +301,7 @@ func (row conversationRow) toDTO(participants []ParticipantDTO) ConversationDTO
|
|||||||
LatestHandoffStatus: row.LatestHandoffStatus,
|
LatestHandoffStatus: row.LatestHandoffStatus,
|
||||||
LatestRefundStatus: row.LatestRefundStatus,
|
LatestRefundStatus: row.LatestRefundStatus,
|
||||||
Type: row.Type,
|
Type: row.Type,
|
||||||
|
SupportScene: row.SupportScene,
|
||||||
Title: row.Title,
|
Title: row.Title,
|
||||||
Status: row.Status,
|
Status: row.Status,
|
||||||
Role: row.Role,
|
Role: row.Role,
|
||||||
|
|||||||
@@ -54,14 +54,14 @@ func (s *Service) FindOrderConversation(ctx context.Context, userID uint64, orde
|
|||||||
return s.repo.FindOrderConversation(ctx, userID, orderID)
|
return s.repo.FindOrderConversation(ctx, userID, orderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) EnsureSupportConversation(ctx context.Context, userID uint64) (*ConversationDTO, error) {
|
func (s *Service) EnsureSupportConversation(ctx context.Context, userID uint64, scene string) (*ConversationDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
}
|
}
|
||||||
if userID == 0 {
|
if userID == 0 {
|
||||||
return nil, ErrPermissionDenied
|
return nil, ErrPermissionDenied
|
||||||
}
|
}
|
||||||
return s.repo.EnsureSupportConversation(ctx, userID)
|
return s.repo.EnsureSupportConversation(ctx, userID, scene)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) Messages(ctx context.Context, principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
|
func (s *Service) Messages(ctx context.Context, principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||||
|
|||||||
@@ -213,7 +213,7 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ
|
|||||||
var rows []conversationRow
|
var rows []conversationRow
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
queryDB := r.adminConversationBase(ctx, principal, keyword).
|
queryDB := r.adminConversationBase(ctx, principal, keyword).
|
||||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.title, c.status, c.last_message_id,
|
Select(`c.id, c.order_id, c.listing_id, c.type, c.support_scene, c.title, c.status, c.last_message_id,
|
||||||
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at,
|
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at,
|
||||||
COALESCE(cp_me.role, 'admin') AS role,
|
COALESCE(cp_me.role, 'admin') AS role,
|
||||||
lo.id AS latest_order_id, lo.order_no AS latest_order_no, lo.status AS latest_order_status,
|
lo.id AS latest_order_id, lo.order_no AS latest_order_no, lo.status AS latest_order_status,
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package chat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"hfb_sys/backend/internal/modules/supportgroup"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 客服入口业务场景(与前端 POST /chats/support 的 scene 对齐)。
|
||||||
|
const (
|
||||||
|
SupportSceneGeneral = "general"
|
||||||
|
SupportSceneMohong = "mohong"
|
||||||
|
)
|
||||||
|
|
||||||
|
// resolveSupportScene 规范化场景并返回标题、客服分组 code、欢迎语。
|
||||||
|
// groupCode 为空时表示不走分组,使用默认客服选取逻辑。
|
||||||
|
func resolveSupportScene(raw string) (scene, title, groupCode, welcome string) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||||
|
case SupportSceneMohong:
|
||||||
|
return SupportSceneMohong,
|
||||||
|
"摸大红客服",
|
||||||
|
supportgroup.GroupCodeMohong,
|
||||||
|
"您好,这里是摸大红客服。请直接描述问题,可将订单信息复制后发送。"
|
||||||
|
default:
|
||||||
|
// 未知场景回落平台客服,避免随意 scene 绕开分配策略
|
||||||
|
return SupportSceneGeneral,
|
||||||
|
"平台客服",
|
||||||
|
"",
|
||||||
|
"您好,客服会尽快回复,请直接描述您遇到的问题。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pickSupportAdminForScene 按分组选人;分组无成员或失败时回退默认客服逻辑。
|
||||||
|
func pickSupportAdminForScene(tx *gorm.DB, groupCode string) uint64 {
|
||||||
|
groupCode = strings.TrimSpace(groupCode)
|
||||||
|
if groupCode != "" {
|
||||||
|
if id, err := supportgroup.PickSupportAdmin(tx, groupCode); err == nil && id > 0 {
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultSupportAdminID(tx)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import "time"
|
|||||||
const (
|
const (
|
||||||
GroupCodeOwnerOnboarding = "owner_onboarding"
|
GroupCodeOwnerOnboarding = "owner_onboarding"
|
||||||
GroupCodeRenterHandoff = "renter_handoff"
|
GroupCodeRenterHandoff = "renter_handoff"
|
||||||
|
GroupCodeMohong = "mohong"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MemberDTO struct {
|
type MemberDTO struct {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func normalizeStatus(status string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isProtectedCode(code string) bool {
|
func isProtectedCode(code string) bool {
|
||||||
return code == GroupCodeOwnerOnboarding || code == GroupCodeRenterHandoff
|
return code == GroupCodeOwnerOnboarding || code == GroupCodeRenterHandoff || code == GroupCodeMohong
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeCustomCode(name string) string {
|
func makeCustomCode(name string) string {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
-- 客服会话按业务场景拆分(同一用户可同时有平台客服 / 摸大红客服等)
|
||||||
|
ALTER TABLE chat_conversations
|
||||||
|
ADD COLUMN support_scene VARCHAR(32) NOT NULL DEFAULT '' COMMENT '客服场景: general/mohong 等,非客服会话为空' AFTER type;
|
||||||
|
|
||||||
|
UPDATE chat_conversations
|
||||||
|
SET support_scene = 'general'
|
||||||
|
WHERE type = 'general_support' AND (support_scene = '' OR support_scene IS NULL);
|
||||||
|
|
||||||
|
CREATE INDEX idx_chat_conversations_type_scene ON chat_conversations (type, support_scene);
|
||||||
|
|
||||||
|
-- 摸大红客服分组(成员可在后台「客服分组」中调整;默认挂入所有 cs 角色)
|
||||||
|
INSERT INTO chat_support_groups (code, name, description, status, sort_order) VALUES
|
||||||
|
('mohong', '摸大红-客服', '用户从摸大红入口联系客服时,按本组成员在线与负载分配', 'active', 30)
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
name = VALUES(name),
|
||||||
|
description = VALUES(description),
|
||||||
|
sort_order = VALUES(sort_order);
|
||||||
|
|
||||||
|
INSERT IGNORE INTO chat_support_group_members (group_id, admin_user_id)
|
||||||
|
SELECT g.id, au.id
|
||||||
|
FROM chat_support_groups g
|
||||||
|
JOIN admin_user_roles aur ON 1 = 1
|
||||||
|
JOIN roles r ON r.id = aur.role_id
|
||||||
|
JOIN admin_users au ON au.id = aur.admin_user_id
|
||||||
|
WHERE g.code = 'mohong'
|
||||||
|
AND r.code = 'cs'
|
||||||
|
AND au.status = 'active';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
DELETE gm FROM chat_support_group_members gm
|
||||||
|
JOIN chat_support_groups g ON g.id = gm.group_id
|
||||||
|
WHERE g.code = 'mohong';
|
||||||
|
|
||||||
|
DELETE FROM chat_support_groups WHERE code = 'mohong';
|
||||||
|
|
||||||
|
DROP INDEX idx_chat_conversations_type_scene ON chat_conversations;
|
||||||
|
|
||||||
|
ALTER TABLE chat_conversations DROP COLUMN support_scene;
|
||||||
@@ -14,6 +14,8 @@ export interface ChatParticipant {
|
|||||||
joined_at: string
|
joined_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SupportScene = 'general' | 'mohong'
|
||||||
|
|
||||||
export interface ChatConversation {
|
export interface ChatConversation {
|
||||||
id: number
|
id: number
|
||||||
order_id: number | null
|
order_id: number | null
|
||||||
@@ -24,6 +26,7 @@ export interface ChatConversation {
|
|||||||
latest_handoff_status?: string
|
latest_handoff_status?: string
|
||||||
latest_refund_status?: string
|
latest_refund_status?: string
|
||||||
type: string
|
type: string
|
||||||
|
support_scene?: string
|
||||||
title: string
|
title: string
|
||||||
status: string
|
status: string
|
||||||
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
||||||
@@ -39,6 +42,19 @@ export interface ChatConversation {
|
|||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据当前路由推断客服场景:摸大红页 → mohong,其它 → general */
|
||||||
|
export function resolveSupportScene(path: string): SupportScene {
|
||||||
|
if (
|
||||||
|
path === '/mohong' ||
|
||||||
|
path.startsWith('/mohong/') ||
|
||||||
|
path === '/m/mohong' ||
|
||||||
|
path.startsWith('/m/mohong/')
|
||||||
|
) {
|
||||||
|
return 'mohong'
|
||||||
|
}
|
||||||
|
return 'general'
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminChatCounts {
|
export interface AdminChatCounts {
|
||||||
ownership?: Record<string, number>
|
ownership?: Record<string, number>
|
||||||
stages?: Record<string, number>
|
stages?: Record<string, number>
|
||||||
@@ -97,8 +113,10 @@ export async function fetchOrderChat(orderId: number) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureSupportChat() {
|
export async function ensureSupportChat(scene: SupportScene | string = 'general') {
|
||||||
const { data } = await apiClient.post<ApiResponse<ChatConversation>>('/chats/support')
|
const { data } = await apiClient.post<ApiResponse<ChatConversation>>('/chats/support', {
|
||||||
|
scene: scene || 'general',
|
||||||
|
})
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||||||
import { onBeforeRouteLeave, RouterLink, useRoute, useRouter } from 'vue-router'
|
import { onBeforeRouteLeave, RouterLink, useRoute, 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 '@/features/chats/api/chats'
|
import { ensureSupportChat, resolveSupportScene } from '@/features/chats/api/chats'
|
||||||
import { formatCent, formatMoney } from '@/shared/utils/money'
|
import { formatCent, formatMoney } from '@/shared/utils/money'
|
||||||
import {
|
import {
|
||||||
emptyListingPublishOptions,
|
emptyListingPublishOptions,
|
||||||
@@ -158,7 +158,8 @@ async function handleSupportClick() {
|
|||||||
if (supportLoading.value) return
|
if (supportLoading.value) return
|
||||||
supportLoading.value = true
|
supportLoading.value = true
|
||||||
try {
|
try {
|
||||||
const chat = await ensureSupportChat()
|
const scene = resolveSupportScene(route.path)
|
||||||
|
const chat = await ensureSupportChat(scene)
|
||||||
router.push(`/m/chats/${chat.id}`)
|
router.push(`/m/chats/${chat.id}`)
|
||||||
} catch {
|
} catch {
|
||||||
showToast({ message: '联系客服失败,请稍后重试', icon: 'cross' })
|
showToast({ message: '联系客服失败,请稍后重试', icon: 'cross' })
|
||||||
|
|||||||
@@ -3,17 +3,21 @@ import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, 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 '@/features/chats/api/chats'
|
||||||
import {
|
import {
|
||||||
fetchMohongCategories,
|
fetchMohongCategories,
|
||||||
fetchMohongProducts,
|
fetchMohongProducts,
|
||||||
type MohongCategory,
|
type MohongCategory,
|
||||||
type MohongProduct,
|
type MohongProduct,
|
||||||
} from '@/features/mohong/api/mohong'
|
} from '@/features/mohong/api/mohong'
|
||||||
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { formatCent } from '@/shared/utils/money'
|
import { formatCent } from '@/shared/utils/money'
|
||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const session = useSessionStore()
|
||||||
|
const supportLoading = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const products = ref<MohongProduct[]>([])
|
const products = ref<MohongProduct[]>([])
|
||||||
const categories = ref<MohongCategory[]>([])
|
const categories = ref<MohongCategory[]>([])
|
||||||
@@ -153,6 +157,23 @@ function markCoverFailed(id: number) {
|
|||||||
function showCover(item: MohongProduct) {
|
function showCover(item: MohongProduct) {
|
||||||
return Boolean(item.cover_url) && !failedCover.value[item.id]
|
return Boolean(item.cover_url) && !failedCover.value[item.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSupportClick() {
|
||||||
|
if (!session.isLoggedIn) {
|
||||||
|
router.push({ path: '/m/login', query: { redirect: route.fullPath } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (supportLoading.value) return
|
||||||
|
supportLoading.value = true
|
||||||
|
try {
|
||||||
|
const chat = await ensureSupportChat('mohong')
|
||||||
|
router.push(`/m/chats/${chat.id}`)
|
||||||
|
} catch {
|
||||||
|
showToast({ message: '联系客服失败,请稍后重试', icon: 'cross' })
|
||||||
|
} finally {
|
||||||
|
supportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -170,6 +191,14 @@ function showCover(item: MohongProduct) {
|
|||||||
@input="onSearchInput"
|
@input="onSearchInput"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="orders-btn"
|
||||||
|
:disabled="supportLoading"
|
||||||
|
@click="handleSupportClick"
|
||||||
|
>
|
||||||
|
{{ supportLoading ? '...' : '客服' }}
|
||||||
|
</button>
|
||||||
<button type="button" class="orders-btn" @click="router.push('/mohong/orders')">订单</button>
|
<button type="button" class="orders-btn" @click="router.push('/mohong/orders')">订单</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,22 @@
|
|||||||
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { Search, Tickets } from '@element-plus/icons-vue'
|
import { Search, Service, Tickets } from '@element-plus/icons-vue'
|
||||||
import MohongProductCard from '@/features/mohong/components/MohongProductCard.vue'
|
import MohongProductCard from '@/features/mohong/components/MohongProductCard.vue'
|
||||||
|
import { ensureSupportChat } from '@/features/chats/api/chats'
|
||||||
import {
|
import {
|
||||||
fetchMohongCategories,
|
fetchMohongCategories,
|
||||||
fetchMohongProducts,
|
fetchMohongProducts,
|
||||||
type MohongCategory,
|
type MohongCategory,
|
||||||
type MohongProduct,
|
type MohongProduct,
|
||||||
} from '@/features/mohong/api/mohong'
|
} from '@/features/mohong/api/mohong'
|
||||||
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { readError } from '@/shared/utils/error'
|
import { readError } from '@/shared/utils/error'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const session = useSessionStore()
|
||||||
|
const supportLoading = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const products = ref<MohongProduct[]>([])
|
const products = ref<MohongProduct[]>([])
|
||||||
@@ -147,6 +151,23 @@ const activeCategoryName = () => {
|
|||||||
if (!activeCategoryId.value) return '全部'
|
if (!activeCategoryId.value) return '全部'
|
||||||
return categories.value.find(c => c.id === activeCategoryId.value)?.name || '全部'
|
return categories.value.find(c => c.id === activeCategoryId.value)?.name || '全部'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleSupportClick() {
|
||||||
|
if (!session.isLoggedIn) {
|
||||||
|
router.push({ path: '/login', query: { redirect: route.fullPath } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (supportLoading.value) return
|
||||||
|
supportLoading.value = true
|
||||||
|
try {
|
||||||
|
const chat = await ensureSupportChat('mohong')
|
||||||
|
router.push(`/messages/${chat.id}`)
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('联系客服失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
supportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -171,6 +192,15 @@ const activeCategoryName = () => {
|
|||||||
<el-icon><Tickets /></el-icon>
|
<el-icon><Tickets /></el-icon>
|
||||||
<span>我的订单</span>
|
<span>我的订单</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="toolbar-btn outline"
|
||||||
|
:disabled="supportLoading"
|
||||||
|
@click="handleSupportClick"
|
||||||
|
>
|
||||||
|
<el-icon><Service /></el-icon>
|
||||||
|
<span>{{ supportLoading ? '接入中' : '联系客服' }}</span>
|
||||||
|
</button>
|
||||||
<button type="button" class="toolbar-btn outline" @click="router.push('/')">
|
<button type="button" class="toolbar-btn outline" @click="router.push('/')">
|
||||||
租号大厅
|
租号大厅
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function loadOrders() {
|
|||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<h1>摸大红订单</h1>
|
<h1>摸大红订单</h1>
|
||||||
<p class="sub">查看支付状态、复制订单信息、进入客服群</p>
|
<p class="sub">查看支付状态、复制订单信息、扫码联系客服</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong')">
|
<button type="button" class="toolbar-btn outline" @click="router.push('/mohong')">
|
||||||
返回商品列表
|
返回商品列表
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
VideoPlay,
|
VideoPlay,
|
||||||
Wallet,
|
Wallet,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { ensureSupportChat } from '@/features/chats/api/chats'
|
import { ensureSupportChat, resolveSupportScene } from '@/features/chats/api/chats'
|
||||||
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
|
import { useChatUnreadCount } from '@/features/chats/composables/useChatUnreadCount'
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
@@ -111,7 +111,8 @@ async function handleSupportClick() {
|
|||||||
if (supportLoading.value) return
|
if (supportLoading.value) return
|
||||||
supportLoading.value = true
|
supportLoading.value = true
|
||||||
try {
|
try {
|
||||||
const chat = await ensureSupportChat()
|
const scene = resolveSupportScene(route.path)
|
||||||
|
const chat = await ensureSupportChat(scene)
|
||||||
router.push(`/messages/${chat.id}`)
|
router.push(`/messages/${chat.id}`)
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('联系客服失败,请稍后重试')
|
ElMessage.error('联系客服失败,请稍后重试')
|
||||||
|
|||||||
Reference in New Issue
Block a user