feat: 前端 UI 增强 - 用户搜索筛选、Agent 面板样式优化、导航更新
- UserManagementComponent.vue: 新增用户名/ID/手机号搜索 + 部门/权限筛选 - AgentManagePanel.vue: 优化后端选择器样式、侧边栏条件渲染 - QuerySection.vue: 微调检索结果文字大小与样式 - AppLayout.vue: 更新导航名称与图标(扩展管理→智能体扩展等)
This commit is contained in:
parent
5b1fd51b27
commit
2ada27b285
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user