fix(ui): channel binding agent dropdown — render pi:* icons + 员工 wording

This commit is contained in:
matevip 2026-05-10 08:27:55 +08:00
parent 78f6ca8d33
commit b54fd7a24a
3 changed files with 278 additions and 20 deletions

View File

@ -39,12 +39,58 @@
</div>
<div class="form-group">
<label class="form-label">{{ t('channels.fields.bindAgent') }}</label>
<select v-model="form.agentId" class="form-input">
<option :value="null">{{ t('channels.placeholders.selectAgent') }}</option>
<option v-for="agent in agents" :key="agent.id" :value="agent.id">
{{ plainTextIcon(agent.icon) }} {{ agent.name }}
</option>
</select>
<!-- Custom dropdown so SkillIcon renders pi:* glyphs; native
<select><option> can't host the component. Mirrors the
ChannelOnboardingWizard step-3 pattern. -->
<div class="agent-select" :class="{ open: agentDropdownOpen }">
<button
type="button"
class="agent-select-trigger"
@click="agentDropdownOpen = !agentDropdownOpen"
>
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
</span>
<span class="agent-select-trigger__name">{{
selectedAgent?.name || t('channels.placeholders.selectAgent')
}}</span>
<svg
class="agent-select-trigger__arrow"
:class="{ open: agentDropdownOpen }"
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div
v-if="agentDropdownOpen"
class="agent-dropdown-backdrop"
@click="agentDropdownOpen = false"
></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="a in agents"
:key="a.id"
class="agent-dropdown-item"
:class="{ active: String(a.id) === String(form.agentId) }"
@click="onSelectAgent(a)"
>
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
</span>
<span class="agent-dropdown-item__name">{{ a.name }}</span>
<span v-if="String(a.id) === String(form.agentId)" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
<div v-if="agents.length === 0" class="agent-dropdown-empty">
{{ t('chat.loadingAgents') }}
</div>
</div>
</Transition>
</div>
</div>
</div>
@ -441,7 +487,8 @@ import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
import { plainTextIcon } from '@/composables/usePixelarticons'
import SkillIcon from '@/components/common/SkillIcon.vue'
import { agentIconColor } from '@/utils/agentIconColor'
interface Props {
modelValue: boolean
@ -491,6 +538,20 @@ const showAdvanced = ref(false)
const accessControl = ref<AccessControlValue>(defaultAccessControl())
const renderConfig = ref<RenderConfigValue>(defaultRenderConfig())
// ========== Custom agent dropdown ==========
// Native <select><option> can't render <SkillIcon> for pi:* glyphs;
// mirror the ChatConsole.vue / ChannelOnboardingWizard.vue pattern.
const agentDropdownOpen = ref(false)
const selectedAgent = computed<Agent | null>(() => {
const id = form.value.agentId
if (id == null) return null
return props.agents.find(a => String(a.id) === String(id)) ?? null
})
function onSelectAgent(a: Agent) {
form.value.agentId = a.id as any
agentDropdownOpen.value = false
}
// ========== Auth composables ==========
const weixin = useWeixinQrcodePoll(({ botToken, baseUrl }) => {
@ -957,4 +1018,70 @@ function save() {
.json-hint { font-size: 12px; color: var(--mc-text-tertiary); margin: 0 0 8px; }
.json-editor { font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace; font-size: 13px; line-height: 1.5; tab-size: 2; }
/* ===== Agent custom dropdown (mirrors ChatConsole.vue / wizard pattern) =====
Native <select><option> can't host <SkillIcon> for pi:* glyphs.
z-index has to clear the modal-overlay (1000). */
.agent-select { position: relative; }
.agent-select-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--mc-border);
border-radius: 10px;
font-size: 14px;
color: var(--mc-text-primary);
background: var(--mc-bg-elevated);
cursor: pointer;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.agent-select-trigger:hover { border-color: var(--mc-primary); }
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-select-trigger__name { flex: 1; text-align: left; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-select-trigger__arrow { flex-shrink: 0; color: var(--mc-text-tertiary); transition: transform 0.2s; }
.agent-select-trigger__arrow.open { transform: rotate(180deg); }
.agent-dropdown-backdrop { position: fixed; inset: 0; z-index: 1099; }
.agent-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 1100;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 12px;
padding: 6px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
max-height: 280px;
overflow-y: auto;
}
.agent-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
}
.agent-dropdown-item:hover { background: var(--mc-bg-sunken); }
.agent-dropdown-item.active { background: var(--mc-primary-bg); }
.agent-dropdown-item__icon { font-size: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-dropdown-item__check { flex-shrink: 0; color: var(--mc-primary); }
.agent-dropdown-empty { padding: 16px; text-align: center; font-size: 13px; color: var(--mc-text-tertiary); }
.agent-dropdown-enter-active { transition: all 0.15s ease-out; }
.agent-dropdown-leave-active { transition: all 0.1s ease-in; }
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@ -219,15 +219,59 @@
</div>
</div>
<!-- Bind agent -->
<!-- Bind agent (custom dropdown so SkillIcon renders pi:* glyphs;
native <select><option> cannot host components) -->
<div class="form-group">
<label class="form-label">{{ t('channels.wizard.bindAgentLabel') }}</label>
<select v-model="form.agentId" class="form-input">
<option :value="null">{{ t('channels.placeholders.selectAgent') }}</option>
<option v-for="a in agents" :key="a.id" :value="a.id">
{{ a.icon || '🤖' }} {{ a.name }}
</option>
</select>
<div class="agent-select" :class="{ open: agentDropdownOpen }">
<button
type="button"
class="agent-select-trigger"
@click="agentDropdownOpen = !agentDropdownOpen"
>
<span class="agent-select-trigger__icon" :style="{ color: agentIconColor(selectedAgent?.icon) }">
<SkillIcon :value="selectedAgent?.icon" :size="20" :fallback="'🤖'" />
</span>
<span class="agent-select-trigger__name">{{
selectedAgent?.name || t('channels.placeholders.selectAgent')
}}</span>
<svg
class="agent-select-trigger__arrow"
:class="{ open: agentDropdownOpen }"
width="12" height="12" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2"
><polyline points="6 9 12 15 18 9"/></svg>
</button>
<Transition name="fade">
<div
v-if="agentDropdownOpen"
class="agent-dropdown-backdrop"
@click="agentDropdownOpen = false"
></div>
</Transition>
<Transition name="agent-dropdown">
<div v-if="agentDropdownOpen" class="agent-dropdown">
<div
v-for="a in agents"
:key="a.id"
class="agent-dropdown-item"
:class="{ active: String(a.id) === String(form.agentId) }"
@click="onSelectAgent(a)"
>
<span class="agent-dropdown-item__icon" :style="{ color: agentIconColor(a.icon) }">
<SkillIcon :value="a.icon" :size="18" :fallback="'🤖'" />
</span>
<span class="agent-dropdown-item__name">{{ a.name }}</span>
<span v-if="String(a.id) === String(form.agentId)" class="agent-dropdown-item__check">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="20 6 9 17 4 12"/></svg>
</span>
</div>
<div v-if="agents.length === 0" class="agent-dropdown-empty">
{{ t('chat.loadingAgents') }}
</div>
</div>
</Transition>
</div>
</div>
<p class="ready-hint">{{ t('channels.wizard.sendTestHint', { service: serviceName }) }}</p>
@ -280,6 +324,8 @@ import { useWecomBotAuth } from '@/composables/channels/useWecomBotAuth'
import { useWeixinQrcodePoll } from '@/composables/channels/useWeixinQrcodePoll'
import { useDingTalkAppRegister } from '@/composables/channels/useDingTalkAppRegister'
import { useFeishuAppRegister } from '@/composables/channels/useFeishuAppRegister'
import SkillIcon from '@/components/common/SkillIcon.vue'
import { agentIconColor } from '@/utils/agentIconColor'
interface Props {
modelValue: boolean
@ -331,6 +377,19 @@ const channelConfig = ref<Record<string, any>>({})
const visibleFields = ref<Record<string, boolean>>({})
const showAdvanced = ref(false)
// Custom agent dropdown native <select><option> can't render SkillIcon
// component for pi:* glyphs; mirror ChatConsole.vue's pattern.
const agentDropdownOpen = ref(false)
const selectedAgent = computed<Agent | null>(() => {
const id = form.value.agentId
if (id == null) return null
return props.agents.find(a => String(a.id) === String(id)) ?? null
})
function onSelectAgent(a: Agent) {
form.value.agentId = a.id as any
agentDropdownOpen.value = false
}
const verifying = ref(false)
const verifyResult = ref<VerifyResult | null>(null)
const invalidField = ref<string | null>(null)
@ -819,4 +878,76 @@ function formatIdentityKey(key: string): string {
.btn-primary:disabled { background: var(--mc-border); cursor: not-allowed; box-shadow: none; }
.btn-secondary { padding: 10px 18px; background: var(--mc-bg-elevated); color: var(--mc-text-primary); border: 1px solid var(--mc-border); border-radius: 12px; font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
.btn-secondary:hover { background: var(--mc-bg-sunken); }
/* ===== Agent custom dropdown (mirrors ChatConsole.vue pattern) =====
Native <select><option> can't render the <SkillIcon> component
needed for pi:* glyphs, so step-3 uses a custom dropdown.
z-index has to clear the wizard modal-overlay (z-index: 1000). */
.agent-select { position: relative; }
.agent-select-trigger {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border: 1px solid var(--mc-border);
border-radius: 10px;
font-size: 14px;
color: var(--mc-text-primary);
background: var(--mc-bg-elevated);
cursor: pointer;
outline: none;
transition: border-color 0.15s;
box-sizing: border-box;
}
.agent-select-trigger:hover { border-color: var(--mc-primary); }
.agent-select.open .agent-select-trigger { border-color: var(--mc-primary); box-shadow: 0 0 0 3px rgba(217,119,87,0.12); }
.agent-select-trigger__icon { font-size: 18px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-select-trigger__name { flex: 1; text-align: left; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-select-trigger__arrow { flex-shrink: 0; color: var(--mc-text-tertiary); transition: transform 0.2s; }
.agent-select-trigger__arrow.open { transform: rotate(180deg); }
.agent-dropdown-backdrop {
position: fixed;
inset: 0;
/* must clear modal-overlay (1000) so clicks fall through to backdrop, not modal */
z-index: 1099;
}
.agent-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 1100;
background: var(--mc-bg-elevated);
border: 1px solid var(--mc-border);
border-radius: 12px;
padding: 6px;
box-shadow: 0 8px 32px rgba(0,0,0,0.12);
max-height: 280px;
overflow-y: auto;
}
.agent-dropdown-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s;
}
.agent-dropdown-item:hover { background: var(--mc-bg-sunken); }
.agent-dropdown-item.active { background: var(--mc-primary-bg); }
.agent-dropdown-item__icon { font-size: 20px; line-height: 1; flex-shrink: 0; display: inline-flex; }
.agent-dropdown-item__name { flex: 1; font-size: 14px; font-weight: 500; color: var(--mc-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.agent-dropdown-item__check { flex-shrink: 0; color: var(--mc-primary); }
.agent-dropdown-empty { padding: 16px; text-align: center; font-size: 13px; color: var(--mc-text-tertiary); }
.agent-dropdown-enter-active { transition: all 0.15s ease-out; }
.agent-dropdown-leave-active { transition: all 0.1s ease-in; }
.agent-dropdown-enter-from { opacity: 0; transform: translateY(-6px) scale(0.97); }
.agent-dropdown-leave-to { opacity: 0; transform: translateY(-4px) scale(0.98); }
.fade-enter-active, .fade-leave-active { transition: opacity 0.15s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>

View File

@ -2337,7 +2337,7 @@ export default {
},
channels: {
title: '渠道管理',
desc: '将 Agent 连接到各消息平台和 API',
desc: '将数字员工连接到各消息平台和 API',
newChannel: '新建渠道',
addChannel: '添加渠道',
status: {
@ -2349,7 +2349,7 @@ export default {
disable: '停用',
empty: {
title: '连接第一个渠道',
desc: '把你的 Agent 接入团队已经在用的 IM 工具——Slack、企业微信、Telegram 等。',
desc: '把你的数字员工接入团队已经在用的 IM 工具——Slack、企业微信、Telegram 等。',
cta: '连接一个渠道',
},
stats: {
@ -2409,12 +2409,12 @@ export default {
name: '名称',
type: '类型',
description: '描述',
bindAgent: '绑定 Agent',
bindAgent: '绑定员工',
},
placeholders: {
name: '渠道名称',
description: '渠道用途说明',
selectAgent: '选择 Agent...',
selectAgent: '选择员工...',
},
types: {
web: 'Web API',
@ -2631,8 +2631,8 @@ export default {
fixIt: '修复',
retry: '重试',
readyHeadline: '一切就绪',
readySubtitle: '绑定一个 Agent,发条测试消息确认。',
bindAgentLabel: '绑定 Agent',
readySubtitle: '绑定一个员工,发条测试消息确认。',
bindAgentLabel: '绑定员工',
sendTest: '发送测试消息',
sendTestHint: '打开 {service} 给机器人发条消息——回复应该几秒内到达。',
saveFailed: '渠道保存失败',