eagleye

是的,Quasar组件筛选功能需要DRF后端配合实现

是的,筛选功能需要DRF后端配合实现。前端组件已经通过`filterOptions`对象收集了筛选参数(角色、状态、部门、搜索词),并通过API请求发送给后端,但后端视图集尚未实现过滤逻辑。下面是完整的解决方案:

### 后端修改方案

```python
from django_filters import rest_framework as filters
from django.db.models import Q

# 1. 添加用户过滤器
class UserProfileFilter(filters.FilterSet):
role = filters.NumberFilter(field_name='role')
status = filters.NumberFilter(field_name='status')
department = filters.NumberFilter(field_name='department__id')
search = filters.CharFilter(method='custom_search')

class Meta:
model = SecureUser
fields = ['role', 'status', 'department', 'search']

def custom_search(self, queryset, name, value):
"""实现多字段搜索"""
return queryset.filter(
Q(nickname__icontains=value) |
Q(email__icontains=value) |
Q(mobile__icontains=value) |
Q(wechat_account__icontains=value)
)

class UserProfileViewSet(viewsets.ModelViewSet):
# 2. 添加过滤后端和过滤器类
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = UserProfileFilter

# 3. 修改get_queryset方法实现权限和过滤
def get_queryset(self):
"""企业级数据访问范围控制+过滤"""
user = self.request.user
queryset = SecureUser.objects.select_related('department')

# 权限控制
if user.role == UserRoles.SYSTEM_ADMIN:
pass # 系统管理员访问所有用户
elif user.role == UserRoles.SECURITY_ADMIN and user.department:
queryset = queryset.filter(department=user.department)
else:
queryset = queryset.filter(id=user.id)

# 应用过滤
return self.filter_queryset(queryset)

# ... 其他方法保持不变 ...
```

### 前端优化方案

```typescript
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from 'stores/auth'
import {
QTable,
QTd,
QIcon,
QBadge,
QAvatar,
QPagination,
QSelect,
QInput,
type QTableColumn,
date,
} from 'quasar'
import type { AxiosResponse } from 'axios'
import { type UserProfile, UserRole } from 'src/types/auth/profiles'
import { apiClient } from 'src/services/axios'

// 状态管理
const router = useRouter()
const authStore = useAuthStore()
const loading = ref(true)
const error = ref<string | null>(null)
const userList = ref<UserProfile[]>([])
const pagination = ref({
page: 1,
rowsPerPage: 10,
rowsNumber: 0,
})
const filterOptions = ref({
role: null as number | null,
status: null as number | null,
department: null as number | null,
search: '',
})

// 角色选项
const roleOptions = [
{ label: '隐患排查员', value: 10 },
{ label: '隐患治理员', value: 15 },
{ label: '安全管理员', value: 20 },
{ label: '安全审计员', value: 30 },
{ label: '系统管理员', value: 99 },
]

// 状态选项
const statusOptions = [
{ label: '正常', value: 0 },
{ label: '已锁定', value: 1 },
{ label: '已停用', value: 2 },
{ label: '密码过期', value: 3 },
{ label: '已删除', value: 4 },
]

// 权限检查
const isAdmin = computed(() => {
return (
authStore.userRole === UserRole.SYSTEM_ADMIN ||
authStore.userRole === UserRole.SECURITY_ADMIN
)
})

// 表头定义
const columns: QTableColumn[] = [
{
name: 'avatar',
label: '头像',
field: 'avatar',
align: 'center',
sortable: false,
},
{
name: 'nickname',
label: '昵称',
field: 'nickname',
align: 'left',
sortable: true,
},
{
name: 'role',
label: '角色',
field: 'role_display',
align: 'center',
sortable: true,
},
{
name: 'department',
label: '部门',
field: (row: UserProfile) => row.department_info?.name || '-',
align: 'center',
sortable: true,
},
{
name: 'status',
label: '状态',
field: 'status_display',
align: 'center',
sortable: true,
},
{
name: 'last_login',
label: '最后登录',
field: (row: UserProfile) =>
row.last_login ? date.formatDate(row.last_login, 'YYYY-MM-DD HH:mm:ss') : '从未登录',
align: 'center',
sortable: true,
},
{
name: 'actions',
label: '操作',
align: 'center',
sortable: false,
field: '',
},
]

// 获取用户列表
const fetchUserList = async () => {
if (!isAdmin.value) {
error.value = '权限不足:仅管理员可访问用户列表'
loading.value = false
userList.value = []
return
}

try {
loading.value = true
error.value = null

// 构建查询参数
const params: Record<string, unknown> = {
page: pagination.value.page,
page_size: pagination.value.rowsPerPage,
}

// 添加筛选参数
if (filterOptions.value.role !== null) params.role = filterOptions.value.role
if (filterOptions.value.status !== null) params.status = filterOptions.value.status
if (filterOptions.value.department !== null) params.department = filterOptions.value.department
if (filterOptions.value.search) params.search = filterOptions.value.search

const response: AxiosResponse<{
count: number;
results: UserProfile[];
}> = await apiClient.get('/users/profile/', { params })

userList.value = response.data.results || []
pagination.value.rowsNumber = response.data.count || 0
} catch (err) {
handleListError(err)
userList.value = []
} finally {
loading.value = false
}
}

// 错误处理
const handleListError = (err: unknown) => {
let errorMessage = '获取用户列表失败,请稍后重试'

if (typeof err === 'object' && err !== null) {
if ('response' in err) {
const axiosError = err as { response?: { status: number; data?: any } }

if (axiosError.response?.status === 403) {
errorMessage = '权限不足:仅管理员可访问用户列表'
} else if (axiosError.response?.data?.detail) {
errorMessage = `获取用户列表失败:${axiosError.response.data.detail}`
} else if (axiosError.response?.data) {
const errorData = axiosError.response.data
if (typeof errorData === 'string') {
errorMessage = `服务器错误:${errorData}`
} else if (Array.isArray(errorData) && errorData.length > 0) {
errorMessage = `错误:${errorData[0]}`
}
}
} else if ('message' in err) {
errorMessage = (err as { message: string }).message
}
}

error.value = errorMessage
}

// 状态标签颜色
const getStatusColor = (status: string) => {
switch (status) {
case '正常': return 'positive'
case '已锁定': return 'negative'
case '已停用': return 'grey'
case '密码过期': return 'warning'
case '已删除': return 'dark'
default: return 'info'
}
}

// 查看用户详情
const viewUserDetail = (userId: string) => {
router.push(`/profile/${userId}`)
}

// 重置过滤器
const resetFilters = () => {
filterOptions.value = {
role: null,
status: null,
department: null,
search: '',
}
fetchUserList()
}

// 分页变化处理
const onPaginationChange = () => {
fetchUserList()
}

// 初始化加载
onMounted(() => {
fetchUserList()
})
</script>

<template>
<div class="q-pa-lg">
<!-- 权限提示 -->
<div v-if="!isAdmin" class="text-center q-pa-xl">
<q-icon name="security" size="xl" color="negative" />
<div class="text-h6 q-mt-md text-negative">您没有权限访问用户列表</div>
<q-btn label="返回首页" color="primary" class="q-mt-md" @click="router.push('/')" />
</div>

<!-- 管理员视图 -->
<div v-else>
<!-- 标题和统计 -->
<div class="row items-center q-mb-md">
<div class="col">
<div class="text-h5">用户管理</div>
<div class="text-caption text-grey">
共 {{ pagination.rowsNumber }} 名用户
<span v-if="filterOptions.search"> | 搜索: "{{ filterOptions.search }}"</span>
</div>
</div>
<div class="col-auto">
<q-btn label="导出报表" color="secondary" icon="description" class="q-mr-sm" />
</div>
</div>

<!-- 过滤区域 -->
<div class="row q-gutter-md q-mb-lg">
<div class="col-3">
<q-select
v-model="filterOptions.role"
label="按角色筛选"
:options="roleOptions"
emit-value
map-options
clearable
filled
/>
</div>
<div class="col-2">
<q-select
v-model="filterOptions.status"
label="按状态筛选"
:options="statusOptions"
emit-value
map-options
clearable
filled
/>
</div>
<div class="col-3">
<q-input
v-model="filterOptions.search"
label="搜索用户"
clearable
filled
debounce="500"
@update:model-value="fetchUserList"
>
<template v-slot:append>
<q-icon name="search" />
</template>
</q-input>
</div>
<div class="col-auto self-end">
<q-btn label="筛选" color="primary" icon="filter_list" @click="fetchUserList" />
</div>
<div class="col-auto self-end">
<q-btn label="重置" color="grey" icon="refresh" @click="resetFilters" />
</div>
</div>

<!-- 错误提示 -->
<q-banner v-if="error" class="bg-negative text-white q-mb-md">
<template v-slot:avatar>
<q-icon name="error" color="white" />
</template>
{{ error }}
</q-banner>

<!-- 加载状态 -->
<div v-if="loading" class="text-center q-pa-xl">
<q-spinner-gears size="xl" color="primary" />
<div class="q-mt-md">加载用户列表...</div>
</div>

<!-- 用户表格 -->
<template v-else>
<q-table
:rows="userList"
:columns="columns"
row-key="id"
:pagination="pagination"
flat
bordered
@update:pagination="onPaginationChange"
binary-state-sort
>
<!-- 头像列 -->
<template v-slot:body-cell-avatar="props">
<q-td :props="props" auto-width>
<q-avatar size="40px">
<img :src="props.row.avatar || 'default-avatar.png'" />
</q-avatar>
</q-td>
</template>

<!-- 状态列 -->
<template v-slot:body-cell-status="props">
<q-td :props="props">
<q-badge :color="getStatusColor(props.value)" class="q-pa-sm">
{{ props.value }}
</q-badge>
</q-td>
</template>

<!-- 操作列 -->
<template v-slot:body-cell-actions="props">
<q-td :props="props" auto-width>
<q-btn
icon="visibility"
size="sm"
color="primary"
dense
round
@click="viewUserDetail(props.row.id)"
>
<q-tooltip>查看详情</q-tooltip>
</q-btn>
</q-td>
</template>

<!-- 空状态 -->
<template v-slot:no-data>
<div class="full-width row flex-center text-grey-8 q-gutter-sm">
<q-icon name="group_remove" size="2em" />
<span v-if="filterOptions.role || filterOptions.status || filterOptions.search">
没有找到匹配的用户
</span>
<span v-else> 系统中还没有用户 </span>
</div>
</template>
</q-table>

<!-- 分页控件 -->
<div v-if="pagination.rowsNumber > 0" class="row justify-center q-mt-md">
<q-pagination
v-model="pagination.page"
:max="Math.ceil(pagination.rowsNumber / pagination.rowsPerPage)"
:max-pages="6"
direction-links
boundary-links
@update:model-value="fetchUserList"
/>
</div>
</template>
</div>
</div>
</template>

<style lang="scss" scoped>
.q-table {
thead tr {
background-color: #f5f7fa;
}

th {
font-weight: bold;
color: #34495e;
}

tbody tr:hover {
background-color: #f0f7ff !important;
cursor: pointer;
}

.q-td {
padding: 12px 16px;
}
}

.q-badge {
min-width: 70px;
text-align: center;
border-radius: 4px;
font-weight: 500;
}

@media (max-width: 768px) {
.row.q-gutter-md > div {
width: 100%;
}
}
</style>
```

### 关键修改说明:

#### 后端修改:
1. **添加过滤器类**:
- 创建`UserProfileFilter`类定义可过滤字段
- 支持角色、状态、部门和搜索字段过滤
- 搜索字段支持昵称、邮箱、手机号等多字段搜索

2. **修改视图集**:
- 添加`filter_backends`使用DjangoFilterBackend
- 设置`filterset_class`为自定义过滤器
- 在`get_queryset`中合并权限控制和过滤逻辑

3. **部门过滤**:
- 假设部门是外键关系,使用`department__id`过滤
- 如果不是外键,需根据实际模型调整

#### 前端优化:
1. **API参数处理**:
- 明确发送`role`、`status`、`department`和`search`参数
- 只发送非空筛选条件(`if (filterOptions.value.role !== null)`)

2. **响应数据结构**:
- 确保API返回分页格式:`{ count: number, results: UserProfile[] }`
- 正确设置总行数:`pagination.value.rowsNumber = response.data.count`

3. **用户体验优化**:
- 添加500ms防抖避免频繁请求
- 优化空状态提示信息
- 确保分页控件正确触发数据刷新

4. **错误处理**:
- 完善错误消息提取逻辑
- 区分权限错误和服务器错误

### 部署说明:

1. 安装依赖:
```bash
pip install django-filter
```

2. 在Django设置中添加:
```python
INSTALLED_APPS = [
# ...
'django_filters',
]
```

3. 迁移数据库(如果部门关系有变化)

这个解决方案实现了完整的前后端协同筛选功能,同时保持了企业级应用的安全性和用户体验标准。筛选参数会通过API请求发送到后端,后端使用Django-filter进行高效的数据过滤。

posted on 2025-08-04 22:22  GoGrid  阅读(11)  评论(0)    收藏  举报

导航