Files
zhuzi-admin/src/views/users/UserPage.vue
2025-09-15 23:55:27 +08:00

356 lines
8.5 KiB
Vue

<template>
<div class="user-page">
<div class="page-header">
<h1 class="page-title">用户管理</h1>
</div>
<div class="page-content">
<!-- 搜索和筛选栏 -->
<div class="toolbar">
<div class="search-section">
<a-input-search
v-model:value="searchKeyword"
placeholder="搜索学生姓名或家长手机号"
style="width: 300px"
@search="handleSearch"
/>
<a-select
v-model:value="filterSchool"
placeholder="选择学校"
style="width: 150px; margin-left: 8px"
allow-clear
show-search
@change="handleSearch"
>
<a-select-option v-for="school in schoolOptions" :key="school.id" :value="school.id">
{{ school.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filterGrade"
placeholder="年级"
style="width: 100px; margin-left: 8px"
allow-clear
@change="handleSearch"
>
<a-select-option v-for="grade in gradeOptions" :key="grade.id" :value="grade.id">
{{ grade.name }}
</a-select-option>
</a-select>
<a-select
v-model:value="filterClass"
placeholder="班级"
style="width: 120px; margin-left: 8px"
allow-clear
@change="handleSearch"
>
<a-select-option v-for="cls in classOptions" :key="cls.id" :value="cls.id">
{{ cls.name }}
</a-select-option>
</a-select>
</div>
<div class="action-section">
<a-button @click="handleRefresh">
<ReloadOutlined/>
刷新
</a-button>
</div>
</div>
<!-- 用户列表 -->
<UserList
:loading="loading"
:data-source="users"
:pagination="pagination"
@view-detail="handleViewDetail"
@unbind="handleUnbind"
@disable="handleDisable"
@enable="handleEnable"
@page-change="handlePageChange"
/>
</div>
<!-- 用户详情弹窗 -->
<UserDetailModal
v-model:visible="detailVisible"
:user="currentUser"
/>
</div>
</template>
<script setup lang="ts">
import {ref, reactive, onMounted} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ReloadOutlined} from '@ant-design/icons-vue';
import {useRequest} from 'alova/client';
import {getUserList, unbindParentStudent, disableUser, enableUser} from '@/apis/users';
import {getSchoolList} from '@/apis/schools';
import type {AppUser, UserQueryParams} from '@/apis/users';
import type {School} from '@/apis/schools';
// 导入子组件
import UserList from './components/UserList.vue';
import UserDetailModal from './components/UserDetailModal.vue';
// 搜索筛选参数
const searchKeyword = ref('');
const filterSchool = ref<string>();
const filterGrade = ref<string>();
const filterClass = ref<string>();
// 列表数据
const users = ref<AppUser[]>([]);
// 分页
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total: number) => `${total} 条记录`
});
// 详情弹窗
const detailVisible = ref(false);
const currentUser = ref<AppUser>();
// 筛选选项
const schoolOptions = ref<School[]>([]);
const gradeOptions = ref<any[]>([]);
const classOptions = ref<any[]>([]);
// 获取用户列表
const {loading, send: fetchUsers} = useRequest((params: UserQueryParams) => getUserList(params), {
immediate: false
});
// 获取学校列表(用于筛选)
const {send: fetchSchools} = useRequest(() => getSchoolList(), {
immediate: false
});
// 构建查询参数
const buildQueryParams = (): UserQueryParams => {
return {
page: pagination.current,
pageSize: pagination.pageSize,
keyword: searchKeyword.value || undefined,
schoolId: filterSchool.value,
gradeId: filterGrade.value,
classId: filterClass.value
};
};
// 搜索处理
const handleSearch = () => {
pagination.current = 1;
loadUsers();
};
// 加载用户列表
const loadUsers = async () => {
try {
const params = buildQueryParams();
const result = await fetchUsers(params);
users.value = result.list;
pagination.total = result.total;
} catch (error: any) {
message.error(error.message || '获取用户列表失败');
}
};
// 加载学校数据
const loadSchools = async () => {
try {
const result = await fetchSchools();
schoolOptions.value = result.list;
// 提取年级和班级选项
const grades = new Map();
const classes = new Map();
result.list.forEach(school => {
if (school.grades) {
school.grades.forEach(grade => {
grades.set(grade.id, {
id: grade.id,
name: grade.name,
schoolId: grade.schoolId
});
if (grade.classes) {
grade.classes.forEach(cls => {
classes.set(cls.id, {
id: cls.id,
name: cls.name,
gradeId: cls.gradeId,
schoolId: cls.schoolId
});
});
}
});
}
});
gradeOptions.value = Array.from(grades.values());
classOptions.value = Array.from(classes.values());
} catch (error: any) {
message.error(error.message || '获取学校数据失败');
}
};
// 查看详情
const handleViewDetail = (user: AppUser) => {
currentUser.value = user;
detailVisible.value = true;
};
// 解绑用户
const handleUnbind = (user: AppUser) => {
Modal.confirm({
title: '确认解绑',
content: `确定要解绑家长"${user.nickname || user.phone}"与学生"${user.studentName}"的关系吗?`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
await unbindParentStudent(user.id);
message.success('解绑成功');
loadUsers();
} catch (error: any) {
message.error(error.message || '解绑失败');
}
}
});
};
// 禁用用户
const handleDisable = (user: AppUser) => {
Modal.confirm({
title: '确认禁用',
content: `确定要禁用用户"${user.nickname || user.phone}"吗?禁用后该用户将无法正常使用小程序。`,
okText: '确定',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
await disableUser(user.id);
message.success('禁用成功');
loadUsers();
} catch (error: any) {
message.error(error.message || '禁用失败');
}
}
});
};
// 启用用户
const handleEnable = (user: AppUser) => {
Modal.confirm({
title: '确认启用',
content: `确定要启用用户"${user.nickname || user.phone}"吗?`,
okText: '确定',
cancelText: '取消',
async onOk() {
try {
await enableUser(user.id);
message.success('启用成功');
loadUsers();
} catch (error: any) {
message.error(error.message || '启用失败');
}
}
});
};
// 刷新数据
const handleRefresh = () => {
loadUsers();
};
// 分页改变
const handlePageChange = (page: number, pageSize: number) => {
pagination.current = page;
pagination.pageSize = pageSize;
loadUsers();
};
// 初始化
onMounted(() => {
loadUsers();
loadSchools();
});
</script>
<style scoped lang="scss">
.user-page {
.page-header {
background: #fff;
padding: 12px 24px;
border-radius: 8px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #262626;
}
}
.page-content {
// margin-top: 24px; // 移除多余的margin
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.search-section {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px 0;
}
.action-section {
display: flex;
align-items: center;
gap: 8px;
}
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.toolbar {
flex-direction: column;
align-items: stretch;
gap: 16px;
.search-section {
justify-content: flex-start;
}
.action-section {
justify-content: flex-end;
}
}
}
</style>