管理员可以看到所有会话了
This commit is contained in:
@@ -149,6 +149,38 @@ func (r *Repository) ListConversations(principal Principal, page, pageSize int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Repository) FindConversation(principal Principal, id uint64) (*ConversationDTO, error) {
|
func (r *Repository) FindConversation(principal Principal, id uint64) (*ConversationDTO, error) {
|
||||||
|
// 管理员可以查看任意会话,无需是 participant
|
||||||
|
if principal.Type == "admin" {
|
||||||
|
var conversation model.ChatConversation
|
||||||
|
if err := r.db.First(&conversation, id).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, ErrConversationNotFound
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
participants, err := r.participants(conversation.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dto := ConversationDTO{
|
||||||
|
ID: conversation.ID,
|
||||||
|
OrderID: conversation.OrderID,
|
||||||
|
Type: conversation.Type,
|
||||||
|
Title: conversation.Title,
|
||||||
|
Status: conversation.Status,
|
||||||
|
Role: "admin", // 管理员角色
|
||||||
|
Participants: participants,
|
||||||
|
LastMessageID: conversation.LastMessageID,
|
||||||
|
LastMessagePreview: conversation.LastMessagePreview,
|
||||||
|
LastMessageAt: conversation.LastMessageAt,
|
||||||
|
UnreadCount: 0, // 管理员不计未读
|
||||||
|
CreatedAt: conversation.CreatedAt,
|
||||||
|
UpdatedAt: conversation.UpdatedAt,
|
||||||
|
}
|
||||||
|
return &dto, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户需要是 participant
|
||||||
var row conversationRow
|
var row conversationRow
|
||||||
err := r.conversationQuery(principal).Where("c.id = ?", id).First(&row).Error
|
err := r.conversationQuery(principal).Where("c.id = ?", id).First(&row).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -269,9 +301,23 @@ func (r *Repository) EnsureSupportConversation(userID uint64) (*ConversationDTO,
|
|||||||
|
|
||||||
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 {
|
|
||||||
return nil, err
|
// 管理员可以查看任意会话的消息,普通用户需要是 participant
|
||||||
|
if principal.Type != "admin" {
|
||||||
|
if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 管理员需要验证会话存在
|
||||||
|
var count int64
|
||||||
|
if err := r.db.Model(&model.ChatConversation{}).Where("id = ?", conversationID).Count(&count).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
return nil, ErrConversationNotFound
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var total int64
|
var total int64
|
||||||
if err := r.db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil {
|
if err := r.db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -295,10 +341,6 @@ func (r *Repository) Messages(principal Principal, conversationID uint64, page,
|
|||||||
func (r *Repository) SendMessage(principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) {
|
func (r *Repository) SendMessage(principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) {
|
||||||
var messageID uint64
|
var messageID uint64
|
||||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
participant, err := r.findParticipant(tx, principal, conversationID, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var conversation model.ChatConversation
|
var conversation model.ChatConversation
|
||||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil {
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -306,11 +348,41 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
|||||||
if conversation.Status != "active" {
|
if conversation.Status != "active" {
|
||||||
return ErrPermissionDenied
|
return ErrPermissionDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var senderRole string
|
||||||
|
var participant *model.ChatParticipant
|
||||||
|
|
||||||
|
// 管理员可以在任意会话发送消息,无需是 participant
|
||||||
|
if principal.Type == "admin" {
|
||||||
|
// 尝试查找管理员的 participant 记录
|
||||||
|
var p model.ChatParticipant
|
||||||
|
err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
|
||||||
|
conversationID, principal.Type, principal.ID).First(&p).Error
|
||||||
|
if err == nil {
|
||||||
|
// 管理员是 participant,使用其角色
|
||||||
|
participant = &p
|
||||||
|
senderRole = p.Role
|
||||||
|
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// 管理员不是 participant,使用特殊角色 "admin"
|
||||||
|
senderRole = "admin"
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 普通用户必须是 participant
|
||||||
|
p, err := r.findParticipant(tx, principal, conversationID, true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
participant = p
|
||||||
|
senderRole = p.Role
|
||||||
|
}
|
||||||
|
|
||||||
message := model.ChatMessage{
|
message := model.ChatMessage{
|
||||||
ConversationID: conversation.ID,
|
ConversationID: conversation.ID,
|
||||||
SenderType: principal.Type,
|
SenderType: principal.Type,
|
||||||
SenderID: principal.ID,
|
SenderID: principal.ID,
|
||||||
SenderRole: participant.Role,
|
SenderRole: senderRole,
|
||||||
ContentType: "text",
|
ContentType: "text",
|
||||||
Content: req.Content,
|
Content: req.Content,
|
||||||
AttachmentURLS: encodeStringList(req.AttachmentURLS),
|
AttachmentURLS: encodeStringList(req.AttachmentURLS),
|
||||||
@@ -324,10 +396,15 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
|||||||
if err := tx.Save(&conversation).Error; err != nil {
|
if err := tx.Save(&conversation).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
|
||||||
if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil {
|
// 更新 participant 的已读时间(仅当是 participant 时)
|
||||||
return err
|
if participant != nil {
|
||||||
|
now := time.Now()
|
||||||
|
if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
messageID = message.ID
|
messageID = message.ID
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@@ -370,6 +447,24 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
|||||||
|
|
||||||
func (r *Repository) MarkRead(principal Principal, conversationID uint64) error {
|
func (r *Repository) MarkRead(principal Principal, conversationID uint64) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 管理员可以不是 participant,直接返回成功
|
||||||
|
if principal.Type == "admin" {
|
||||||
|
// 尝试查找 participant 记录,如果有就更新
|
||||||
|
var participant model.ChatParticipant
|
||||||
|
err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
|
||||||
|
conversationID, principal.Type, principal.ID).First(&participant).Error
|
||||||
|
if err == nil {
|
||||||
|
// 有 participant 记录,更新已读时间
|
||||||
|
now := time.Now()
|
||||||
|
return tx.Model(&participant).Update("last_read_at", now).Error
|
||||||
|
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// 没有 participant 记录,直接返回成功(管理员无需记录已读)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 普通用户必须是 participant
|
||||||
participant, err := r.findParticipant(tx, principal, conversationID, true)
|
participant, err := r.findParticipant(tx, principal, conversationID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -475,6 +570,10 @@ func (r *Repository) toMessageDTOs(principal Principal, rows []model.ChatMessage
|
|||||||
}
|
}
|
||||||
if row.SenderType == "admin" {
|
if row.SenderType == "admin" {
|
||||||
name = adminNames[row.SenderID]
|
name = adminNames[row.SenderID]
|
||||||
|
// 如果角色是 "admin"(不是 participant 的管理员),在名字后添加标识
|
||||||
|
if row.SenderRole == "admin" {
|
||||||
|
name = name + " (管理员)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items = append(items, MessageDTO{
|
items = append(items, MessageDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
@@ -830,8 +929,50 @@ func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) {
|
|||||||
// ListConversationsWithFilter 支持筛选的会话列表
|
// ListConversationsWithFilter 支持筛选的会话列表
|
||||||
func (r *Repository) ListConversationsWithFilter(principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) {
|
func (r *Repository) ListConversationsWithFilter(principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) {
|
||||||
page, pageSize = normalizePagination(page, pageSize)
|
page, pageSize = normalizePagination(page, pageSize)
|
||||||
var total int64
|
|
||||||
|
|
||||||
|
// 管理员在"全部"模式下直接查询所有会话
|
||||||
|
if principal.Type == "admin" && filter == "all" {
|
||||||
|
var total int64
|
||||||
|
if err := r.db.Model(&model.ChatConversation{}).Count(&total).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var conversations []model.ChatConversation
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := r.db.Order("COALESCE(last_message_at, created_at) DESC, id DESC").
|
||||||
|
Offset(offset).
|
||||||
|
Limit(pageSize).
|
||||||
|
Find(&conversations).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]ConversationDTO, 0, len(conversations))
|
||||||
|
for _, conv := range conversations {
|
||||||
|
participants, err := r.participants(conv.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, ConversationDTO{
|
||||||
|
ID: conv.ID,
|
||||||
|
OrderID: conv.OrderID,
|
||||||
|
Type: conv.Type,
|
||||||
|
Title: conv.Title,
|
||||||
|
Status: conv.Status,
|
||||||
|
Role: "admin", // 管理员角色
|
||||||
|
Participants: participants,
|
||||||
|
LastMessageID: conv.LastMessageID,
|
||||||
|
LastMessagePreview: conv.LastMessagePreview,
|
||||||
|
LastMessageAt: conv.LastMessageAt,
|
||||||
|
UnreadCount: 0, // 管理员不计未读
|
||||||
|
CreatedAt: conv.CreatedAt,
|
||||||
|
UpdatedAt: conv.UpdatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他情况使用原有逻辑
|
||||||
|
var total int64
|
||||||
countDB := r.db.Table("chat_conversations AS c").
|
countDB := r.db.Table("chat_conversations AS 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")
|
||||||
|
|
||||||
@@ -844,12 +985,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page
|
|||||||
countDB = countDB.Where("c.id NOT IN (?)",
|
countDB = countDB.Where("c.id NOT IN (?)",
|
||||||
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
||||||
default:
|
default:
|
||||||
// 全部会话(admin 可以看所有)
|
// 普通用户的全部会话
|
||||||
if principal.Type == "admin" {
|
countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||||
// 管理员看所有会话
|
|
||||||
} else {
|
|
||||||
countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := countDB.Count(&total).Error; err != nil {
|
if err := countDB.Count(&total).Error; err != nil {
|
||||||
@@ -867,11 +1004,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page
|
|||||||
queryDB = queryDB.Where("c.id NOT IN (?)",
|
queryDB = queryDB.Where("c.id NOT IN (?)",
|
||||||
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
||||||
default:
|
default:
|
||||||
if principal.Type == "admin" {
|
// 普通用户的全部会话
|
||||||
// 管理员看所有会话
|
queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||||
} else {
|
|
||||||
queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := queryDB.
|
err := queryDB.
|
||||||
|
|||||||
@@ -276,12 +276,17 @@ function roleLabel(role: string) {
|
|||||||
support: '客服',
|
support: '客服',
|
||||||
customer: '咨询',
|
customer: '咨询',
|
||||||
system: '系统',
|
system: '系统',
|
||||||
|
admin: '管理员',
|
||||||
}
|
}
|
||||||
return map[role] || '成员'
|
return map[role] || '成员'
|
||||||
}
|
}
|
||||||
|
|
||||||
function senderLabel(item: ChatMessage) {
|
function senderLabel(item: ChatMessage) {
|
||||||
if (item.sender_type === 'system') return '系统'
|
if (item.sender_type === 'system') return '系统'
|
||||||
|
// 管理员消息特殊标识
|
||||||
|
if (item.sender_role === 'admin') {
|
||||||
|
return `管理员 · ${item.sender_name}`
|
||||||
|
}
|
||||||
return `${roleLabel(item.sender_role)} · ${item.sender_name}`
|
return `${roleLabel(item.sender_role)} · ${item.sender_name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,13 +379,19 @@ function getSupportName(item: ChatConversation) {
|
|||||||
v-for="item in messages"
|
v-for="item in messages"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="message-row"
|
class="message-row"
|
||||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
:class="{
|
||||||
|
self: item.is_self,
|
||||||
|
system: item.sender_type === 'system',
|
||||||
|
admin: item.sender_role === 'admin'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="item.sender_type === 'system'">
|
<template v-if="item.sender_type === 'system'">
|
||||||
<span>{{ item.content }}</span>
|
<span>{{ item.content }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<small>{{ senderLabel(item) }} · {{ formatDateMinute(item.created_at) }}</small>
|
<small :class="{ 'admin-label': item.sender_role === 'admin' }">
|
||||||
|
{{ senderLabel(item) }} · {{ formatDateMinute(item.created_at) }}
|
||||||
|
</small>
|
||||||
<p v-if="item.content">{{ item.content }}</p>
|
<p v-if="item.content">{{ item.content }}</p>
|
||||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||||
<ChatAttachmentImage
|
<ChatAttachmentImage
|
||||||
@@ -678,6 +689,16 @@ function getSupportName(item: ChatConversation) {
|
|||||||
background: #dff5eb;
|
background: #dff5eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-row.admin p {
|
||||||
|
background: #fff4e6;
|
||||||
|
border-left: 3px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-label {
|
||||||
|
color: #ff9800 !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.message-attachments {
|
.message-attachments {
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: start;
|
justify-items: start;
|
||||||
|
|||||||
@@ -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' | 'customer'
|
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
||||||
remark: string
|
remark: string
|
||||||
display_name: string
|
display_name: string
|
||||||
avatar_url: string
|
avatar_url: string
|
||||||
@@ -20,7 +20,7 @@ export interface ChatConversation {
|
|||||||
type: string
|
type: string
|
||||||
title: string
|
title: string
|
||||||
status: string
|
status: string
|
||||||
role: 'renter' | 'owner' | 'support' | 'customer'
|
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
||||||
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' | 'customer' | 'system'
|
sender_role: 'renter' | 'owner' | 'support' | 'customer' | 'system' | 'admin'
|
||||||
sender_name: string
|
sender_name: string
|
||||||
sender_avatar: string
|
sender_avatar: string
|
||||||
is_self: boolean
|
is_self: boolean
|
||||||
|
|||||||
@@ -204,12 +204,17 @@ function roleLabel(role: string) {
|
|||||||
support: '客服',
|
support: '客服',
|
||||||
customer: '咨询',
|
customer: '咨询',
|
||||||
system: '系统',
|
system: '系统',
|
||||||
|
admin: '管理员',
|
||||||
}
|
}
|
||||||
return map[role] || '成员'
|
return map[role] || '成员'
|
||||||
}
|
}
|
||||||
|
|
||||||
function senderLabel(message: ChatMessage) {
|
function senderLabel(message: ChatMessage) {
|
||||||
if (message.sender_type === 'system') return '系统'
|
if (message.sender_type === 'system') return '系统'
|
||||||
|
// 管理员消息特殊标识
|
||||||
|
if (message.sender_role === 'admin') {
|
||||||
|
return `管理员 · ${message.sender_name}`
|
||||||
|
}
|
||||||
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -255,7 +260,11 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
v-for="item in messages"
|
v-for="item in messages"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="message-row"
|
class="message-row"
|
||||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
:class="{
|
||||||
|
self: item.is_self,
|
||||||
|
system: item.sender_type === 'system',
|
||||||
|
admin: item.sender_role === 'admin' && !item.is_self
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="item.sender_type === 'system'">
|
<template v-if="item.sender_type === 'system'">
|
||||||
<span class="system-message">{{ item.content }}</span>
|
<span class="system-message">{{ item.content }}</span>
|
||||||
@@ -263,7 +272,9 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="avatar" :class="{ self: item.is_self }">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
<div class="avatar" :class="{ self: item.is_self }">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
||||||
<div class="bubble-wrap" :class="{ self: item.is_self }">
|
<div class="bubble-wrap" :class="{ self: item.is_self }">
|
||||||
<span class="sender-name">{{ senderLabel(item) }}</span>
|
<span class="sender-name" :class="{ 'admin-label': item.sender_role === 'admin' && !item.is_self }">
|
||||||
|
{{ senderLabel(item) }}
|
||||||
|
</span>
|
||||||
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
||||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||||
<ChatAttachmentImage
|
<ChatAttachmentImage
|
||||||
@@ -469,6 +480,20 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
color: #0f5132;
|
color: #0f5132;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-row.admin .bubble {
|
||||||
|
background: #fff4e6;
|
||||||
|
border-left: 3px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row.admin .avatar {
|
||||||
|
background: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-label {
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.message-attachments {
|
.message-attachments {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
@@ -196,12 +196,17 @@ function roleLabel(role: string) {
|
|||||||
support: '客服',
|
support: '客服',
|
||||||
customer: '咨询',
|
customer: '咨询',
|
||||||
system: '系统',
|
system: '系统',
|
||||||
|
admin: '管理员',
|
||||||
}
|
}
|
||||||
return map[role] || '成员'
|
return map[role] || '成员'
|
||||||
}
|
}
|
||||||
|
|
||||||
function senderLabel(message: ChatMessage) {
|
function senderLabel(message: ChatMessage) {
|
||||||
if (message.sender_type === 'system') return '系统'
|
if (message.sender_type === 'system') return '系统'
|
||||||
|
// 管理员消息特殊标识
|
||||||
|
if (message.sender_role === 'admin') {
|
||||||
|
return `管理员 · ${message.sender_name}`
|
||||||
|
}
|
||||||
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -228,7 +233,11 @@ function senderLabel(message: ChatMessage) {
|
|||||||
v-for="item in messages"
|
v-for="item in messages"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="message-row"
|
class="message-row"
|
||||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
:class="{
|
||||||
|
self: item.is_self,
|
||||||
|
system: item.sender_type === 'system',
|
||||||
|
admin: item.sender_role === 'admin' && !item.is_self
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<template v-if="item.sender_type === 'system'">
|
<template v-if="item.sender_type === 'system'">
|
||||||
<span class="system-message">{{ item.content }}</span>
|
<span class="system-message">{{ item.content }}</span>
|
||||||
@@ -236,7 +245,9 @@ function senderLabel(message: ChatMessage) {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="avatar">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
<div class="avatar">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
||||||
<div class="bubble-wrap">
|
<div class="bubble-wrap">
|
||||||
<span class="sender-name">{{ senderLabel(item) }}</span>
|
<span class="sender-name" :class="{ 'admin-label': item.sender_role === 'admin' && !item.is_self }">
|
||||||
|
{{ senderLabel(item) }}
|
||||||
|
</span>
|
||||||
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
||||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||||
<ChatAttachmentImage
|
<ChatAttachmentImage
|
||||||
@@ -421,6 +432,20 @@ function senderLabel(message: ChatMessage) {
|
|||||||
background: #dff5eb;
|
background: #dff5eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.message-row.admin .bubble {
|
||||||
|
background: #fff4e6;
|
||||||
|
border-left: 3px solid #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-row.admin .avatar {
|
||||||
|
background: #ff9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-label {
|
||||||
|
color: #ff9800;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.message-attachments {
|
.message-attachments {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
Reference in New Issue
Block a user