356 lines
8.5 KiB
Vue
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>
|