feat: 前端 UI 增强 - 用户搜索筛选、Agent 面板样式优化、导航更新

- UserManagementComponent.vue: 新增用户名/ID/手机号搜索 + 部门/权限筛选
- AgentManagePanel.vue: 优化后端选择器样式、侧边栏条件渲染
- QuerySection.vue: 微调检索结果文字大小与样式
- AppLayout.vue: 更新导航名称与图标(扩展管理→智能体扩展等)
This commit is contained in:
Wenjie Zhang 2026-05-26 17:24:04 +08:00
parent 5b1fd51b27
commit 2ada27b285
4 changed files with 213 additions and 36 deletions

View File

@ -59,7 +59,7 @@
</div>
<div v-else>
<div class="result-summary">
<strong>检索到 {{ queryResult.length }} 个相关文档块</strong>
<span>检索到 {{ queryResult.length }} 个相关文档块</span>
<a-button
type="text"
size="small"
@ -514,7 +514,8 @@ defineExpose({
background-color: var(--main-50);
border-radius: 6px;
color: var(--gray-800);
font-weight: 500;
font-size: 13px;
span { font-weight: 500; }
}
.clear-results-btn {
@ -578,6 +579,7 @@ defineExpose({
.result-content {
padding: 8px 0;
line-height: 1.6;
font-size: 13px;
color: var(--gray-900);
white-space: pre-wrap;
word-break: break-word;

View File

@ -15,7 +15,9 @@
title="刷新"
class="refresh-btn lucide-icon-btn"
>
<template #icon><RefreshCw :size="16" :class="{ spin: userManagement.refreshing }" /></template>
<template #icon>
<RefreshCw :size="16" :class="{ spin: userManagement.refreshing }" />
</template>
</a-button>
<a-button type="primary" @click="showAddUserModal" class="add-btn lucide-icon-btn">
<template #icon><Plus :size="16" /></template>
@ -24,6 +26,35 @@
</div>
</div>
<div class="filter-section">
<a-input
v-model:value="userManagement.searchKeyword"
class="search-input"
placeholder="搜索用户名 / ID / 手机号"
allow-clear
>
<template #prefix><Search :size="16" /></template>
</a-input>
<div class="filter-actions">
<a-select v-model:value="userManagement.departmentFilter" class="filter-select">
<a-select-option value="">全部部门</a-select-option>
<a-select-option
v-for="dept in departmentFilterOptions"
:key="dept.value"
:value="dept.value"
>
{{ dept.label }}
</a-select-option>
</a-select>
<a-select v-model:value="userManagement.roleFilter" class="filter-select">
<a-select-option value="">全部权限</a-select-option>
<a-select-option value="superadmin">超级管理员</a-select-option>
<a-select-option value="admin">管理员</a-select-option>
<a-select-option value="user">普通用户</a-select-option>
</a-select>
</div>
</div>
<!-- 主内容区域 -->
<div class="content-section">
<a-spin :spinning="userManagement.loading">
@ -32,11 +63,13 @@
</div>
<div class="cards-container">
<div v-if="userManagement.users.length === 0" class="empty-state">
<a-empty description="暂无用户数据" />
<div v-if="filteredUsers.length === 0" class="empty-state">
<a-empty
:description="userManagement.users.length === 0 ? '暂无用户数据' : '没有匹配的用户'"
/>
</div>
<div v-else class="user-cards-grid">
<div v-for="user in userManagement.users" :key="user.id" class="user-card">
<div v-for="user in filteredUsers" :key="user.id" class="user-card">
<div class="card-header">
<div class="user-info-main">
<div class="user-avatar">
@ -225,11 +258,11 @@
</template>
<script setup>
import { reactive, onMounted, watch } from 'vue'
import { reactive, onMounted, watch, computed } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { useUserStore } from '@/stores/user'
import { departmentApi } from '@/apis'
import { Plus, Pencil, Trash2, User, UserLock, UserStar, RefreshCw } from 'lucide-vue-next'
import { Plus, Pencil, Trash2, User, UserLock, UserStar, RefreshCw, Search } from 'lucide-vue-next'
import { formatDateTime } from '@/utils/time'
const userStore = useUserStore()
@ -239,6 +272,9 @@ const userManagement = reactive({
loading: false,
refreshing: false,
users: [],
searchKeyword: '',
departmentFilter: '',
roleFilter: '',
error: null,
modalVisible: false,
modalTitle: '添加用户',
@ -263,6 +299,55 @@ const departmentManagement = reactive({
departments: []
})
const departmentFilterOptions = computed(() => {
const options = new Map()
departmentManagement.departments.forEach((dept) => {
options.set(String(dept.id), {
value: String(dept.id),
label: dept.name
})
})
userManagement.users.forEach((user) => {
const departmentId = user.department_id
const departmentName = user.department_name
if (departmentId == null && !departmentName) return
const value = String(departmentId ?? departmentName)
if (!options.has(value)) {
options.set(value, {
value,
label: departmentName || `部门 ${departmentId}`
})
}
})
return [...options.values()]
})
const filteredUsers = computed(() => {
const keyword = userManagement.searchKeyword.trim().toLowerCase()
return userManagement.users.filter((user) => {
const matchesKeyword =
!keyword ||
[user.username, user.uid, user.phone_number].some((value) =>
String(value || '')
.toLowerCase()
.includes(keyword)
)
const matchesDepartment =
!userManagement.departmentFilter ||
String(user.department_id ?? user.department_name ?? '') === userManagement.departmentFilter
const matchesRole = !userManagement.roleFilter || user.role === userManagement.roleFilter
return matchesKeyword && matchesDepartment && matchesRole
})
})
//
const fetchDepartments = async () => {
if (!userStore.isSuperAdmin) return //
@ -613,6 +698,57 @@ onMounted(async () => {
}
}
.filter-section {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
.search-input {
width: 300px;
max-width: 100%;
:deep(.ant-input-prefix) {
color: var(--gray-500);
margin-right: 6px;
}
}
.filter-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
margin-left: auto;
}
.filter-select {
width: 150px;
}
}
@media (max-width: 640px) {
.filter-section {
align-items: stretch;
.search-input,
.filter-actions {
width: 100%;
}
.filter-actions {
margin-left: 0;
}
.filter-select {
flex: 1;
min-width: 0;
}
}
}
.content-section {
overflow: hidden;

View File

@ -34,6 +34,7 @@ const agentLoading = ref(false)
const saving = ref(false)
const searchQuery = ref('')
const DEFAULT_AGENT_BACKEND_ID = 'ChatbotAgent'
const agentBackendOptions = ref([])
const showAgentModal = ref(false)
@ -46,7 +47,7 @@ const agentShareConfig = ref({ access_level: 'user', department_ids: [], user_ui
const agentForm = reactive({
slug: '',
name: '',
backend_id: 'ChatbotAgent',
backend_id: DEFAULT_AGENT_BACKEND_ID,
description: '',
icon: ''
})
@ -63,6 +64,7 @@ const agentModalMenuItems = computed(() => {
}
return items
})
const showAgentModalSidebar = computed(() => agentModalMenuItems.value.length > 1)
const runtimeConfigSegment = computed(() =>
runtimeAgentModalTabs.includes(agentModalActiveTab.value) ? agentModalActiveTab.value : 'model'
)
@ -91,7 +93,7 @@ const agentStats = computed(() => ({
manageable: agentStore.agents.filter((agent) => agent.can_manage).length,
global: agentStore.agents.filter((agent) => agent.share_config?.access_level === 'global').length
}))
const getDefaultBackendId = () => agentBackendOptions.value[0]?.value || 'ChatbotAgent'
const getDefaultBackendId = () => DEFAULT_AGENT_BACKEND_ID
const getInitialShareConfig = () => ({
access_level: userStore.isAdmin ? 'global' : 'user',
@ -183,7 +185,7 @@ const openEditAgentModal = async (agent) => {
Object.assign(agentForm, {
slug: detail.id || detail.slug || '',
name: detail.name || '',
backend_id: detail.backend_id || 'ChatbotAgent',
backend_id: detail.backend_id || DEFAULT_AGENT_BACKEND_ID,
description: detail.description || '',
icon: detail.icon || ''
})
@ -404,8 +406,8 @@ defineExpose({
</div>
</div>
</template>
<div class="agent-modal-content">
<aside class="agent-modal-sidebar" aria-label="智能体配置分组">
<div class="agent-modal-content" :class="{ 'without-sidebar': !showAgentModalSidebar }">
<aside v-if="showAgentModalSidebar" class="agent-modal-sidebar" aria-label="智能体配置分组">
<button
v-for="item in agentModalMenuItems"
:key="item.key"
@ -461,15 +463,17 @@ defineExpose({
<span v-else class="agent-inline-slug">{{ agentForm.slug || editingAgentId }}</span>
</div>
</div>
<div class="agent-backend-summary" aria-label="智能体后端">
<component :is="selectedBackendIcon" :size="16" />
<div class="agent-backend-summary" :class="{ editable: !editingAgentId }" aria-label="智能体后端">
<span class="agent-backend-icon">
<component :is="selectedBackendIcon" :size="16" />
</span>
<div class="agent-backend-text">
<span class="agent-backend-label">后端</span>
<span class="agent-backend-label">智能体后端</span>
<a-select
v-if="!editingAgentId"
v-model:value="agentForm.backend_id"
class="agent-backend-select"
size="small"
:bordered="false"
:options="agentBackendOptions"
/>
<span v-else class="agent-backend-name">{{ selectedBackendLabel }}</span>
@ -611,6 +615,10 @@ defineExpose({
border: 1px solid var(--gray-150);
border-radius: 12px;
background: var(--gray-0);
&.without-sidebar {
grid-template-columns: minmax(0, 1fr);
}
}
.agent-modal-sidebar {
@ -656,10 +664,11 @@ defineExpose({
}
&.active {
border-color: var(--main-100);
background: var(--gray-0);
color: var(--main-800);
font-weight: 600;
span {
font-weight: 600;
}
}
}
@ -862,19 +871,38 @@ defineExpose({
display: inline-flex;
align-items: center;
flex-shrink: 0;
gap: 8px;
min-height: 40px;
padding: 7px 10px;
border: 1px solid var(--gray-150);
gap: 10px;
min-width: 220px;
min-height: 56px;
padding: 10px 12px;
border: 1px solid var(--main-100);
border-radius: 12px;
background: linear-gradient(135deg, var(--gray-0), var(--main-10));
color: var(--main-700);
&.editable {
padding-right: 8px;
}
}
.agent-backend-icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 32px;
height: 32px;
border-radius: 10px;
background: var(--gray-0);
background: var(--main-50);
color: var(--main-700);
}
.agent-backend-text {
display: flex;
flex: 1;
flex-direction: column;
gap: 2px;
min-width: 0;
gap: 3px;
line-height: 1.2;
}
@ -886,19 +914,30 @@ defineExpose({
.agent-backend-name {
max-width: 180px;
overflow: hidden;
color: var(--gray-800);
font-size: 12px;
color: var(--gray-900);
font-size: 13px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-backend-select {
min-width: 160px;
width: 170px;
margin: -3px 0 -5px -11px;
:deep(.ant-select-selector) {
border-color: var(--gray-150) !important;
border-radius: 8px !important;
background: transparent !important;
box-shadow: none !important;
}
:deep(.ant-select-selection-item) {
color: var(--gray-900);
font-size: 13px;
font-weight: 600;
}
:deep(.ant-select-arrow) {
color: var(--gray-500);
}
}

View File

@ -5,7 +5,7 @@ import { GithubOutlined } from '@ant-design/icons-vue'
import {
BarChart3,
ClipboardList,
Blocks,
LibraryBig,
Box,
FolderKanban,
PanelLeftClose,
@ -137,22 +137,22 @@ const mainList = computed(() => {
if (userStore.isAdmin) {
items.push({
name: '扩展管理',
name: '智能体扩展',
path: '/extensions',
activePaths: ['/extensions'],
icon: Blocks,
activeIcon: Blocks
icon: LibraryBig,
activeIcon: LibraryBig
})
items.push({
name: '模型管理',
name: '智能体管理',
path: '/model-manage',
icon: Box,
activeIcon: Box
})
items.push({
name: 'Dashboard',
name: '数据总览',
path: '/dashboard',
icon: BarChart3,
activeIcon: BarChart3