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>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="result-summary">
|
<div class="result-summary">
|
||||||
<strong>检索到 {{ queryResult.length }} 个相关文档块:</strong>
|
<span>检索到 {{ queryResult.length }} 个相关文档块:</span>
|
||||||
<a-button
|
<a-button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
@ -514,7 +514,8 @@ defineExpose({
|
|||||||
background-color: var(--main-50);
|
background-color: var(--main-50);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--gray-800);
|
color: var(--gray-800);
|
||||||
font-weight: 500;
|
font-size: 13px;
|
||||||
|
span { font-weight: 500; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-results-btn {
|
.clear-results-btn {
|
||||||
@ -578,6 +579,7 @@ defineExpose({
|
|||||||
.result-content {
|
.result-content {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
font-size: 13px;
|
||||||
color: var(--gray-900);
|
color: var(--gray-900);
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|||||||
@ -15,7 +15,9 @@
|
|||||||
title="刷新"
|
title="刷新"
|
||||||
class="refresh-btn lucide-icon-btn"
|
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>
|
||||||
<a-button type="primary" @click="showAddUserModal" class="add-btn lucide-icon-btn">
|
<a-button type="primary" @click="showAddUserModal" class="add-btn lucide-icon-btn">
|
||||||
<template #icon><Plus :size="16" /></template>
|
<template #icon><Plus :size="16" /></template>
|
||||||
@ -24,6 +26,35 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="content-section">
|
||||||
<a-spin :spinning="userManagement.loading">
|
<a-spin :spinning="userManagement.loading">
|
||||||
@ -32,11 +63,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cards-container">
|
<div class="cards-container">
|
||||||
<div v-if="userManagement.users.length === 0" class="empty-state">
|
<div v-if="filteredUsers.length === 0" class="empty-state">
|
||||||
<a-empty description="暂无用户数据" />
|
<a-empty
|
||||||
|
:description="userManagement.users.length === 0 ? '暂无用户数据' : '没有匹配的用户'"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="user-cards-grid">
|
<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="card-header">
|
||||||
<div class="user-info-main">
|
<div class="user-info-main">
|
||||||
<div class="user-avatar">
|
<div class="user-avatar">
|
||||||
@ -225,11 +258,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, onMounted, watch } from 'vue'
|
import { reactive, onMounted, watch, computed } from 'vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { departmentApi } from '@/apis'
|
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'
|
import { formatDateTime } from '@/utils/time'
|
||||||
|
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
@ -239,6 +272,9 @@ const userManagement = reactive({
|
|||||||
loading: false,
|
loading: false,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
users: [],
|
users: [],
|
||||||
|
searchKeyword: '',
|
||||||
|
departmentFilter: '',
|
||||||
|
roleFilter: '',
|
||||||
error: null,
|
error: null,
|
||||||
modalVisible: false,
|
modalVisible: false,
|
||||||
modalTitle: '添加用户',
|
modalTitle: '添加用户',
|
||||||
@ -263,6 +299,55 @@ const departmentManagement = reactive({
|
|||||||
departments: []
|
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 () => {
|
const fetchDepartments = async () => {
|
||||||
if (!userStore.isSuperAdmin) return // 普通管理员不需要获取所有部门列表
|
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 {
|
.content-section {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
|||||||
@ -34,6 +34,7 @@ const agentLoading = ref(false)
|
|||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
|
|
||||||
|
const DEFAULT_AGENT_BACKEND_ID = 'ChatbotAgent'
|
||||||
const agentBackendOptions = ref([])
|
const agentBackendOptions = ref([])
|
||||||
|
|
||||||
const showAgentModal = ref(false)
|
const showAgentModal = ref(false)
|
||||||
@ -46,7 +47,7 @@ const agentShareConfig = ref({ access_level: 'user', department_ids: [], user_ui
|
|||||||
const agentForm = reactive({
|
const agentForm = reactive({
|
||||||
slug: '',
|
slug: '',
|
||||||
name: '',
|
name: '',
|
||||||
backend_id: 'ChatbotAgent',
|
backend_id: DEFAULT_AGENT_BACKEND_ID,
|
||||||
description: '',
|
description: '',
|
||||||
icon: ''
|
icon: ''
|
||||||
})
|
})
|
||||||
@ -63,6 +64,7 @@ const agentModalMenuItems = computed(() => {
|
|||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
const showAgentModalSidebar = computed(() => agentModalMenuItems.value.length > 1)
|
||||||
const runtimeConfigSegment = computed(() =>
|
const runtimeConfigSegment = computed(() =>
|
||||||
runtimeAgentModalTabs.includes(agentModalActiveTab.value) ? agentModalActiveTab.value : 'model'
|
runtimeAgentModalTabs.includes(agentModalActiveTab.value) ? agentModalActiveTab.value : 'model'
|
||||||
)
|
)
|
||||||
@ -91,7 +93,7 @@ const agentStats = computed(() => ({
|
|||||||
manageable: agentStore.agents.filter((agent) => agent.can_manage).length,
|
manageable: agentStore.agents.filter((agent) => agent.can_manage).length,
|
||||||
global: agentStore.agents.filter((agent) => agent.share_config?.access_level === 'global').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 = () => ({
|
const getInitialShareConfig = () => ({
|
||||||
access_level: userStore.isAdmin ? 'global' : 'user',
|
access_level: userStore.isAdmin ? 'global' : 'user',
|
||||||
@ -183,7 +185,7 @@ const openEditAgentModal = async (agent) => {
|
|||||||
Object.assign(agentForm, {
|
Object.assign(agentForm, {
|
||||||
slug: detail.id || detail.slug || '',
|
slug: detail.id || detail.slug || '',
|
||||||
name: detail.name || '',
|
name: detail.name || '',
|
||||||
backend_id: detail.backend_id || 'ChatbotAgent',
|
backend_id: detail.backend_id || DEFAULT_AGENT_BACKEND_ID,
|
||||||
description: detail.description || '',
|
description: detail.description || '',
|
||||||
icon: detail.icon || ''
|
icon: detail.icon || ''
|
||||||
})
|
})
|
||||||
@ -404,8 +406,8 @@ defineExpose({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="agent-modal-content">
|
<div class="agent-modal-content" :class="{ 'without-sidebar': !showAgentModalSidebar }">
|
||||||
<aside class="agent-modal-sidebar" aria-label="智能体配置分组">
|
<aside v-if="showAgentModalSidebar" class="agent-modal-sidebar" aria-label="智能体配置分组">
|
||||||
<button
|
<button
|
||||||
v-for="item in agentModalMenuItems"
|
v-for="item in agentModalMenuItems"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
@ -461,15 +463,17 @@ defineExpose({
|
|||||||
<span v-else class="agent-inline-slug">{{ agentForm.slug || editingAgentId }}</span>
|
<span v-else class="agent-inline-slug">{{ agentForm.slug || editingAgentId }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="agent-backend-summary" aria-label="智能体后端">
|
<div class="agent-backend-summary" :class="{ editable: !editingAgentId }" aria-label="智能体后端">
|
||||||
<component :is="selectedBackendIcon" :size="16" />
|
<span class="agent-backend-icon">
|
||||||
|
<component :is="selectedBackendIcon" :size="16" />
|
||||||
|
</span>
|
||||||
<div class="agent-backend-text">
|
<div class="agent-backend-text">
|
||||||
<span class="agent-backend-label">后端</span>
|
<span class="agent-backend-label">智能体后端</span>
|
||||||
<a-select
|
<a-select
|
||||||
v-if="!editingAgentId"
|
v-if="!editingAgentId"
|
||||||
v-model:value="agentForm.backend_id"
|
v-model:value="agentForm.backend_id"
|
||||||
class="agent-backend-select"
|
class="agent-backend-select"
|
||||||
size="small"
|
:bordered="false"
|
||||||
:options="agentBackendOptions"
|
:options="agentBackendOptions"
|
||||||
/>
|
/>
|
||||||
<span v-else class="agent-backend-name">{{ selectedBackendLabel }}</span>
|
<span v-else class="agent-backend-name">{{ selectedBackendLabel }}</span>
|
||||||
@ -611,6 +615,10 @@ defineExpose({
|
|||||||
border: 1px solid var(--gray-150);
|
border: 1px solid var(--gray-150);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
background: var(--gray-0);
|
background: var(--gray-0);
|
||||||
|
|
||||||
|
&.without-sidebar {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-modal-sidebar {
|
.agent-modal-sidebar {
|
||||||
@ -656,10 +664,11 @@ defineExpose({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
border-color: var(--main-100);
|
|
||||||
background: var(--gray-0);
|
background: var(--gray-0);
|
||||||
color: var(--main-800);
|
color: var(--main-800);
|
||||||
font-weight: 600;
|
span {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -862,19 +871,38 @@ defineExpose({
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 8px;
|
gap: 10px;
|
||||||
min-height: 40px;
|
min-width: 220px;
|
||||||
padding: 7px 10px;
|
min-height: 56px;
|
||||||
border: 1px solid var(--gray-150);
|
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;
|
border-radius: 10px;
|
||||||
background: var(--gray-0);
|
background: var(--main-50);
|
||||||
color: var(--main-700);
|
color: var(--main-700);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-backend-text {
|
.agent-backend-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
min-width: 0;
|
||||||
|
gap: 3px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -886,19 +914,30 @@ defineExpose({
|
|||||||
.agent-backend-name {
|
.agent-backend-name {
|
||||||
max-width: 180px;
|
max-width: 180px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--gray-800);
|
color: var(--gray-900);
|
||||||
font-size: 12px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-backend-select {
|
.agent-backend-select {
|
||||||
min-width: 160px;
|
width: 170px;
|
||||||
|
margin: -3px 0 -5px -11px;
|
||||||
|
|
||||||
:deep(.ant-select-selector) {
|
:deep(.ant-select-selector) {
|
||||||
border-color: var(--gray-150) !important;
|
background: transparent !important;
|
||||||
border-radius: 8px !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 {
|
import {
|
||||||
BarChart3,
|
BarChart3,
|
||||||
ClipboardList,
|
ClipboardList,
|
||||||
Blocks,
|
LibraryBig,
|
||||||
Box,
|
Box,
|
||||||
FolderKanban,
|
FolderKanban,
|
||||||
PanelLeftClose,
|
PanelLeftClose,
|
||||||
@ -137,22 +137,22 @@ const mainList = computed(() => {
|
|||||||
|
|
||||||
if (userStore.isAdmin) {
|
if (userStore.isAdmin) {
|
||||||
items.push({
|
items.push({
|
||||||
name: '扩展管理',
|
name: '智能体扩展',
|
||||||
path: '/extensions',
|
path: '/extensions',
|
||||||
activePaths: ['/extensions'],
|
activePaths: ['/extensions'],
|
||||||
icon: Blocks,
|
icon: LibraryBig,
|
||||||
activeIcon: Blocks
|
activeIcon: LibraryBig
|
||||||
})
|
})
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
name: '模型管理',
|
name: '智能体管理',
|
||||||
path: '/model-manage',
|
path: '/model-manage',
|
||||||
icon: Box,
|
icon: Box,
|
||||||
activeIcon: Box
|
activeIcon: Box
|
||||||
})
|
})
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
name: 'Dashboard',
|
name: '数据总览',
|
||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
icon: BarChart3,
|
icon: BarChart3,
|
||||||
activeIcon: BarChart3
|
activeIcon: BarChart3
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user