feat(communication): 优化通讯录与会话消息发送体验
1. 重构DirectoryDetailDrawer为DirectoryDetailModal并完善详情弹窗功能 2. 调整MessageSendModal布局与样式,优化预填模式提示信息 3. 补充发送目标的名称展示,修复渠道标签获取逻辑 4. 统一通讯录发消息时的参数传递,添加目标名称参数 5. 修复channelTypeOptions可选链调用问题
This commit is contained in:
parent
eb80382da0
commit
76981cd9bc
@ -1,10 +1,13 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
<a-modal
|
||||
:open="visible"
|
||||
:title="drawerTitle"
|
||||
:title="modalTitle"
|
||||
:width="640"
|
||||
placement="right"
|
||||
@close="handleClose"
|
||||
:footer="null"
|
||||
:destroy-on-close="true"
|
||||
:mask-closable="!loading"
|
||||
:body-style="{ maxHeight: '70vh', overflowY: 'auto' }"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<!-- 用户资料 -->
|
||||
<template v-if="type === 'user'">
|
||||
@ -21,8 +24,41 @@
|
||||
</template>
|
||||
</a-alert>
|
||||
<template v-else-if="data">
|
||||
<h4 class="ch-dir-drawer__section-title">基本信息</h4>
|
||||
<a-descriptions :column="1" size="small" bordered>
|
||||
<!-- 身份卡片 -->
|
||||
<div class="ch-dir-modal__identity">
|
||||
<a
|
||||
v-if="data.avatar_url && !avatarError"
|
||||
:href="data.avatar_url"
|
||||
target="_blank"
|
||||
class="ch-dir-modal__avatar-link"
|
||||
>
|
||||
<img
|
||||
:src="data.avatar_url"
|
||||
alt="头像"
|
||||
class="ch-dir-modal__avatar"
|
||||
@error="avatarError = true"
|
||||
/>
|
||||
</a>
|
||||
<a-avatar
|
||||
v-else
|
||||
:size="64"
|
||||
:style="{ backgroundColor: avatarColor(data.name) }"
|
||||
>
|
||||
{{ avatarInitial(data.name) }}
|
||||
</a-avatar>
|
||||
<div class="ch-dir-modal__identity-main">
|
||||
<div class="ch-dir-modal__identity-title">
|
||||
<span class="ch-dir-modal__name">{{ data.name || '-' }}</span>
|
||||
<a-tag size="small" color="processing">用户</a-tag>
|
||||
</div>
|
||||
<a-tooltip :title="targetId">
|
||||
<span class="ch-dir-modal__identity-id">{{ targetId }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="ch-dir-modal__section-title">基本信息</h4>
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item v-for="item in descriptionItems" :key="item.key" :label="item.label">
|
||||
<template v-if="item.key === 'avatar_url'">
|
||||
<template v-if="item.value">
|
||||
@ -30,12 +66,12 @@
|
||||
v-if="!avatarError"
|
||||
:href="item.value"
|
||||
target="_blank"
|
||||
class="ch-dir-drawer__avatar-link"
|
||||
class="ch-dir-modal__avatar-link"
|
||||
>
|
||||
<img
|
||||
:src="item.value"
|
||||
alt="头像"
|
||||
class="ch-dir-drawer__avatar"
|
||||
class="ch-dir-modal__avatar--small"
|
||||
@error="avatarError = true"
|
||||
/>
|
||||
</a>
|
||||
@ -50,7 +86,7 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'tags'">
|
||||
<span v-if="normalizeTags(item.value).length" class="ch-dir-drawer__tags">
|
||||
<span v-if="normalizeTags(item.value).length" class="ch-dir-modal__tags">
|
||||
<a-tag v-for="tag in normalizeTags(item.value)" :key="tag">{{ tag }}</a-tag>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
@ -59,14 +95,14 @@
|
||||
{{ formatTime(item.value) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="ch-dir-drawer__mono" :title="formatValue(item.value)">
|
||||
<span class="ch-dir-modal__mono" :title="formatValue(item.value)">
|
||||
{{ formatValue(item.value) }}
|
||||
</span>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div v-if="targetId" class="ch-dir-drawer__actions">
|
||||
<div v-if="targetId" class="ch-dir-modal__actions">
|
||||
<a-tooltip
|
||||
v-if="showSendMessage"
|
||||
:title="canSendMessage ? '' : '请先选择渠道与账户'"
|
||||
@ -79,18 +115,18 @@
|
||||
</a-tooltip>
|
||||
<router-link
|
||||
:to="{ name: 'ChannelsSessions', query: sessionQuery }"
|
||||
class="ch-dir-drawer__action-btn"
|
||||
class="ch-dir-modal__action-btn"
|
||||
>
|
||||
<History :size="14" />
|
||||
查看历史会话
|
||||
</router-link>
|
||||
<a-button type="link" class="ch-dir-drawer__action-btn" @click="copyId">
|
||||
<a-button type="link" class="ch-dir-modal__action-btn" @click="copyId">
|
||||
<template #icon><Copy :size="14" /></template>
|
||||
复制 ID
|
||||
</a-button>
|
||||
<router-link
|
||||
:to="{ name: 'ChannelsRouteBindings', query: routeBindingQuery }"
|
||||
class="ch-dir-drawer__action-btn"
|
||||
class="ch-dir-modal__action-btn"
|
||||
>
|
||||
<Route :size="14" />
|
||||
查看路由绑定
|
||||
@ -114,8 +150,41 @@
|
||||
</template>
|
||||
</a-alert>
|
||||
<template v-else-if="data">
|
||||
<h4 class="ch-dir-drawer__section-title">基本信息</h4>
|
||||
<a-descriptions :column="1" size="small" bordered>
|
||||
<!-- 身份卡片 -->
|
||||
<div class="ch-dir-modal__identity">
|
||||
<a
|
||||
v-if="data.avatar_url && !avatarError"
|
||||
:href="data.avatar_url"
|
||||
target="_blank"
|
||||
class="ch-dir-modal__avatar-link"
|
||||
>
|
||||
<img
|
||||
:src="data.avatar_url"
|
||||
alt="头像"
|
||||
class="ch-dir-modal__avatar"
|
||||
@error="avatarError = true"
|
||||
/>
|
||||
</a>
|
||||
<a-avatar
|
||||
v-else
|
||||
:size="64"
|
||||
:style="{ backgroundColor: avatarColor(data.name) }"
|
||||
>
|
||||
{{ avatarInitial(data.name) }}
|
||||
</a-avatar>
|
||||
<div class="ch-dir-modal__identity-main">
|
||||
<div class="ch-dir-modal__identity-title">
|
||||
<span class="ch-dir-modal__name">{{ data.name || '-' }}</span>
|
||||
<a-tag size="small" color="success">群组</a-tag>
|
||||
</div>
|
||||
<a-tooltip :title="targetId">
|
||||
<span class="ch-dir-modal__identity-id">{{ targetId }}</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="ch-dir-modal__section-title">基本信息</h4>
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item v-for="item in descriptionItems" :key="item.key" :label="item.label">
|
||||
<template v-if="item.key === 'avatar_url'">
|
||||
<template v-if="item.value">
|
||||
@ -123,12 +192,12 @@
|
||||
v-if="!avatarError"
|
||||
:href="item.value"
|
||||
target="_blank"
|
||||
class="ch-dir-drawer__avatar-link"
|
||||
class="ch-dir-modal__avatar-link"
|
||||
>
|
||||
<img
|
||||
:src="item.value"
|
||||
alt="头像"
|
||||
class="ch-dir-drawer__avatar"
|
||||
class="ch-dir-modal__avatar--small"
|
||||
@error="avatarError = true"
|
||||
/>
|
||||
</a>
|
||||
@ -143,7 +212,7 @@
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-else-if="item.key === 'tags'">
|
||||
<span v-if="normalizeTags(item.value).length" class="ch-dir-drawer__tags">
|
||||
<span v-if="normalizeTags(item.value).length" class="ch-dir-modal__tags">
|
||||
<a-tag v-for="tag in normalizeTags(item.value)" :key="tag">{{ tag }}</a-tag>
|
||||
</span>
|
||||
<span v-else>-</span>
|
||||
@ -152,14 +221,14 @@
|
||||
{{ formatTime(item.value) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="ch-dir-drawer__mono" :title="formatValue(item.value)">
|
||||
<span class="ch-dir-modal__mono" :title="formatValue(item.value)">
|
||||
{{ formatValue(item.value) }}
|
||||
</span>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div v-if="targetId" class="ch-dir-drawer__actions">
|
||||
<div v-if="targetId" class="ch-dir-modal__actions">
|
||||
<a-tooltip
|
||||
v-if="showSendMessage"
|
||||
:title="canSendMessage ? '' : '请先选择渠道与账户'"
|
||||
@ -172,27 +241,27 @@
|
||||
</a-tooltip>
|
||||
<router-link
|
||||
:to="{ name: 'ChannelsSessions', query: sessionQuery }"
|
||||
class="ch-dir-drawer__action-btn"
|
||||
class="ch-dir-modal__action-btn"
|
||||
>
|
||||
<History :size="14" />
|
||||
查看历史会话
|
||||
</router-link>
|
||||
<a-button type="link" class="ch-dir-drawer__action-btn" @click="copyId">
|
||||
<a-button type="link" class="ch-dir-modal__action-btn" @click="copyId">
|
||||
<template #icon><Copy :size="14" /></template>
|
||||
复制 ID
|
||||
</a-button>
|
||||
<router-link
|
||||
:to="{ name: 'ChannelsRouteBindings', query: routeBindingQuery }"
|
||||
class="ch-dir-drawer__action-btn"
|
||||
class="ch-dir-modal__action-btn"
|
||||
>
|
||||
<Route :size="14" />
|
||||
查看路由绑定
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div class="ch-dir-drawer__members-header">
|
||||
<h4 class="ch-dir-drawer__section-title">群组成员</h4>
|
||||
<span v-if="members.length" class="ch-dir-drawer__count"
|
||||
<div class="ch-dir-modal__members-header">
|
||||
<h4 class="ch-dir-modal__section-title">群组成员</h4>
|
||||
<span v-if="members.length" class="ch-dir-modal__count"
|
||||
>共 {{ members.length }} 条已加载</span
|
||||
>
|
||||
</div>
|
||||
@ -206,36 +275,37 @@
|
||||
style="margin-bottom: 12px"
|
||||
/>
|
||||
<LoadingState v-if="membersLoading" type="table" :rows="4" />
|
||||
<a-table
|
||||
v-else
|
||||
:columns="memberColumns"
|
||||
:data-source="members"
|
||||
:pagination="false"
|
||||
row-key="user_id"
|
||||
size="small"
|
||||
:locale="{ emptyText: '暂无成员数据' }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'user_id'">
|
||||
<a-tooltip :title="record.user_id">
|
||||
<span class="ch-dir-drawer__ellipsis">{{ record.user_id }}</span>
|
||||
</a-tooltip>
|
||||
<div v-else class="ch-dir-modal__members-table">
|
||||
<a-table
|
||||
:columns="memberColumns"
|
||||
:data-source="members"
|
||||
:pagination="false"
|
||||
row-key="user_id"
|
||||
size="small"
|
||||
:locale="{ emptyText: '暂无成员数据' }"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'user_id'">
|
||||
<a-tooltip :title="record.user_id">
|
||||
<span class="ch-dir-modal__ellipsis">{{ record.user_id }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'display_name'">
|
||||
<a-tooltip :title="getMemberDisplayName(record)">
|
||||
<span class="ch-dir-modal__ellipsis">{{ getMemberDisplayName(record) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="roleColor(record.role)">{{ roleLabel(record.role) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'joined_at'">
|
||||
{{ formatTime(record.joined_at) }}
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'display_name'">
|
||||
<a-tooltip :title="getMemberDisplayName(record)">
|
||||
<span class="ch-dir-drawer__ellipsis">{{ getMemberDisplayName(record) }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'role'">
|
||||
<a-tag :color="roleColor(record.role)">{{ roleLabel(record.role) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'joined_at'">
|
||||
{{ formatTime(record.joined_at) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<div v-if="members.length" class="ch-dir-drawer__more">
|
||||
<div v-if="members.length" class="ch-dir-modal__more">
|
||||
<a-button
|
||||
v-if="hasMoreMembers"
|
||||
type="link"
|
||||
@ -244,11 +314,11 @@
|
||||
>
|
||||
加载更多
|
||||
</a-button>
|
||||
<span v-else class="ch-dir-drawer__end">没有更多数据了</span>
|
||||
<span v-else class="ch-dir-modal__end">没有更多数据了</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-drawer>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@ -311,13 +381,26 @@ function avatarColor(name) {
|
||||
|
||||
const emit = defineEmits(['close', 'retry', 'load-more-members', 'send-message'])
|
||||
|
||||
const drawerTitle = computed(() => (props.type === 'user' ? '用户资料' : '群组详情'))
|
||||
const modalTitle = computed(() => (props.type === 'user' ? '用户资料' : '群组详情'))
|
||||
|
||||
const errorInfo = computed(() => useDirectoryError(props.error))
|
||||
const membersErrorInfo = computed(() => useDirectoryError(props.membersError))
|
||||
|
||||
/** 动态描述中排除的内部字段 */
|
||||
const EXCLUDED_FIELDS = ['_raw', 'raw', 'metadata', 'extra', 'channel_type', 'account_id']
|
||||
/** 动态描述中排除的字段(身份卡片已展示的不重复列出) */
|
||||
const EXCLUDED_FIELDS = [
|
||||
'_raw',
|
||||
'raw',
|
||||
'metadata',
|
||||
'extra',
|
||||
'channel_type',
|
||||
'account_id',
|
||||
'name',
|
||||
'avatar_url',
|
||||
'peer_id',
|
||||
'peerId',
|
||||
'group_id',
|
||||
'groupId'
|
||||
]
|
||||
|
||||
/** 字段中文标签映射 */
|
||||
const FIELD_LABELS = {
|
||||
@ -432,6 +515,7 @@ function handleSendMessage() {
|
||||
if (!targetId.value) return
|
||||
emit('send-message', {
|
||||
peerId: targetId.value,
|
||||
peerName: props.data?.name || '',
|
||||
channelType: props.channelType,
|
||||
accountId: props.accountId
|
||||
})
|
||||
@ -449,7 +533,69 @@ async function copyId() {
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.ch-dir-drawer {
|
||||
.ch-dir-modal {
|
||||
&__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--gray-10);
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
&__avatar-link {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
|
||||
&--small {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
&__identity-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__identity-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__identity-id {
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
color: var(--gray-600);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 14px;
|
||||
@ -490,7 +636,9 @@ async function copyId() {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 16px;
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--gray-150);
|
||||
}
|
||||
|
||||
&__action-btn {
|
||||
@ -507,23 +655,19 @@ async function copyId() {
|
||||
}
|
||||
}
|
||||
|
||||
&__avatar-link {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__members-table {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--gray-150);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__more {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@ -535,4 +679,10 @@ async function copyId() {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
color: var(--gray-600);
|
||||
font-weight: 500;
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
@ -262,11 +262,12 @@ function handlePanelSearch() {
|
||||
const sendDrawerVisible = ref(false)
|
||||
const sendPreset = ref(null)
|
||||
|
||||
function handleSendMessage({ peerId, channelType: ct, accountId: aid }) {
|
||||
function handleSendMessage({ peerId, peerName, channelType: ct, accountId: aid }) {
|
||||
sendPreset.value = {
|
||||
channelType: ct,
|
||||
accountId: aid,
|
||||
target: peerId,
|
||||
targetName: peerName || '',
|
||||
targetType: 'peer_id',
|
||||
readonly: true
|
||||
}
|
||||
|
||||
@ -258,8 +258,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 详情抽屉 -->
|
||||
<DirectoryDetailDrawer
|
||||
<!-- 详情弹窗 -->
|
||||
<DirectoryDetailModal
|
||||
:visible="detailVisible"
|
||||
:type="detailType === 'users' ? 'user' : 'group'"
|
||||
:loading="detailType === 'users' ? userProfileLoading : groupDetailLoading"
|
||||
@ -287,7 +287,7 @@ import { Empty, message } from 'ant-design-vue'
|
||||
import { Search, List, Table, ChevronRight, Download, Trash2, MessageSquare } from 'lucide-vue-next'
|
||||
import ErrorRetryAlert from '@/components/channels/common/ErrorRetryAlert.vue'
|
||||
import LoadingState from '@/components/external-systems/common/LoadingState.vue'
|
||||
import DirectoryDetailDrawer from './DirectoryDetailDrawer.vue'
|
||||
import DirectoryDetailModal from './DirectoryDetailModal.vue'
|
||||
import {
|
||||
useDirectoryQuery,
|
||||
DIRECTORY_TAB,
|
||||
@ -619,15 +619,17 @@ function handleViewDetail(entry) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleSendMessage({ peerId, channelType, accountId }) {
|
||||
emit('send-message', { peerId, channelType, accountId })
|
||||
function handleSendMessage({ peerId, peerName, channelType, accountId }) {
|
||||
emit('send-message', { peerId, peerName, channelType, accountId })
|
||||
}
|
||||
|
||||
// ===== 批量操作 =====
|
||||
function handleBatchSend() {
|
||||
if (!selectedPeerIds.value.length || selectedPeerIds.value.length > 1) return
|
||||
const entry = selectedEntries.value[0]
|
||||
emit('send-message', {
|
||||
peerId: selectedPeerIds.value[0],
|
||||
peerName: entry?.name || '',
|
||||
channelType: props.channelType,
|
||||
accountId: props.accountId
|
||||
})
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
width="560"
|
||||
width="600"
|
||||
title="发送消息"
|
||||
:destroy-on-close="true"
|
||||
:mask-closable="false"
|
||||
:closable="!sending"
|
||||
:body-style="{ maxHeight: '70vh', overflowY: 'auto', overflowX: 'hidden' }"
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-form
|
||||
@ -18,7 +19,7 @@
|
||||
@finish="handleSubmit"
|
||||
@finishFailed="handleFinishFailed"
|
||||
>
|
||||
<!-- 预填模式上下文提示 -->
|
||||
<!-- 预填模式上下文提示:直接替代「发送目标」区域,避免信息重复 -->
|
||||
<a-alert
|
||||
v-if="isPresetReadonly"
|
||||
type="info"
|
||||
@ -27,87 +28,88 @@
|
||||
class="ch-send-form__preset-hint"
|
||||
/>
|
||||
|
||||
<!-- 发送目标 -->
|
||||
<h4 class="ch-send-form__section-title">发送目标</h4>
|
||||
<!-- 发送目标(仅非预填模式显示) -->
|
||||
<template v-if="!isPresetReadonly">
|
||||
<h4 class="ch-send-form__section-title">发送目标</h4>
|
||||
|
||||
<template v-if="isPresetReadonly">
|
||||
<a-form-item label="渠道类型">
|
||||
<a-tag>{{ presetChannelLabel }}</a-tag>
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="渠道类型" name="channel_type">
|
||||
<a-select
|
||||
v-model:value="form.channel_type"
|
||||
:options="channelTypes"
|
||||
:loading="channelTypesLoading"
|
||||
:disabled="sending"
|
||||
placeholder="请选择渠道类型"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleChannelTypeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="账户" name="account_id">
|
||||
<a-select
|
||||
v-model:value="form.account_id"
|
||||
:options="accounts"
|
||||
:loading="accountsLoading"
|
||||
:disabled="sending || !form.channel_type"
|
||||
placeholder="请选择账户"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="账户">
|
||||
<span class="ch-send-form__readonly-value">{{ presetAccountLabel }}</span>
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="目标类型" name="target_type">
|
||||
<a-radio-group
|
||||
v-model:value="form.target_type"
|
||||
:options="targetTypeOptions"
|
||||
:disabled="sending"
|
||||
option-type="button"
|
||||
size="small"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="目标" name="target">
|
||||
<a-tooltip
|
||||
:title="form.target"
|
||||
:disabled="!form.target || form.target.length <= 40"
|
||||
placement="topLeft"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.target"
|
||||
:disabled="sending"
|
||||
:placeholder="targetPlaceholder"
|
||||
allow-clear
|
||||
/>
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="目标">
|
||||
<span class="ch-send-form__readonly-value" :title="form.target">{{ form.target }}</span>
|
||||
</a-form-item>
|
||||
<div class="target-type-hint">{{ targetTypeHint }}</div>
|
||||
<div v-if="targetPatternHint" class="target-pattern-hint">
|
||||
{{ targetPatternHint }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<a-form-item label="渠道类型" name="channel_type">
|
||||
<a-select
|
||||
v-model:value="form.channel_type"
|
||||
:options="channelTypes"
|
||||
:loading="channelTypesLoading"
|
||||
:disabled="sending"
|
||||
placeholder="请选择渠道类型"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
@change="handleChannelTypeChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="账户" name="account_id">
|
||||
<a-select
|
||||
v-model:value="form.account_id"
|
||||
:options="accounts"
|
||||
:loading="accountsLoading"
|
||||
:disabled="sending || !form.channel_type"
|
||||
placeholder="请选择账户"
|
||||
show-search
|
||||
option-filter-prop="label"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="目标类型" name="target_type">
|
||||
<a-radio-group
|
||||
v-model:value="form.target_type"
|
||||
:options="targetTypeOptions"
|
||||
:disabled="sending"
|
||||
option-type="button"
|
||||
size="small"
|
||||
/>
|
||||
<div class="target-type-hint">{{ targetTypeHint }}</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="目标" name="target">
|
||||
<a-tooltip
|
||||
:title="form.target"
|
||||
:disabled="!form.target || form.target.length <= 40"
|
||||
placement="topLeft"
|
||||
>
|
||||
<a-input
|
||||
v-model:value="form.target"
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="isPresetReadonly ? 24 : 12">
|
||||
<a-form-item label="会话策略" name="conversation_policy">
|
||||
<a-select
|
||||
v-model:value="form.conversation_policy"
|
||||
:options="policyOptions"
|
||||
:disabled="sending"
|
||||
:placeholder="targetPlaceholder"
|
||||
allow-clear
|
||||
placeholder="请选择会话策略"
|
||||
/>
|
||||
</a-tooltip>
|
||||
<div v-if="targetPatternHint" class="target-pattern-hint">
|
||||
{{ targetPatternHint }}
|
||||
</div>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<a-form-item label="会话策略" name="conversation_policy">
|
||||
<a-select
|
||||
v-model:value="form.conversation_policy"
|
||||
:options="policyOptions"
|
||||
:disabled="sending"
|
||||
placeholder="请选择会话策略"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 消息内容 -->
|
||||
<h4 class="ch-send-form__section-title">消息内容</h4>
|
||||
@ -115,7 +117,7 @@
|
||||
<a-form-item label="内容" name="content">
|
||||
<a-textarea
|
||||
v-model:value="form.content"
|
||||
:auto-size="{ minRows: 4, maxRows: 8 }"
|
||||
:auto-size="{ minRows: 5, maxRows: 10 }"
|
||||
:disabled="sending"
|
||||
placeholder="请输入消息内容(文本 / 图片 / 链接 / @提及)"
|
||||
show-count
|
||||
@ -129,9 +131,9 @@
|
||||
:multiple="true"
|
||||
:disabled="!canUploadAttachment || sending"
|
||||
>
|
||||
<a-button :disabled="!canUploadAttachment || sending">
|
||||
<a-button :disabled="!canUploadAttachment || sending" size="small">
|
||||
<template #icon>
|
||||
<Upload :size="16" />
|
||||
<Upload :size="14" />
|
||||
</template>
|
||||
选择文件上传
|
||||
</a-button>
|
||||
@ -197,14 +199,16 @@ import { message } from 'ant-design-vue'
|
||||
import { Upload, X, KeyRound, FileText, Image as ImageIcon } from 'lucide-vue-next'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useMessageSend } from '@/composables/channels/useMessageSend'
|
||||
import { useChannelTypes } from '@/composables/channels/useChannelTypes'
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, default: false },
|
||||
/**
|
||||
* 预填数据(由会话详情/通讯录「发消息」入口注入)。
|
||||
* 形如 { channelType, accountId, target, targetType, readonly: true }:
|
||||
* 形如 { channelType, accountId, target, targetName, targetType, readonly: true }:
|
||||
* readonly=true 时渠道/账户/目标只读预填,用户仅需编辑内容。
|
||||
* targetType='peer_id' 时按对端 ID 触达(通讯录直发场景)。
|
||||
* targetName 为目标展示名(如好友昵称),优先在提示中显示。
|
||||
* 不传或为 null 时进入「新建消息」模式,所有字段可编辑。
|
||||
*/
|
||||
preset: { type: Object, default: null }
|
||||
@ -213,6 +217,7 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:open', 'close', 'success'])
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { getLabel: getChannelTypeLabel } = useChannelTypes()
|
||||
const {
|
||||
channelTypes,
|
||||
channelTypesLoading,
|
||||
@ -274,8 +279,8 @@ const isPresetReadonly = computed(() => !!props.preset?.readonly)
|
||||
const presetChannelLabel = computed(() => {
|
||||
const type = props.preset?.channelType
|
||||
if (!type) return '-'
|
||||
const found = channelTypes.value.find((t) => t.value === type)
|
||||
return found?.label || type
|
||||
// 优先从插件目录取中文展示名,fallback 到 capability 选项或原值
|
||||
return getChannelTypeLabel(type) || channelTypes.value.find((t) => t.value === type)?.label || type
|
||||
})
|
||||
|
||||
const presetAccountLabel = computed(() => {
|
||||
@ -288,8 +293,10 @@ const presetAccountLabel = computed(() => {
|
||||
const presetHint = computed(() => {
|
||||
const channel = presetChannelLabel.value
|
||||
const account = presetAccountLabel.value
|
||||
const target = form.target || '-'
|
||||
return `将从「${channel} / ${account}」向「${target}」发送消息`
|
||||
const targetName = props.preset?.targetName
|
||||
const targetId = form.target || '-'
|
||||
const targetLabel = targetName || targetId
|
||||
return `将从「${channel} / ${account}」向「${targetLabel}」发送消息`
|
||||
})
|
||||
|
||||
// ===== 校验规则(对齐原型图 §6)=====
|
||||
@ -583,30 +590,29 @@ const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024
|
||||
|
||||
<style scoped lang="less">
|
||||
.ch-send-form {
|
||||
:deep(.ant-modal-body) {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
&__preset-hint {
|
||||
:deep(.ant-form-item) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__preset-hint {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
&__section-title {
|
||||
margin: 0 0 16px;
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--gray-900);
|
||||
|
||||
&:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
&__readonly-value {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
color: var(--gray-600);
|
||||
color: var(--gray-700);
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
|
||||
@ -294,7 +294,7 @@ function handleHeaderCheckChange(checked) {
|
||||
|
||||
function channelLabel(type) {
|
||||
if (!type) return '跨渠道'
|
||||
const opt = channelTypeOptions.find((o) => o.value === type)
|
||||
const opt = channelTypeOptions.value?.find((o) => o.value === type)
|
||||
return opt ? opt.label : type
|
||||
}
|
||||
|
||||
|
||||
@ -371,6 +371,8 @@ const sendPreset = computed(() => {
|
||||
channelType: sessionDetail.value.channel_type,
|
||||
accountId: sessionDetail.value.owner_id || undefined,
|
||||
target: sessionDetail.value.peer_id || sessionDetail.value.session_id || '',
|
||||
targetName: sessionDetail.value.peer_name || '',
|
||||
targetType: 'peer_id',
|
||||
readonly: true
|
||||
}
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user