Files
X-Agents/web/src/views/Agents.vue
DESKTOP-72TV0V4\caoxiaozhu e2c9bbd0d1 feat: 更新skill相关前端页面
- 新增 web/src/views/skill/skill.ts skill视图组件
- 更新 Agents.vue 页面
- 更新 Skill.vue 页面
- 移除旧的 useSkills.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:23:13 +08:00

1033 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
const API_BASE = 'http://localhost:8082'
// Skills 接口
interface Skill {
id: string
skill_name: string
skill_type: string
skill_desc: string
path: string
status: string
}
// Agents 数据
const agents = ref([
{ id: 1, name: 'Claude Agent', avatar: '🧠', description: 'General purpose AI assistant', accentColor: '#f97316', gradient: 'from-orange-500/20 to-amber-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-2.0-flash', mcpServers: 2, createdAt: '2025-04-10' },
{ id: 2, name: 'Code Assistant', avatar: '💻', description: 'Specialized in code generation', accentColor: '#3b82f6', gradient: 'from-blue-500/20 to-cyan-500/20', status: 'running' as const, framework: 'OpenAI', model: 'gpt-4o', mcpServers: 1, createdAt: '2025-04-08' },
{ id: 3, name: 'Data Analyst', avatar: '📊', description: 'Data analysis and visualization', accentColor: '#10b981', gradient: 'from-emerald-500/20 to-green-500/20', status: 'stopped' as const, framework: 'PydanticAI', model: 'gpt-4o-mini', mcpServers: 3, createdAt: '2025-04-05' },
{ id: 4, name: 'Research Bot', avatar: '🔬', description: 'Academic research assistant', accentColor: '#8b5cf6', gradient: 'from-violet-500/20 to-purple-500/20', status: 'running' as const, framework: 'LangChain', model: 'claude-3-5-sonnet', mcpServers: 2, createdAt: '2025-04-12' },
{ id: 5, name: '客服助手', avatar: '🎧', description: 'Customer support agent', accentColor: '#ec4899', gradient: 'from-pink-500/20 to-rose-500/20', status: 'running' as const, framework: 'Google ADK', model: 'gemini-1.5-pro', mcpServers: 4, createdAt: '2025-04-11' },
])
// 创建智能体弹窗状态
const showCreateModal = ref(false)
const isCreating = ref(false)
const newAgent = ref({
name: '',
description: '',
skillsMode: 'all' as 'all' | 'include' | 'exclude',
selectedSkills: [] as string[],
knowledge: '',
prompt: '',
avatar: '🤖',
})
// Skills 选择器状态
const showSkillsDropdown = ref(false)
const showSubSkillsDropdown = ref(false)
// Skills 模式选项
const skillsModeOptions = [
{ value: 'all', label: 'All Skills', desc: 'Use all available skills' },
{ value: 'include', label: 'Include Skills', desc: 'Select specific skills to use' },
{ value: 'exclude', label: 'Exclude Skills', desc: 'Select skills to exclude' },
]
// 切换技能模式下拉框
const toggleSkillsMode = () => {
showSkillsDropdown.value = !showSkillsDropdown.value
showSubSkillsDropdown.value = false
}
// 选择技能模式
const selectSkillsMode = (mode: 'all' | 'include' | 'exclude') => {
newAgent.value.skillsMode = mode
if (mode === 'all') {
newAgent.value.selectedSkills = []
}
showSkillsDropdown.value = false
if (mode === 'include' || mode === 'exclude') {
showSubSkillsDropdown.value = true
}
}
// 处理技能模式点击(用于模板)
const handleSkillsModeClick = (mode: 'all' | 'include' | 'exclude') => {
selectSkillsMode(mode)
}
// 切换子下拉框
const toggleSubSkillsDropdown = () => {
showSubSkillsDropdown.value = !showSubSkillsDropdown.value
}
// 关闭所有下拉框
const closeAllDropdowns = () => {
showSkillsDropdown.value = false
showSubSkillsDropdown.value = false
}
// 获取显示文本
const getSkillsDisplayText = () => {
if (newAgent.value.skillsMode === 'all') {
return 'All Skills'
}
const count = newAgent.value.selectedSkills.length
if (count === 0) {
return newAgent.value.skillsMode === 'include' ? 'Select skills to include...' : 'Select skills to exclude...'
}
return `${count} skill${count > 1 ? 's' : ''} ${newAgent.value.skillsMode === 'include' ? 'included' : 'excluded'}`
}
// 切换子下拉框中的技能选择
const toggleSkillSelection = (skillId: string) => {
const index = newAgent.value.selectedSkills.indexOf(skillId)
if (index > -1) {
newAgent.value.selectedSkills.splice(index, 1)
} else {
newAgent.value.selectedSkills.push(skillId)
}
}
// 头像选项
const avatarOptions = [
'🤖', '🧠', '💻', '📊', '🔬', '🎧', '✨', '💬', '🔮', '🌙',
'🐉', '☁️', '🎨', '🎯', '🚀', '⚡', '🔥', '💡', '🎭', '🎪'
]
// Skills 列表
const skillsList = ref<Skill[]>([])
const skillsLoading = ref(false)
// 获取 skills 列表
const fetchSkills = async () => {
skillsLoading.value = true
try {
const response = await fetch(`${API_BASE}/skill/list`)
const result = await response.json()
if (result.list) {
skillsList.value = result.list
}
} catch (error) {
console.error('Failed to fetch skills:', error)
} finally {
skillsLoading.value = false
}
}
// Skills 选项(从真实列表生成)
const skillsOptions = computed(() => {
return skillsList.value.map(skill => ({
value: skill.id,
label: skill.skill_name,
desc: skill.skill_desc,
}))
})
// Knowledge 选项
const knowledgeOptions = [
{ value: 'general', label: 'General Knowledge' },
{ value: 'codebase', label: 'Codebase' },
{ value: 'docs', label: 'Documentation' },
{ value: 'api', label: 'API Reference' },
]
// Skills 选择器状态
const skillsSearch = ref('')
// 获取 skill 标签
const getSkillLabel = (id: string) => {
return skillsOptions.value.find(s => s.value === id)?.label || id
}
// 过滤后的 skills
const filteredSkills = computed(() => {
if (!skillsSearch.value) return skillsOptions.value
const search = skillsSearch.value.toLowerCase()
return skillsOptions.value.filter(s =>
s.label.toLowerCase().includes(search) ||
(s.desc && s.desc.toLowerCase().includes(search))
)
})
// 是否全选
const isAllSelected = computed(() => {
return skillsOptions.value.length > 0 && newAgent.value.selectedSkills.length === skillsOptions.value.length
})
// 是否半选
const isIndeterminate = computed(() => {
return newAgent.value.selectedSkills.length > 0 && newAgent.value.selectedSkills.length < skillsOptions.value.length
})
// 切换下拉框
const toggleSkillsDropdown = () => {
showSkillsDropdown.value = !showSkillsDropdown.value
if (!showSkillsDropdown.value) {
skillsSearch.value = ''
}
}
// 关闭下拉框
const closeSkillsDropdown = () => {
showSkillsDropdown.value = false
skillsSearch.value = ''
}
// 全选 skills
const selectAllSkills = () => {
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
}
// 清空 skills
const clearSkills = () => {
newAgent.value.selectedSkills = []
}
// 切换全选
const toggleSelectAll = () => {
if (isAllSelected.value) {
newAgent.value.selectedSkills = []
} else {
newAgent.value.selectedSkills = skillsOptions.value.map(s => s.value)
}
}
// 点击外部关闭下拉框
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.skills-selector')) {
closeSkillsDropdown()
}
}
// 页面加载时获取 skills
onMounted(() => {
fetchSkills()
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// 打开创建弹窗
const openCreateModal = () => {
newAgent.value = {
name: '',
description: '',
skillsMode: 'all' as const,
selectedSkills: [],
knowledge: '',
prompt: '',
avatar: '🤖'
}
showCreateModal.value = true
}
// 创建智能体
const createAgent = async () => {
if (!newAgent.value.name || (newAgent.value.skillsMode !== 'all' && newAgent.value.selectedSkills.length === 0) || !newAgent.value.knowledge) {
return
}
isCreating.value = true
try {
// 处理 skills 标签
const skillsLabels = newAgent.value.selectedSkills.map((id: string) => getSkillLabel(id)).join(', ')
// 模拟创建
const newId = Math.max(...agents.value.map(a => a.id)) + 1
agents.value.unshift({
id: newId,
name: newAgent.value.name,
avatar: newAgent.value.avatar,
description: newAgent.value.description,
accentColor: '#f97316',
gradient: 'from-orange-500/20 to-amber-500/20',
status: 'stopped',
framework: skillsLabels || 'None',
model: knowledgeOptions.find(k => k.value === newAgent.value.knowledge)?.label || newAgent.value.knowledge,
mcpServers: 0,
createdAt: new Date().toISOString().split('T')[0],
})
showCreateModal.value = false
} finally {
isCreating.value = false
}
}
const searchQuery = ref('')
const filterStatus = ref('all')
// 过滤后的 agents
const filteredAgents = computed(() => {
return agents.value.filter(agent => {
const matchSearch = agent.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
agent.framework.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = filterStatus.value === 'all' || agent.status === filterStatus.value
return matchSearch && matchStatus
})
})
// 统计数据
const stats = computed(() => ({
total: agents.value.length,
running: agents.value.filter(a => a.status === 'running').length,
stopped: agents.value.filter(a => a.status === 'stopped').length,
}))
// 状态颜色
const statusClass = (status: string) => {
switch (status) {
case 'running': return 'bg-primary-success'
case 'stopped': return 'bg-gray-500'
default: return 'bg-gray-500'
}
}
// 切换状态
const toggleStatus = (agent: any) => {
agent.status = agent.status === 'running' ? 'stopped' : 'running'
}
// 删除 Agent
const deleteAgent = (id: number) => {
agents.value = agents.value.filter(a => a.id !== id)
}
</script>
<template>
<!-- 主内容区域 -->
<div class="p-6 min-h-screen">
<!-- 顶部导航 -->
<div class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<i class="fa-solid fa-robot text-orange-500"></i>
<span class="font-medium">Agents</span>
</div>
<button @click="openCreateModal" class="btn-primary">
<i class="fa-solid fa-plus"></i>
New Agent
</button>
</div>
<!-- 搜索和筛选 -->
<div class="flex gap-4 mb-6">
<div class="flex-1 relative">
<i class="fa-solid fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
<input
v-model="searchQuery"
type="text"
placeholder="Search agents by name or framework..."
class="search-input w-full"
>
</div>
<el-select v-model="filterStatus" placeholder="Select" class="w-40" size="large">
<el-option label="All Status" value="all" />
<el-option label="Running" value="running" />
<el-option label="Stopped" value="stopped" />
</el-select>
</div>
<!-- Agents 列表 -->
<div class="bg-dark-700 rounded-xl overflow-hidden">
<table class="w-full">
<thead class="bg-dark-600">
<tr>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Agent Name</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Framework</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Model</th>
<th class="text-center px-5 py-3 text-sm font-medium text-gray-400">MCP</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Status</th>
<th class="text-left px-5 py-3 text-sm font-medium text-gray-400">Created</th>
<th class="text-right px-5 py-3 text-sm font-medium text-gray-400">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="agent in filteredAgents" :key="agent.id" class="table-row">
<td class="px-5 py-4">
<div class="flex items-center gap-3">
<div
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg"
:style="{ backgroundColor: agent.accentColor + '20', color: agent.accentColor }"
>
{{ agent.avatar }}
</div>
<div>
<div class="font-medium text-white">{{ agent.name }}</div>
<div class="text-xs text-gray-500">{{ agent.description }}</div>
</div>
</div>
</td>
<td class="px-5 py-4">
<span class="bg-dark-500 px-2 py-1 rounded text-sm text-gray-300">{{ agent.framework }}</span>
</td>
<td class="px-5 py-4 text-gray-300">{{ agent.model }}</td>
<td class="px-5 py-4 text-center">
<span class="text-primary-cyan">{{ agent.mcpServers }}</span>
</td>
<td class="px-5 py-4">
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full" :class="statusClass(agent.status)"></span>
<span class="capitalize text-sm text-gray-300">{{ agent.status }}</span>
</div>
</td>
<td class="px-5 py-4 text-gray-400 text-sm">{{ agent.createdAt }}</td>
<td class="px-5 py-4">
<div class="flex items-center justify-end gap-2">
<button
@click="toggleStatus(agent)"
class="btn-icon"
:title="agent.status === 'running' ? 'Stop' : 'Start'"
>
<i :class="['fa-solid', agent.status === 'running' ? 'fa-stop' : 'fa-play', 'text-gray-400']"></i>
</button>
<button class="btn-icon" title="Edit">
<i class="fa-solid fa-pen text-gray-400"></i>
</button>
<button class="btn-icon" title="Settings">
<i class="fa-solid fa-gear text-gray-400"></i>
</button>
<button
@click="deleteAgent(agent.id)"
class="btn-icon"
title="Delete"
>
<i class="fa-solid fa-trash text-gray-400 hover:text-primary-danger"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
<!-- 空状态 -->
<div v-if="filteredAgents.length === 0" class="py-12 text-center text-gray-500">
<i class="fa-solid fa-robot text-4xl mb-3"></i>
<p>No agents found</p>
</div>
</div>
</div>
<!-- 创建智能体弹窗 -->
<Teleport to="body">
<div v-if="showCreateModal" class="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div class="bg-dark-700 rounded-2xl w-full max-w-lg border border-dark-500 shadow-2xl">
<div class="flex items-center justify-between p-5 border-b border-dark-500">
<h3 class="text-lg font-semibold">Create New Agent</h3>
<button @click="showCreateModal = false" class="text-gray-400 hover:text-white transition-colors">
<i class="fa-solid fa-xmark text-xl"></i>
</button>
</div>
<div class="p-5 space-y-4">
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Agent Name *</label>
<input
v-model="newAgent.name"
type="text"
placeholder="Enter agent name..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange"
>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Description</label>
<textarea
v-model="newAgent.description"
rows="3"
placeholder="Describe what this agent does..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Avatar</label>
<div class="flex flex-wrap gap-2">
<button
v-for="avatar in avatarOptions"
:key="avatar"
type="button"
@click="newAgent.avatar = avatar"
class="w-10 h-10 rounded-lg flex items-center justify-center text-lg transition-all"
:class="newAgent.avatar === avatar ? 'bg-primary-orange text-white ring-2 ring-orange-400' : 'bg-dark-600 text-gray-300 hover:bg-dark-500'"
>
{{ avatar }}
</button>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Skills *</label>
<!-- 技能模式选择 - 两级下拉框 -->
<div class="skills-selector">
<!-- 已选 tags 显示区域 -->
<div class="selected-tags" @click="toggleSkillsDropdown">
<div v-if="newAgent.skillsMode === 'all'" class="placeholder">
All Skills
</div>
<div v-else class="tags-container">
<span
v-for="skillId in newAgent.selectedSkills.slice(0, 3)"
:key="skillId"
class="selected-tag"
>
{{ getSkillLabel(skillId) }}
</span>
<span v-if="newAgent.selectedSkills.length > 3" class="more-tag">
+{{ newAgent.selectedSkills.length - 3 }}
</span>
</div>
<svg class="dropdown-icon" :class="{ 'rotate-180': showSkillsDropdown }" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</div>
<!-- 第一级下拉面板模式选择 -->
<Transition name="dropdown">
<div v-if="showSkillsDropdown" class="dropdown-panel">
<div class="skills-mode-options">
<div
v-for="option in skillsModeOptions"
:key="option.value"
class="skills-mode-item"
:class="{ 'active': newAgent.skillsMode === option.value }"
@click="handleSkillsModeClick(option.value as 'all' | 'include' | 'exclude')"
>
<div class="mode-radio">
<div v-if="newAgent.skillsMode === option.value" class="radio-dot"></div>
</div>
<div class="mode-content">
<span class="mode-label">{{ option.label }}</span>
<span class="mode-desc">{{ option.desc }}</span>
</div>
<!-- 显示展开图标 -->
<svg v-if="option.value !== 'all'" class="sub-arrow" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m9 18 6-6-6-6"/>
</svg>
</div>
</div>
</div>
</Transition>
<!-- 第二级下拉面板技能列表悬浮窗形式 -->
<Transition name="dropdown">
<div v-if="showSubSkillsDropdown" class="sub-dropdown-panel" @click.stop>
<!-- 标题 -->
<div class="sub-dropdown-header">
<span class="sub-dropdown-title">
{{ newAgent.skillsMode === 'include' ? 'Select skills to include' : 'Select skills to exclude' }}
</span>
<button @click="showSubSkillsDropdown = false" class="sub-dropdown-close">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18"/><path d="m6 6 12 12"/>
</svg>
</button>
</div>
<!-- 搜索框 -->
<div class="search-box">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="search-icon">
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/>
</svg>
<input
v-model="skillsSearch"
type="text"
placeholder="Search skills..."
class="search-input"
>
</div>
<!-- 操作按钮 -->
<div class="action-bar">
<label class="checkbox-label cursor-pointer">
<input
type="checkbox"
:checked="isAllSelected"
:indeterminate="isIndeterminate"
@change="toggleSelectAll"
class="checkbox"
>
<span class="checkbox-text">Select All</span>
</label>
<button
v-if="newAgent.selectedSkills.length > 0"
type="button"
@click="clearSkills"
class="clear-btn"
>
Clear
</button>
</div>
<!-- 技能列表 -->
<div class="options-list">
<template v-if="filteredSkills.length > 0">
<label
v-for="skill in filteredSkills"
:key="skill.value"
class="option-item"
>
<input
type="checkbox"
:value="skill.value"
v-model="newAgent.selectedSkills"
class="checkbox"
>
<div class="option-content">
<span class="option-label">{{ skill.label }}</span>
<span v-if="skill.desc" class="option-desc">{{ skill.desc }}</span>
</div>
</label>
</template>
<div v-else class="no-results">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="no-results-icon">
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
<path d="M21 3v5h-5"/>
</svg>
<span>No skills available</span>
</div>
</div>
</div>
</Transition>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Knowledge *</label>
<el-select v-model="newAgent.knowledge" placeholder="Select knowledge" class="w-full" size="large" popper-class="dark-select-dropdown">
<el-option v-for="k in knowledgeOptions" :key="k.value" :label="k.label" :value="k.value" />
</el-select>
</div>
<div>
<label class="block text-sm font-medium text-gray-300 mb-2">Custom Prompt</label>
<textarea
v-model="newAgent.prompt"
rows="4"
placeholder="Define the agent's behavior and instructions..."
class="w-full bg-dark-600 border border-dark-500 rounded-lg px-4 py-2.5 text-white placeholder-gray-500 focus:outline-none focus:border-primary-orange resize-none"
></textarea>
</div>
</div>
<div class="flex items-center justify-end gap-3 p-5 border-t border-dark-500">
<button
@click="showCreateModal = false"
class="px-4 py-2 rounded-lg bg-dark-600 text-gray-300 hover:bg-dark-500 transition-colors"
>
Cancel
</button>
<button
@click="createAgent"
:disabled="isCreating || !newAgent.name || (newAgent.skillsMode !== 'all' && newAgent.selectedSkills.length === 0) || !newAgent.knowledge"
class="px-4 py-2 rounded-lg bg-gradient-to-r from-primary-orange to-red-500 text-white hover:from-orange-500 hover:to-red-600 transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
>
<i v-if="isCreating" class="fa-solid fa-circle-notch fa-spin"></i>
{{ isCreating ? 'Creating...' : 'Create Agent' }}
</button>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
/* Skills Selector Styles */
.skills-selector {
position: relative;
}
/* Skills Mode Options */
.skills-mode-options {
padding: 8px;
border-bottom: 1px solid #4b5563;
}
.skills-mode-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.skills-mode-item:hover {
background: #374151;
}
.skills-mode-item.active {
background: rgba(249, 115, 22, 0.1);
}
.mode-radio {
width: 18px;
height: 18px;
border: 2px solid #6b7280;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.skills-mode-item.active .mode-radio {
border-color: #f97316;
}
.radio-dot {
width: 10px;
height: 10px;
background: #f97316;
border-radius: 50%;
}
.mode-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.mode-label {
font-size: 14px;
font-weight: 500;
color: #f3f4f6;
}
.mode-desc {
font-size: 12px;
color: #9ca3af;
}
/* 子下拉框箭头 */
.sub-arrow {
margin-left: auto;
color: #9ca3af;
transition: transform 0.2s ease;
}
.skills-mode-item:hover .sub-arrow {
color: #f97316;
}
/* 子下拉面板 */
.sub-dropdown-panel {
position: absolute;
top: 0;
left: 100%;
margin-left: 8px;
width: 280px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
z-index: 100;
overflow: hidden;
}
.sub-dropdown-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
border-bottom: 1px solid #4b5563;
background: #4b5563;
}
.sub-dropdown-title {
font-size: 13px;
font-weight: 500;
color: #f3f4f6;
}
.sub-dropdown-close {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
color: #9ca3af;
transition: all 0.2s ease;
}
.sub-dropdown-close:hover {
background: #374151;
color: #f3f4f6;
}
/* 无结果样式 */
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 24px;
color: #6b7280;
gap: 8px;
}
.no-results-icon {
opacity: 0.5;
}
.selected-tags {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 44px;
padding: 0 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.selected-tags:hover {
border-color: #f97316;
}
.selected-tags .placeholder {
color: #9ca3af;
font-size: 14px;
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
flex: 1;
overflow: hidden;
}
.selected-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
color: white;
font-size: 12px;
font-weight: 500;
border-radius: 4px;
white-space: nowrap;
}
.more-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
background: #374151;
color: #d1d5db;
font-size: 12px;
border-radius: 4px;
}
.dropdown-icon {
color: #9ca3af;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.dropdown-icon.rotate-180 {
transform: rotate(180deg);
}
/* Dropdown Panel */
.dropdown-panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: #374151;
border: 1px solid #4b5563;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
z-index: 100;
overflow: hidden;
}
/* Search Box */
.search-box {
position: relative;
padding: 12px;
border-bottom: 1px solid #4b5563;
}
.search-icon {
position: absolute;
left: 24px;
top: 50%;
transform: translateY(-50%);
color: #9ca3af;
}
.search-input {
width: 100%;
padding: 8px 12px 8px 36px;
background: #111827;
border: 1px solid #374151;
border-radius: 6px;
color: white;
font-size: 14px;
outline: none;
transition: border-color 0.2s ease;
}
.search-input:focus {
border-color: #f97316;
}
.search-input::placeholder {
color: #6b7280;
}
/* Action Bar */
.action-bar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: #111827;
border-bottom: 1px solid #374151;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox {
width: 16px;
height: 16px;
accent-color: #f97316;
cursor: pointer;
}
.checkbox-text {
font-size: 13px;
font-weight: 500;
color: #f97316;
}
.clear-btn {
padding: 4px 10px;
background: transparent;
border: 1px solid #4b5563;
border-radius: 4px;
color: #9ca3af;
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.clear-btn:hover {
background: #374151;
color: white;
}
/* Options List */
.options-list {
max-height: 240px;
overflow-y: auto;
padding: 8px;
}
.options-list::-webkit-scrollbar {
width: 6px;
}
.options-list::-webkit-scrollbar-track {
background: #374151;
}
.options-list::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 3px;
}
.option-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
}
.option-item:hover {
background: #374151;
}
.option-item .checkbox {
margin-top: 2px;
flex-shrink: 0;
}
.option-content {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.option-label {
font-size: 14px;
font-weight: 500;
color: white;
}
.option-desc {
font-size: 12px;
color: #9ca3af;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.no-results {
padding: 20px;
text-align: center;
color: #6b7280;
font-size: 14px;
}
/* Dropdown Transition */
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px);
}
/* Indeterminate checkbox */
input[type="checkbox"]:indeterminate {
accent-color: #f97316;
}
</style>