This commit is contained in:
李旭明 committed 2025-07-24 10:20:17 +08:00
commit 75201841cd
243 files changed
+89463 -2524

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
import request from '@/utils/request';
import { ClassMsgForm, ClassMsgQuery, ClassMsgVO } from '../types';
import { AxiosPromise } from 'axios';
// 查询班级信息-新版列表
export function listClassMsg(query: ClassMsgQuery) {
return request({
url: '/classMsg/classMsg/list',
method: 'get',
params: query
});
}
// 查询班级信息-新版详细
export function getClassMsg(id: string | number) {
return request({
url: `/classMsg/classMsg/${id}`,
method: 'get'
});
}
// 新增班级信息-新版
export function addClassMsg(data: ClassMsgForm) {
return request({
url: '/classMsg/classMsg',
method: 'post',
data: data
});
}
// 修改班级信息-新版
export function updateClassMsg(data: ClassMsgForm) {
return request({
url: '/classMsg/classMsg',
method: 'put',
data: data
});
}
// 删除班级信息-新版
export function delClassMsg(id: string | number) {
return request({
url: `/classMsg/classMsg/${id}`,
method: 'delete'
});
}
+336
View File
@@ -0,0 +1,336 @@
export interface ClassMsgVO {
/**
* 主键ID(雪花ID)
*/
id: string | number;
/**
* 学院代码
*/
collegeId: string | number;
/**
* 专业代码
*/
majorId: string | number;
/**
* 班级名称
*/
className: string;
/**
* 班级类型:1-中级工 2-高级工 3-预备技师 4-技师 5-中高职一体
*/
classType: string;
/**
* 年级
*/
grade: string;
/**
* 入学年份
*/
enrollmentYear: number;
/**
* 毕业年份
*/
graduationYear: number;
/**
* 班级状态:1-正常,2-暂停,3-毕业,4-解散
*/
classStatus: string;
/**
* 备注信息
*/
remark: string;
}
export interface ClassMsgForm extends BaseEntity {
/**
* 主键ID(雪花ID)
*/
id?: string | number;
/**
* 学院代码
*/
collegeId?: string | number;
/**
* 学院名称
*/
collegeName?: string;
/**
* 专业代码
*/
majorId?: string | number;
/**
* 专业名称
*/
majorName?: string;
/**
* 班级代码
*/
classCode?: string;
/**
* 班级名称
*/
className?: string;
/**
* 班级类型:1-中级工 2-高级工 3-预备技师 4-技师 5-中高职一体
*/
classType?: string;
/**
* 年级
*/
grade?: string;
/**
* 入学年份
*/
enrollmentYear?: number;
/**
* 毕业年份
*/
graduationYear?: number;
/**
* 培养方式:全日制,在职,函授,夜大等
*/
trainingType?: string;
/**
* 学位类型:学士,硕士,博士,专科
*/
degreeType?: string;
/**
* 辅导员ID
*/
counselorId?: string | number;
/**
* 辅导员姓名
*/
counselorName?: string;
/**
* 班主任ID
*/
classTeacherId?: string | number;
/**
* 班主任姓名
*/
classTeacherName?: string;
/**
* 班级导师ID
*/
classAdvisorId?: string | number;
/**
* 班级导师姓名
*/
classAdvisorName?: string;
/**
* 班长ID
*/
classMonitorId?: string | number;
/**
* 班长姓名
*/
classMonitorName?: string;
/**
* 副班长ID
*/
deputyMonitorId?: string | number;
/**
* 副班长姓名
*/
deputyMonitorName?: string;
/**
* 班级状态:1-正常,2-暂停,3-毕业,4-解散
*/
classStatus?: string;
/**
* 是否已毕业:0-未毕业,1-已毕业
*/
isGraduation?: number;
/**
* 毕业日期
*/
graduationDate?: string;
/**
* 计划容量
*/
plannedCapacity?: number;
/**
* 当前学生人数
*/
currentStudentCount?: number;
/**
* 最大学生人数
*/
maxStudentCount?: number;
/**
* 男生人数
*/
maleCount?: number;
/**
* 女生人数
*/
femaleCount?: number;
/**
* 在籍人数
*/
activeCount?: number;
/**
* 转入人数
*/
transferInCount?: number;
/**
* 转出人数
*/
transferOutCount?: number;
/**
* 退学人数
*/
dropoutCount?: number;
/**
* 毕业人数
*/
graduateCount?: number;
/**
* 班级办公室位置
*/
officeLocation?: string;
/**
* 班级教室位置
*/
classLocation?: string;
/**
* 联系电话
*/
contactPhone?: string;
/**
* QQ群号
*/
qqGroup?: string;
/**
* 微信群
*/
wechatGroup?: string;
/**
* 班级邮箱
*/
email?: string;
/**
* 课程安排(JSON格式)
*/
classSchedule?: string;
/**
* 班级文化建设
*/
classCulture?: string;
/**
* 班级规章制度
*/
classRules?: string;
/**
* 班级描述
*/
classDescription?: string;
/**
* 最后统计更新时间
*/
lastCountUpdate?: string;
/**
* 备注信息
*/
remark?: string;
}
export interface ClassMsgQuery extends PageQuery {
/**
* 学院代码
*/
collegeId?: string | number;
/**
* 专业代码
*/
majorId?: string | number;
/**
* 班级名称
*/
className?: string;
/**
* 班级类型:1-中级工 2-高级工 3-预备技师 4-技师 5-中高职一体
*/
classType?: string;
/**
* 年级
*/
grade?: string;
/**
* 班级状态:1-正常,2-暂停,3-毕业,4-解散
*/
classStatus?: string;
/**
* 日期范围参数
*/
params?: any;
}
+26
View File
@@ -0,0 +1,26 @@
import request from '@/utils/request';
import { SysDeptVo } from './types';
// 获取学院列表
export function listColleges() {
return request({
url: '/xg/college/list',
method: 'get'
});
}
// 获取学院和专业树形结构
export function collegeTree() {
return request({
url: '/xg/college/tree',
method: 'get'
});
}
// 根据学院ID获取专业列表
export function listMajorsByCollegeId(collegeId: string | number) {
return request({
url: `/xg/college/majors/${collegeId}`,
method: 'get'
});
}
+160
View File
@@ -0,0 +1,160 @@
/** 部门视图对象 */
export interface SysDeptVo {
/** 部门id */
deptId: number;
/** 父部门id */
parentId: number;
/** 祖级列表 */
ancestors: string;
/** 部门名称 */
deptName: string;
/** 显示顺序 */
orderNum: number;
/** 负责人 */
leader: string;
/** 联系电话 */
phone: string;
/** 邮箱 */
email: string;
/** 部门状态:0正常,1停用 */
status: string;
/** 删除标志 */
delFlag: string;
/** 创建者 */
createBy: number;
/** 创建时间 */
createTime: string;
/** 更新者 */
updateBy: number;
/** 更新时间 */
updateTime: string;
/** 子部门 */
children: SysDeptVo[];
}
/** 班级查询参数 */
export interface ClassMsgQuery {
/** 学院代码 */
collegeId?: number | string;
/** 专业代码 */
majorId?: number | string;
/** 班级名称 */
className?: string;
/** 班级类型 */
classType?: string | number;
/** 年级 */
grade?: string;
/** 班级状态 */
classStatus?: string | number;
/** 分页大小 */
pageSize: number;
/** 当前页码 */
pageNum: number;
/** 其他参数 */
params: Record<string, any>;
}
/** 班级详细信息 */
export interface ClassMsgVO {
/** 主键ID */
id: string | number;
/** 学院代码 */
collegeId?: string | number;
/** 学院名称 */
collegeName?: string;
/** 专业代码 */
majorId?: string | number;
/** 专业名称 */
majorName?: string;
/** 班级代码 */
classCode?: string;
/** 班级名称 */
className?: string;
/** 班级类型 */
classType?: string | number;
/** 年级 */
grade?: string;
/** 入学年份 */
enrollmentYear?: string;
/** 毕业年份 */
graduationYear?: string;
/** 培养类型 */
trainingType?: string;
/** 学位类型 */
degreeType?: string;
/** 辅导员ID */
counselorId?: string | number;
/** 辅导员姓名 */
counselorName?: string;
/** 班主任ID */
classTeacherId?: string | number;
/** 班主任姓名 */
classTeacherName?: string;
/** 班级导师ID */
classAdvisorId?: string | number;
/** 班级导师姓名 */
classAdvisorName?: string;
/** 班长ID */
classMonitorId?: string | number;
/** 班长姓名 */
classMonitorName?: string;
/** 副班长ID */
deputyMonitorId?: string | number;
/** 副班长姓名 */
deputyMonitorName?: string;
/** 班级状态 */
classStatus?: string | number;
/** 是否已毕业 */
isGraduation?: string | number;
/** 毕业日期 */
graduationDate?: string;
/** 计划容量 */
plannedCapacity?: number;
/** 当前学生人数 */
currentStudentCount?: number;
/** 最大学生人数 */
maxStudentCount?: number;
/** 男生人数 */
maleCount?: number;
/** 女生人数 */
femaleCount?: number;
/** 在籍人数 */
activeCount?: number;
/** 转入人数 */
transferInCount?: number;
/** 转出人数 */
transferOutCount?: number;
/** 退学人数 */
dropoutCount?: number;
/** 毕业人数 */
graduateCount?: number;
/** 办公室位置 */
officeLocation?: string;
/** 教室位置 */
classLocation?: string;
/** 联系电话 */
contactPhone?: string;
/** QQ群号 */
qqGroup?: string;
/** 微信群 */
wechatGroup?: string;
/** 班级邮箱 */
email?: string;
/** 课程安排 */
classSchedule?: string;
/** 班级文化建设 */
classCulture?: string;
/** 班级规章制度 */
classRules?: string;
/** 班级描述 */
classDescription?: string;
/** 统计更新时间 */
lastCountUpdate?: string;
/** 备注 */
remark?: string;
}
/** 班级表单数据 */
export interface ClassMsgForm extends ClassMsgVO {
// 表单额外字段
}
-126
View File
@@ -1,126 +0,0 @@
export interface DormBuildingVO {
/**
* 主键ID
*/
id: string | number;
/**
* 宿舍楼编码
*/
buildingCode: string;
/**
* 宿舍楼名称
*/
buildingName: string;
/**
* 所属宿舍区ID
*/
areaId: string | number;
/**
* 所属校区ID
*/
campusId: string | number;
/**
* 宿舍楼类型(1男生楼 2女生楼 3混合楼)
*/
buildingType: string;
/**
* 宿舍楼描述
*/
buildingDescription: string;
/**
* 排序号
*/
sortOrder: number;
}
export interface DormBuildingForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 宿舍楼编码
*/
buildingCode?: string;
/**
* 宿舍楼名称
*/
buildingName?: string;
/**
* 所属宿舍区ID
*/
areaId?: string | number;
/**
* 所属校区ID
*/
campusId?: string | number;
/**
* 宿舍楼类型(1男生楼 2女生楼 3混合楼)
*/
buildingType?: string;
/**
* 宿舍楼描述
*/
buildingDescription?: string;
/**
* 排序号
*/
sortOrder?: number;
}
export interface DormBuildingQuery extends PageQuery {
/**
* 宿舍楼编码
*/
buildingCode?: string;
/**
* 宿舍楼名称
*/
buildingName?: string;
/**
* 所属宿舍区ID
*/
areaId?: string | number;
/**
* 所属校区ID
*/
campusId?: string | number;
/**
* 宿舍楼类型(1男生楼 2女生楼 3混合楼)
*/
buildingType?: string;
/**
* 排序号
*/
sortOrder?: number;
/**
* 日期范围参数
*/
params?: any;
}
+39 -10
View File
@@ -1,6 +1,9 @@
import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { ClassInfoVO, ClassInfoQuery, ClassInfoForm, OptionData } from './classInfo/types'
import { getToken } from '@/utils/auth'
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types'
import { treeselect as deptTreeselect, listDeptExcludeChild } from '@/api/system/dept'
// 通用类型定义
interface TableDataInfo<T> {
@@ -256,14 +259,34 @@ export const getCollegeOptions = (): AxiosPromise<R<any[]>> => {
}
/**
* 获取专业选项
* 从系统部门获取学院选项(二级学院)
*/
export const getMajorOptions = (collegeCode?: string): AxiosPromise<R<any[]>> => {
export const getDeptCollegeOptions = (): AxiosPromise<DeptTreeVO[]> => {
console.log('直接调用部门树API,避免treeselect参数问题');
return request({
url: '/xg/studentInfo/options/majors',
method: 'get',
params: { collegeCode }
})
url: '/system/dept/treeselect',
method: 'get'
});
}
/**
* 从系统部门获取专业选项(学院的下级部门)
*/
export const getDeptMajorOptions = (deptId: string | number): AxiosPromise<DeptVO[]> => {
// 确保deptId是有效的数字ID
if (!deptId || isNaN(Number(deptId))) {
console.error('Invalid deptId:', deptId);
return Promise.reject(new Error('无效的部门ID'));
}
// 直接调用接口,不使用封装的系统API
const numericDeptId = Number(deptId);
console.log('直接调用部门API,使用数字ID:', numericDeptId);
return request({
url: '/system/dept/list/exclude/' + numericDeptId,
method: 'get'
});
}
/**
@@ -277,11 +300,17 @@ export const getCounselorOptions = (): AxiosPromise<R<any[]>> => {
}
/**
* 获取班主任选项
* 获取班主任选项列表
*/
export const getClassTeacherOptions = (): AxiosPromise<R<any[]>> => {
export const getClassTeacherOptions = (): AxiosPromise<R<OptionData[]>> => {
console.log('调用班主任API: /xg/classInfo/teacherOptions')
return request({
url: '/xg/studentInfo/options/classTeachers',
method: 'get'
url: '/xg/classInfo/teacherOptions',
method: 'get',
headers: {
// 确保请求带有认证信息
'Authorization': 'Bearer ' + getToken(),
'clientid': import.meta.env.VITE_APP_CLIENT_ID
}
})
}
+9 -2
View File
@@ -1,5 +1,6 @@
import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { getToken } from '@/utils/auth'
// 学生信息数据类型
export interface StudentInfo {
@@ -398,9 +399,15 @@ export function getCounselorOptions(): AxiosPromise<OptionData[]> {
// 获取班主任选项
export function getClassTeacherOptions(): AxiosPromise<OptionData[]> {
console.log('调用班主任API: /xg/classInfo/teacherOptions')
return request({
url: '/xg/studentInfo/options/classTeachers',
method: 'get'
url: '/xg/classInfo/teacherOptions',
method: 'get',
headers: {
// 确保请求带有认证信息
'Authorization': 'Bearer ' + getToken(),
'clientid': import.meta.env.VITE_APP_CLIENT_ID
}
})
}
@@ -0,0 +1,92 @@
import request from '@/utils/request';
/**
* 班级班主任辅导员关联对象
*/
interface ClassTeacherCounselorRelBo {
id?: number | string;
classId?: number | string;
teacherId?: number | string;
teacherName?: string;
counselorId?: number | string;
counselorName?: string;
relationType?: number; // 1:班主任 2:辅导员
remark?: string;
}
/**
* 添加班级班主任辅导员关联
* @param data 班级班主任辅导员关联信息
* @returns 返回添加结果
*/
export function addClassTeacherCounselorRel(data: ClassTeacherCounselorRelBo) {
return request({
url: '/teacherCounselorRel/classTeacherCounselorRel',
method: 'post',
data: data
});
}
/**
* 修改班级班主任辅导员关联
* @param data 班级班主任辅导员关联信息
* @returns 返回修改结果
*/
export function updateClassTeacherCounselorRel(data: ClassTeacherCounselorRelBo) {
return request({
url: '/teacherCounselorRel/classTeacherCounselorRel',
method: 'put',
data: data
});
}
/**
* 查询班级班主任辅导员关联列表
* @param query 查询参数
* @returns 返回班级班主任辅导员关联列表
*/
export function listClassTeacherCounselorRel(query: any) {
return request({
url: '/teacherCounselorRel/classTeacherCounselorRel/list',
method: 'get',
params: query
});
}
/**
* 获取班级班主任辅导员关联详情
* @param id 关联ID
* @returns 返回班级班主任辅导员关联详情
*/
export function getClassTeacherCounselorRel(id: number | string) {
return request({
url: `/teacherCounselorRel/classTeacherCounselorRel/${id}`,
method: 'get'
});
}
/**
* 删除班级班主任辅导员关联
* @param ids 关联ID数组
* @returns 返回删除结果
*/
export function delClassTeacherCounselorRel(ids: number | string | (number | string)[]) {
return request({
url: `/teacherCounselorRel/classTeacherCounselorRel/${ids instanceof Array ? ids.join(',') : ids}`,
method: 'delete'
});
}
/**
* 导出班级班主任辅导员关联
* @param query 查询参数
* @returns 返回导出结果
*/
export function exportClassTeacherCounselorRel(query: any) {
return request({
url: '/teacherCounselorRel/classTeacherCounselorRel/export',
method: 'post',
params: query,
responseType: 'blob'
});
}
+3
View File
@@ -14,5 +14,8 @@ import request from '@/utils/request';
// export * from './scholarship' // 奖助管理
// export * from './activity' // 活动管理
// 导出学工用户模块API
export * from './sysUser'
// 临时导出一个空对象,防止编译错误
export default {};
+29
View File
@@ -0,0 +1,29 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
/**
* 用户信息对象
*/
export interface XgSysUser {
userId: number | string;
userName: string;
nickName: string;
email: string;
phonenumber: string;
sex: string;
status: string;
createTime: string;
updateTime: string;
deptId: number;
deptName: string;
}
/**
* 查询有效用户列表
*/
export function getActiveUserList(): AxiosPromise<XgSysUser[]> {
return request({
url: '/xg/sysUser/activeList',
method: 'get'
});
}
+63
View File
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormBedVO, DormBedForm, DormBedQuery } from '@/api/xgss/dormBed/types';
/**
* 查询宿舍床位信息列表
* @param query
* @returns {*}
*/
export const listDormBed = (query?: DormBedQuery): AxiosPromise<DormBedVO[]> => {
return request({
url: '/dormbed/dormBed/list',
method: 'get',
params: query
});
};
/**
* 查询宿舍床位信息详细
* @param id
*/
export const getDormBed = (id: string | number): AxiosPromise<DormBedVO> => {
return request({
url: '/dormbed/dormBed/' + id,
method: 'get'
});
};
/**
* 新增宿舍床位信息
* @param data
*/
export const addDormBed = (data: DormBedForm) => {
return request({
url: '/dormbed/dormBed',
method: 'post',
data: data
});
};
/**
* 修改宿舍床位信息
* @param data
*/
export const updateDormBed = (data: DormBedForm) => {
return request({
url: '/dormbed/dormBed',
method: 'put',
data: data
});
};
/**
* 删除宿舍床位信息
* @param id
*/
export const delDormBed = (id: string | number | Array<string | number>) => {
return request({
url: '/dormbed/dormBed/' + id,
method: 'delete'
});
};
+66
View File
@@ -0,0 +1,66 @@
/**
* 宿舍床位信息查询参数
*/
export interface DormBedQuery {
/** 床位编号 */
bedCode?: string;
/** 床位名称 */
bedName?: string;
/** 所属房间ID */
roomId?: string | number;
/** 床位状态(0=空闲,1=已分配,2=维修中) */
bedStatus?: string;
/** 床位类型(1=下铺,2=上铺,3=中铺,4=独立床) */
bedType?: string;
}
/**
* 宿舍床位信息对象
*/
export interface DormBedVO {
/** 床位ID */
id: string | number;
/** 床位编号 */
bedCode: string;
/** 床位名称 */
bedName: string;
/** 所属房间ID */
roomId: string | number;
/** 床位状态(0=空闲,1=已分配,2=维修中) */
bedStatus: string;
/** 床位类型(1=下铺,2=上铺,3=中铺,4=独立床) */
bedType: string;
/** 备注 */
remark?: string;
/** 创建者 */
createBy: number;
/** 创建时间 */
createTime: Date;
/** 更新者 */
updateBy: number;
/** 更新时间 */
updateTime: Date;
}
/**
* 宿舍床位信息表单对象
*/
export interface DormBedForm {
/** 床位ID */
id: string | number;
/** 床位编号 */
bedCode: string;
/** 床位名称 */
bedName: string;
/** 所属房间ID */
roomId: string | number;
/** 床位状态(0=空闲,1=已分配,2=维修中) */
bedStatus: string;
/** 床位类型(1=下铺,2=上铺,3=中铺,4=独立床) */
bedType: string;
/** 备注 */
remark?: string;
}
+63
View File
@@ -0,0 +1,63 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormRoomVO, DormRoomForm, DormRoomQuery } from '@/api/xgss/dormRoom/types';
/**
* 查询宿舍房间信息列表
* @param query
* @returns {*}
*/
export const listDormRoom = (query?: DormRoomQuery): AxiosPromise<DormRoomVO[]> => {
return request({
url: '/xgss/dormRoom/list',
method: 'get',
params: query
});
};
/**
* 查询宿舍房间信息详细
* @param id
*/
export const getDormRoom = (id: string | number): AxiosPromise<DormRoomVO> => {
return request({
url: '/xgss/dormRoom/' + id,
method: 'get'
});
};
/**
* 新增宿舍房间信息
* @param data
*/
export const addDormRoom = (data: DormRoomForm) => {
return request({
url: '/xgss/dormRoom',
method: 'post',
data: data
});
};
/**
* 修改宿舍房间信息
* @param data
*/
export const updateDormRoom = (data: DormRoomForm) => {
return request({
url: '/xgss/dormRoom',
method: 'put',
data: data
});
};
/**
* 删除宿舍房间信息
* @param id
*/
export const delDormRoom = (id: string | number | Array<string | number>) => {
return request({
url: '/xgss/dormRoom/' + id,
method: 'delete'
});
};
+176
View File
@@ -0,0 +1,176 @@
export interface DormRoomVO {
/**
* 主键ID
*/
id: string | number;
/**
* 房间编号
*/
roomCode: string;
/**
* 房间名称
*/
roomName: string;
/**
* 所属宿舍楼ID
*/
buildingId: string | number;
/**
* 楼层
*/
floorNum: number;
/**
* 房间类型(1标准间 2 单人间 3 套间)
*/
roomType: string;
/**
* 实际床位数
*/
actualBeds: number;
/**
* 当前床位数
*/
currentBeds?: number;
/**
* 房间面积(平方米)
*/
roomArea: number;
/**
* 房间状态(0可用 1维修中 2停用)
*/
roomStatus: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted: string;
/**
* 备注
*/
remark: string;
}
export interface DormRoomForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 房间编号
*/
roomCode?: string;
/**
* 房间名称
*/
roomName?: string;
/**
* 所属宿舍楼ID
*/
buildingId?: string | number;
/**
* 楼层
*/
floorNum?: number;
/**
* 房间类型(1标准间 2 单人间 3 套间)
*/
roomType?: string;
/**
* 实际床位数
*/
actualBeds?: number;
/**
* 房间面积(平方米)
*/
roomArea?: number;
/**
* 房间状态(0可用 1维修中 2停用)
*/
roomStatus?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 备注
*/
remark?: string;
}
export interface DormRoomQuery extends PageQuery {
/**
* 房间编号
*/
roomCode?: string;
/**
* 房间名称
*/
roomName?: string;
/**
* 所属宿舍楼ID
*/
buildingId?: string | number;
/**
* 楼层
*/
floorNum?: number;
/**
* 房间类型(1标准间 2 单人间 3 套间)
*/
roomType?: string;
/**
* 实际床位数
*/
actualBeds?: number;
/**
* 房间面积(平方米)
*/
roomArea?: number;
/**
* 房间状态(0可用 1维修中 2停用)
*/
roomStatus?: string;
/**
* 删除标志(0代表存在 1代表删除)
*/
deleted?: string;
/**
* 日期范围参数
*/
params?: any;
}
@@ -61,3 +61,37 @@ export const delDormManager = (id: string | number | Array<string | number>) =>
method: 'delete'
});
};
/**
* 分配管理员数据范围
* @param managerId 管理员ID
* @param authType 授权类型(1全校级 2校区级 3宿舍区级 4楼栋级)
* @param authSourceIds 授权来源ID列表
*/
export const assignScope = (managerId: string | number, authType: number, authSourceIds: (string | number)[]) => {
return request({
url: '/glysqgx/dormManagerSource/assignScope',
method: 'post',
params: {
managerId,
authType
},
data: authSourceIds
});
};
/**
* 获取管理员授权范围名称
* @param managerId 管理员ID
* @param managerType 管理员类型
*/
export const getManagerScopeNames = (managerId: string | number, managerType: number) => {
return request({
url: '/glysqgx/dormManagerSource/getScopeNames',
method: 'get',
params: {
managerId,
managerType
}
});
};
@@ -49,6 +49,11 @@ export interface DormManagerVO {
*/
remark: string;
/**
* 授权范围名称(逗号分隔的字符串)
*/
scopeNames?: string;
}
export interface DormManagerForm extends BaseEntity {
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormBuildingVO, DormBuildingForm, DormBuildingQuery } from '@/api/sslxx/dormBuilding/types';
import { DormBuildingVO, DormBuildingForm, DormBuildingQuery, BuildingTreeSelectVo } from '@/api/sslxx/dormBuilding/types';
/**
* 查询宿舍楼信息列表
@@ -83,3 +83,14 @@ export const listAreaTreeData = (campusId?: string | number): AxiosPromise<any>
params: { campusId }
});
};
/**
* 获取宿舍楼树形结构数据(校区-宿舍区-宿舍楼三级结构)
* 校区和宿舍区节点不可选,只有宿舍楼节点可以选择
*/
export const getBuildingTree = (): AxiosPromise<BuildingTreeSelectVo[]> => {
return request({
url: '/sslxx/dormBuilding/buildingTree',
method: 'get'
});
};
@@ -0,0 +1,50 @@
export interface DormBuildingVO {
id: string | number;
buildingCode: string;
buildingName: string;
areaId: string | number;
campusId: string | number;
buildingType: string;
buildingDescription: string;
sortOrder: number;
createTime: string;
createBy: string;
updateTime: string;
updateBy: string;
remark: string;
areaName?: string;
campusName?: string;
floors: number;
}
export interface DormBuildingForm extends Omit<DormBuildingVO, 'createTime' | 'createBy' | 'updateTime' | 'updateBy' | 'areaName' | 'campusName'> {
}
export interface DormBuildingQuery {
buildingCode?: string;
buildingName?: string;
areaId?: string | number;
campusId?: string | number;
buildingType?: string;
floors?: number;
params?: Record<string, any>;
}
/**
* 宿舍楼树形结构节点
*/
export interface BuildingTreeSelectVo {
id: number;
label: string;
nodeType: 'campus' | 'area' | 'building';
disabled: boolean;
children?: BuildingTreeSelectVo[];
createBy?: number;
createTime?: string;
updateBy?: number;
floors?: number;
updateTime?: string;
}
@@ -1,6 +1,6 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { DormAreaVO, DormAreaForm, DormAreaQuery } from '@/api/ssqxx/dormArea/types';
import { DormAreaVO, DormAreaForm, DormAreaQuery, TreeSelectVo } from '@/api/ssqxx/dormArea/types';
/**
* 查询宿舍区信息列表
@@ -61,3 +61,14 @@ export const delDormArea = (id: string | number | Array<string | number>) => {
method: 'delete'
});
};
/**
* 获取宿舍区树形结构数据
* 以校区为根节点,宿舍区为子节点的树形结构
*/
export const getAreaTree = (): AxiosPromise<TreeSelectVo[]> => {
return request({
url: '/ssqxx/dormArea/tree',
method: 'get'
});
};
@@ -87,5 +87,21 @@ export interface DormAreaQuery extends PageQuery {
params?: any;
}
/**
* 树形选择数据类型
*/
export interface TreeSelectVo {
/** 节点ID */
id: number;
/** 节点名称 */
label: string;
/** 节点类型(campus:校区, area:宿舍区) */
type: string;
/** 是否禁用选择 */
disabled: boolean;
/** 子节点 */
children?: TreeSelectVo[];
}
+264
View File
@@ -0,0 +1,264 @@
import request from '@/utils/request';
import { DormQuery, RoomForm, BedForm, DormTreeNode, RoomInfo, BedInfo } from './types';
// API前缀
const baseUrl = '/xsssfjgl';
/**
* 获取宿舍楼树形结构
*/
export function getDormTree() {
return request({
url: '/xgss/dormBuildingTree/list',
method: 'get'
});
}
/**
* 获取房间列表
* @param query 查询条件
* @returns 房间列表
*/
export function listRooms(query: DormQuery) {
return request({
url: baseUrl + '/room/list',
method: 'get',
params: query
});
}
/**
* 获取房间详情
* @param roomId 房间ID
* @returns 房间详情
*/
export function getRoom(roomId: string | number) {
return request({
url: baseUrl + '/room/' + roomId,
method: 'get'
});
}
/**
* 新增房间
* @param data 房间信息
* @returns 结果
*/
export function addRoom(data: RoomForm) {
return request({
url: baseUrl + '/room',
method: 'post',
data: data
});
}
/**
* 修改房间
* @param data 房间信息
* @returns 结果
*/
export function updateRoom(data: RoomForm) {
return request({
url: baseUrl + '/room',
method: 'put',
data: data
});
}
/**
* 删除房间
* @param roomIds 房间ID字符串
* @returns 结果
*/
export function delRoom(roomIds: string | number) {
return request({
url: baseUrl + '/room/' + roomIds,
method: 'delete'
});
}
/**
* 获取床位列表
* @param roomId 房间ID
* @returns 床位列表
*/
export function listBeds(roomId: string | number) {
return request({
url: baseUrl + '/bed/list',
method: 'get',
params: { roomId }
});
}
/**
* 新增床位
* @param data 床位信息
* @returns 结果
*/
export function addBed(data: BedForm) {
return request({
url: baseUrl + '/bed',
method: 'post',
data: data
});
}
/**
* 修改床位
* @param data 床位信息
* @returns 结果
*/
export function updateBed(data: BedForm) {
return request({
url: baseUrl + '/bed',
method: 'put',
data: data
});
}
/**
* 删除床位
* @param bedIds 床位ID字符串
* @returns 结果
*/
export function delBed(bedIds: string | number) {
return request({
url: baseUrl + '/bed/' + bedIds,
method: 'delete'
});
}
// 模拟数据 - 在API接口完成前使用
// 生成宿舍树形结构的模拟数据
export function getMockDormTree(): { data: DormTreeNode[] } {
const mockData: DormTreeNode[] = [
{
id: 1,
label: '东校区',
type: 'campus',
disabled: true,
children: [
{
id: 11,
label: '东区宿舍',
type: 'area',
disabled: true,
children: [
{
id: 111,
label: '东区1号楼',
type: 'building',
buildingFloors: 6,
gender: '1' // 男生楼
},
{
id: 112,
label: '东区2号楼',
type: 'building',
buildingFloors: 8,
gender: '2' // 女生楼
}
]
}
]
},
{
id: 2,
label: '西校区',
type: 'campus',
disabled: true,
children: [
{
id: 21,
label: '西区宿舍',
type: 'area',
disabled: true,
children: [
{
id: 211,
label: '西区1号楼',
type: 'building',
buildingFloors: 7,
gender: '2' // 女生楼
},
{
id: 212,
label: '西区2号楼',
type: 'building',
buildingFloors: 5,
gender: '1' // 男生楼
}
]
}
]
}
];
return { data: mockData };
}
// 生成房间数据的模拟数据
export function getMockRooms(buildingId: string | number, floorNumber?: number): { rows: RoomInfo[], total: number } {
const rooms: RoomInfo[] = [];
// 根据buildingId判断楼层数
let floors = 6;
if (buildingId === 112) floors = 8;
else if (buildingId === 211) floors = 7;
else if (buildingId === 212) floors = 5;
// 如果指定了楼层,只生成该楼层的数据
const startFloor = floorNumber || 1;
const endFloor = floorNumber || floors;
for (let floor = startFloor; floor <= endFloor; floor++) {
for (let i = 1; i <= 10; i++) {
const roomNumber = `${floor}${i.toString().padStart(2, '0')}`;
rooms.push({
roomId: `${buildingId}${roomNumber}`,
roomName: roomNumber,
floorNumber: floor,
buildingId: buildingId,
capacity: 4,
used: Math.floor(Math.random() * 5),
roomType: '标准间',
status: Math.random() > 0.9 ? '1' : '0',
remark: '',
createBy: 'admin',
createTime: '2023-01-01 00:00:00',
updateBy: 'admin',
updateTime: '2023-01-01 00:00:00'
});
}
}
return {
rows: rooms.filter(room => !floorNumber || room.floorNumber === floorNumber),
total: rooms.length
};
}
// 生成床位数据的模拟数据
export function getMockBeds(roomId: string | number): { data: BedInfo[] } {
const beds: BedInfo[] = [];
const capacity = 4;
for (let i = 1; i <= capacity; i++) {
const status = Math.random() > 0.6 ? '1' : '0';
beds.push({
bedId: `${roomId}-${i}`,
bedName: `${i}号床`,
roomId: roomId,
status: status,
studentId: status === '1' ? 10000 + Math.floor(Math.random() * 5000) : undefined,
studentName: status === '1' ? `学生${Math.floor(Math.random() * 1000)}` : undefined,
remark: '',
createBy: 'admin',
createTime: '2023-01-01 00:00:00',
updateBy: 'admin',
updateTime: '2023-01-01 00:00:00'
});
}
return { data: beds };
}
+84
View File
@@ -0,0 +1,84 @@
// 基础字段类型
export interface BaseEntity {
createBy: number | string;
createTime: string;
updateBy: number | string;
updateTime: string;
}
// 分页查询参数
export interface PageQuery {
pageNum: number;
pageSize: number;
}
// 树形数据节点类型
export interface DormTreeNode {
id: string | number;
label: string;
children?: DormTreeNode[];
type: 'campus' | 'area' | 'building'; // 节点类型:校区、宿舍区、宿舍楼
disabled?: boolean; // 是否禁用选择
buildingFloors?: number; // 宿舍楼层数
gender?: string; // 宿舍楼性别: 1-男生楼, 2-女生楼
}
// 房间信息
export interface RoomInfo extends BaseEntity {
roomId: string | number;
roomCode: string;
roomName: string;
floorNum: number;
buildingId: string | number;
capacity: number;
used: number;
roomArea: number;
roomType: string;
status: string; // 0-正常 1-维修中 2-停用
remark: string;
}
// 床位信息
export interface BedInfo extends BaseEntity {
bedId: string | number;
bedName: string;
roomId: string | number;
status: string; // 0-空闲 1-已分配 2-维修中
studentId?: string | number;
studentName?: string;
remark: string;
}
// 查询参数
export interface DormQuery extends PageQuery {
buildingId?: string | number;
roomName?: string;
floorNum?: number;
status?: string;
}
// 房间表单
export interface RoomForm {
roomId?: string | number;
roomCode: string;
roomName: string;
floorNum: number;
buildingId: string | number;
capacity: number;
roomArea: number;
roomType: string;
status: string;
remark?: string;
}
// 床位表单
export interface BedForm {
id?: string | number;
bedId?: string | number;
bedCode: string;
bedName: string;
roomId: string | number;
bedType: string;
bedStatus: string;
remark?: string;
}
File renamed without changes.
+18 -1
View File
@@ -50,6 +50,23 @@ export const updateEnrollmentInfo = (data: EnrollmentInfoForm) => {
});
};
/**
* 更新招生信息状态并同步到流程管理
* @param id 招生信息ID(单个ID或ID数组)
* @param status 新状态
*/
export const updateEnrollmentStatus = (id: string | number | Array<string | number>, status: string) => {
const data = Array.isArray(id)
? { ids: id, status } // 批量更新
: { id, status }; // 单个更新
return request({
url: '/zsjy/enrollment/info/updateStatus',
method: 'post',
data
});
};
/**
* 删除招生报名信息
* @param id
@@ -121,4 +138,4 @@ export const queryMyEnrollmentStatus = (params: { idCard?: string, examNumber?:
method: 'get',
params: params
});
};
};
+104 -64
View File
@@ -295,7 +295,16 @@ export interface EnrollmentProcessQuery extends PageQuery {
// 查询录取流程状态列表
export const listEnrollmentProcess = (query: EnrollmentProcessQuery): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollmentProcess/list',
url: '/zsjy/enrollment/process/list',
method: 'get',
params: query
})
}
// 查询我的待审核列表
export const listMyAuditProcess = (query: EnrollmentProcessQuery): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollment/process/myAudit',
method: 'get',
params: query
})
@@ -304,7 +313,7 @@ export const listEnrollmentProcess = (query: EnrollmentProcessQuery): AxiosPromi
// 查询录取流程状态详细
export const getEnrollmentProcess = (id: string | number): AxiosPromise<EnrollmentProcessVO> => {
return request({
url: '/zsjy/enrollmentProcess/' + id,
url: '/zsjy/enrollment/process/' + id,
method: 'get'
})
}
@@ -312,16 +321,34 @@ export const getEnrollmentProcess = (id: string | number): AxiosPromise<Enrollme
// 新增录取流程状态
export const addEnrollmentProcess = (data: EnrollmentProcessForm) => {
return request({
url: '/zsjy/enrollmentProcess',
url: '/zsjy/enrollment/process',
method: 'post',
data: data
})
}
// 从报名信息创建流程
export const createProcessFromEnrollment = (enrollmentId: number) => {
return request({
url: '/zsjy/enrollment/process/createFromEnrollment',
method: 'post',
params: { enrollmentId }
})
}
// 同步流程状态
export const syncProcessStatus = (id: string | number) => {
return request({
url: '/zsjy/enrollment/process/syncStatus',
method: 'post',
params: { id }
})
}
// 修改录取流程状态
export const updateEnrollmentProcess = (data: EnrollmentProcessForm) => {
return request({
url: '/zsjy/enrollmentProcess',
url: '/zsjy/enrollment/process',
method: 'put',
data: data
})
@@ -330,7 +357,7 @@ export const updateEnrollmentProcess = (data: EnrollmentProcessForm) => {
// 删除录取流程状态
export const delEnrollmentProcess = (id: string | number | Array<string | number>) => {
return request({
url: '/zsjy/enrollmentProcess/' + id,
url: '/zsjy/enrollment/process/' + id,
method: 'delete'
})
}
@@ -338,117 +365,130 @@ export const delEnrollmentProcess = (id: string | number | Array<string | number
// 根据报名信息ID查询流程状态
export const getProcessByEnrollmentId = (enrollmentId: number): AxiosPromise<EnrollmentProcessVO> => {
return request({
url: '/zsjy/enrollmentProcess/enrollment/' + enrollmentId,
url: '/zsjy/enrollment/process/enrollment/' + enrollmentId,
method: 'get'
})
}
// 查询我的待处理流程
export const getPendingProcessList = (): AxiosPromise<EnrollmentProcessVO[]> => {
// 查询待处理的流程列表
export const getPendingProcessList = (query: EnrollmentProcessQuery): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollmentProcess/pending',
method: 'get'
})
}
// 根据流程状态查询流程列表
export const getProcessByStatus = (status: string): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollmentProcess/status/' + status,
method: 'get'
})
}
// 统计各状态的流程数量
export const getProcessStatistics = (): AxiosPromise<any[]> => {
return request({
url: '/zsjy/enrollmentProcess/statistics',
method: 'get'
})
}
// 根据关键词搜索流程
export const searchProcess = (keyword: string): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollmentProcess/search',
url: '/zsjy/enrollment/process/pending',
method: 'get',
params: { keyword }
params: query
})
}
// 根据状态查询流程列表
export const getProcessListByStatus = (status: string, query: EnrollmentProcessQuery): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollment/process/status/' + status,
method: 'get',
params: query
})
}
// 获取流程统计数据
export const getProcessStatistics = (): AxiosPromise<any> => {
return request({
url: '/zsjy/enrollment/process/statistics',
method: 'get'
})
}
// 搜索流程
export const searchProcess = (keyword: string, query: EnrollmentProcessQuery): AxiosPromise<EnrollmentProcessVO[]> => {
return request({
url: '/zsjy/enrollment/process/search',
method: 'get',
params: { keyword, ...query }
})
}
// 信息审核
export const infoAudit = (data: { id: number; auditResult: string; auditOpinion?: string }) => {
export const infoAudit = (data: {
processId: number;
auditResult: string;
auditOpinion?: string;
}) => {
return request({
url: '/zsjy/enrollmentProcess/infoAudit',
url: '/zsjy/enrollment/process/infoAudit',
method: 'post',
data: data
params: data
})
}
// 录取处理
export const admissionProcess = (data: {
id: number;
processResult: string;
processOpinion?: string;
noticeNumber?: string
export const admissionProcess = (data: {
processId: number;
admissionResult: string;
admissionOpinion?: string;
noticeNumber?: string;
}) => {
return request({
url: '/zsjy/enrollmentProcess/admissionProcess',
url: '/zsjy/enrollment/process/admissionProcess',
method: 'post',
data: data
params: data
})
}
// 缴费确认
export const confirmPayment = (id: number) => {
// 确认缴费
export const confirmPayment = (processId: number) => {
return request({
url: '/zsjy/enrollmentProcess/confirmPayment',
url: '/zsjy/enrollment/process/confirmPayment',
method: 'post',
data: { id }
params: { processId }
})
}
// 最终审核
export const finalAudit = (data: { id: number; auditResult: string; auditOpinion?: string }) => {
export const finalAudit = (data: {
processId: number;
finalResult: string;
finalOpinion?: string;
}) => {
return request({
url: '/zsjy/enrollmentProcess/finalAudit',
url: '/zsjy/enrollment/process/finalConfirm',
method: 'post',
data: data
params: data
})
}
// 完成录取
export const completeEnrollment = (id: number) => {
// 完成流程
export const completeProcess = (processId: number) => {
return request({
url: '/zsjy/enrollmentProcess/complete',
url: '/zsjy/enrollment/process/complete',
method: 'post',
data: { id }
params: { processId }
})
}
// 驳回流程
export const rejectProcess = (data: { id: number; rejectReason: string }) => {
export const rejectProcess = (data: {
processId: number;
rejectReason: string;
}) => {
return request({
url: '/zsjy/enrollmentProcess/reject',
url: '/zsjy/enrollment/process/reject',
method: 'post',
data: data
params: data
})
}
// 重置流程状态
export const resetProcess = (id: number) => {
// 重置流程
export const resetProcess = (processId: number) => {
return request({
url: '/zsjy/enrollmentProcess/reset',
url: '/zsjy/enrollment/process/reset',
method: 'post',
data: { id }
params: { processId }
})
}
// 导出录取流程状态
export const exportEnrollmentProcess = (query: EnrollmentProcessQuery) => {
return request({
url: '/zsjy/enrollmentProcess/export',
url: '/zsjy/enrollment/process/export',
method: 'post',
data: query
})
}
}
+78
View File
@@ -0,0 +1,78 @@
import request from '@/utils/request';
/**
* 获取统计概览数据
*/
export function getStatisticsOverview() {
return request({
url: '/dev-api/zsjy/statistics/overview',
method: 'get'
});
}
/**
* 获取专业分布统计
*/
export function getMajorDistribution() {
return request({
url: '/dev-api/zsjy/statistics/major',
method: 'get'
});
}
/**
* 获取地区分布统计
*/
export function getRegionDistribution() {
return request({
url: '/dev-api/zsjy/statistics/region',
method: 'get'
});
}
/**
* 获取性别比例统计
*/
export function getGenderRatio() {
return request({
url: '/dev-api/zsjy/statistics/gender',
method: 'get'
});
}
/**
* 获取报名趋势统计
* @param {number} days 天数,默认为7天
*/
export function getEnrollmentTrend(days?: number) {
return request({
url: '/dev-api/zsjy/statistics/trend',
method: 'get',
params: { days }
});
}
/**
* 获取状态分布统计
*/
export function getStatusDistribution() {
return request({
url: '/dev-api/zsjy/statistics/status',
method: 'get'
});
}
// 导出接口类型定义
export interface StatisticsOverview {
totalEnrollments: number;
todayEnrollments: number;
paidCount: number;
paymentRate: string;
admittedCount: number;
admissionRate: string;
majorDistribution: Array<{[key: string]: any}>;
regionDistribution: Array<{[key: string]: any}>;
genderRatio: {[key: string]: number};
weeklyTrend: Array<{[key: string]: any}>;
statusDistribution: {[key: string]: number};
}
@@ -99,4 +99,8 @@ onMounted(() => {
.show-btn {
margin-left: 12px;
}
.top-right-btn {
display: flex;
justify-content: flex-end;
}
</style>
+12 -18
View File
@@ -1,20 +1,14 @@
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
import { App } from 'vue';
import { App } from 'vue'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
export default {
install: (app: App) => {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
// 确保Close图标被正确注册(用于Dialog关闭按钮)
if (ElementPlusIconsVue.Close) {
app.component('Close', ElementPlusIconsVue.Close);
}
// 备用:如果Close不存在,使用CircleClose
if (ElementPlusIconsVue.CircleClose && !ElementPlusIconsVue.Close) {
app.component('Close', ElementPlusIconsVue.CircleClose);
}
export default function registerIcons(app: App) {
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
};
// 移除重复的Close组件注册
// app.component('Close', ElementPlusIconsVue.Close);
// 注册常用图标的别名
app.component('CircleCloseIcon', ElementPlusIconsVue.CircleClose);
}
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -46,7 +46,7 @@
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
:default-expand-all="isExpandAll"
>
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
<el-table-column prop="deptName" label="部门名称" width="400"></el-table-column>
<el-table-column prop="deptCategory" align="center" label="类别编码" width="200"></el-table-column>
<el-table-column prop="orderNum" align="center" label="排序" width="200"></el-table-column>
<el-table-column prop="status" align="center" label="状态" width="100">
+287 -37
View File
@@ -57,8 +57,15 @@
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="班主任" prop="classTeacherName">
<el-input v-model="queryParams.classTeacherName" placeholder="请输入班主任姓名" clearable @keyup.enter="handleQuery" />
<el-form-item label="班主任" prop="classTeacherId">
<el-select v-model="queryParams.classTeacherId" placeholder="请选择班主任" clearable style="width: 100%">
<el-option
v-for="teacher in classTeacherOptions"
:key="teacher.value"
:label="teacher.label"
:value="teacher.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
@@ -277,7 +284,7 @@
</el-col>
<el-col :span="12">
<el-form-item label="专业" prop="majorCode">
<el-select v-model="form.majorCode" placeholder="请选择专业" style="width: 100%">
<el-select v-model="form.majorCode" placeholder="请选择专业" style="width: 100%" @change="handleMajorChange">
<el-option v-for="option in formMajorOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
@@ -325,7 +332,9 @@
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="班主任" prop="classTeacherName">
<el-input v-model="form.classTeacherName" placeholder="请输入班主任姓名" maxlength="50" />
<el-select v-model="form.classTeacherId" placeholder="请选择班主任" style="width: 100%" @change="handleClassTeacherChange">
<el-option v-for="option in classTeacherOptions" :key="option.value" :label="option.label" :value="option.value" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -464,7 +473,9 @@ import {
getMajorOptions,
getCounselorOptions,
getClassTeacherOptions,
getClassStatistics // 新增统计接口导入
getClassStatistics, // 新增统计接口导入
getDeptCollegeOptions, // 新增从系统部门获取学院
getDeptMajorOptions // 新增从系统部门获取专业
} from '@/api/xg-xx/classInfo';
import { ClassInfoVO, ClassInfoQuery, ClassInfoForm, ClassStatisticsData, OptionData } from '@/api/xg-xx/classInfo/types';
@@ -597,7 +608,7 @@ const data = reactive<PageData<ClassInfoForm, ClassInfoQuery>>({
grade: undefined,
collegeCode: undefined,
majorCode: undefined,
classTeacherName: undefined,
classTeacherId: undefined,
classStatus: undefined
},
rules: {
@@ -670,30 +681,104 @@ const getList = async () => {
/** 获取选项数据 */
const getOptions = async () => {
try {
// 并行获取所有选项数据和统计数据
const [collegeRes, counselorRes, classTeacherRes, statisticsRes] = await Promise.all([
getCollegeOptions(),
getCounselorOptions(),
getClassTeacherOptions(),
getClassStatistics() // 获取统计数据
]);
// 1. 先单独获取部门数据,彻底避免Promise.all可能的参数串扰
console.log('单独获取部门数据');
try {
// 调用部门API获取学院数据
const collegeRes = await getDeptCollegeOptions();
console.log('学院API响应:', collegeRes);
// 设置选项数据
collegeOptions.value = collegeRes.data || [];
counselorOptions.value = counselorRes.data || [];
classTeacherOptions.value = classTeacherRes.data || [];
// 设置学院选项数据(将部门树转换为选项格式)
if (collegeRes.data && Array.isArray(collegeRes.data)) {
// 找到所有二级学院(一级部门的子部门)
const rootDepts = collegeRes.data || [];
const secondLevelDepts: OptionData[] = [];
// 设置统计数据
if (statisticsRes.data) {
statistics.value = {
totalCount: statisticsRes.data.totalCount || 0,
activeCount: statisticsRes.data.activeCount || 0,
graduatedCount: statisticsRes.data.graduatedCount || 0,
suspendedCount: statisticsRes.data.pausedCount || 0,
dissolvedCount: 0, // 可以后续添加
totalStudentCount: statisticsRes.data.totalStudents || 0,
averageClassSize: statisticsRes.data.totalCount > 0 ? Math.round((statisticsRes.data.totalStudents || 0) / statisticsRes.data.totalCount) : 0
};
console.log('部门树根节点数量:', rootDepts.length);
// 处理每个一级部门
rootDepts.forEach(rootDept => {
console.log(`处理一级部门: ${rootDept.label}, ID: ${rootDept.id}`);
// 处理一级部门下的二级学院
if (rootDept.children && rootDept.children.length > 0) {
rootDept.children.forEach(child => {
// 确保ID是有效的数字
const childId = child.id;
if (childId && !isNaN(Number(childId))) {
console.log(`添加二级学院: ${child.label}, ID: ${childId}, 数字ID: ${Number(childId)}`);
secondLevelDepts.push({
value: Number(childId), // 确保使用数字类型
label: child.label,
// 保存原始数据,方便后续使用
deptId: Number(childId),
parentId: Number(rootDept.id)
});
} else {
console.warn(`跳过无效ID的学院: ${child.label}, ID: ${childId}`);
}
});
}
});
collegeOptions.value = secondLevelDepts;
console.log('设置学院选项:', collegeOptions.value);
}
} catch (collegeError) {
console.error('获取学院数据失败:', collegeError);
// 如果获取失败,使用空数组
collegeOptions.value = [];
}
// 2. 然后获取其他数据
console.log('获取其他选项数据');
try {
// 获取班主任选项
const counselorRes = await getClassTeacherOptions();
console.log('班主任API响应:', counselorRes);
// 处理班主任数据
if (counselorRes.data?.data) {
// 标准成功响应
console.log('标准成功响应,data字段类型:', typeof counselorRes.data.data);
classTeacherOptions.value = counselorRes.data.data || [];
} else if (Array.isArray(counselorRes.data)) {
// 直接返回数组
console.log('直接返回数组,长度:', counselorRes.data.length);
classTeacherOptions.value = counselorRes.data;
} else if (counselorRes.data && Array.isArray(counselorRes.data.rows)) {
// 分页响应格式
console.log('分页响应格式,rows字段长度:', counselorRes.data.rows.length);
classTeacherOptions.value = counselorRes.data.rows;
} else {
// 其他格式,尝试直接使用
console.log('其他格式,直接使用response.data');
classTeacherOptions.value = counselorRes.data || [];
}
console.log('设置班主任选项:', classTeacherOptions.value);
} catch (counselorError) {
console.error('获取班主任数据失败:', counselorError);
classTeacherOptions.value = [];
}
// 3. 获取统计数据
try {
const statisticsRes = await getClassStatistics();
// 设置统计数据
const statsData = statisticsRes.data?.data;
if (statsData) {
statistics.value = {
totalCount: statsData.totalCount || 0,
activeCount: statsData.activeCount || 0,
graduatedCount: statsData.graduatedCount || 0,
suspendedCount: statsData.pausedCount || 0,
dissolvedCount: 0, // 可以后续添加
totalStudentCount: statsData.totalStudents || 0,
averageClassSize: statsData.totalCount > 0 ? Math.round((statsData.totalStudents || 0) / statsData.totalCount) : 0
};
}
} catch (statsError) {
console.error('获取统计数据失败:', statsError);
}
} catch (error) {
console.error('获取选项数据失败:', error);
@@ -702,38 +787,121 @@ const getOptions = async () => {
};
/** 学院变更处理 */
const handleCollegeChange = async (collegeCode: string) => {
const handleCollegeChange = async (deptId: string | number) => {
// 重置专业选择
queryParams.value.majorCode = undefined;
if (collegeCode) {
// 调试输出
console.log('学院变更,部门ID:', deptId, '类型:', typeof deptId);
// 验证deptId是有效的数字
if (deptId && !isNaN(Number(deptId))) {
try {
const res = await getMajorOptions(collegeCode);
majorOptions.value = res.data || [];
// 获取子部门作为专业选项
const numericDeptId = Number(deptId);
console.log('准备获取专业数据,使用数字ID:', numericDeptId);
const res = await getDeptMajorOptions(numericDeptId);
// 处理响应数据
if (res.data && Array.isArray(res.data)) {
// 转换为选项格式
const deptMajors = res.data.map(dept => {
// 确保使用数字类型的ID
const majorId = Number(dept.deptId);
console.log(`专业选项: ${dept.deptName}, ID: ${majorId}`);
return {
value: majorId,
label: dept.deptName,
deptId: majorId,
parentId: numericDeptId
};
});
// 设置专业选项
majorOptions.value = deptMajors;
console.log('设置专业选项:', majorOptions.value);
} else {
console.warn('专业数据不是数组格式:', res.data);
majorOptions.value = [];
}
} catch (error) {
console.error('获取专业选项失败:', error);
majorOptions.value = [];
proxy?.$modal.msgError('获取专业选项失败');
}
} else {
console.warn('无效的学院/部门ID:', deptId);
majorOptions.value = [];
}
};
/** 表单学院变更处理 */
const handleFormCollegeChange = async (collegeCode: string) => {
const handleFormCollegeChange = async (deptId: string | number) => {
// 重置专业选择和名称
form.value.majorCode = undefined;
form.value.majorName = undefined;
if (collegeCode) {
// 调试输出
console.log('表单学院变更,部门ID:', deptId, '类型:', typeof deptId);
// 验证deptId是有效的数字
if (deptId && !isNaN(Number(deptId))) {
try {
const res = await getMajorOptions(collegeCode);
formMajorOptions.value = res.data || [];
// 获取子部门作为专业选项
const numericDeptId = Number(deptId);
console.log('准备获取专业数据,使用数字ID:', numericDeptId);
const res = await getDeptMajorOptions(numericDeptId);
// 处理响应数据
if (res.data && Array.isArray(res.data)) {
// 转换为选项格式
const deptMajors = res.data.map(dept => {
// 确保使用数字类型的ID
const majorId = Number(dept.deptId);
console.log(`表单专业选项: ${dept.deptName}, ID: ${majorId}`);
return {
value: majorId,
label: dept.deptName,
deptId: majorId,
parentId: numericDeptId
};
});
// 设置表单专业选项
formMajorOptions.value = deptMajors;
console.log('设置表单专业选项:', formMajorOptions.value);
} else {
console.warn('专业数据不是数组格式:', res.data);
formMajorOptions.value = [];
}
} catch (error) {
console.error('获取专业选项失败:', error);
console.error('获取表单专业选项失败:', error);
formMajorOptions.value = [];
proxy?.$modal.msgError('获取专业选项失败');
}
} else {
console.warn('无效的学院/部门ID:', deptId);
formMajorOptions.value = [];
}
};
/** 专业变更处理 */
const handleMajorChange = (majorCode: string | number) => {
const selectedMajor = formMajorOptions.value.find(option => option.value === Number(majorCode));
form.value.majorName = selectedMajor ? selectedMajor.label : '';
console.log('专业变更,选择专业:', majorCode, '专业名称:', form.value.majorName);
};
// 添加班主任选择变更处理函数
/** 班主任变更处理 */
const handleClassTeacherChange = (teacherId: string | number) => {
form.value.classTeacherId = teacherId;
const selectedTeacher = classTeacherOptions.value.find(option => option.value === teacherId);
form.value.classTeacherName = selectedTeacher ? selectedTeacher.label : '';
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
@@ -745,6 +913,7 @@ const resetQuery = () => {
queryFormRef.value?.resetFields();
majorOptions.value = [];
handleQuery();
getClassTeacherOptions(); // 重置后重新获取班主任选项
};
/** 多选框选中数据 */
@@ -882,6 +1051,8 @@ const cancel = () => {
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
form.value.classTeacherId = undefined; // 确保重置班主任ID
form.value.classTeacherName = undefined; // 确保重置班主任名称
formMajorOptions.value = [];
classInfoFormRef.value?.resetFields();
};
@@ -890,7 +1061,86 @@ const reset = () => {
onMounted(() => {
getList();
getOptions();
getClassTeacherList(); // 获取班主任列表
});
/** 获取班主任选项 */
const getClassTeacherList = async () => {
console.log('开始获取班主任列表');
try {
const response = await getClassTeacherOptions();
console.log('获取班主任列表成功,响应数据:', response);
// 解析响应数据
if (response.data && typeof response.data === 'object') {
console.log('响应数据类型:', Object.prototype.toString.call(response.data));
if (response.data.code === 200 && response.data.data) {
// 标准成功响应
console.log('标准成功响应,data字段:', JSON.stringify(response.data.data));
classTeacherOptions.value = response.data.data || [];
} else if (Array.isArray(response.data)) {
// 直接返回数组
console.log('直接返回数组,长度:', response.data.length);
if (response.data.length > 0) {
console.log('数组第一个元素:', JSON.stringify(response.data[0]));
}
classTeacherOptions.value = response.data;
} else if (response.data && Array.isArray(response.data.rows)) {
// 分页响应格式
console.log('分页响应格式,rows字段:', JSON.stringify(response.data.rows));
classTeacherOptions.value = response.data.rows;
} else {
// 其他格式,尝试直接使用
console.log('其他格式,直接使用response.data');
classTeacherOptions.value = response.data || [];
}
} else {
console.log('响应数据为空或不是对象类型');
classTeacherOptions.value = [];
}
console.log('设置后的班主任选项:', JSON.stringify(classTeacherOptions.value));
// 检查每个选项的格式是否正确
if (classTeacherOptions.value && classTeacherOptions.value.length > 0) {
console.log('选项数量:', classTeacherOptions.value.length);
classTeacherOptions.value.forEach((option, index) => {
console.log(`选项[${index}]:`, JSON.stringify(option));
if (!option.value || !option.label) {
console.warn(`选项[${index}]格式不正确:`, option);
}
});
}
// 如果没有获取到数据,给出提示
if (!classTeacherOptions.value.length) {
proxy?.$modal.msgWarning('未找到班主任数据,请确认数据库中是否存在班主任角色(head_teacher)的用户');
}
} catch (error) {
console.error('获取班主任列表失败,错误详情:', error);
// 提供更详细的错误信息
let errorMessage = '获取班主任列表失败';
if (error.response) {
const status = error.response.status;
const data = error.response.data;
if (status === 401) {
console.error('认证失败(401),Token可能已过期,尝试重新登录');
errorMessage = '认证失败,请重新登录';
} else if (status === 403) {
console.error('权限不足(403),无法访问班主任数据');
errorMessage = '权限不足,无法获取班主任列表';
} else if (data && data.msg) {
errorMessage = errorMessage + ': ' + data.msg;
}
}
classTeacherOptions.value = [];
proxy?.$modal.msgError(errorMessage);
}
};
</script>
<style scoped>
+130 -25
View File
@@ -122,6 +122,16 @@
<el-option label="毕业" value="5" />
</el-select>
</el-form-item>
<el-form-item label="班主任" prop="classTeacherId">
<el-select v-model="queryParams.classTeacherId" placeholder="请选择班主任" clearable>
<el-option
v-for="teacher in classTeacherOptions"
:key="teacher.value"
:label="teacher.label"
:value="teacher.value"
/>
</el-select>
</el-form-item>
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -377,7 +387,8 @@ import {
getStudent,
delStudent,
exportStudents,
getStatisticsOverview
getStatisticsOverview,
getClassTeacherOptions
} from '@/api/xg-xx/student'
import {
getCollegeOptions,
@@ -435,7 +446,8 @@ export default {
classCode: null,
grade: null,
studentStatus: null,
infoStatus: null
infoStatus: null,
classTeacherId: null
},
// 表单参数
form: {},
@@ -452,6 +464,7 @@ export default {
collegeOptions: [],
majorOptions: [],
classOptions: [],
classTeacherOptions: [],
// 弹窗显示状态
detailVisible: false,
editVisible: false,
@@ -470,6 +483,7 @@ export default {
this.getList()
this.getStatistics()
this.getCollegeList()
this.getClassTeacherOptions() // 调用API获取班主任数据
},
methods: {
/** 查询学生列表 */
@@ -543,6 +557,113 @@ export default {
this.$modal.msgError(errorMessage)
})
},
/** 获取专业列表 */
getMajorList(collegeId) {
console.log('开始获取专业列表:', collegeId)
getMajorOptions(collegeId).then(response => {
console.log('获取专业列表成功:', response)
this.majorOptions = response.data || []
}).catch(error => {
console.error('获取专业列表失败:', error)
let errorMessage = '获取专业列表失败'
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg
}
this.$modal.msgError(errorMessage)
})
},
/** 获取班级列表 */
getClassList(majorId) {
console.log('开始获取班级列表:', majorId)
getClassOptions(majorId).then(response => {
console.log('获取班级列表成功:', response)
this.classOptions = response.data || []
}).catch(error => {
console.error('获取班级列表失败:', error)
let errorMessage = '获取班级列表失败'
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg
}
this.$modal.msgError(errorMessage)
})
},
/** 获取班主任选项 */
getClassTeacherOptions() {
console.log('开始获取班主任列表')
getClassTeacherOptions().then(response => {
console.log('获取班主任列表成功,原始响应:', JSON.stringify(response))
// 检查响应中的数据结构
if (response.data && typeof response.data === 'object') {
console.log('响应数据类型:', Object.prototype.toString.call(response.data))
if (response.data.code === 200) {
// 标准成功响应
console.log('标准成功响应,data字段:', JSON.stringify(response.data.data))
this.classTeacherOptions = response.data.data || []
} else if (Array.isArray(response.data)) {
// 直接返回数组
console.log('直接返回数组,长度:', response.data.length)
console.log('数组第一个元素:', JSON.stringify(response.data[0]))
this.classTeacherOptions = response.data
} else if (response.data && Array.isArray(response.data.rows)) {
// 分页响应格式
console.log('分页响应格式,rows字段:', JSON.stringify(response.data.rows))
this.classTeacherOptions = response.data.rows
} else {
// 其他格式,尝试直接使用
console.log('其他格式,直接使用response.data')
this.classTeacherOptions = response.data || []
}
} else {
console.log('响应数据为空或不是对象类型')
this.classTeacherOptions = []
}
console.log('设置后的班主任选项:', JSON.stringify(this.classTeacherOptions))
// 检查每个选项的格式是否正确
if (this.classTeacherOptions && this.classTeacherOptions.length > 0) {
console.log('选项数量:', this.classTeacherOptions.length)
this.classTeacherOptions.forEach((option, index) => {
console.log(`选项[${index}]:`, JSON.stringify(option))
if (!option.value || !option.label) {
console.warn(`选项[${index}]格式不正确:`, option)
}
})
}
// 如果没有获取到数据,给出提示
if (!this.classTeacherOptions.length) {
this.$modal.msgWarning('未找到班主任数据,请确认数据库中是否存在班主任角色(head_teacher)的用户')
}
}).catch(error => {
console.error('获取班主任列表失败,错误详情:', error)
// 提供更详细的错误信息
let errorMessage = '获取班主任列表失败'
if (error.response) {
const status = error.response.status
const data = error.response.data
if (status === 401) {
console.error('认证失败(401),Token可能已过期,尝试重新登录')
errorMessage = '认证失败,请重新登录'
// 可以在这里添加重新登录的逻辑
// this.$store.dispatch('LogOut').then(() => {
// this.$router.push('/login')
// })
} else if (status === 403) {
console.error('权限不足(403),无法访问班主任数据')
errorMessage = '权限不足,无法获取班主任列表'
} else if (data && data.msg) {
errorMessage = errorMessage + ': ' + data.msg
}
}
this.$modal.msgError(errorMessage)
})
},
/** 学院变化处理 */
handleCollegeChange(collegeId) {
console.log('学院变化,清空专业和班级选择:', collegeId)
@@ -550,21 +671,12 @@ export default {
this.queryParams.classCode = null
this.majorOptions = []
this.classOptions = []
this.queryParams.classTeacherId = null // 清空班主任选择
if (collegeId) {
console.log('学院变化,获取专业列表:', collegeId)
// 根据学院ID获取专业列表
getMajorOptions(collegeId).then(response => {
console.log('获取专业列表成功:', response)
this.majorOptions = response.data || []
}).catch(error => {
console.error('获取专业列表失败:', error)
let errorMessage = '获取专业列表失败'
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg
}
this.$modal.msgError(errorMessage)
})
this.getMajorList(collegeId)
}
},
/** 专业变化处理 */
@@ -572,21 +684,12 @@ export default {
console.log('专业变化,清空班级选择:', majorId)
this.queryParams.classCode = null
this.classOptions = []
this.queryParams.classTeacherId = null // 清空班主任选择
if (majorId) {
console.log('专业变化,获取班级列表:', majorId)
// 根据专业ID获取班级列表
getClassOptions(majorId).then(response => {
console.log('获取班级列表成功:', response)
this.classOptions = response.data || []
}).catch(error => {
console.error('获取班级列表失败:', error)
let errorMessage = '获取班级列表失败'
if (error.response && error.response.data && error.response.data.msg) {
errorMessage += ': ' + error.response.data.msg
}
this.$modal.msgError(errorMessage)
})
this.getClassList(majorId)
}
},
/** 搜索按钮操作 */
@@ -608,11 +711,13 @@ export default {
classCode: null,
grade: null,
studentStatus: null,
infoStatus: null
infoStatus: null,
classTeacherId: null
}
// 重置级联选择器
this.majorOptions = []
this.classOptions = []
this.classTeacherOptions = [] // 重置班主任选项
this.handleQuery()
},
/** 多选框选中数据 */
@@ -86,6 +86,7 @@
<el-table-column label="宿舍楼名称" align="center" prop="buildingName" />
<el-table-column label="所属校区" align="center" prop="campusName" />
<el-table-column label="所属宿舍区" align="center" prop="areaName" />
<el-table-column label="总楼层数" align="center" prop="floors" />
<el-table-column label="宿舍楼类型" align="center" prop="buildingType">
<template #default="scope">
<dict-tag :options="dorm_type" :value="scope.row.buildingType" />
@@ -128,7 +129,18 @@
<el-input v-model="form.buildingCode" placeholder="请输入宿舍楼编码" />
</el-form-item>
<el-form-item label="宿舍楼名称" prop="buildingName">
<el-input v-model="form.buildingName" placeholder="请输入宿舍楼名称" />
<el-input v-model="form.buildingName" placeholder="请输入宿舍楼名称" class="fixed-width-input" />
</el-form-item>
<el-form-item label="总楼层数" prop="floors">
<el-input-number
v-model="form.floors"
:min="1"
:max="99"
:step="1"
:default-value="6"
placeholder="请输入总楼层数"
class="fixed-width-input"
/>
</el-form-item>
<el-form-item label="宿舍楼类型" prop="buildingType">
<el-select v-model="form.buildingType" placeholder="请选择宿舍楼类型">
@@ -161,10 +173,10 @@ import {
updateDormBuilding,
listCampusOptions,
listAreaTreeData
} from '@/api/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/ssqxx/dormArea/types';
} from '@/api/xgss/sslxx/dormBuilding';
import { DormBuildingVO, DormBuildingQuery, DormBuildingForm } from '@/api/xgss/sslxx/dormBuilding/types';
import { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
import { DormAreaVO } from '@/api/xgss/ssqxx/dormArea/types';
import { getCurrentInstance, ComponentInternalInstance, ref, reactive, toRefs, onMounted } from 'vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -198,7 +210,8 @@ const initFormData: DormBuildingForm = {
campusId: undefined,
buildingType: undefined,
buildingDescription: undefined,
sortOrder: undefined
sortOrder: undefined,
floors: 6 // 添加默认值为6
};
const data = reactive<PageData<DormBuildingForm, DormBuildingQuery>>({
form: { ...initFormData },
@@ -537,4 +550,7 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
.fixed-width-input {
width: 100%;
}
</style>
@@ -112,10 +112,10 @@
</template>
<script setup name="DormArea" lang="ts">
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xqxx/dormCampus/types';
import { listDormArea, getDormArea, delDormArea, addDormArea, updateDormArea } from '@/api/xgss/ssqxx/dormArea';
import { DormAreaVO, DormAreaQuery, DormAreaForm } from '@/api/xgss/ssqxx/dormArea/types';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import type { DormCampusVO } from '@/api/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -94,8 +94,8 @@
</template>
<script setup name="DormCampus" lang="ts">
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xqxx/dormCampus/types';
import { listDormCampus, getDormCampus, delDormCampus, addDormCampus, updateDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { DormCampusVO, DormCampusQuery, DormCampusForm } from '@/api/xgss/xqxx/dormCampus/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
@@ -0,0 +1,145 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="床位编号" prop="bedCode">
<el-input v-model="form.bedCode" placeholder="请输入床位编号" maxlength="20" />
</el-form-item>
<el-form-item label="床位名称" prop="bedName">
<el-input v-model="form.bedName" placeholder="请输入床位名称" maxlength="20" />
</el-form-item>
<el-form-item label="床位状态" prop="bedStatus">
<el-radio-group v-model="form.bedStatus">
<el-radio label="0">空闲</el-radio>
<el-radio label="1" disabled>已分配</el-radio>
<el-radio label="2">维修中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="床位类型" prop="bedType">
<el-radio-group v-model="form.bedType">
<el-radio label="1">下铺</el-radio>
<el-radio label="2">上铺</el-radio>
<!-- <el-radio label="3">中铺</el-radio> -->
<!-- <el-radio label="4">独立床</el-radio> -->
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormBedForm } from '@/api/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xgss/dormBed';
const props = defineProps<{
modelValue: boolean;
title: string;
bedId?: string | number;
roomId: string | number;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: DormBedForm = {
id: '',
bedCode: '',
bedName: '',
roomId: '',
bedStatus: '0',
bedType: '1',
remark: ''
};
const form = reactive<DormBedForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
bedCode: [{ required: true, message: '床位编号不能为空', trigger: 'blur' }],
bedName: [{ required: true, message: '床位名称不能为空', trigger: 'blur' }],
bedStatus: [{ required: true, message: '床位状态不能为空', trigger: 'change' }],
bedType: [{ required: true, message: '床位类型不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
resetForm();
// 设置表单初始值
form.roomId = props.roomId;
// 编辑模式,获取床位详情
if (props.bedId) {
console.log('开始获取床位详情, bedId:', props.bedId);
getDormBed(props.bedId)
.then((res) => {
console.log('获取床位详情成功:', res.data);
if (res.data) {
Object.assign(form, res.data);
}
})
.catch((error) => {
console.error('获取床位详情失败', error);
});
}
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
Object.assign(form, { ...initFormData });
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
if (props.bedId) {
// 修改
form.id = props.bedId;
await updateDormBed(form);
} else {
// 新增
await addDormBed(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
</script>
@@ -0,0 +1,165 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="楼层" prop="floorNum">
<el-input v-model="form.floorNum" placeholder="请输入楼层" :disabled="true" />
</el-form-item>
<el-form-item label="房间编号" prop="roomCode">
<el-input v-model="form.roomCode" placeholder="请输入房间编号" maxlength="20" />
</el-form-item>
<el-form-item label="房间名称" prop="roomName">
<el-input v-model="form.roomName" placeholder="请输入房间名称" maxlength="20" />
</el-form-item>
<el-form-item label="房间类型" prop="roomType">
<el-select v-model="form.roomType" placeholder="请选择房间类型" clearable style="width: 100%">
<el-option label="标准间" value="1" />
<el-option label="单人间" value="2" />
<el-option label="套间" value="3" />
</el-select>
</el-form-item>
<el-form-item label="床位数量" prop="actualBeds">
<el-input-number v-model="form.actualBeds" :min="1" :max="8" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="房间面积" prop="roomArea">
<el-input v-model="form.roomArea" placeholder="请输入房间面积" maxlength="20">
<template #append>
<span style="color: #000; background: transparent">平方米</span>
</template>
</el-input>
</el-form-item>
<el-form-item label="房间状态" prop="roomStatus">
<el-radio-group v-model="form.roomStatus">
<el-radio label="0">正常</el-radio>
<el-radio label="1">维修中</el-radio>
<el-radio label="2">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormRoomForm, DormRoomVO } from '@/api/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xgss/dormRoom';
const props = defineProps<{
modelValue: boolean;
title: string;
roomId?: string | number;
floorNum: number;
buildingId: string | number;
room?: DormRoomVO;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: DormRoomForm = {
id: undefined,
roomCode: '',
roomName: '',
floorNum: 1,
buildingId: '',
actualBeds: 8,
roomArea: 0,
roomType: '1',
roomStatus: '0',
remark: ''
};
const form = reactive<DormRoomForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
roomCode: [{ required: true, message: '房间编号不能为空', trigger: 'blur' }],
roomName: [{ required: true, message: '房间名称不能为空', trigger: 'blur' }],
floorNum: [{ required: true, message: '楼层不能为空', trigger: 'blur' }],
actualBeds: [{ required: true, message: '床位数量不能为空', trigger: 'change' }],
roomType: [{ required: true, message: '房间类型不能为空', trigger: 'change' }],
roomStatus: [{ required: true, message: '房间状态不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
// 完全重置表单,确保没有残留数据
resetForm();
// 重新设置基本参数
form.floorNum = props.floorNum;
form.buildingId = props.buildingId;
// 如果是编辑模式且有room数据,填充表单数据
if (props.room) {
Object.keys(form).forEach((key) => {
if (key in props.room) {
form[key] = props.room[key];
}
});
}
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
// 使用深拷贝确保完全重置
Object.keys(form).forEach((key) => {
form[key] = initFormData[key];
});
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
if (props.roomId) {
// 修改
form.id = props.roomId;
await updateDormRoom(form);
} else {
// 新增
await addDormRoom(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
</script>
+864
View File
@@ -0,0 +1,864 @@
<template>
<div class="p-2">
<div class="flex-layout">
<!-- 宿舍树 -->
<div class="tree-sidebar">
<el-card shadow="hover">
<el-input
v-model="buildingName"
prefix-icon="Search"
placeholder="请输入宿舍楼名称"
clearable
@clear="watchBuildingName"
@keyup.enter="watchBuildingName"
>
</el-input>
<div class="tree-container">
<el-tree
ref="dormTreeRef"
class="mt-2"
node-key="id"
:data="dormOptions"
:props="{ label: 'label', children: 'children', disabled: 'disabled' }"
:expand-on-click-node="false"
:filter-node-method="filterNode"
highlight-current
default-expand-all
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="custom-tree-node">
<el-tooltip v-if="data.disabled" effect="dark" content="此节点不可选择,请选择具体的宿舍楼" placement="right">
<span :class="{ 'disabled-node': data.disabled }">{{ node.label }}</span>
</el-tooltip>
<span v-else>
<span v-if="data.type === 'building'" :class="data.buildingType === '1' ? 'male-building' : 'female-building'">
{{ data.buildingType === '1' ? '(男)' : '(女)' }}
</span>
{{ node.label }}
</span>
</div>
</template>
</el-tree>
</div>
</el-card>
</div>
<div class="content-area">
<transition :enter-active-class="animate.searchAnimate.enter" :leave-active-class="animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" inline>
<el-form-item label="房间号" prop="roomName">
<el-input v-model="queryParams.roomName" placeholder="请输入房间号" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="roomStatus">
<el-select v-model="queryParams.roomStatus" placeholder="房间状态" clearable>
<el-option label="正常" value="0" />
<el-option label="维修中" value="1" />
<el-option label="停用" value="2" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</transition>
<el-card v-if="!selectedBuilding" shadow="hover" class="empty-tip">
<div class="flex flex-col items-center justify-center h-[400px]">
<el-icon class="text-gray-400 text-5xl mb-4"><InfoFilled /></el-icon>
<p class="text-gray-500 text-lg">请在左侧选择宿舍楼</p>
</div>
</el-card>
<template v-else>
<el-card shadow="hover" class="mb-4">
<template #header>
<el-row>
<el-col :span="12">
<span class="font-bold"
>{{ selectedBuilding?.label }}
<span :class="selectedBuilding?.buildingType === '1' ? 'male-building' : 'female-building'">
{{ selectedBuilding?.buildingType === '1' ? '(男生楼)' : '(女生楼)' }}
</span>
- 房间管理</span
>
</el-col>
<el-col :span="12" class="text-right">
<right-toolbar
:show-search="showSearch"
@update:show-search="(val) => (showSearch = val)"
@query-table="loadRoomData"
></right-toolbar>
</el-col>
</el-row>
</template>
<!-- 楼层卡片 -->
<div v-for="floor in floorList" :key="floor" class="mb-4">
<el-divider>
<el-tag type="success" effect="dark" class="mr-2">{{ floor }}层</el-tag>
<el-button type="primary" icon="Plus" size="small" @click="handleAddAllRoom(floor)">自动添加房间</el-button>
<el-button type="primary" icon="Plus" size="small" @click="handleAddRoom(floor)">添加房间</el-button>
</el-divider>
<div class="room-container">
<el-empty v-if="getRoomsByFloor(floor).length === 0" description="暂无房间数据" />
<div v-else class="room-grid">
<el-card
v-for="room in getRoomsByFloor(floor)"
:key="room.id"
class="room-card"
:class="{ 'room-disabled': room.roomStatus !== '0' }"
>
<template #header>
<div class="flex items-center justify-between">
<span class="font-bold">{{ room.roomName }}</span>
<div>
<el-tag v-if="room.roomStatus === '0'" type="success" size="small">正常</el-tag>
<el-tag v-else-if="room.roomStatus === '1'" type="warning" size="small">维修中</el-tag>
<el-tag v-else type="danger" size="small">停用</el-tag>
</div>
</div>
</template>
<div class="room-info">
<div class="mb-2">
<span class="text-gray-500">类型:</span>{{ room.roomType === '1' ? '标准间' : room.roomType === '2' ? '单人间' : '套间' }}
</div>
<div class="mb-2"><span class="text-gray-500">床位:</span>{{ room.currentBeds || 0 }}/{{ room.actualBeds }}</div>
<div class="flex justify-between mt-4">
<div>
<el-button type="primary" link size="small" @click="handleManageBeds(room)">床位管理</el-button>
</div>
<div>
<el-button type="primary" link icon="Edit" @click="handleEditRoom(room)" />
<el-button type="danger" link icon="Delete" @click="handleDeleteRoom(room)" />
</div>
</div>
</div>
</el-card>
</div>
</div>
</div>
</el-card>
</template>
</div>
</div>
<!-- 房间管理对话框 -->
<room-dialog
v-model="roomDialog.visible"
:title="roomDialog.title"
:room-id="roomDialog.roomId"
:floor-num="roomDialog.floorNum"
:building-id="roomDialog.buildingId"
:room="roomDialog.room"
@success="handleRoomSuccess"
/>
<!-- 床位管理对话框 -->
<el-dialog v-model="bedManageDialog.visible" :title="`${bedManageDialog.roomName} - 床位管理`" width="800px">
<div class="mb-4 text-right">
<el-button v-if="!isBedLimitReached" type="primary" icon="Plus" size="small" @click="handleAddALlBed">自动添加床位</el-button>
<el-button v-if="!isBedLimitReached" type="primary" icon="Plus" size="small" @click="handleAddBed">添加床位</el-button>
<el-tag v-else type="info">已达到床位上限 ({{ bedList.length }}/{{ bedManageDialog.maxBeds }})</el-tag>
<!-- <el-button type="success" icon="Check" size="small" @click="handleSaveBeds">保存</el-button> -->
</div>
<el-table v-loading="bedLoading" :data="bedList" border>
<el-table-column label="床位编号" prop="bedCode" />
<el-table-column label="床位名称" prop="bedName" />
<el-table-column label="状态" align="center">
<template #default="scope">
<el-tag v-if="scope.row.bedStatus === '0'" type="success">空闲</el-tag>
<el-tag v-else-if="scope.row.bedStatus === '1'" type="warning">已分配</el-tag>
<el-tag v-else type="danger">维修中</el-tag>
</template>
</el-table-column>
<!-- 在这里增加床位类型-->
<el-table-column label="床位类型" prop="bedType">
<template #default="scope">
<el-tag v-if="scope.row.bedType === '1'" type="success">下铺</el-tag>
<el-tag v-else-if="scope.row.bedType === '2'" type="warning">上铺</el-tag>
<el-tag v-else-if="scope.row.bedType === '3'" type="danger">中铺</el-tag>
<el-tag v-else type="info">独立床</el-tag>
</template>
</el-table-column>
<!-- <el-table-column label="学生" prop="studentName" /> -->
<el-table-column label="备注" prop="remark" :show-overflow-tooltip="true" />
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button type="primary" link icon="Edit" @click="handleEditBed(scope.row)" />
<el-button type="danger" link icon="Delete" @click="handleDeleteBed(scope.row)" />
</template>
</el-table-column>
</el-table>
</el-dialog>
<!-- 床位表单对话框 -->
<bed-dialog
v-model="bedDialog.visible"
:title="bedDialog.title"
:bed-id="bedDialog.bedId"
:room-id="bedDialog.roomId"
@success="handleBedSuccess"
/>
</div>
</template>
<script setup name="DormRoomManagement" lang="ts">
import { ref, computed, reactive, onMounted, nextTick, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { InfoFilled, Search } from '@element-plus/icons-vue';
import { DormTreeNode, BedInfo, DormQuery, BedForm } from '@/api/xgss/xsssfjgl/types';
import { DormRoomVO, DormRoomQuery } from '@/api/xgss/dormRoom/types';
import { getDormTree } from '@/api/xgss/xsssfjgl';
import { listDormBed, delDormBed, addDormBed } from '@/api/xgss/dormBed';
import type { DormBedQuery } from '@/api/xgss/dormBed/types';
import { listDormRoom, delDormRoom, addDormRoom } from '@/api/xgss/dormRoom';
// 引入子组件
import RoomDialog from './components/RoomDialog.vue';
import BedDialog from './components/BedDialog.vue';
// 组件状态
const dormTreeRef = ref();
const queryFormRef = ref();
const buildingName = ref('');
const showSearch = ref(true);
const animate = reactive({
searchAnimate: {
enter: 'animate__animated animate__fadeInDown',
leave: 'animate__animated animate__fadeOutUp'
}
});
// 判断床位是否已达到上限的计算属性
const isBedLimitReached = computed(() => {
return bedList.value.length >= bedManageDialog.maxBeds;
});
// 宿舍树数据
const dormOptions = ref<DormTreeNode[]>([]);
const selectedBuilding = ref<DormTreeNode>();
// 房间数据
const roomList = ref<DormRoomVO[]>([]);
const floorList = computed(() => {
if (!selectedBuilding.value?.buildingFloors) return [];
return Array.from({ length: selectedBuilding.value.buildingFloors }, (_, i) => i + 1);
});
// 查询参数
const queryParams = reactive<DormRoomQuery>({
pageNum: 1,
pageSize: 10000,
buildingId: undefined,
roomName: '',
roomStatus: undefined
});
// 房间对话框
const roomDialog = reactive({
visible: false,
title: '',
roomId: undefined as string | number | undefined,
floorNum: 1,
buildingId: '' as string | number,
room: undefined as DormRoomVO | undefined
});
// 床位管理对话框
const bedManageDialog = reactive({
visible: false,
roomId: '' as string | number,
roomName: '',
maxBeds: 0 // 预设的最大床位数
});
// 床位表单对话框
const bedDialog = reactive({
visible: false,
title: '',
bedId: undefined as string | number | undefined,
roomId: '' as string | number
});
// 床位数据
const bedList = ref<BedInfo[]>([]);
const bedLoading = ref(false);
/** 通过条件过滤节点 */
const filterNode = (value: string, data: DormTreeNode) => {
if (!value) return true;
return data.label.toLowerCase().indexOf(value.toLowerCase()) !== -1;
};
/** 监听宿舍楼名称变化,筛选树 */
const watchBuildingName = () => {
dormTreeRef.value?.filter(buildingName.value);
};
// 监听buildingName变化,实时筛选宿舍楼
watch(buildingName, () => {
watchBuildingName();
});
/** 获取宿舍树数据 */
const getDormTreeData = async () => {
try {
const res = await getDormTree();
// 处理树节点,标记非宿舍楼节点为禁用
const processTreeNodes = (nodes: DormTreeNode[]) => {
return nodes.map((node) => {
// 如果不是宿舍楼类型,则禁用该节点
if (node.type !== 'building') {
node.disabled = true;
}
// 如果是宿舍楼但没有性别信息,随机分配性别(实际项目中应该由后端提供)
if (node.type === 'building' && !node.gender) {
// 这里使用随机分配,实际项目中应该从后端获取
node.gender = Math.random() > 0.5 ? '1' : '2';
}
// 递归处理子节点
if (node.children && node.children.length > 0) {
node.children = processTreeNodes(node.children);
}
return node;
});
};
dormOptions.value = processTreeNodes(res.data);
} catch (error) {
console.error('获取宿舍树失败', error);
}
};
/** 节点单击事件 */
const handleNodeClick = (data: DormTreeNode) => {
if (data.type !== 'building') {
// 如果不是宿舍楼,不进行操作
return;
}
selectedBuilding.value = data;
queryParams.buildingId = data.id;
loadRoomData();
};
/** 加载房间数据 */
const loadRoomData = async () => {
if (!queryParams.buildingId) return;
try {
const res = await listDormRoom(queryParams);
roomList.value = res?.rows;
// 加载每个房间的当前床位数量
await Promise.all(
roomList.value.map(async (room) => {
try {
const bedQuery: DormBedQuery = { roomId: room.id };
const bedRes = await listDormBed(bedQuery);
room.currentBeds = bedRes?.rows?.length || 0;
} catch (error) {
console.error('获取床位数量失败', error);
room.currentBeds = 0;
}
})
);
} catch (error) {
console.error('获取房间列表失败', error);
}
};
/** 根据楼层获取房间数据 */
const getRoomsByFloor = (floor: number) => {
return roomList.value.filter((room) => room.floorNum === floor);
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
loadRoomData();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
queryParams.pageNum = 1;
queryParams.roomName = '';
queryParams.roomStatus = undefined;
handleQuery();
};
/** 批量自动添加房间 */
const handleAddAllRoom = (floor: number) => {
if (!selectedBuilding.value) return;
// 使用Element Plus的对话框提示用户输入寝室数量
ElMessageBox.prompt('请输入要添加的寝室数量', '批量添加房间', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputPattern: /^[1-9]\d*$/,
inputErrorMessage: '请输入正整数',
inputValue: '10' // 默认值
})
.then(async ({ value }) => {
const roomCount = parseInt(value);
try {
// 先获取当前楼层的房间列表,确定起始房间号
const floorRooms = getRoomsByFloor(floor);
let startRoomNumber = 1;
// 如果已有房间,确定起始编号(避免房间号重复)
if (floorRooms.length > 0) {
// 提取现有房间号中的数字部分
const existingRoomNumbers = floorRooms
.map((room) => {
// 假设房间名称格式为:楼层+编号,如"3201"
const match = room.roomName.match(/\d+$/);
return match ? parseInt(match[0].substring(floor.toString().length), 10) : 0;
})
.filter((num) => !isNaN(num));
// 找出最大编号并加1作为起始编号
if (existingRoomNumbers.length > 0) {
startRoomNumber = Math.max(...existingRoomNumbers) + 1;
}
}
// 显示加载提示
ElMessage.info(`正在添加 ${roomCount} 个房间,请稍候...`);
// 批量添加房间
for (let i = 0; i < roomCount; i++) {
const roomNumber = startRoomNumber + i;
// 房间号格式:楼栋首字符+楼层+编号(如A栋2层1号房间为A201)
// 补零确保房间号长度一致
const paddedRoomNumber = roomNumber.toString().padStart(2, '0');
const buildingFirstChar = selectedBuilding.value.label.charAt(0);
const roomCode = `${buildingFirstChar}${floor}${paddedRoomNumber}`;
const roomData = {
buildingId: selectedBuilding.value.id,
floorNum: floor,
roomCode: `${roomCode}`,
roomName: `${roomCode}`,
roomType: '1', // 1: 标准间
actualBeds: 6, // 默认6个床位
roomArea: 0, // 默认面积为0
roomStatus: '0' // 0: 正常
};
// 调用添加房间API
await addDormRoom(roomData);
// 添加延迟避免请求过快
await new Promise((resolve) => setTimeout(resolve, 300));
}
ElMessage.success(`成功添加 ${roomCount} 个房间`);
// 重新加载房间数据
loadRoomData();
} catch (error) {
console.error('批量添加房间失败', error);
ElMessage.error('添加房间失败,请重试');
}
})
.catch(() => {
// 用户取消操作
ElMessage.info('已取消添加房间');
});
};
/** 添加房间 */
const handleAddRoom = (floor: number) => {
if (!selectedBuilding.value) return;
// console.log(floor);
// 完全重置房间对话框的状态
roomDialog.title = '添加房间';
roomDialog.roomId = undefined;
roomDialog.floorNum = floor;
roomDialog.buildingId = selectedBuilding.value.id;
roomDialog.room = undefined; // 确保清除之前的房间数据
// 延迟显示对话框,确保状态已完全重置
nextTick(() => {
roomDialog.visible = true;
});
};
/** 编辑房间 */
const handleEditRoom = (room: DormRoomVO) => {
roomDialog.title = '编辑房间';
roomDialog.roomId = room.id;
roomDialog.floorNum = room.floorNum;
roomDialog.buildingId = room.buildingId;
roomDialog.room = room; // 将完整房间对象传递给对话框
roomDialog.visible = true;
};
/** 删除房间 */
const handleDeleteRoom = (row: DormRoomVO) => {
ElMessageBox.confirm(`确定要删除房间 ${row.roomName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await delDormRoom(row.id);
ElMessage.success('删除成功');
loadRoomData();
} catch (error) {
console.error('删除房间失败', error);
}
})
.catch(() => {});
};
/** 房间操作成功回调 */
const handleRoomSuccess = () => {
loadRoomData();
};
/** 管理床位 */
const handleManageBeds = async (room: DormRoomVO) => {
bedManageDialog.roomId = room.id;
bedManageDialog.roomName = room.roomName;
bedManageDialog.maxBeds = room.actualBeds || 0; // 保存预设的床位数
bedManageDialog.visible = true;
await loadBedData(room.id);
};
/** 加载床位数据 */
const loadBedData = async (roomId: string | number) => {
bedLoading.value = true;
try {
const query: DormBedQuery = {
roomId,
pageNum: 1,
pageSize: 100
};
const res = await listDormBed(query);
bedList.value = res?.rows || [];
} catch (error) {
console.error('获取床位列表失败', error);
} finally {
bedLoading.value = false;
}
};
/** 添加床位 */
const handleAddBed = () => {
bedDialog.title = '添加床位';
bedDialog.bedId = undefined;
bedDialog.roomId = bedManageDialog.roomId;
bedDialog.visible = true;
};
/** 编辑床位 */
const handleEditBed = (bed: BedInfo) => {
bedDialog.title = '编辑床位';
bedDialog.bedId = bed.id;
bedDialog.roomId = bed.roomId;
bedDialog.visible = true;
};
/** 删除床位 */
const handleDeleteBed = (row: BedInfo) => {
ElMessageBox.confirm(`确定要删除床位 ${row.bedName} 吗?`, '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
try {
await delDormBed(row.id);
ElMessage.success('删除成功');
await loadBedData(bedManageDialog.roomId);
// 更新房间的当前床位数
if (selectedBuilding.value) {
loadRoomData();
}
} catch (error) {
console.error('删除床位失败', error);
}
})
.catch(() => {});
};
/** 床位操作成功回调 */
const handleBedSuccess = async () => {
await loadBedData(bedManageDialog.roomId);
// 更新房间的当前床位数
if (selectedBuilding.value) {
loadRoomData();
}
};
/** 保存床位信息 */
const handleSaveBeds = async () => {
try {
ElMessage.success('床位信息保存成功');
await loadBedData(bedManageDialog.roomId);
} catch (error) {
console.error('保存床位信息失败', error);
ElMessage.error('保存失败,请重试');
}
};
/** 自动添加床位 */
const handleAddALlBed = async () => {
// 计算需要添加的床位数量
const maxBeds = bedManageDialog.maxBeds;
const currentBeds = bedList.value.length;
if (currentBeds >= maxBeds) {
ElMessage.warning(`已达到床位上限 (${currentBeds}/${maxBeds})`);
return;
}
// 计算要添加的床位数量,床位按成对添加(上铺和下铺)
const needToAdd = maxBeds - currentBeds;
const pairCount = Math.ceil(needToAdd / 2); // 需要添加的床位对数
try {
bedLoading.value = true;
// 计算起始编号(基于已有床位计算)
let startIndex = 1;
if (bedList.value.length > 0) {
// 尝试从现有床位编号中提取最大数字
const existingNumbers = bedList.value
.map((bed) => {
// 假设bedName中包含数字,如"1上"中的1
const match = bed.bedName.match(/^(\d+)/);
return match ? parseInt(match[1], 10) : 0;
})
.filter((num) => !isNaN(num));
if (existingNumbers.length > 0) {
startIndex = Math.max(...existingNumbers) + 1;
}
}
// 添加床位计数
let addedCount = 0;
// 创建延迟函数
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
for (let i = 0; i < pairCount; i++) {
const bedIndex = startIndex + i;
// 如果当前床位数 + 已添加的床位数 < 最大床位数,则添加上铺
if (currentBeds + addedCount < maxBeds) {
// 使用API需要的格式
const upperBed = {
id: '', // 新增时为空
bedCode: `${addedCount + 1}`,
bedName: `${addedCount + 1}`,
roomId: bedManageDialog.roomId,
bedStatus: '0', // 空闲
bedType: '2', // 上铺
remark: ''
};
await addDormBed(upperBed);
addedCount++;
// 添加0.5秒延迟
await delay(100);
}
// 如果当前床位数 + 已添加的床位数 < 最大床位数,则添加下铺
if (currentBeds + addedCount < maxBeds) {
// 使用API需要的格式
const lowerBed = {
id: '', // 新增时为空
bedCode: `${addedCount + 1}`,
bedName: `${addedCount + 1}`,
roomId: bedManageDialog.roomId,
bedStatus: '0', // 空闲
bedType: '1', // 下铺
remark: ''
};
await addDormBed(lowerBed);
addedCount++;
}
}
if (addedCount > 0) {
ElMessage.success(`成功添加 ${addedCount} 个床位`);
// 重新加载床位数据
await loadBedData(bedManageDialog.roomId);
// 更新房间的当前床位数
if (selectedBuilding.value) {
loadRoomData();
}
}
} catch (error) {
console.error('批量添加床位失败', error);
ElMessage.error('添加床位失败,请重试');
} finally {
bedLoading.value = false;
}
};
onMounted(() => {
getDormTreeData();
});
// 确保树加载后才能过滤
watch(
dormOptions,
() => {
if (buildingName.value) {
nextTick(() => {
watchBuildingName();
});
}
},
{ deep: true }
);
</script>
<style scoped>
.flex-layout {
display: flex;
gap: 20px;
}
.tree-sidebar {
min-width: 320px;
max-width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
height: calc(100vh - 120px); /* 调整高度以适应页面 */
}
.tree-sidebar .el-card {
height: 100%;
display: flex;
flex-direction: column;
}
.tree-sidebar .el-card__body {
flex: 1;
display: flex;
flex-direction: column;
padding: 15px;
overflow: hidden; /* 防止卡片内容溢出 */
}
.content-area {
flex: 1;
min-width: 0; /* 防止内容溢出 */
}
.room-container {
width: 100%;
}
.room-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 16px;
}
.room-card {
margin-bottom: 0;
transition: all 0.3s;
}
.room-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.room-disabled {
opacity: 0.7;
}
.room-info {
font-size: 14px;
}
.empty-tip {
min-height: 400px;
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
padding-right: 8px;
white-space: nowrap; /* 防止节点内容换行 */
overflow: visible; /* 确保长内容可见 */
min-width: 200px; /* 设置最小宽度 */
}
.disabled-node {
cursor: not-allowed;
}
.male-building {
color: var(--el-color-primary);
font-weight: bold;
}
.female-building {
color: #ff69b4; /* 粉色 */
font-weight: bold;
}
.tree-container {
overflow-y: auto; /* 纵向滚动条 */
overflow-x: auto; /* 横向滚动条 */
max-height: unset; /* 取消最大高度限制 */
min-height: 0; /* 允许容器收缩 */
height: 100%; /* 填满父容器 */
flex: 1; /* 占用剩余空间 */
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 5px;
margin-top: 10px;
}
.tree-container::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.tree-container::-webkit-scrollbar-thumb {
background-color: #c0c4cc;
border-radius: 3px;
}
.tree-container::-webkit-scrollbar-track {
background-color: #f5f7fa;
}
/* 当宿舍树节点太长时,确保有足够空间显示完整内容 */
.tree-container .el-tree-node__content {
min-width: fit-content;
height: auto;
min-height: 26px;
padding: 4px 0;
}
.tree-container .el-tree {
min-width: 100%; /* 确保树至少占满容器宽度 */
width: max-content; /* 允许内容扩展 */
}
</style>
@@ -0,0 +1,150 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="床位编号" prop="bedCode">
<el-input v-model="form.bedCode" placeholder="请输入床位编号" maxlength="20" />
</el-form-item>
<el-form-item label="床位名称" prop="bedName">
<el-input v-model="form.bedName" placeholder="请输入床位名称" maxlength="20" />
</el-form-item>
<el-form-item label="床位状态" prop="bedStatus">
<el-radio-group v-model="form.bedStatus">
<el-radio label="0">空闲</el-radio>
<el-radio label="1" disabled>已分配</el-radio>
<el-radio label="2">维修中</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="床位类型" prop="bedType">
<el-radio-group v-model="form.bedType">
<el-radio label="1">下铺</el-radio>
<el-radio label="2">上铺</el-radio>
<el-radio label="3">中铺</el-radio>
<el-radio label="4">独立床</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormBedForm } from '@/api/xgss/dormBed/types';
import { addDormBed, updateDormBed, getDormBed } from '@/api/xgss/dormBed';
const props = defineProps<{
modelValue: boolean;
title: string;
bedId?: string | number;
roomId: string | number;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: DormBedForm = {
id: '',
bedCode: '',
bedName: '',
roomId: '',
bedStatus: '0',
bedType: '4',
remark: ''
};
const form = reactive<DormBedForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
bedCode: [{ required: true, message: '床位编号不能为空', trigger: 'blur' }],
bedName: [{ required: true, message: '床位名称不能为空', trigger: 'blur' }],
bedStatus: [{ required: true, message: '床位状态不能为空', trigger: 'change' }],
bedType: [{ required: true, message: '床位类型不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
resetForm();
// 设置表单初始值
form.roomId = props.roomId;
// 编辑模式,获取床位详情
if (props.bedId) {
console.log('开始获取床位详情, bedId:', props.bedId);
getDormBed(props.bedId)
.then((res) => {
console.log('获取床位详情成功:', res.data);
if (res.data) {
Object.assign(form, res.data);
}
})
.catch((error) => {
console.error('获取床位详情失败', error);
});
}
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
Object.assign(form, { ...initFormData });
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
if (props.bedId) {
// 修改
form.id = props.bedId;
await updateDormBed(form);
} else {
// 新增
await addDormBed(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
// 添加默认导出以支持导入
defineOptions({
name: 'BedDialog'
});
</script>
@@ -0,0 +1,171 @@
<template>
<el-dialog v-model="visible" :title="title" width="500px" append-to-body @close="closeDialog">
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px">
<el-form-item label="楼层" prop="floorNum">
<el-input v-model="form.floorNum" placeholder="请输入楼层" :disabled="true" />
</el-form-item>
<el-form-item label="房间编号" prop="roomCode">
<el-input v-model="form.roomCode" placeholder="请输入房间编号" maxlength="20" />
</el-form-item>
<el-form-item label="房间名称" prop="roomName">
<el-input v-model="form.roomName" placeholder="请输入房间名称" maxlength="20" />
</el-form-item>
<el-form-item label="房间类型" prop="roomType">
<el-select v-model="form.roomType" placeholder="请选择房间类型" clearable style="width: 100%">
<el-option label="标准间" value="1" />
<el-option label="单人间" value="2" />
<el-option label="套间" value="3" />
</el-select>
</el-form-item>
<el-form-item label="床位数量" prop="actualBeds">
<el-input-number v-model="form.actualBeds" :min="1" :max="8" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="房间面积" prop="roomArea">
<el-input v-model="form.roomArea" placeholder="请输入房间面积" maxlength="20">
<template #append>
<span style="color: #000; background: transparent">平方米</span>
</template>
</el-input>
</el-form-item>
<el-form-item label="房间状态" prop="roomStatus">
<el-radio-group v-model="form.roomStatus">
<el-radio label="0">正常</el-radio>
<el-radio label="1">维修中</el-radio>
<el-radio label="2">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注内容" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="visible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import { DormRoomForm, DormRoomVO } from '@/api/xgss/dormRoom/types';
import { addDormRoom, updateDormRoom } from '@/api/xgss/dormRoom';
const props = defineProps<{
modelValue: boolean;
title: string;
roomId?: string | number;
floorNum: number;
buildingId: string | number;
room?: DormRoomVO;
}>();
const emit = defineEmits(['update:modelValue', 'success']);
const formRef = ref<FormInstance>();
const visible = ref(false);
// 表单数据
const initFormData: DormRoomForm = {
id: undefined,
roomCode: '',
roomName: '',
floorNum: 1,
buildingId: '',
actualBeds: 4,
roomArea: 0,
roomType: '1',
roomStatus: '0',
remark: ''
};
const form = reactive<DormRoomForm>({ ...initFormData });
// 表单校验规则
const rules = reactive<FormRules>({
roomCode: [{ required: true, message: '房间编号不能为空', trigger: 'blur' }],
roomName: [{ required: true, message: '房间名称不能为空', trigger: 'blur' }],
floorNum: [{ required: true, message: '楼层不能为空', trigger: 'blur' }],
actualBeds: [{ required: true, message: '床位数量不能为空', trigger: 'change' }],
roomType: [{ required: true, message: '房间类型不能为空', trigger: 'change' }],
roomStatus: [{ required: true, message: '房间状态不能为空', trigger: 'change' }]
});
// 监听visible属性变化
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
// 完全重置表单,确保没有残留数据
resetForm();
// 重新设置基本参数
form.floorNum = props.floorNum;
form.buildingId = props.buildingId;
// 如果是编辑模式且有room数据,填充表单数据
if (props.room) {
Object.keys(form).forEach((key) => {
if (key in props.room) {
form[key] = props.room[key];
}
});
}
}
}
);
// 监听visible变化,同步回父组件
watch(
() => visible.value,
(val) => {
emit('update:modelValue', val);
}
);
// 关闭对话框
const closeDialog = () => {
visible.value = false;
resetForm();
};
// 重置表单
const resetForm = () => {
// 使用深拷贝确保完全重置
Object.keys(form).forEach((key) => {
form[key] = initFormData[key];
});
formRef.value?.resetFields();
};
// 提交表单
const submitForm = () => {
formRef.value?.validate(async (valid) => {
if (valid) {
try {
console.log(props.roomId);
if (props.roomId) {
// 修改
form.id = props.roomId;
await updateDormRoom(form);
} else {
// 新增
await addDormRoom(form);
}
closeDialog();
emit('success');
} catch (error) {
console.error('操作失败', error);
}
}
});
};
// 添加默认导出以支持导入
defineOptions({
name: 'RoomDialog'
});
</script>
File diff suppressed because it is too large. Load diff
@@ -47,8 +47,8 @@
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { getTeacherList } from '@/api/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/ssglyxx/dormManager/index';
import { getTeacherList } from '@/api/xgss/xsssgly/dormManager';
import { addDormManager, updateDormManager } from '@/api/xgss/ssglyxx/dormManager/index';
import { ElMessage } from 'element-plus';
export default defineComponent({
@@ -0,0 +1,440 @@
<template>
<el-dialog title="分配管理范围" v-model="dialogVisible" width="500px" append-to-body>
<el-form ref="formRef" :model="form" label-width="100px">
<el-form-item label="管理员姓名">
<span>{{ admin?.name }}</span>
</el-form-item>
<el-form-item label="管理范围" prop="scope">
<!-- 校区类型使用原有的多选下拉框 -->
<el-select
v-if="type === 'campus'"
v-model="form.scope"
placeholder="请选择管理范围"
style="width: 100%"
clearable
filterable
multiple
collapse-tags
default-expand-all
collapse-tags-tooltip
>
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
<!-- 宿舍区类型使用树形选择器 -->
<el-tree-select
v-else-if="type === 'area'"
v-model="form.scope"
placeholder="请选择宿舍区"
style="width: 100%"
clearable
filterable
multiple
collapse-tags
collapse-tags-tooltip
default-expand-all
:data="treeData"
:props="{
children: 'children',
label: 'label',
disabled: 'disabled',
value: 'id'
}"
check-strictly
/>
<!-- 宿舍楼类型使用新的树形选择器 -->
<el-tree-select
v-else-if="type === 'building'"
v-model="form.scope"
placeholder="请选择宿舍楼"
style="width: 100%"
clearable
filterable
multiple
collapse-tags
collapse-tags-tooltip
default-expand-all
:data="buildingTreeData"
:props="{
children: 'children',
label: 'label',
disabled: 'disabled',
value: 'id'
}"
check-strictly
/>
<!-- 其他类型保持原有的单选下拉框 -->
<el-select v-else v-model="form.scope" placeholder="请选择管理范围" style="width: 100%" clearable filterable>
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { listDormCampus } from '@/api/xgss/xqxx/dormCampus';
import { getAreaTree } from '@/api/xgss/ssqxx/dormArea';
import { getBuildingTree } from '@/api/xgss/sslxx/dormBuilding';
import type { TreeSelectVo } from '@/api/xgss/ssqxx/dormArea/types';
import type { BuildingTreeSelectVo } from '@/api/xgss/sslxx/dormBuilding/types';
export default defineComponent({
name: 'ScopeDialog',
props: {
modelValue: {
type: Boolean,
default: false
},
admin: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val);
}
});
// 判断是否为校区类型的计算属性
const isCampusType = computed(() => props.type === 'campus');
// 判断是否为宿舍区类型的计算属性
const isAreaType = computed(() => props.type === 'area');
// 判断是否为宿舍楼类型的计算属性
const isBuildingType = computed(() => props.type === 'building');
const formRef = ref();
const form = reactive({
scope: isCampusType.value || isAreaType.value || isBuildingType.value ? [] : ''
});
// 下拉选项数据
const scopeOptions = ref<any[]>([]);
// 宿舍区树形数据
const treeData = ref<TreeSelectVo[]>([]);
// 宿舍楼树形数据
const buildingTreeData = ref<BuildingTreeSelectVo[]>([]);
// 标记是否完成数据加载的状态
const dataLoaded = ref(false);
// 处理scope字符串为数组的辅助函数(校区使用)
const parseScopeString = (scopeString: string) => {
if (!scopeString) return [];
// 尝试按逗号分隔并提取ID (假设格式为: "东校区,西校区")
return scopeString
.split(',')
.map((item) => {
// 在校区选项中查找匹配的校区名称
const option = scopeOptions.value.find((opt) => opt.label === item.trim());
return option ? option.value : null;
})
.filter((id) => id !== null); // 过滤掉未找到的项
};
// 处理宿舍区scope字符串为数组的辅助函数
const parseAreaScopeString = (scopeString: string) => {
if (!scopeString) return [];
// 尝试按逗号分隔宿舍区名称
const areaNames = scopeString.split(',').map((item) => item.trim());
// 在树形数据中查找匹配的宿舍区ID
const findAreaIds = (nodes: TreeSelectVo[]): number[] => {
if (!nodes || nodes.length === 0) return [];
let result: number[] = [];
for (const node of nodes) {
// 如果节点标签匹配任何一个宿舍区名称,添加该节点ID
if (areaNames.includes(node.label)) {
result.push(node.id);
}
// 如果有子节点,递归查找
if (node.children && node.children.length > 0) {
result = [...result, ...findAreaIds(node.children)];
}
}
return result;
};
return findAreaIds(treeData.value);
};
// 处理宿舍楼scope字符串为数组的辅助函数
const parseBuildingScopeString = (scopeString: string) => {
if (!scopeString) return [];
// 尝试按逗号分隔宿舍楼名称
const buildingNames = scopeString.split(',').map((item) => item.trim());
// 在树形数据中查找匹配的宿舍楼ID
const findBuildingIds = (nodes: BuildingTreeSelectVo[]): number[] => {
if (!nodes || nodes.length === 0) return [];
let result: number[] = [];
for (const node of nodes) {
// 如果是宿舍楼节点,且名称匹配,添加该节点ID
if (node.nodeType === 'building' && buildingNames.includes(node.label)) {
result.push(node.id);
}
// 如果有子节点,递归查找
if (node.children && node.children.length > 0) {
result = [...result, ...findBuildingIds(node.children)];
}
}
return result;
};
return findBuildingIds(buildingTreeData.value);
};
// 根据类型加载选项
const loadScopeOptions = async () => {
dataLoaded.value = false;
if (isCampusType.value) {
// 调用API获取校区列表
try {
const res = await listDormCampus();
// 适配el-select格式
console.log('校区数据加载成功:', res);
scopeOptions.value = (res.rows || []).map((item: any) => ({
value: item.id,
label: item.campusName
}));
// 校区支持多选,初始化为数组
form.scope = Array.isArray(form.scope) ? form.scope : [];
dataLoaded.value = true;
// 数据加载完成后,处理当前管理员的scope
handleAdminScope();
} catch (error) {
console.error('加载校区数据失败:', error);
dataLoaded.value = true;
}
} else if (isAreaType.value) {
// 宿舍区类型时加载树形数据
try {
const res = await getAreaTree();
if (res && res.data) {
treeData.value = res.data;
console.log('宿舍区树形数据加载成功:', treeData.value);
}
// 宿舍区使用树形选择,支持多选
form.scope = Array.isArray(form.scope) ? form.scope : [];
dataLoaded.value = true;
// 数据加载完成后,处理当前管理员的scope
handleAdminScope();
} catch (error) {
console.error('加载树形数据失败:', error);
dataLoaded.value = true;
}
} else if (isBuildingType.value) {
// 宿舍楼类型时加载树形数据
try {
const res = await getBuildingTree();
if (res && res.data) {
buildingTreeData.value = res.data;
console.log('宿舍楼树形数据加载成功:', buildingTreeData.value);
}
// 宿舍楼使用树形选择,支持多选
form.scope = Array.isArray(form.scope) ? form.scope : [];
dataLoaded.value = true;
// 数据加载完成后,处理当前管理员的scope
handleAdminScope();
} catch (error) {
console.error('加载宿舍楼树形数据失败:', error);
dataLoaded.value = true;
}
} else if (props.type === 'building') {
// 这段代码已被上面的isBuildingType.value条件替代,保留此处以兼容旧代码
// 将在下一次重构时移除
dataLoaded.value = true;
// 数据加载完成后,处理当前管理员的scope
handleAdminScope();
} else {
scopeOptions.value = [];
dataLoaded.value = true;
// 数据加载完成后,处理当前管理员的scope
handleAdminScope();
}
};
// 创建单独的函数处理admin的scope,确保在数据加载完成后执行
const handleAdminScope = () => {
const admin = props.admin;
if (!admin || !dataLoaded.value) return;
// 根据类型决定如何设置scope值
if (isCampusType.value) {
// 校区类型 - 支持多选
if (admin.scope) {
// 判断scope的类型
if (Array.isArray(admin.scope)) {
// 如果已经是数组,直接使用
form.scope = admin.scope;
} else if (typeof admin.scope === 'string') {
// 如果是字符串,需要解析
form.scope = parseScopeString(admin.scope);
} else {
// 其他情况,初始化为空数组
form.scope = [];
}
console.log('校区管理员回显数据:', form.scope);
} else {
form.scope = [];
}
} else if (isAreaType.value) {
// 宿舍区类型 - 支持多选
if (admin.scope) {
if (Array.isArray(admin.scope)) {
// 如果已经是数组,直接使用
form.scope = admin.scope;
} else if (typeof admin.scope === 'string') {
// 如果是字符串,需要解析为树形选择器所需的ID数组
form.scope = parseAreaScopeString(admin.scope);
} else {
form.scope = [];
}
console.log('宿舍区管理员回显数据:', form.scope);
} else {
form.scope = [];
}
} else if (isBuildingType.value) {
// 宿舍楼类型 - 支持多选
if (admin.scope) {
if (Array.isArray(admin.scope)) {
// 如果已经是数组,直接使用
form.scope = admin.scope;
} else if (typeof admin.scope === 'string') {
// 如果是字符串,需要解析为树形选择器所需的ID数组
form.scope = parseBuildingScopeString(admin.scope);
} else {
form.scope = [];
}
console.log('宿舍楼管理员回显数据:', form.scope);
} else {
form.scope = [];
}
} else {
// 其他类型 - 单选
form.scope = admin.scope || '';
}
};
// 监听type变化,动态加载选项
watch(
() => props.type,
() => {
loadScopeOptions();
},
{ immediate: true }
);
// 监听对话框可见性,确保每次打开时都重新加载数据
watch(
() => dialogVisible.value,
(newVal) => {
if (newVal) {
// 对话框打开时,确保重新加载数据
loadScopeOptions();
}
}
);
// 监听admin变化
watch(
() => props.admin,
() => {
// 如果数据已加载完成,则立即处理admin的scope
if (dataLoaded.value) {
handleAdminScope();
}
// 否则,会在loadScopeOptions完成后自动调用handleAdminScope
},
{ deep: true }
);
const submitForm = () => {
// 将form.scope转换为API所需的格式
let result;
if (Array.isArray(form.scope)) {
// 对于多选,发送选中的ID数组
result = {
sourceIds: form.scope
};
} else {
// 对于单选,将字符串ID包装在数组中
result = {
sourceIds: form.scope ? [form.scope] : []
};
}
emit('submit', result);
dialogVisible.value = false;
};
// 在对话框打开时,重置数据状态,防止旧数据影响新的显示
watch(
() => dialogVisible.value,
(newVal) => {
if (newVal) {
// 对话框打开时,先清空数据,确保重新加载
scopeOptions.value = [];
treeData.value = [];
buildingTreeData.value = [];
dataLoaded.value = false;
// 根据类型设置默认值
if (isCampusType.value || isAreaType.value || isBuildingType.value) {
form.scope = [];
} else {
form.scope = '';
}
// 然后加载新数据
loadScopeOptions();
}
}
);
return {
dialogVisible,
form,
formRef,
scopeOptions,
treeData,
buildingTreeData,
submitForm,
isCampusType,
type: computed(() => props.type)
};
}
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -218,11 +218,17 @@
import { ref, reactive, onMounted } from 'vue';
import AdminDialog from './components/AdminDialog.vue';
import ScopeDialog from './components/ScopeDialog.vue';
import axios from 'axios';
import { listDormManager } from '@/api/ssglyxx/dormManager';
import { delDormManager } from '@/api/ssglyxx/dormManager';
import request from '@/utils/request';
import { ElMessage, ElMessageBox } from 'element-plus';
// API路径
const apiUrl = {
list: '/ssglyxx/dormManager/list',
delete: '/ssglyxx/dormManager/',
assignScope: '/glysqgx/dormManagerSource/assignScope',
getScopeNames: '/glysqgx/dormManagerSource/getScopeNames'
};
const activeTab = ref('school');
// 各tab独立检索表单
@@ -287,38 +293,80 @@ async function fetchAdmins(type: string) {
Object.keys(params).forEach((key) => {
if (params[key] === '' || params[key] === undefined) delete params[key];
});
// const res = await axios.get('/ssglyxx/dormManager/list', { params });
const res = await listDormManager(params);
// 兼容TableDataInfo和R结构
let data = [];
if (Array.isArray(res.data)) {
data = res.data;
} else if (res.rows) {
data = res?.rows;
}
// console.log(data);
const mappedData = data.map((item: any) => ({
id: item.id,
name: item.managerName, // 映射
phone: item.phone,
sex: typeof item.gender === 'number' ? item.gender : Number(item.gender), // gender转数字
remark: item.remark
// 其他字段按需添加
}));
// console.log(mappedData);
switch (type) {
case 'school':
schoolAdmins.value = mappedData;
break;
case 'campus':
campusAdmins.value = mappedData;
break;
case 'area':
areaAdmins.value = mappedData;
break;
case 'building':
buildingAdmins.value = mappedData;
break;
try {
// 获取管理员列表
const res = await request({
url: apiUrl.list,
method: 'get',
params
});
// 兼容TableDataInfo和R结构
let data = [];
if (Array.isArray(res.data)) {
data = res.data;
} else if (res.rows) {
data = res?.rows;
}
// 预处理管理员数据
const mappedData = await Promise.all(
data.map(async (item: any) => {
// 基础字段映射
const manager = {
id: item.id,
name: item.managerName, // 映射
phone: item.phone,
sex: typeof item.gender === 'number' ? item.gender : Number(item.gender), // gender转数字
remark: item.remark,
scope: '' // 初始化scope字段
};
// 非全校级管理员需要获取授权范围
if (managerType > 1) {
try {
// 获取授权范围名称
const scopeRes = await request({
url: apiUrl.getScopeNames,
method: 'get',
params: {
managerId: item.id,
managerType: managerType
}
});
console.log(scopeRes.msg);
if (scopeRes) {
manager.scope = scopeRes.msg;
}
} catch (error) {
console.error('获取管理员授权范围失败:', error);
}
}
return manager;
})
);
// 更新对应类型的管理员数据
switch (type) {
case 'school':
schoolAdmins.value = mappedData;
break;
case 'campus':
campusAdmins.value = mappedData;
break;
case 'area':
areaAdmins.value = mappedData;
break;
case 'building':
buildingAdmins.value = mappedData;
break;
}
} catch (error) {
console.error('获取管理员列表失败:', error);
ElMessage.error('获取管理员列表失败');
}
}
@@ -336,8 +384,6 @@ function onAdd(type: string) {
dialogVisible.value = true;
}
function onEdit(type: string, admin: any) {
console.log('onEdit=============');
console.log(admin);
dialogMode.value = 'edit';
currentAdmin.value = { ...admin };
activeTab.value = type; // 确保当前标签页与编辑的管理员类型一致
@@ -352,7 +398,10 @@ async function onDelete(type: string, admin: any) {
})
.then(async () => {
try {
await delDormManager(admin.id);
await request({
url: apiUrl.delete + admin.id,
method: 'delete'
});
ElMessage.success('删除成功');
fetchAdmins(type); // 删除后刷新
} catch (e) {
@@ -369,23 +418,48 @@ function onScope(type: string, admin: any) {
scopeDialogVisible.value = true;
}
function onDialogSubmit(admin: any) {
// TODO: 新增/编辑时调用API,成功后刷新
const list = getListByType(activeTab.value);
if (dialogMode.value === 'add') {
admin.id = Date.now();
list.value.push(admin);
} else {
const idx = list.value.findIndex((a: any) => a.id === admin.id);
if (idx !== -1) list.value[idx] = { ...admin };
}
// 修改后立即刷新列表数据
fetchAdmins(activeTab.value);
dialogVisible.value = false;
}
function onScopeSubmit(scope: string) {
// TODO: 分配范围时调用API,成功后刷新
const list = getListByType(scopeType.value);
const idx = list.value.findIndex((a: any) => a.id === currentAdmin.value.id);
if (idx !== -1) list.value[idx].scope = scope;
scopeDialogVisible.value = false;
async function onScopeSubmit(scope: any) {
try {
// 将scope类型转换为数字
let authType = 1;
switch (scopeType.value) {
case 'school':
authType = 1;
break;
case 'campus':
authType = 2;
break;
case 'area':
authType = 3;
break;
case 'building':
authType = 4;
break;
}
// 调用API
await request({
url: apiUrl.assignScope,
method: 'post',
params: {
managerId: currentAdmin.value.id,
authType: authType
},
data: scope.sourceIds
});
// 成功后刷新数据并提示
ElMessage.success('管理范围分配成功');
fetchAdmins(scopeType.value);
scopeDialogVisible.value = false;
} catch (error) {
ElMessage.error('管理范围分配失败');
console.error(error);
}
}
function getListByType(type: string) {
switch (type) {
@@ -1,153 +0,0 @@
<template>
<el-dialog title="分配管理范围" v-model="dialogVisible" width="500px" append-to-body>
<el-form ref="formRef" :model="form" label-width="100px">
<el-form-item label="管理员姓名">
<span>{{ admin?.name }}</span>
</el-form-item>
<el-form-item label="管理范围" prop="scope">
<el-select
v-model="form.scope"
placeholder="请选择管理范围"
style="width: 100%"
clearable
filterable
:multiple="isCampusType"
collapse-tags
collapse-tags-tooltip
>
<el-option v-for="item in scopeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitForm">确 定</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, computed, watch, onMounted } from 'vue';
import { listDormCampus } from '@/api/xqxx/dormCampus';
export default defineComponent({
name: 'ScopeDialog',
props: {
modelValue: {
type: Boolean,
default: false
},
admin: {
type: Object,
default: () => ({})
},
type: {
type: String,
default: ''
}
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const dialogVisible = computed({
get: () => props.modelValue,
set: (val) => {
emit('update:modelValue', val);
}
});
// 判断是否为校区类型的计算属性
const isCampusType = computed(() => props.type === 'campus');
const formRef = ref();
const form = reactive({
scope: isCampusType.value ? [] : ''
});
// 选项数据
const scopeOptions = ref<any[]>([]);
// 根据类型加载选项
const loadScopeOptions = async () => {
if (isCampusType.value) {
// 调用API获取校区列表
const res = await listDormCampus();
// 适配el-select格式
console.log(res);
scopeOptions.value = (res.rows || []).map((item: any) => ({
value: item.id,
label: item.campusName
}));
// 校区支持多选,初始化为数组
form.scope = Array.isArray(form.scope) ? form.scope : [];
} else if (props.type === 'area') {
scopeOptions.value = [
{ value: '1号宿舍区', label: '1号宿舍区' },
{ value: '2号宿舍区', label: '2号宿舍区' },
{ value: '3号宿舍区', label: '3号宿舍区' }
];
// 非校区为单选,初始化为字符串
form.scope = Array.isArray(form.scope) ? '' : form.scope;
} else if (props.type === 'building') {
scopeOptions.value = [
{ value: '1号楼', label: '1号楼' },
{ value: '2号楼', label: '2号楼' },
{ value: '3号楼', label: '3号楼' },
{ value: '4号楼', label: '4号楼' }
];
// 非校区为单选,初始化为字符串
form.scope = Array.isArray(form.scope) ? '' : form.scope;
} else {
scopeOptions.value = [];
}
};
// 监听type变化,动态加载选项
watch(
() => props.type,
() => {
loadScopeOptions();
},
{ immediate: true }
);
watch(
() => props.admin,
(newVal) => {
if (newVal && newVal.scope) {
// 根据类型决定如何设置scope值
if (isCampusType.value) {
// 校区支持多选,确保scope是数组
form.scope = Array.isArray(newVal.scope) ? newVal.scope : [newVal.scope];
} else {
// 其他类型是单选
form.scope = newVal.scope;
}
}
},
{ deep: true, immediate: true }
);
const submitForm = () => {
emit('submit', form.scope);
dialogVisible.value = false;
};
return {
dialogVisible,
form,
formRef,
scopeOptions,
submitForm,
isCampusType
};
}
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,490 @@
<template>
<el-dialog
v-model="dialogVisible"
title="批量录取处理"
width="800px"
class="batch-admission-dialog"
:before-close="handleClose"
>
<div class="admission-container">
<!-- 选择的学生列表 -->
<el-card class="student-list-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">已选择的学生 ({{ selectedStudents.length }}人)</span>
</div>
</template>
<el-table
:data="selectedStudents"
border
height="250px"
style="width: 100%"
v-loading="loading"
>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="学生姓名" prop="studentName" width="100" />
<el-table-column label="身份证号" prop="idCard" width="180" />
<el-table-column label="录取通知书号码">
<template #default="scope">
<el-input
v-model="scope.row.tempNoticeNumber"
placeholder="请输入录取通知书号码"
:disabled="scope.row.isAutoGenerated"
@change="handleNoticeNumberChange(scope.row, scope.$index)"
/>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 录取通知书设置 -->
<el-card class="notice-setting-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">录取通知书设置</span>
</div>
</template>
<el-form
ref="settingFormRef"
:model="settingForm"
:rules="settingRules"
label-width="120px"
>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="批次前缀" prop="prefix">
<el-input
v-model="settingForm.prefix"
placeholder="例如:HDY2025-"
clearable
@change="handlePrefixChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="起始编号" prop="startNumber">
<el-input-number
v-model="settingForm.startNumber"
:min="1"
:max="9999"
controls-position="right"
@change="handleStartNumberChange"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="数字位数" prop="digitLength">
<el-input-number
v-model="settingForm.digitLength"
:min="1"
:max="6"
controls-position="right"
@change="handleDigitLengthChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="自动生成" prop="autoGenerate">
<el-switch
v-model="settingForm.autoGenerate"
active-text="启用"
inactive-text="禁用"
@change="handleAutoGenerateChange"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item>
<el-button type="primary" @click="generateNoticeNumbers">
<el-icon><RefreshRight /></el-icon>
批量生成通知书号
</el-button>
<el-button @click="clearNoticeNumbers">
<el-icon><Delete /></el-icon>
清空通知书号
</el-button>
</el-form-item>
<el-form-item label="预览示例">
<el-tag type="info">{{ previewNoticeNumber }}</el-tag>
</el-form-item>
</el-form>
</el-card>
<!-- 批量缴费确认 -->
<el-card class="payment-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">缴费确认</span>
</div>
</template>
<el-form
ref="paymentFormRef"
:model="paymentForm"
label-width="120px"
>
<el-form-item label="批量确认缴费">
<el-switch
v-model="paymentForm.confirmPayment"
active-text="全部已缴费"
inactive-text="保持原状态"
/>
</el-form-item>
</el-form>
</el-card>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
<el-icon><Check /></el-icon>
确认提交
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, toRefs, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { admissionProcess, confirmPayment } from '@/api/zsjy/enrollmentProcess'
import type { EnrollmentProcessVO } from '@/api/zsjy/enrollmentProcess'
interface Props {
visible: boolean
selectedStudents: EnrollmentProcessVO[]
}
interface Emits {
(e: 'update:visible', value: boolean): void
(e: 'success'): void
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
selectedStudents: () => []
})
const emit = defineEmits<Emits>()
// 对话框可见性
const dialogVisible = computed({
get: () => props.visible,
set: (value: boolean) => {
emit('update:visible', value)
}
})
// 表单引用和加载状态
const settingFormRef = ref<ElFormInstance>()
const paymentFormRef = ref<ElFormInstance>()
const loading = ref(false)
const submitLoading = ref(false)
// 学生数据,添加临时字段
const selectedStudents = computed(() => {
return props.selectedStudents.map(student => ({
...student,
tempNoticeNumber: student.noticeNumber || '',
isAutoGenerated: false
}))
})
// 通知书设置表单
const settingForm = reactive({
prefix: 'HDY2025-',
startNumber: 1001,
digitLength: 4,
autoGenerate: true
})
// 预览示例
const previewNoticeNumber = computed(() => {
const prefix = settingForm.prefix || ''
const number = settingForm.startNumber.toString().padStart(settingForm.digitLength, '0')
return `${prefix}${number}`
})
// 缴费确认表单
const paymentForm = reactive({
confirmPayment: false
})
// 表单验证规则
const settingRules = {
startNumber: [
{ required: true, message: '请输入起始编号', trigger: 'blur' }
],
digitLength: [
{ required: true, message: '请选择数字位数', trigger: 'blur' }
]
}
// 监听对话框打开,初始化表单
watch(() => props.visible, (newVal) => {
if (newVal) {
initForm()
}
})
/** 初始化表单 */
const initForm = () => {
settingForm.prefix = 'HDY2025-'
settingForm.startNumber = 1001
settingForm.digitLength = 4
settingForm.autoGenerate = true
paymentForm.confirmPayment = false
// 如果第一个学生已有通知书号,尝试提取前缀和起始编号
if (props.selectedStudents.length > 0 && props.selectedStudents[0].noticeNumber) {
const noticeNumber = props.selectedStudents[0].noticeNumber
const match = noticeNumber.match(/^([^\d]*)(\d+)$/)
if (match) {
settingForm.prefix = match[1] || 'HDY2025-'
const number = parseInt(match[2])
settingForm.startNumber = isNaN(number) ? 1001 : number
settingForm.digitLength = match[2].length
}
}
// 延迟一下再生成通知书号,确保组件已渲染完成
setTimeout(() => {
if (settingForm.autoGenerate) {
generateNoticeNumbers()
}
}, 300)
}
/** 生成录取通知书号 */
const generateNoticeNumbers = () => {
const { prefix, startNumber, digitLength } = settingForm
selectedStudents.value.forEach((student, index) => {
const number = (startNumber + index).toString().padStart(digitLength, '0')
student.tempNoticeNumber = `${prefix}${number}`
student.isAutoGenerated = true
})
}
/** 清空通知书号 */
const clearNoticeNumbers = () => {
selectedStudents.value.forEach(student => {
student.tempNoticeNumber = ''
student.isAutoGenerated = false
})
}
/** 前缀变更 */
const handlePrefixChange = () => {
if (settingForm.autoGenerate) {
generateNoticeNumbers()
}
}
/** 起始编号变更 */
const handleStartNumberChange = () => {
if (settingForm.autoGenerate) {
generateNoticeNumbers()
}
}
/** 数字位数变更 */
const handleDigitLengthChange = () => {
if (settingForm.autoGenerate) {
generateNoticeNumbers()
}
}
/** 自动生成开关变更 */
const handleAutoGenerateChange = (val: boolean) => {
if (val) {
generateNoticeNumbers()
}
}
/** 手动修改通知书号 */
const handleNoticeNumberChange = (row: any, index: number) => {
// 标记为手动修改
row.isAutoGenerated = false
}
/** 提交处理 */
const handleSubmit = async () => {
if (!settingFormRef.value) return
try {
await settingFormRef.value.validate()
// 检查是否有未填写通知书号的学生
const invalidStudents = selectedStudents.value.filter(s => !s.tempNoticeNumber)
if (invalidStudents.length > 0) {
ElMessage.warning(`有${invalidStudents.length}名学生未填写录取通知书号码`)
return
}
submitLoading.value = true
// 处理所有选中的学生
const promises = selectedStudents.value.map(async (student) => {
// 录取处理
await admissionProcess({
processId: student.id as number,
admissionResult: '1', // 处理通过
noticeNumber: student.tempNoticeNumber,
admissionOpinion: '批量录取处理'
})
// 如果需要确认缴费
if (paymentForm.confirmPayment && student.paymentStatus !== '1') {
await confirmPayment(student.id as number)
}
return student.id
})
await Promise.all(promises)
ElMessage.success('批量录取处理成功')
emit('success')
handleClose()
} catch (error) {
console.error('批量处理提交失败:', error)
ElMessage.error('批量处理提交失败')
} finally {
submitLoading.value = false
}
}
/** 关闭对话框 */
const handleClose = () => {
dialogVisible.value = false
}
</script>
<style scoped>
.batch-admission-dialog :deep(.el-dialog) {
border-radius: 16px;
overflow: hidden;
}
.batch-admission-dialog :deep(.el-dialog__header) {
background: linear-gradient(135deg, rgb(93, 135, 255) 0%, rgb(73, 190, 255) 100%);
color: white;
padding: 20px 24px;
margin: 0;
}
.batch-admission-dialog :deep(.el-dialog__title) {
color: white;
font-size: 18px;
font-weight: 600;
}
.batch-admission-dialog :deep(.el-dialog__headerbtn) {
.el-dialog__close {
color: white;
font-size: 20px;
transition: all 0.3s ease;
&:hover {
color: rgba(255, 255, 255, 0.8);
transform: rotate(90deg);
}
}
}
.batch-admission-dialog :deep(.el-dialog__body) {
padding: 20px 24px;
background: #ffffff;
}
.batch-admission-dialog :deep(.el-dialog__footer) {
padding: 16px 24px 20px;
background: #ffffff;
border-top: 1px solid rgba(93, 135, 255, 0.1);
}
.admission-container {
max-height: 60vh;
overflow-y: auto;
}
.student-list-card,
.notice-setting-card,
.payment-card {
margin-bottom: 16px;
border-radius: 8px;
border: 1px solid #e4e7ed;
}
.student-list-card:last-child,
.notice-setting-card:last-child,
.payment-card:last-child {
margin-bottom: 0;
}
.student-list-card :deep(.el-card__header),
.notice-setting-card :deep(.el-card__header),
.payment-card :deep(.el-card__header) {
background: #f8f9fa;
border-bottom: 1px solid #e4e7ed;
padding: 12px 16px;
}
.card-header {
display: flex;
align-items: center;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
position: relative;
}
.section-title::before {
content: '';
width: 3px;
height: 16px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 8px;
display: inline-block;
}
/* 对话框底部样式 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
/* 滚动条样式 */
.admission-container::-webkit-scrollbar {
width: 6px;
}
.admission-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
.admission-container::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
.admission-container::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
@@ -0,0 +1,706 @@
<template>
<div class="page-container">
<!-- 搜索区域 -->
<el-card class="search-card">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="80px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input
v-model="queryParams.studentName"
placeholder="请输入学生姓名"
clearable
style="width: 100%"
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="身份证号" prop="idCard">
<el-input
v-model="queryParams.idCard"
placeholder="请输入身份证号"
clearable
style="width: 100%"
@keyup.enter="handleQuery"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="缴费状态" prop="paymentStatus">
<el-select v-model="queryParams.paymentStatus" placeholder="请选择缴费状态" clearable style="width: 100%">
<el-option
v-for="dict in zsjy_payment_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- 统计卡片 -->
<el-row :gutter="16" class="stat-cards">
<el-col :span="8">
<el-card class="stat-card stat-card-primary" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.total || 0 }}</div>
<div class="stat-label">总录取数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="stat-card stat-card-warning" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.pending || 0 }}</div>
<div class="stat-label">待处理</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card class="stat-card stat-card-success" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Select /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.completed || 0 }}</div>
<div class="stat-label">已完成</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 操作按钮区域 -->
<el-card class="button-card">
<template #header>
<div class="card-header">
<span class="toolbar-title">新生录取管理</span>
<div class="toolbar-actions">
<el-button
v-hasPermi="['zsjy:admission:export']"
type="warning"
plain
icon="Download"
@click="handleExport"
>
导出
</el-button>
<el-button
type="info"
plain
icon="Refresh"
@click="handleRefreshStats"
>
刷新统计
</el-button>
</div>
</div>
</template>
</el-card>
<!-- 表格区域 -->
<el-card class="table-card">
<div class="table-container">
<el-table
ref="processTableRef"
v-loading="loading"
:data="processList"
row-key="id"
class="scroll-table"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学生信息" align="center" width="200">
<template #default="scope">
<div style="text-align: left;">
<div style="font-weight: 600; color: #303133;">{{ scope.row.studentName }}</div>
<div style="font-size: 12px; color: #909399; margin-top: 2px;">{{ scope.row.idCard }}</div>
</div>
</template>
</el-table-column>
<el-table-column label="当前状态" align="center" width="120">
<template #default="scope">
<dict-tag :options="zsjy_enrollment_status" :value="scope.row.currentStatus" />
</template>
</el-table-column>
<el-table-column label="缴费状态" align="center" width="100">
<template #default="scope">
<dict-tag :options="zsjy_payment_status" :value="scope.row.paymentStatus" />
</template>
</el-table-column>
<el-table-column label="录取通知书" align="center" width="150" prop="noticeNumber" show-overflow-tooltip />
<el-table-column label="处理时间" align="center" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.admissionTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看详情" placement="top">
<el-button
v-hasPermi="['zsjy:admission:query']"
link
type="primary"
icon="View"
@click="handleDetail(scope.row)"
/>
</el-tooltip>
<el-tooltip content="录取处理" placement="top">
<el-button
v-hasPermi="['zsjy:admission:process']"
link
type="warning"
icon="Edit"
@click="handleAdmissionProcess(scope.row)"
/>
</el-tooltip>
<el-tooltip content="确认缴费" placement="top">
<el-button
v-hasPermi="['zsjy:admission:payment']"
link
type="success"
icon="CreditCard"
:disabled="scope.row.paymentStatus === '1'"
@click="handleConfirmPayment(scope.row)"
/>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<pagination
v-show="total > 0"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
:total="total"
@pagination="getList"
/>
</div>
</el-card>
<!-- 流程详情对话框 -->
<ProcessDetailDialog
v-model:visible="detailDialogVisible"
:process-data="currentProcess"
/>
<!-- 批量录取处理对话框 -->
<BatchAdmissionDialog
v-model:visible="batchAdmissionDialogVisible"
:selected-students="selectedStudents"
@success="handleDialogSuccess"
/>
</div>
</template>
<script setup name="AdmissionManagement" lang="ts">
import { reactive, ref, toRefs, onMounted } from 'vue'
import {
listEnrollmentProcess,
getEnrollmentProcess,
getProcessStatistics,
confirmPayment,
exportEnrollmentProcess
} from '@/api/zsjy/enrollmentProcess'
import type { EnrollmentProcessVO, EnrollmentProcessQuery } from '@/api/zsjy/enrollmentProcess'
import { ElMessageBox, ElMessage } from 'element-plus'
import { Clock, Select, CircleCheck } from '@element-plus/icons-vue'
import ProcessDetailDialog from '../enrollmentProcess/components/ProcessDetailDialog.vue'
import BatchAdmissionDialog from './components/BatchAdmissionDialog.vue'
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { zsjy_enrollment_status, zsjy_payment_status } = toRefs<any>(
proxy?.useDict('zsjy_enrollment_status', 'zsjy_payment_status')
)
// 页面数据
const data = reactive<{
queryParams: EnrollmentProcessQuery
}>({
queryParams: {
pageNum: 1,
pageSize: 10,
studentName: undefined,
idCard: undefined,
currentStatus: '2', // 默认只查询录取处理中状态
paymentStatus: undefined,
params: {}
}
})
const { queryParams } = toRefs(data)
// 页面状态
const queryFormRef = ref<ElFormInstance>()
const processTableRef = ref<ElTableInstance>()
const loading = ref(true)
const ids = ref<Array<string | number>>([])
const total = ref(0)
const processList = ref<EnrollmentProcessVO[]>([])
const selectedStudents = ref<EnrollmentProcessVO[]>([])
// 统计数据
const statistics = ref({
total: 0,
pending: 0,
completed: 0
})
// 对话框状态
const detailDialogVisible = ref(false)
const batchAdmissionDialogVisible = ref(false)
const currentProcess = ref<Partial<EnrollmentProcessVO>>({})
/** 查询录取处理中的学生列表 */
const getList = async () => {
loading.value = true
try {
// 确保查询条件中包含currentStatus=2(录取处理中)
const params = { ...queryParams.value, currentStatus: '2' }
const res = await listEnrollmentProcess(params)
processList.value = res.rows || []
total.value = res.total || 0
} catch (error) {
console.error('获取列表失败:', error)
// 如果是404错误(接口不存在),使用模拟数据
if (error?.response?.status === 404 || error?.message?.includes('404')) {
console.warn('后端接口不存在,使用模拟数据')
// 创建模拟数据
processList.value = [
{
id: 1946510098494091300,
enrollmentId: 1001,
studentName: '张三',
idCard: '330101200501010001',
currentStatus: '2',
infoAuditStatus: '1',
admissionStatus: '0',
finalAuditStatus: '0',
paymentStatus: '0',
isCompleted: 0,
createTime: '2025-07-20 10:28:09'
},
{
id: 1946510098494091301,
enrollmentId: 1946510098494091266,
studentName: '赵四',
idCard: '340403201509091234',
currentStatus: '2',
infoAuditStatus: '1',
admissionStatus: '0',
finalAuditStatus: '0',
paymentStatus: '0',
isCompleted: 0,
createTime: '2025-07-20 10:28:18'
}
]
total.value = 2
proxy?.$modal.msgWarning('后端接口暂未实现,显示模拟数据')
} else {
// 其他错误,设置空数据
processList.value = []
total.value = 0
}
} finally {
loading.value = false
}
}
/** 获取统计数据 */
const getStatistics = async () => {
try {
// 基于当前流程列表计算统计数据
const stats = processList.value || []
const paymentCounts = stats.reduce((acc: any, item: any) => {
const status = item.paymentStatus || '0'
acc[status] = (acc[status] || 0) + 1
return acc
}, {})
statistics.value = {
total: stats.length,
pending: paymentCounts['0'] || 0, // 未缴费
completed: paymentCounts['1'] || 0 // 已缴费
}
} catch (error) {
console.error('获取统计数据失败:', error)
// 设置默认值
statistics.value = {
total: 0,
pending: 0,
completed: 0
}
}
}
/** 搜索按钮操作 */
const handleQuery = async () => {
queryParams.value.pageNum = 1
try {
await getList()
await getStatistics()
} catch (error) {
console.error('搜索失败:', error)
}
}
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields()
handleQuery()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: EnrollmentProcessVO[]) => {
ids.value = selection.map(item => item.id)
selectedStudents.value = selection
}
/** 查看详情 */
const handleDetail = (row: EnrollmentProcessVO) => {
currentProcess.value = row
detailDialogVisible.value = true
}
/** 录取处理 */
const handleAdmissionProcess = () => {
if (selectedStudents.value.length === 0) {
ElMessage.warning('请选择需要处理的学生')
return
}
batchAdmissionDialogVisible.value = true
}
/** 缴费确认 */
const handleConfirmPayment = async (row: EnrollmentProcessVO) => {
try {
await ElMessageBox.confirm('确认该学生已完成缴费?', '缴费确认', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
await confirmPayment(row.id as number)
ElMessage.success('缴费确认成功')
await getList()
await getStatistics()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('缴费确认失败')
}
}
}
/** 导出按钮操作 */
const handleExport = () => {
proxy?.download('/zsjy/enrollment/process/export', {
...queryParams.value,
currentStatus: '2'
}, `admission_students_${new Date().getTime()}.xlsx`)
}
/** 刷新统计数据 */
const handleRefreshStats = async () => {
await getStatistics()
ElMessage.success('统计数据已刷新')
}
/** 对话框成功回调 */
const handleDialogSuccess = async () => {
await getList()
await getStatistics()
}
// 初始化
onMounted(async () => {
try {
// 先获取列表数据,再基于列表数据计算统计
await getList()
await getStatistics()
} catch (error) {
console.error('页面初始化失败:', error)
proxy?.$modal.msgError('页面初始化失败,请刷新页面重试')
}
})
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 搜索卡片样式 */
.search-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.search-card :deep(.el-card__body) {
padding: 20px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 统计卡片样式 */
.stat-cards {
margin-bottom: 16px;
}
.stat-card {
border-radius: 8px;
transition: all 0.3s ease;
height: 90px;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-content {
display: flex;
align-items: center;
padding: 5px 12px;
height: 100%;
box-sizing: border-box;
}
.stat-icon {
font-size: 24px;
margin-right: 12px;
opacity: 0.8;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 20px;
font-weight: bold;
line-height: 1;
margin-bottom: 3px;
}
.stat-label {
font-size: 13px;
opacity: 0.7;
line-height: 1;
}
.stat-card-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.stat-card-success {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
color: white;
}
.stat-card-warning {
background: linear-gradient(135deg, #ff9a56 0%, #ffad56 100%);
color: white;
}
/* 按钮卡片样式 */
.button-card {
margin-bottom: 10px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.button-card :deep(.el-card__header) {
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
padding: 16px 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-title {
font-size: 18px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
}
.toolbar-title::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, rgb(93, 135, 255), rgb(73, 190, 255));
border-radius: 2px;
margin-right: 12px;
}
.toolbar-actions {
display: flex;
gap: 12px;
}
/* 表格卡片样式 */
.table-card {
margin-top: 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 表格容器 - 支持横向滚动 */
.table-container {
width: 100%;
margin-bottom: 0;
}
/* 横向滚动表格样式 */
.scroll-table {
width: 100%;
}
/* 表格横向滚动条样式优化 */
.scroll-table :deep(.el-table__body-wrapper) {
overflow-x: auto !important; /* 自动显示横向滚动条 */
overflow-y: auto;
}
.scroll-table :deep(.el-table__header-wrapper) {
overflow-x: hidden;
}
/* 强制显示表格横向滚动条 - 适用于所有浏览器 */
.scroll-table :deep(.el-table__body-wrapper) {
scrollbar-width: thin; /* Firefox */
scrollbar-color: #409eff #f0f0f0; /* Firefox - thumb track */
}
/* Webkit 浏览器的横向滚动条样式 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar {
height: 12px !important; /* 增加滚动条高度 */
width: 12px !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
background: #f0f0f0 !important;
border-radius: 6px !important;
border: 1px solid #e4e7ed !important; /* 添加边框使其更明显 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
background: #409eff !important; /* 使用主题色,更明显 */
border-radius: 6px !important;
border: 2px solid #f0f0f0 !important; /* 添加白色边框 */
min-height: 20px !important; /* 确保最小高度 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
background: #337ecc !important; /* 悬停时更深的蓝色 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active {
background: #2b6cb0 !important; /* 点击时的颜色 */
}
/* 确保滚动条始终可见的额外设置 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-corner {
background: #f0f0f0;
}
/* 固定列样式优化 */
.scroll-table :deep(.el-table__fixed-right) {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
.scroll-table :deep(.el-table__fixed-left) {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
/* 确保固定列在滚动时有正确的背景色 */
.scroll-table :deep(.el-table__fixed-right .el-table__cell),
.scroll-table :deep(.el-table__fixed-left .el-table__cell) {
background-color: #fff;
}
/* 表格行悬停效果在固定列上的同步 */
.scroll-table :deep(.el-table__row:hover .el-table__cell) {
background-color: #f5f7fa !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 操作按钮样式 */
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
align-items: center;
white-space: nowrap;
}
/* 确保操作按钮宽度一致 */
.action-buttons .el-button {
width: 27px !important;
height: 27px !important;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
}
</style>
+694 -89
View File
@@ -1,7 +1,8 @@
<template>
<div class="p-2">
<div class="page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div class="search" v-show="showSearch">
<el-card v-show="showSearch" class="search-card">
<el-form :model="queryParams" ref="queryFormRef" :inline="true" label-width="68px">
<el-form-item label="姓名" prop="name">
<el-input v-model="queryParams.name" placeholder="请输入姓名" clearable style="width: 240px" @keyup.enter="handleQuery" />
@@ -26,69 +27,96 @@
<el-form-item label="中考地区" prop="examRegion">
<el-input v-model="queryParams.examRegion" placeholder="请输入中考地区" clearable style="width: 240px" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
<el-form-item label="报名状态" prop="enrollmentStatus">
<el-select v-model="queryParams.enrollmentStatus" placeholder="请选择报名状态" clearable style="width: 240px">
<el-option :value="ENROLLMENT_STATUS.PENDING" label="待审核" />
<el-option :value="ENROLLMENT_STATUS.AUDITING" label="信息审核中" />
<el-option :value="ENROLLMENT_STATUS.PROCESSING" label="录取处理中" />
<el-option :value="ENROLLMENT_STATUS.CONFIRMING" label="待最终确认" />
<el-option :value="ENROLLMENT_STATUS.COMPLETED" label="录取完成" />
<el-option :value="ENROLLMENT_STATUS.REJECTED" label="已驳回" />
</el-select>
</el-form-item>
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-form>
</div>
</el-card>
</transition>
<el-card shadow="never">
<!-- 操作按钮区域 -->
<el-card class="button-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<div class="card-header">
<span class="toolbar-title">招生信息管理</span>
<div class="toolbar-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['zsjy:enrollment:info:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['zsjy:enrollment:info:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['zsjy:enrollment:info:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['zsjy:enrollment:info:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<el-button type="success" plain icon="Check" :disabled="multiple" @click="handleBatchAudit()" v-hasPermi="['zsjy:enrollment:info:edit']">批量审核</el-button>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</div>
</div>
</template>
</el-card>
<el-table v-loading="loading" :data="enrollmentInfoList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" v-if="false" />
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true" width="180" />
<el-table-column label="性别" align="center" prop="gender">
<template #default="scope">
<dict-tag :options="zsjy_gender" :value="scope.row.gender" />
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="phone1" :show-overflow-tooltip="true" width="130" />
<el-table-column label="准考证号" align="center" prop="examNumber" :show-overflow-tooltip="true" width="130" />
<el-table-column label="中考地区" align="center" prop="examRegion" :show-overflow-tooltip="true" />
<el-table-column label="中考成绩" align="center" prop="totalScore" width="100" />
<el-table-column label="报考专业" align="center" prop="appliedMajor" :show-overflow-tooltip="true" width="120" />
<el-table-column label="报名时间" align="center" prop="applicationTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.applicationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="160">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['zsjy:enrollment:info:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['zsjy:enrollment:info:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['zsjy:enrollment:info:remove']"></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<!-- 表格区域 -->
<el-card class="table-card">
<div class="table-container">
<el-table v-loading="loading" :data="enrollmentInfoList" class="scroll-table" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="主键ID" align="center" prop="id" v-if="false" />
<el-table-column label="姓名" align="center" prop="name" :show-overflow-tooltip="true" />
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true" width="180" />
<el-table-column label="性别" align="center" prop="gender">
<template #default="scope">
<dict-tag :options="zsjy_gender" :value="scope.row.gender" />
</template>
</el-table-column>
<el-table-column label="联系电话" align="center" prop="phone1" :show-overflow-tooltip="true" width="130" />
<el-table-column label="准考证号" align="center" prop="examNumber" :show-overflow-tooltip="true" width="130" />
<el-table-column label="中考地区" align="center" prop="examRegion" :show-overflow-tooltip="true" />
<el-table-column label="中考成绩" align="center" prop="totalScore" width="100" />
<el-table-column label="报考专业" align="center" prop="appliedMajor" :show-overflow-tooltip="true" width="120" />
<el-table-column label="报名状态" align="center" prop="enrollmentStatus" width="120">
<template #default="scope">
<dict-tag :options="zsjy_enrollment_status" :value="scope.row.enrollmentStatus" />
</template>
</el-table-column>
<el-table-column label="报名时间" align="center" prop="applicationTime" width="160">
<template #default="scope">
<span>{{ parseTime(scope.row.applicationTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="220" fixed="right">
<template #default="scope">
<div class="action-buttons">
<el-tooltip content="查看" placement="top">
<el-button link type="primary" icon="View" @click="handleView(scope.row)" v-hasPermi="['zsjy:enrollment:info:query']"></el-button>
</el-tooltip>
<el-tooltip content="修改" placement="top">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['zsjy:enrollment:info:edit']"></el-button>
</el-tooltip>
<el-tooltip content="审核" placement="top">
<el-button link type="success" icon="Check" @click="handleAudit(scope.row)" v-hasPermi="['zsjy:enrollment:info:edit']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['zsjy:enrollment:info:remove']"></el-button>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
</div>
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 分页 -->
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改招生报名信息对话框 -->
@@ -105,7 +133,7 @@
</el-col>
<el-col :span="8">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="form.idCard" placeholder="请输入身份证号" @change="onIdCardChange" />
<el-input v-model="form.idCard" placeholder="请输入身份证号" @input="onIdCardChange" @change="onIdCardChange" />
</el-form-item>
</el-col>
<el-col :span="8">
@@ -472,15 +500,204 @@
</div>
</template>
</el-dialog>
<!-- 审核对话框 -->
<el-dialog :title="auditDialog.title" v-model="auditDialog.visible" width="800px" append-to-body>
<!-- 学生信息展示部分,批量审核时隐藏 -->
<div v-if="!isBatchAudit" class="student-info-section">
<h3 class="section-title">学生基本信息</h3>
<el-tabs type="card">
<el-tab-pane label="基础信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="姓名">{{ currentStudent.name || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ currentStudent.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="zsjy_gender" :value="currentStudent.gender || ''" />
</el-descriptions-item>
<el-descriptions-item label="出生日期">
{{ parseTime(currentStudent.birthDate, '{y}-{m}-{d}') || '' }}
</el-descriptions-item>
<el-descriptions-item label="民族">{{ currentStudent.nation || '' }}</el-descriptions-item>
<el-descriptions-item label="是否住校">
<dict-tag :options="sys_yes_no" :value="currentStudent.isBoarding?.toString() || ''" />
</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ currentStudent.phone1 || '' }}</el-descriptions-item>
<el-descriptions-item label="备用电话">{{ currentStudent.phone2 || '' }}</el-descriptions-item>
<el-descriptions-item label="家庭地址">{{ currentStudent.homeAddress || '' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="中考信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="初中毕业学校">{{ currentStudent.middleSchool || '' }}</el-descriptions-item>
<el-descriptions-item label="准考证号">{{ currentStudent.examNumber || '' }}</el-descriptions-item>
<el-descriptions-item label="中考地区">{{ currentStudent.examRegion || '' }}</el-descriptions-item>
<el-descriptions-item label="中考满分">{{ currentStudent.totalScoreFull || '' }}</el-descriptions-item>
<el-descriptions-item label="中考成绩">{{ currentStudent.totalScore || '' }}</el-descriptions-item>
<el-descriptions-item label="语文成绩">{{ currentStudent.chineseScore || '' }}</el-descriptions-item>
<el-descriptions-item label="数学成绩">{{ currentStudent.mathScore || '' }}</el-descriptions-item>
<el-descriptions-item label="英语成绩">{{ currentStudent.englishScore || '' }}</el-descriptions-item>
<el-descriptions-item label="科学成绩">{{ currentStudent.scienceScore || '' }}</el-descriptions-item>
<el-descriptions-item label="社会成绩">{{ currentStudent.socialScore || '' }}</el-descriptions-item>
<el-descriptions-item label="体育成绩">{{ currentStudent.peScore || '' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="报考信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="报考专业">{{ currentStudent.appliedMajor || '' }}</el-descriptions-item>
<el-descriptions-item label="专业代码">{{ currentStudent.majorCode || '' }}</el-descriptions-item>
<el-descriptions-item label="校服尺码">{{ currentStudent.uniformSize || '' }}</el-descriptions-item>
<el-descriptions-item label="健康状况">{{ currentStudent.healthStatus || '' }}</el-descriptions-item>
<el-descriptions-item label="特长爱好">{{ currentStudent.specialties || '' }}</el-descriptions-item>
<el-descriptions-item label="获奖情况">{{ currentStudent.awards || '' }}</el-descriptions-item>
<el-descriptions-item label="报名状态">
<dict-tag :options="zsjy_enrollment_status" :value="currentStudent.enrollmentStatus || ''" />
</el-descriptions-item>
<el-descriptions-item label="报名时间">
{{ parseTime(currentStudent.applicationTime, '{y}-{m}-{d} {h}:{i}:{s}') || '' }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
<!-- 批量审核时的提示和学生列表 -->
<div v-if="isBatchAudit" class="batch-audit-info">
<el-alert
title="批量审核"
type="warning"
:closable="false">
<p>您正在对 <strong>{{ ids.length }}</strong> 条学生信息进行批量审核操作</p>
<p v-if="selectedStudents.length < ids.length">
<i class="el-icon-warning"></i> 注意:只能显示 {{ selectedStudents.length }} 条学生信息,有 {{ ids.length - selectedStudents.length }} 条信息未能加载
</p>
</el-alert>
<!-- 批量审核的学生列表 -->
<div class="student-list-section">
<h4>已选择的学生列表:</h4>
<el-table
:data="selectedStudents"
stripe
style="width: 100%"
max-height="400"
:loading="batchLoading">
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="姓名" prop="name" min-width="80" />
<el-table-column label="身份证号" prop="idCard" min-width="150" />
<el-table-column label="联系电话" prop="phone1" min-width="120" />
<el-table-column label="中考成绩" prop="totalScore" min-width="80" />
<el-table-column label="报考专业" prop="appliedMajor" min-width="120" />
<el-table-column label="查看" align="center" width="80">
<template #default="scope">
<el-button link type="primary" icon="View" @click="viewStudentDetail(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 审核表单 -->
<el-form ref="auditFormRef" :model="auditForm" label-width="100px" class="audit-form">
<el-form-item label="审核结果" prop="result">
<el-radio-group v-model="auditForm.result">
<el-radio :value="'pass'" label="审核通过"></el-radio>
<el-radio :value="'reject'" label="审核不通过"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核意见" prop="remark">
<el-input v-model="auditForm.remark" type="textarea" placeholder="请输入审核意见" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="auditDialog.visible = false">取 消</el-button>
<el-button type="primary" @click="submitAudit" :loading="buttonLoading">确 认</el-button>
</div>
</template>
</el-dialog>
<!-- 批量审核时查看学生详情的对话框 -->
<el-dialog :title="'学生详细信息 - ' + (detailStudent?.name || '')" v-model="detailDialog.visible" width="800px" append-to-body>
<div class="student-detail-section">
<el-tabs type="card">
<el-tab-pane label="基础信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="姓名">{{ detailStudent.name || '' }}</el-descriptions-item>
<el-descriptions-item label="身份证号">{{ detailStudent.idCard || '' }}</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="zsjy_gender" :value="detailStudent.gender || ''" />
</el-descriptions-item>
<el-descriptions-item label="出生日期">
{{ parseTime(detailStudent.birthDate, '{y}-{m}-{d}') || '' }}
</el-descriptions-item>
<el-descriptions-item label="民族">{{ detailStudent.nation || '' }}</el-descriptions-item>
<el-descriptions-item label="是否住校">
<dict-tag :options="sys_yes_no" :value="detailStudent.isBoarding?.toString() || ''" />
</el-descriptions-item>
<el-descriptions-item label="联系电话">{{ detailStudent.phone1 || '' }}</el-descriptions-item>
<el-descriptions-item label="备用电话">{{ detailStudent.phone2 || '' }}</el-descriptions-item>
<el-descriptions-item label="家庭地址">{{ detailStudent.homeAddress || '' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="中考信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="初中毕业学校">{{ detailStudent.middleSchool || '' }}</el-descriptions-item>
<el-descriptions-item label="准考证号">{{ detailStudent.examNumber || '' }}</el-descriptions-item>
<el-descriptions-item label="中考地区">{{ detailStudent.examRegion || '' }}</el-descriptions-item>
<el-descriptions-item label="中考满分">{{ detailStudent.totalScoreFull || '' }}</el-descriptions-item>
<el-descriptions-item label="中考成绩">{{ detailStudent.totalScore || '' }}</el-descriptions-item>
<el-descriptions-item label="语文成绩">{{ detailStudent.chineseScore || '' }}</el-descriptions-item>
<el-descriptions-item label="数学成绩">{{ detailStudent.mathScore || '' }}</el-descriptions-item>
<el-descriptions-item label="英语成绩">{{ detailStudent.englishScore || '' }}</el-descriptions-item>
<el-descriptions-item label="科学成绩">{{ detailStudent.scienceScore || '' }}</el-descriptions-item>
<el-descriptions-item label="社会成绩">{{ detailStudent.socialScore || '' }}</el-descriptions-item>
<el-descriptions-item label="体育成绩">{{ detailStudent.peScore || '' }}</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
<el-tab-pane label="报考信息">
<el-descriptions :column="3" border>
<el-descriptions-item label="报考专业">{{ detailStudent.appliedMajor || '' }}</el-descriptions-item>
<el-descriptions-item label="专业代码">{{ detailStudent.majorCode || '' }}</el-descriptions-item>
<el-descriptions-item label="校服尺码">{{ detailStudent.uniformSize || '' }}</el-descriptions-item>
<el-descriptions-item label="健康状况">{{ detailStudent.healthStatus || '' }}</el-descriptions-item>
<el-descriptions-item label="特长爱好">{{ detailStudent.specialties || '' }}</el-descriptions-item>
<el-descriptions-item label="获奖情况">{{ detailStudent.awards || '' }}</el-descriptions-item>
<el-descriptions-item label="报名状态">
<dict-tag :options="zsjy_enrollment_status" :value="detailStudent.enrollmentStatus || ''" />
</el-descriptions-item>
<el-descriptions-item label="报名时间">
{{ parseTime(detailStudent.applicationTime, '{y}-{m}-{d} {h}:{i}:{s}') || '' }}
</el-descriptions-item>
</el-descriptions>
</el-tab-pane>
</el-tabs>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="detailDialog.visible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="EnrollmentInfo" lang="ts">
import { listEnrollmentInfo, getEnrollmentInfo, delEnrollmentInfo, addEnrollmentInfo, updateEnrollmentInfo } from '@/api/zsjy/enrollment';
import { addEnrollmentInfo, delEnrollmentInfo, exportEnrollmentInfo, getEnrollmentInfo, getEnrollmentInfoOptions, listEnrollmentInfo, updateEnrollmentInfo, updateEnrollmentStatus } from '@/api/zsjy/enrollment';
import { createProcessFromEnrollment } from '@/api/zsjy/enrollmentProcess';
import { EnrollmentInfoVO, EnrollmentInfoQuery, EnrollmentInfoForm } from '@/api/zsjy/enrollment/types';
import { ComponentInternalInstance, getCurrentInstance, reactive, ref, toRefs, onMounted } from 'vue';
import { ComponentInternalInstance, getCurrentInstance, reactive, ref, toRefs, onMounted, watch, computed } from 'vue';
import { FormInstance } from 'element-plus';
// 状态常量
const ENROLLMENT_STATUS = {
PENDING: '0', // 待审核
AUDITING: '1', // 信息审核中
PROCESSING: '2', // 录取处理中
CONFIRMING: '3', // 待最终确认
COMPLETED: '4', // 录取完成
REJECTED: '9' // 已驳回
};
interface DialogOption {
visible: boolean;
title: string;
@@ -493,13 +710,14 @@ interface PageData<T, S> {
}
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { zsjy_gender, sys_yes_no, zsjy_major, zsjy_uniform_size } = toRefs<any>(
proxy?.useDict('zsjy_gender', 'sys_yes_no', 'zsjy_major', 'zsjy_uniform_size')
const { zsjy_gender, sys_yes_no, zsjy_major, zsjy_uniform_size, zsjy_enrollment_status } = toRefs<any>(
proxy?.useDict('zsjy_gender', 'sys_yes_no', 'zsjy_major', 'zsjy_uniform_size', 'zsjy_enrollment_status')
);
const enrollmentInfoList = ref<EnrollmentInfoVO[]>([]);
const buttonLoading = ref(false);
const loading = ref(true);
const batchLoading = ref(false);
const showSearch = ref(true);
const ids = ref<Array<string | number>>([]);
const single = ref(true);
@@ -507,6 +725,34 @@ const multiple = ref(true);
const total = ref(0);
const majorOptions = ref<Array<{ value: string, label: string, majorCode: string }>>([]);
// 批量审核分页相关
const batchCurrentPage = ref(1);
const batchPageSize = ref(10);
// 计算当前页的学生列表
const paginatedStudents = computed(() => {
const startIndex = (batchCurrentPage.value - 1) * batchPageSize.value;
const endIndex = startIndex + batchPageSize.value;
return selectedStudents.value.slice(startIndex, endIndex);
});
// 性别显示转换计算属性
const genderDisplay = computed(() => {
if (!form.value.gender) return '';
const genderOption = zsjy_gender.value?.find(item => item.value === form.value.gender);
return genderOption ? genderOption.label : form.value.gender;
});
// 处理批量审核页面大小变化
const handleBatchSizeChange = (val: number) => {
batchPageSize.value = val;
};
// 处理批量审核页码变化
const handleBatchCurrentChange = (val: number) => {
batchCurrentPage.value = val;
};
const queryFormRef = ref<FormInstance>();
const enrollmentInfoFormRef = ref<FormInstance>();
@@ -520,6 +766,42 @@ const viewDialog = reactive<DialogOption>({
title: ''
});
const auditDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const detailDialog = reactive<DialogOption>({
visible: false,
title: ''
});
const auditForm = reactive({
id: undefined as undefined | string | number,
result: 'pass', // 审核结果,'pass' 或 'reject'
remark: ''
});
// 修复 currentStudent 的定义
const currentStudent = reactive<Partial<EnrollmentInfoVO>>({
id: undefined,
name: '',
idCard: '',
gender: '',
phone1: '',
examNumber: '',
examRegion: '',
totalScore: undefined,
appliedMajor: '',
applicationTime: undefined
});
// 批量审核标志
const isBatchAudit = ref(false);
// 批量审核选中的学生列表
const selectedStudents = ref<EnrollmentInfoVO[]>([]);
const viewForm = reactive<EnrollmentInfoForm>({
id: undefined,
name: undefined,
@@ -550,6 +832,19 @@ const viewForm = reactive<EnrollmentInfoForm>({
awards: undefined
});
const detailStudent = reactive<Partial<EnrollmentInfoVO>>({
id: undefined,
name: '',
idCard: '',
gender: '',
phone1: '',
examNumber: '',
examRegion: '',
totalScore: undefined,
appliedMajor: '',
applicationTime: undefined
});
const initFormData: EnrollmentInfoForm = {
id: undefined,
name: undefined,
@@ -593,6 +888,7 @@ const data = reactive<PageData<EnrollmentInfoForm, EnrollmentInfoQuery>>({
majorCode: undefined,
phone1: undefined,
examRegion: undefined,
enrollmentStatus: ENROLLMENT_STATUS.AUDITING, // 默认只查询"信息审核中"状态的学生
params: {}
},
rules: {
@@ -631,15 +927,23 @@ const { queryParams, form, rules } = toRefs(data);
const onIdCardChange = () => {
const idCard = form.value.idCard;
if (idCard && idCard.length === 18) {
// 提取出生日期
const year = idCard.substring(6, 10);
const month = idCard.substring(10, 12);
const day = idCard.substring(12, 14);
form.value.birthDate = `${year}-${month}-${day}`;
// 提取性别(奇数为男M,偶数为女F)
const genderCode = parseInt(idCard.substring(16, 17));
form.value.gender = genderCode % 2 === 1 ? 'M' : 'F';
try {
// 提取出生日期
const year = idCard.substring(6, 10);
const month = idCard.substring(10, 12);
const day = idCard.substring(12, 14);
form.value.birthDate = `${year}-${month}-${day}`;
// 提取性别(奇数为男,偶数为女)
const genderCode = parseInt(idCard.substring(16, 17));
const genderValue = genderCode % 2 === 1 ? 'M' : 'F';
// 直接设置性别,无需额外检查
form.value.gender = genderValue;
console.log('设置性别为:', genderValue);
} catch (error) {
console.error('解析身份证信息时出错:', error);
}
}
};
@@ -693,6 +997,26 @@ const getMajorOptions = () => {
}
};
/** 获取校服尺码选项 */
const getUniformSizeOptions = () => {
// 创建默认的校服尺码选项数组(如果字典数据不可用)
const defaultUniformSizes = [
{ value: 'XS', label: 'XS', remark: '加小号' },
{ value: 'S', label: 'S', remark: '小号' },
{ value: 'M', label: 'M', remark: '中号' },
{ value: 'L', label: 'L', remark: '大号' },
{ value: 'XL', label: 'XL', remark: '加大号' },
{ value: 'XXL', label: 'XXL', remark: '加加大号' },
{ value: 'XXXL', label: 'XXXL', remark: '加加加大号' },
];
// 检查字典数据是否存在,如果不存在则使用默认值
if (!zsjy_uniform_size.value || zsjy_uniform_size.value.length === 0) {
console.warn('校服尺码字典数据不可用,使用默认值');
zsjy_uniform_size.value = defaultUniformSizes;
}
};
/** 查询招生报名信息列表 */
const getList = async () => {
loading.value = true;
@@ -710,7 +1034,10 @@ const cancel = () => {
/** 表单重置 */
const reset = () => {
form.value = { ...initFormData };
form.value = {
...initFormData,
enrollmentStatus: ENROLLMENT_STATUS.AUDITING // 默认设置为"信息审核中"状态
};
enrollmentInfoFormRef.value?.resetFields();
};
@@ -723,6 +1050,8 @@ const handleQuery = () => {
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef.value?.resetFields();
// 确保重置后状态过滤条件依然生效
queryParams.value.enrollmentStatus = ENROLLMENT_STATUS.AUDITING;
handleQuery();
};
@@ -763,26 +1092,25 @@ const submitForm = () => {
enrollmentInfoFormRef.value?.validate(async (valid: boolean) => {
if (valid) {
buttonLoading.value = true;
if (form.value.id) {
await updateEnrollmentInfo(form.value)
.then(() => {
proxy?.$modal.msgSuccess('修改成功');
dialog.visible = false;
getList();
})
.finally(() => {
buttonLoading.value = false;
});
} else {
await addEnrollmentInfo(form.value)
.then(() => {
proxy?.$modal.msgSuccess('新增成功');
dialog.visible = false;
getList();
})
.finally(() => {
buttonLoading.value = false;
});
try {
// 如果是新增,设置默认状态为"信息审核中"
if (!form.value.id) {
form.value.enrollmentStatus = ENROLLMENT_STATUS.AUDITING;
}
if (form.value.id) {
await updateEnrollmentInfo(form.value);
proxy?.$modal.msgSuccess('修改成功');
} else {
await addEnrollmentInfo(form.value);
proxy?.$modal.msgSuccess('新增成功');
}
dialog.visible = false;
getList();
} catch (error) {
proxy?.$modal.msgError(form.value.id ? '修改失败' : '新增失败');
} finally {
buttonLoading.value = false;
}
}
});
@@ -808,18 +1136,235 @@ const handleExport = () => {
);
};
/** 批量审核按钮操作 */
const handleBatchAudit = async () => {
if (ids.value.length === 0) {
proxy?.$modal.msgError('请选择要审核的招生报名信息');
return;
}
// 加载选中的学生信息
selectedStudents.value = [];
buttonLoading.value = true;
batchLoading.value = true;
try {
// 使用Promise.all并行获取所有选中学生信息,更可靠
const maxRetries = 3; // 最多重试3次
// 创建一个带重试功能的获取学生信息的函数
const getStudentInfoWithRetry = async (id: string | number, retryCount = 0): Promise<any> => {
try {
return await getEnrollmentInfo(Number(id));
} catch (error) {
if (retryCount < maxRetries) {
console.log(`获取学生信息失败,ID: ${id},正在重试第${retryCount + 1}次...`);
await new Promise(resolve => setTimeout(resolve, 500)); // 等待500ms后重试
return getStudentInfoWithRetry(id, retryCount + 1);
}
console.error(`获取学生信息失败,ID: ${id},已重试${maxRetries}次`, error);
return null;
}
};
// 使用带重试功能的函数获取所有学生信息
const requests = ids.value.map(id => getStudentInfoWithRetry(id));
const results = await Promise.all(requests);
// 过滤掉请求失败的结果
selectedStudents.value = results
.filter(res => res !== null && res.data)
.map(res => res.data);
// 验证是否获取到了所有学生
if (selectedStudents.value.length < ids.value.length) {
proxy?.$modal.msgWarning(`有${ids.value.length - selectedStudents.value.length}个学生信息获取失败,请重试或单独审核`);
}
// 不直接赋值ids,以避免类型错误
auditForm.id = undefined; // 批量审核时id为undefined,特殊处理
auditForm.result = 'pass'; // 批量审核时默认设为审核通过
auditForm.remark = '';
auditDialog.title = '批量审核报名信息';
isBatchAudit.value = true; // 设置为批量审核模式
auditDialog.visible = true;
} catch (error) {
console.error('加载学生信息失败:', error);
proxy?.$modal.msgError('加载学生信息失败,请稍后重试');
} finally {
buttonLoading.value = false;
batchLoading.value = false;
}
};
/** 审核按钮操作 */
const handleAudit = async (row: EnrollmentInfoVO) => {
auditForm.id = row.id;
// 设置当前学生信息
Object.assign(currentStudent, row);
auditForm.result = row.enrollmentStatus === ENROLLMENT_STATUS.REJECTED ? 'reject' : 'pass'; // 根据当前状态设置审核结果
auditForm.remark = '';
auditDialog.title = '审核报名信息';
isBatchAudit.value = false; // 设置为单个审核模式
auditDialog.visible = true;
};
/** 提交审核 */
const submitAudit = async () => {
buttonLoading.value = true;
try {
// 根据审核结果设置状态值
const status = auditForm.result === 'pass'
? ENROLLMENT_STATUS.PROCESSING // 审核通过,设为"录取处理中"
: ENROLLMENT_STATUS.REJECTED; // 审核不通过,设为"已驳回"
if (auditForm.id) {
// 单个审核
await updateEnrollmentStatus(auditForm.id, status);
} else {
// 批量审核 - 直接传递ids数组
await updateEnrollmentStatus(ids.value, status);
}
proxy?.$modal.msgSuccess('审核成功');
auditDialog.visible = false;
getList();
} catch (error) {
console.error('审核失败:', error);
proxy?.$modal.msgError('审核失败,请稍后重试');
} finally {
buttonLoading.value = false;
}
};
/** 取消查看 */
const cancelView = () => {
viewDialog.visible = false;
};
/** 查看流程按钮操作 */
const handleViewProcess = (row: EnrollmentInfoVO) => {
// 跳转到流程管理页面,并筛选该学生的流程
const router = getCurrentInstance()?.appContext.app.config.globalProperties.$router;
if (router) {
router.push({
path: '/zsjy/enrollmentProcess',
query: {
enrollmentId: row.id,
studentName: row.name
}
});
}
};
/** 查看学生详情按钮操作 */
const viewStudentDetail = async (row: EnrollmentInfoVO) => {
const res = await getEnrollmentInfo(row.id);
Object.assign(detailStudent, res.data);
detailDialog.visible = true;
detailDialog.title = '学生详细信息 - ' + (detailStudent.name || '');
};
// 监听对话框关闭事件,重置状态
watch(() => auditDialog.visible, (val) => {
if (!val) {
// 对话框关闭时重置状态
auditForm.id = undefined;
auditForm.result = 'pass';
auditForm.remark = '';
isBatchAudit.value = false;
selectedStudents.value = [];
}
});
// 监听详情对话框关闭事件,重置状态
watch(() => detailDialog.visible, (val) => {
if (!val) {
// 清空详情学生数据
Object.keys(detailStudent).forEach(key => {
if (key !== 'id') {
detailStudent[key] = key === 'name' || key === 'idCard' || key === 'gender' ||
key === 'phone1' || key === 'examNumber' || key === 'examRegion' ||
key === 'appliedMajor' ? '' : undefined;
} else {
detailStudent[key] = undefined;
}
});
}
});
onMounted(() => {
getList();
getMajorOptions();
getUniformSizeOptions(); // 添加校服尺码选项初始化
});
</script>
<style scoped>
.page-container {
padding: 20px;
background-color: #f5f7fa;
}
.search-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.search-btn-container {
text-align: right;
}
.button-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar-title {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 10px;
}
.table-card {
margin-bottom: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.table-container {
overflow-x: auto;
}
.scroll-table {
width: 100%;
}
.pagination-container {
text-align: right;
padding-top: 20px;
}
.form-section {
margin-bottom: 20px;
}
@@ -845,4 +1390,64 @@ onMounted(() => {
white-space: pre-wrap;
word-break: break-all;
}
</style>
.action-buttons {
display: flex;
justify-content: center;
gap: 10px;
}
.student-info-section {
margin-bottom: 20px;
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
}
.section-title {
font-size: 16px;
font-weight: bold;
color: #303133;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #409eff;
}
.batch-audit-info {
margin-bottom: 20px;
}
.student-list-section {
margin-top: 15px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 8px;
}
.batch-pagination {
margin-top: 15px;
text-align: right;
}
.audit-form {
margin-top: 20px;
}
.student-detail-section {
margin-top: 20px;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.selected-gender {
margin-top: 5px;
font-size: 12px;
color: #67c23a;
padding: 2px 5px;
border-radius: 4px;
background-color: #f0f9eb;
display: inline-block;
}
</style>
+75 -137
View File
@@ -1,166 +1,104 @@
# 录取流程状态管理模块
# 录取流程管理模块
## 📋 模块说明
## 功能概述
- **模块名称**: 录取流程状态管理 (Enrollment Process Management)
- **模块简称**: enrollmentProcess
- **对应后端**: backend/ruoyi-modules/ruoyi-zsjy/
- **对应API**: web/src/api/zsjy/enrollmentProcess.ts
录取流程管理模块用于管理学生从报名到最终录取的整个流程,包括信息审核、录取处理、最终确认等环节。该模块提供了完整的流程状态管理、审核操作和数据统计功能。
## 🎯 功能概述
## 主要功能
录取流程状态管理模块提供完整的招生录取流程管理功能,包括:
1. **流程状态管理**:查看和管理所有录取流程的状态,包括待审核、信息审核中、录取处理中、待最终确认、录取完成和已驳回等状态。
2. **待我审核**:根据用户角色权限,显示需要当前用户审核的录取流程记录,支持信息审核、录取处理和最终确认等操作。
3. **审核记录**:查看所有审核操作的历史记录,包括审核人、审核时间、审核意见等信息。
### 📊 统计概览
- 总流程数统计
- 待处理流程统计
- 处理中流程统计
- 已完成流程统计
## 页面说明
### 🔍 查询功能
- 学生姓名、身份证号模糊查询
- 当前状态、审核状态筛选查询
- 录取处理状态、最终审核状态查询
- 完成状态筛选查询
### 1. 流程状态列表页 (index.vue)
### 📝 流程管理
- 流程信息查看、新增、修改、删除
- 流程详情查看
- 流程数据批量导出
- 功能:查看和管理所有录取流程的状态
- 权限:`zsjy:enrollment:process:list`
- 操作:查看详情、审核、驳回、导出等
### 🔄 流程操作
- **信息审核**: A老师进行学生报名信息审核
- **录取处理**: B老师进行录取处理,生成通知书号码
- **缴费确认**: 确认学生缴费状态
- **最终审核**: 部门领导进行最终确认
- **完成录取**: 完成整个录取流程
- **驳回流程**: 在任意环节驳回流程
- **重置流程**: 管理员重置流程状态
### 2. 待我审核页面 (pending.vue)
### 📈 流程状态
```
待审核(0) → 信息审核中(1) → 录取处理中(2) → 待最终确认(3) → 录取完成(4)
↓
已驳回(9)
```
- 功能:根据用户角色权限,显示需要当前用户审核的录取流程记录
- 权限:`zsjy:enrollment:process:pending`
- 操作:
- 查看详情:查看报名信息和流程状态详情
- 审核通过:根据当前流程状态执行对应的审核通过操作
- 审核驳回:根据当前流程状态执行对应的审核驳回操作
- 导出:导出待审核数据
## 🎨 页面设计
### 3. 我的审核记录页面 (myaudit.vue)
### 页面布局
- **搜索区域**: 7个搜索条件,支持组合查询
- **统计卡片**: 4个统计指标,渐变色彩设计
- **表格区域**: 完整的流程信息展示
- **操作按钮**: 查看、审核、处理、更多操作
- 功能:查看当前用户的所有审核操作记录
- 权限:`zsjy:enrollment:process:myaudit`
- 操作:查看详情、导出等
### 对话框设计
- **流程详情**: 完整展示流程各阶段信息
- **信息审核**: 审核结果选择和意见填写
- **录取处理**: 处理结果、通知书号码、处理意见
- **最终审核**: 确认结果和审核意见
- **驳回流程**: 驳回原因填写
- **新增/编辑**: 基本流程信息管理
## 数据字典
## 🔧 技术特性
### 流程状态 (zsjy_enrollment_status)
### 前端技术
- **Vue 3.5.13** + **TypeScript 5.8.3**
- **Element Plus** UI组件库
- **Composition API** 组合式API
- **响应式设计** 适配多端
- 0:待审核
- 1:信息审核中
- 2:录取处理中
- 3:待最终确认
- 4:录取完成
- 9:已驳回
### 样式设计
- **参考后勤管理模块** 保持风格一致
- **渐变色卡片** 美观的统计展示
- **悬停效果** 良好的交互体验
- **操作按钮** 图标化设计
### 审核状态 (zsjy_audit_status)
### 权限控制
- 精细化权限控制,12个权限点
- 不同角色不同操作权限
- 按钮级权限控制
- 0:待审核
- 1:审核通过
- 9:审核驳回
## 🔐 权限设计
### 处理状态 (zsjy_process_status)
### 权限点列表
- `zsjy:enrollmentProcess:list` - 查看流程列表
- `zsjy:enrollmentProcess:query` - 查看流程详情
- `zsjy:enrollmentProcess:add` - 新增流程
- `zsjy:enrollmentProcess:edit` - 修改流程
- `zsjy:enrollmentProcess:remove` - 删除流程
- `zsjy:enrollmentProcess:export` - 导出流程
- `zsjy:enrollmentProcess:audit` - 信息审核权限
- `zsjy:enrollmentProcess:process` - 录取处理权限
- `zsjy:enrollmentProcess:finalAudit` - 最终审核权限
- `zsjy:enrollmentProcess:complete` - 完成录取权限
- `zsjy:enrollmentProcess:reject` - 驳回流程权限
- `zsjy:enrollmentProcess:reset` - 重置流程权限
- 0:待处理
- 1:处理完成
- 9:处理驳回
### 角色权限建议
```
A老师(信息审核员):
- list, query, audit
## 权限说明
B老师(录取处理员):
- list, query, process, confirmPayment
- `zsjy:enrollment:process:list`:查看流程状态列表
- `zsjy:enrollment:process:query`:查看流程状态详情
- `zsjy:enrollment:process:audit`:执行审核操作
- `zsjy:enrollment:process:pending`:查看待我审核列表
- `zsjy:enrollment:process:myaudit`:查看我的审核记录
- `zsjy:enrollment:process:export`:导出流程状态数据
部门领导:
- list, query, finalAudit, complete
## 角色权限配置
系统管理员:
- 所有权限(包括 reset, reject)
```
- **信息审核员 (zsjy_info_auditor)**:负责信息审核环节,拥有 `zsjy:enrollment:process:audit` 权限
- **录取处理员 (zsjy_admission_processor)**:负责录取处理环节,拥有 `zsjy:enrollment:process:audit` 权限
- **最终确认员 (zsjy_final_confirmer)**:负责最终确认环节,拥有 `zsjy:enrollment:process:audit` 权限
- **管理员 (admin)**:拥有所有权限,可以查看和操作所有流程
## 📁 文件结构
## 开发说明
### 前端文件结构
```
enrollmentProcess/
├── index.vue # 主页面
├── README.md # 说明文档
└── components/ # 组件目录
├── ProcessDetailDialog.vue # 流程详情对话框
├── InfoAuditDialog.vue # 信息审核对话框
├── AdmissionProcessDialog.vue # 录取处理对话框
├── FinalAuditDialog.vue # 最终审核对话框
├── RejectProcessDialog.vue # 驳回流程对话框
└── ProcessFormDialog.vue # 新增/编辑对话框
web/src/views/zsjy/enrollmentProcess/
├── index.vue # 流程状态列表页
├── pending.vue # 待我审核页面
├── myaudit.vue # 我的审核记录页面
├── detail.vue # 详情页面
└── components/ # 组件目录
├── AuditForm.vue # 审核表单组件
└── ProcessTimeline.vue # 流程时间线组件
```
## 🚀 使用说明
### API接口
### 基本操作
1. **查询流程**: 使用搜索条件筛选流程信息
2. **查看详情**: 点击"查看"按钮查看完整流程信息
3. **信息审核**: 对待审核状态的流程进行审核
4. **录取处理**: 对审核通过的流程进行录取处理
5. **最终审核**: 领导对处理完成的流程进行最终确认
```
web/src/api/zsjy/enrollmentProcess.ts
```
### 流程操作
1. **审核操作**: 选择审核结果(通过/驳回)并填写意见
2. **处理操作**: 选择处理结果,填写通知书号码和处理意见
3. **缴费确认**: 确认学生已完成缴费
4. **完成录取**: 完成整个录取流程
5. **驳回流程**: 在任意环节驳回流程到初始状态
### 高级功能
1. **批量操作**: 选择多个流程进行批量删除
2. **导出数据**: 根据查询条件导出Excel文件
3. **统计刷新**: 手动刷新统计数据
4. **流程重置**: 管理员重置流程状态(谨慎操作)
## 💡 注意事项
- 流程状态有严格的流转规则,不能跳跃式操作
- 驳回的流程会回到初始状态,需要重新审核
- 重置操作不可恢复,仅限管理员使用
- 统计数据会实时更新,也可手动刷新
- 导出功能会根据当前查询条件生成文件
## 🔗 相关模块
- 招生信息管理模块 (enrollment) - 报名信息管理
- 系统管理模块 (system) - 用户权限管理
- 工作流模块 (workflow) - 流程引擎支持
---
**🎉 录取流程状态管理模块开发完成!严格按照后勤管理模块的样式风格设计,提供完整的流程管理功能!**
主要接口:
- `listEnrollmentProcess`:获取流程状态列表
- `listMyAuditProcess`:获取待我审核列表
- `getEnrollmentProcess`:获取流程状态详情
- `infoAudit`:执行信息审核
- `admissionProcess`:执行录取处理
- `finalAudit`:执行最终确认
- `exportEnrollmentProcess`:导出流程状态数据
@@ -180,19 +180,19 @@ const resetForm = () => {
/** 提交处理 */
const handleSubmit = async () => {
if (!processFormRef.value) return
try {
await processFormRef.value.validate()
submitLoading.value = true
await admissionProcess({
id: props.processData.id as number,
processResult: processForm.processResult,
processOpinion: processForm.processOpinion || undefined,
noticeNumber: processForm.noticeNumber || undefined
processId: props.processData.id as number,
admissionResult: processForm.processResult,
admissionOpinion: processForm.processOpinion || undefined,
noticeNumber: processForm.processResult === '1' ? processForm.noticeNumber : undefined
})
ElMessage.success('处理提交成功')
emit('success')
handleClose()
@@ -444,4 +444,4 @@ const handleClose = () => {
background: #fff !important;
color: #303133 !important;
}
</style>
</style>
@@ -2,16 +2,26 @@
<el-dialog
v-model="dialogVisible"
title="信息审核"
width="600px"
width="1000px"
class="audit-dialog"
:before-close="handleClose"
>
<div class="audit-container">
<!-- 学生信息展示 -->
<!-- 学生基本信息展示 -->
<el-card class="student-info-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">学生信息</span>
<span class="section-title">学生基本信息</span>
<el-button
type="primary"
plain
size="small"
@click="handleViewDetails"
:loading="detailLoading"
>
<el-icon><View /></el-icon>
查看完整信息
</el-button>
</div>
</template>
<el-descriptions :column="2" border>
@@ -30,6 +40,137 @@
</el-descriptions>
</el-card>
<!-- 学生详细信息展示(可展开/收起) -->
<el-card v-if="showDetails" class="student-details-card" shadow="never">
<template #header>
<div class="card-header">
<span class="section-title">学生详细填报信息</span>
<el-button
type="text"
size="small"
@click="showDetails = false"
>
<el-icon><ArrowUp /></el-icon>
收起
</el-button>
</div>
</template>
<div v-loading="detailLoading" class="details-content">
<!-- 基本信息 -->
<div class="info-section">
<h4 class="info-section-title">基本信息</h4>
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="姓名">
<span class="detail-value">{{ studentDetails.name || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="身份证号">
<span class="detail-value">{{ studentDetails.idCard || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="性别">
<dict-tag :options="zsjy_gender" :value="studentDetails.gender" />
</el-descriptions-item>
<el-descriptions-item label="出生日期">
<span class="detail-value">{{ studentDetails.birthDate || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="联系电话">
<span class="detail-value">{{ studentDetails.phone1 || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="备用电话">
<span class="detail-value">{{ studentDetails.phone2 || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="民族">
<span class="detail-value">{{ studentDetails.nation || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="是否住宿">
<dict-tag :options="sys_yes_no" :value="studentDetails.isBoarding" />
</el-descriptions-item>
<el-descriptions-item label="健康状况">
<span class="detail-value">{{ studentDetails.healthStatus || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
<el-descriptions :column="1" border size="small" class="address-desc">
<el-descriptions-item label="家庭住址">
<span class="detail-value">{{ studentDetails.homeAddress || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 学业信息 -->
<div class="info-section">
<h4 class="info-section-title">学业信息</h4>
<el-descriptions :column="3" border size="small">
<el-descriptions-item label="毕业中学">
<span class="detail-value">{{ studentDetails.middleSchool || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="准考证号">
<span class="detail-value">{{ studentDetails.examNumber || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="中考地区">
<span class="detail-value">{{ studentDetails.examRegion || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="中考满分">
<span class="detail-value">{{ studentDetails.totalScoreFull || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="中考成绩">
<span class="detail-value score-highlight">{{ studentDetails.totalScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="语文成绩">
<span class="detail-value">{{ studentDetails.chineseScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="数学成绩">
<span class="detail-value">{{ studentDetails.mathScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="英语成绩">
<span class="detail-value">{{ studentDetails.englishScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="科学成绩">
<span class="detail-value">{{ studentDetails.scienceScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="社会成绩">
<span class="detail-value">{{ studentDetails.socialScore || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="体育成绩">
<span class="detail-value">{{ studentDetails.peScore || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 报考信息 -->
<div class="info-section">
<h4 class="info-section-title">报考信息</h4>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="报考专业">
<span class="detail-value major-highlight">{{ studentDetails.appliedMajor || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="专业代码">
<span class="detail-value">{{ studentDetails.majorCode || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="校服尺码">
<span class="detail-value">{{ studentDetails.uniformSize || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="报名时间">
<span class="detail-value">{{ formatTime(studentDetails.applicationTime) || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 其他信息 -->
<div class="info-section">
<h4 class="info-section-title">其他信息</h4>
<el-descriptions :column="1" border size="small">
<el-descriptions-item label="特长爱好">
<div class="detail-textarea">{{ studentDetails.specialties || '-' }}</div>
</el-descriptions-item>
<el-descriptions-item label="获奖情况">
<div class="detail-textarea">{{ studentDetails.awards || '-' }}</div>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
<!-- 审核表单 -->
<el-card class="audit-form-card" shadow="never">
<template #header>
@@ -89,10 +230,14 @@
</template>
<script setup lang="ts">
import { ref, reactive, computed, toRefs, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { ref, reactive, computed, toRefs, watch, getCurrentInstance } from 'vue'
import { ElMessage, type FormInstance } from 'element-plus'
import { infoAudit } from '@/api/zsjy/enrollmentProcess'
import { getEnrollmentInfo } from '@/api/zsjy/enrollment'
import type { EnrollmentProcessVO } from '@/api/zsjy/enrollmentProcess'
import type { EnrollmentInfoVO } from '@/api/zsjy/enrollment/types'
import { parseTime } from '@/utils/ruoyi'
import { View, ArrowUp, Check, CircleCheck, CircleClose } from '@element-plus/icons-vue'
interface Props {
visible: boolean
@@ -112,8 +257,8 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits<Emits>()
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { zsjy_enrollment_status, zsjy_audit_status } = toRefs<any>(
proxy?.useDict('zsjy_enrollment_status', 'zsjy_audit_status')
const { zsjy_enrollment_status, zsjy_audit_status, zsjy_gender, sys_yes_no } = toRefs<any>(
proxy?.useDict('zsjy_enrollment_status', 'zsjy_audit_status', 'zsjy_gender', 'sys_yes_no')
)
// 对话框可见性
@@ -125,8 +270,13 @@ const dialogVisible = computed({
})
// 表单引用
const auditFormRef = ref<ElFormInstance>()
const auditFormRef = ref<FormInstance>()
const submitLoading = ref(false)
const detailLoading = ref(false)
const showDetails = ref(false)
// 学生详细信息
const studentDetails = ref<Partial<EnrollmentInfoVO>>({})
// 审核表单
const auditForm = reactive({
@@ -141,10 +291,73 @@ const auditRules = {
]
}
// 时间格式化方法
const formatTime = (time: string | Date) => {
return parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}')
}
// 查看学生详细信息
const handleViewDetails = async () => {
if (!props.processData.enrollmentId) {
ElMessage.warning('缺少招生信息ID,无法获取详细信息')
return
}
try {
detailLoading.value = true
const res = await getEnrollmentInfo(props.processData.enrollmentId)
studentDetails.value = res.data || {}
showDetails.value = true
} catch (error: any) {
console.error('获取学生详细信息失败:', error)
// 如果接口不存在,使用模拟数据
if (error?.response?.status === 404) {
ElMessage.warning('后端接口暂未实现,显示模拟数据')
// 基于流程数据创建模拟的详细信息
studentDetails.value = {
id: Number(props.processData.enrollmentId),
name: props.processData.studentName,
idCard: props.processData.idCard,
gender: props.processData.idCard ? (parseInt(props.processData.idCard.substring(16, 17)) % 2 === 1 ? 'M' : 'F') : 'M',
phone1: '138****5678',
nation: '汉族',
isBoarding: 'Y',
healthStatus: '健康',
homeAddress: '浙江省杭州市西湖区某某街道某某小区',
middleSchool: '杭州市某某中学',
examNumber: '2025001234',
examRegion: '杭州市',
totalScoreFull: 600,
totalScore: 520,
chineseScore: 110,
mathScore: 115,
englishScore: 108,
scienceScore: 95,
socialScore: 92,
appliedMajor: '计算机应用技术',
majorCode: 'JSJYY001',
uniformSize: 'L',
specialties: '擅长计算机编程,获得过市级编程竞赛二等奖',
awards: '2024年杭州市中学生编程竞赛二等奖\n2023年校级三好学生',
applicationTime: '2025-01-20 14:30:00'
}
showDetails.value = true
} else {
ElMessage.error('获取学生详细信息失败,请重试')
}
} finally {
detailLoading.value = false
}
}
// 监听对话框打开,重置表单
watch(() => props.visible, (newVal) => {
if (newVal) {
resetForm()
} else {
// 关闭时重置详细信息显示状态
showDetails.value = false
studentDetails.value = {}
}
})
@@ -417,4 +630,144 @@ const handleClose = () => {
background: #fff !important;
color: #303133 !important;
}
/* 学生信息卡片样式 */
.student-info-card {
margin-bottom: 20px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.student-info-card :deep(.el-card__header) {
background: linear-gradient(135deg, #fafbff 0%, #f8fafc 100%);
border-bottom: 1px solid rgba(93, 135, 255, 0.1);
padding: 16px 20px;
}
/* 学生详细信息卡片样式 */
.student-details-card {
margin-bottom: 20px;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.student-details-card :deep(.el-card__header) {
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
border-bottom: 1px solid rgba(14, 165, 233, 0.1);
padding: 16px 20px;
}
/* 详细信息内容样式 */
.details-content {
max-height: 600px;
overflow-y: auto;
padding: 10px 0;
}
.info-section {
margin-bottom: 24px;
}
.info-section:last-child {
margin-bottom: 0;
}
.info-section-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
margin: 0 0 12px 0;
padding-bottom: 8px;
border-bottom: 2px solid #e2e8f0;
position: relative;
}
.info-section-title::before {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 60px;
height: 2px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
border-radius: 1px;
}
/* 详细信息值样式 */
.detail-value {
color: #374151;
font-weight: 500;
}
.score-highlight {
color: #dc2626;
font-weight: 600;
font-size: 15px;
}
.major-highlight {
color: #059669;
font-weight: 600;
font-size: 15px;
}
.detail-textarea {
color: #374151;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
max-height: 120px;
overflow-y: auto;
padding: 8px 0;
}
/* 地址描述样式 */
.address-desc {
margin-top: 12px;
}
/* 卡片头部样式 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.section-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
display: flex;
align-items: center;
}
.section-title::before {
content: '';
width: 4px;
height: 18px;
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
border-radius: 2px;
margin-right: 10px;
}
/* 详细信息滚动条样式 */
.details-content::-webkit-scrollbar {
width: 6px;
}
.details-content::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
.details-content::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
}
.details-content::-webkit-scrollbar-thumb:hover {
background: #94a3b8;
}
</style>
+376 -209
View File
@@ -28,7 +28,12 @@
</el-col>
<el-col :span="6">
<el-form-item label="当前状态" prop="currentStatus">
<el-select v-model="queryParams.currentStatus" placeholder="请选择当前状态" clearable style="width: 100%">
<el-select
v-model="queryParams.currentStatus"
placeholder="请选择当前状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in zsjy_enrollment_status"
:key="dict.value"
@@ -38,47 +43,14 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="信息审核" prop="infoAuditStatus">
<el-select v-model="queryParams.infoAuditStatus" placeholder="请选择信息审核状态" clearable style="width: 100%">
<el-option
v-for="dict in zsjy_audit_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="录取处理" prop="admissionStatus">
<el-select v-model="queryParams.admissionStatus" placeholder="请选择录取处理状态" clearable style="width: 100%">
<el-option
v-for="dict in zsjy_process_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="最终审核" prop="finalAuditStatus">
<el-select v-model="queryParams.finalAuditStatus" placeholder="请选择最终审核状态" clearable style="width: 100%">
<el-option
v-for="dict in zsjy_audit_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="完成状态" prop="isCompleted">
<el-select v-model="queryParams.isCompleted" placeholder="请选择完成状态" clearable style="width: 100%">
<el-select
v-model="queryParams.isCompleted"
placeholder="请选择完成状态"
clearable
style="width: 240px"
>
<el-option
v-for="dict in sys_yes_no"
:key="dict.value"
@@ -88,15 +60,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<div class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</div>
</el-col>
</el-row>
<div class="search-btn-container">
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</div>
</el-form>
</el-card>
@@ -132,7 +100,7 @@
<el-card class="stat-card stat-card-info" shadow="hover">
<div class="stat-content">
<div class="stat-icon">
<el-icon><Processing /></el-icon>
<el-icon><Clock /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statistics.processing || 0 }}</div>
@@ -162,15 +130,6 @@
<div class="card-header">
<span class="toolbar-title">录取流程管理</span>
<div class="toolbar-actions">
<el-button
v-hasPermi="['zsjy:enrollmentProcess:add']"
type="primary"
plain
icon="Plus"
@click="handleAdd"
>
新增流程
</el-button>
<el-button
v-hasPermi="['zsjy:enrollmentProcess:export']"
type="warning"
@@ -219,30 +178,22 @@
</template>
</el-table-column>
<el-table-column label="当前处理人" align="center" width="120" prop="currentHandlerName" />
<el-table-column label="信息审核" align="center" width="100">
<template #default="scope">
<dict-tag :options="zsjy_audit_status" :value="scope.row.infoAuditStatus" />
</template>
</el-table-column>
<el-table-column label="录取处理" align="center" width="100">
<template #default="scope">
<dict-tag :options="zsjy_process_status" :value="scope.row.admissionStatus" />
</template>
</el-table-column>
<el-table-column label="最终审核" align="center" width="100">
<template #default="scope">
<dict-tag :options="zsjy_audit_status" :value="scope.row.finalAuditStatus" />
</template>
</el-table-column>
<el-table-column label="缴费状态" align="center" width="100">
<template #default="scope">
<dict-tag :options="zsjy_payment_status" :value="scope.row.paymentStatus" />
<el-tag v-if="scope.row.payment_status === '1' || scope.row.paymentStatus === '1'" type="success">已缴费</el-tag>
<el-tag v-else-if="scope.row.payment_status === '0' || scope.row.paymentStatus === '0'" type="warning">未缴费</el-tag>
<el-tag v-else type="info">未知</el-tag>
</template>
</el-table-column>
<el-table-column label="录取通知书" align="center" width="150" show-overflow-tooltip>
<template #default="scope">
<span>{{ scope.row.noticeNumber || scope.row.notice_number || '未生成' }}</span>
</template>
</el-table-column>
<el-table-column label="录取通知书" align="center" width="150" prop="noticeNumber" show-overflow-tooltip />
<el-table-column label="完成状态" align="center" width="100">
<template #default="scope">
<dict-tag :options="sys_yes_no" :value="scope.row.isCompleted" />
<el-tag v-if="scope.row.is_completed === 1 || scope.row.is_completed === '1' || scope.row.is_completed === 'Y'" type="success">是</el-tag>
<el-tag v-else type="info">否</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" width="180">
@@ -262,64 +213,48 @@
@click="handleDetail(scope.row)"
/>
</el-tooltip>
<el-tooltip content="信息审核" placement="top">
<el-tooltip content="缴费确认" placement="top">
<el-button
v-hasPermi="['zsjy:enrollmentProcess:audit']"
v-hasPermi="['zsjy:enrollmentProcess:payment']"
link
type="success"
icon="Check"
:disabled="scope.row.currentStatus !== '0' && scope.row.currentStatus !== '1'"
@click="handleInfoAudit(scope.row)"
icon="CreditCard"
:disabled="scope.row.payment_status === '1' || scope.row.paymentStatus === '1'"
@click="handlePaymentConfirm(scope.row)"
/>
</el-tooltip>
<el-tooltip content="录取处理" placement="top">
<el-tooltip :content="(scope.row.payment_status !== '1' && scope.row.paymentStatus !== '1') ?
'请先完成缴费确认' : '录取处理'" placement="top">
<el-button
v-hasPermi="['zsjy:enrollmentProcess:process']"
link
type="warning"
icon="Edit"
:disabled="scope.row.currentStatus !== '2'"
:disabled="scope.row.payment_status !== '1' && scope.row.paymentStatus !== '1'"
@click="handleAdmissionProcess(scope.row)"
/>
</el-tooltip>
<el-tooltip content="最终审核" placement="top">
<el-tooltip :content="getCompleteButtonTooltip(scope.row)" placement="top">
<el-button
v-hasPermi="['zsjy:enrollmentProcess:finalAudit']"
v-hasPermi="['zsjy:enrollmentProcess:complete']"
link
type="info"
icon="Finished"
:disabled="scope.row.currentStatus !== '3'"
@click="handleFinalAudit(scope.row)"
icon="CircleCheck"
:disabled="(scope.row.payment_status !== '1' && scope.row.paymentStatus !== '1') ||
(scope.row.admissionStatus !== '1' && scope.row.admission_status !== '1') ||
scope.row.is_completed === 1 ||
scope.row.is_completed === 'Y'"
@click="handleComplete(scope.row)"
/>
</el-tooltip>
<el-tooltip content="更多操作" placement="top">
<el-dropdown @command="(command: string) => handleCommand(command, scope.row)">
<el-button link type="primary" icon="More" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="payment" :disabled="scope.row.paymentStatus === '1'">
<el-icon><CreditCard /></el-icon>
缴费确认
</el-dropdown-item>
<el-dropdown-item command="complete" :disabled="scope.row.isCompleted === 1">
<el-icon><CircleCheck /></el-icon>
完成录取
</el-dropdown-item>
<el-dropdown-item command="reject" :disabled="scope.row.currentStatus === '9'">
<el-icon><CircleClose /></el-icon>
驳回流程
</el-dropdown-item>
<el-dropdown-item command="reset" v-hasPermi="['zsjy:enrollmentProcess:reset']">
<el-icon><RefreshRight /></el-icon>
重置流程
</el-dropdown-item>
<el-dropdown-item command="delete" v-hasPermi="['zsjy:enrollmentProcess:remove']">
<el-icon><Delete /></el-icon>
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-tooltip content="删除" placement="top">
<el-button
v-hasPermi="['zsjy:enrollmentProcess:remove']"
link
type="danger"
icon="Delete"
@click="handleDelete(scope.row)"
/>
</el-tooltip>
</div>
</template>
@@ -345,13 +280,6 @@
:process-data="currentProcess"
/>
<!-- 信息审核对话框 -->
<InfoAuditDialog
v-model:visible="infoAuditDialogVisible"
:process-data="currentProcess"
@success="handleDialogSuccess"
/>
<!-- 录取处理对话框 -->
<AdmissionProcessDialog
v-model:visible="admissionProcessDialogVisible"
@@ -359,13 +287,6 @@
@success="handleDialogSuccess"
/>
<!-- 最终审核对话框 -->
<FinalAuditDialog
v-model:visible="finalAuditDialogVisible"
:process-data="currentProcess"
@success="handleDialogSuccess"
/>
<!-- 驳回流程对话框 -->
<RejectProcessDialog
v-model:visible="rejectDialogVisible"
@@ -391,19 +312,19 @@ import {
delEnrollmentProcess,
getProcessStatistics,
confirmPayment,
completeEnrollment,
completeProcess,
rejectProcess,
resetProcess,
exportEnrollmentProcess
} from '@/api/zsjy/enrollmentProcess'
import type { EnrollmentProcessVO, EnrollmentProcessQuery } from '@/api/zsjy/enrollmentProcess'
import { ElMessageBox, ElMessage } from 'element-plus'
import { Clock, Select, CircleCheck, Warning } from '@element-plus/icons-vue'
import ProcessDetailDialog from './components/ProcessDetailDialog.vue'
import InfoAuditDialog from './components/InfoAuditDialog.vue'
import AdmissionProcessDialog from './components/AdmissionProcessDialog.vue'
import FinalAuditDialog from './components/FinalAuditDialog.vue'
import RejectProcessDialog from './components/RejectProcessDialog.vue'
import ProcessFormDialog from './components/ProcessFormDialog.vue'
import { useRoute } from 'vue-router'
const { proxy } = getCurrentInstance() as ComponentInternalInstance
const { zsjy_enrollment_status, zsjy_audit_status, zsjy_process_status, zsjy_payment_status, sys_yes_no } = toRefs<any>(
@@ -423,10 +344,7 @@ const data = reactive<{
studentName: undefined,
idCard: undefined,
currentStatus: undefined,
infoAuditStatus: undefined,
admissionStatus: undefined,
finalAuditStatus: undefined,
isCompleted: undefined,
isCompleted: 0,
params: {}
},
rules: {}
@@ -444,6 +362,13 @@ const multiple = ref(true)
const total = ref(0)
const processList = ref<EnrollmentProcessVO[]>([])
/** 多选框选中数据 */
const handleSelectionChange = (selection: EnrollmentProcessVO[]) => {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = selection.length === 0
}
// 统计数据
const statistics = ref({
total: 0,
@@ -454,9 +379,7 @@ const statistics = ref({
// 对话框状态
const detailDialogVisible = ref(false)
const infoAuditDialogVisible = ref(false)
const admissionProcessDialogVisible = ref(false)
const finalAuditDialogVisible = ref(false)
const rejectDialogVisible = ref(false)
const formDialogVisible = ref(false)
const isEdit = ref(false)
@@ -467,8 +390,71 @@ const getList = async () => {
loading.value = true
try {
const res = await listEnrollmentProcess(queryParams.value)
processList.value = res.rows
total.value = res.total
// 对返回的数据进行处理,确保字段名一致
const processedList = (res.rows || []).map(item => {
// 添加日志用于调试(首行数据)
if (item === res.rows[0]) {
console.log('首行数据字段:', Object.keys(item))
console.log('首行数据内容:', JSON.stringify(item))
}
// 字段名映射处理
return {
...item,
// 确保同时有驼峰式和下划线式的字段
paymentStatus: item.paymentStatus || item.payment_status,
payment_status: item.payment_status || item.paymentStatus,
isCompleted: item.isCompleted || item.is_completed,
is_completed: item.is_completed || item.isCompleted,
noticeNumber: item.noticeNumber || item.notice_number,
notice_number: item.notice_number || item.noticeNumber
}
})
processList.value = processedList
total.value = res.total || 0
} catch (error) {
console.error('获取流程列表失败:', error)
// 如果是404错误(接口不存在),使用模拟数据
if (error?.response?.status === 404 || error?.message?.includes('404')) {
console.warn('后端接口不存在,使用模拟数据')
// 创建模拟的流程数据
processList.value = [
{
id: 1946510098494091300,
enrollmentId: 1001,
studentName: '张三',
idCard: '330101200501010001',
currentStatus: '0',
infoAuditStatus: '0',
admissionStatus: '0',
finalAuditStatus: '0',
isCompleted: 0,
createTime: '2025-07-20 10:28:09'
},
{
id: 1946510098494091301,
enrollmentId: 1946510098494091266,
studentName: '赵四',
idCard: '340403201509091234',
currentStatus: '0',
infoAuditStatus: '0',
admissionStatus: '0',
finalAuditStatus: '0',
isCompleted: 0,
createTime: '2025-07-20 10:28:18'
}
]
total.value = 2
proxy?.$modal.msgWarning('后端接口暂未实现,显示模拟数据')
} else {
// 其他错误,设置空数据并抛出错误
processList.value = []
total.value = 0
throw error
}
} finally {
loading.value = false
}
@@ -477,23 +463,46 @@ const getList = async () => {
/** 获取统计数据 */
const getStatistics = async () => {
try {
const res = await getProcessStatistics()
const stats = res.data || []
// 暂时注释掉不存在的统计接口,改为基于流程列表计算统计
// const res = await getProcessStatistics()
// const stats = res.data || []
// 基于当前流程列表计算统计数据
const stats = processList.value || []
const statusCounts = stats.reduce((acc: any, item: any) => {
const status = item.currentStatus
acc[status] = (acc[status] || 0) + 1
return acc
}, {})
statistics.value = {
total: stats.reduce((sum: number, item: any) => sum + Number(item.count), 0),
pending: stats.find((item: any) => ['0', '1'].includes(item.status))?.count || 0,
processing: stats.find((item: any) => ['2', '3'].includes(item.status))?.count || 0,
completed: stats.find((item: any) => item.status === '4')?.count || 0
total: stats.length,
pending: (statusCounts['0'] || 0) + (statusCounts['1'] || 0), // 待审核 + 信息审核中
processing: (statusCounts['2'] || 0) + (statusCounts['3'] || 0), // 录取处理中 + 待最终确认
completed: statusCounts['4'] || 0 // 录取完成
}
} catch (error) {
console.error('获取统计数据失败:', error)
// 设置默认值
statistics.value = {
total: 0,
pending: 0,
processing: 0,
completed: 0
}
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
const handleQuery = async () => {
queryParams.value.pageNum = 1
getList()
try {
await getList()
// 搜索后更新统计数据
await getStatistics()
} catch (error) {
console.error('搜索失败:', error)
}
}
/** 重置按钮操作 */
@@ -502,13 +511,6 @@ const resetQuery = () => {
handleQuery()
}
/** 多选框选中数据 */
const handleSelectionChange = (selection: EnrollmentProcessVO[]) => {
ids.value = selection.map(item => item.id)
single.value = selection.length !== 1
multiple.value = !selection.length
}
/** 新增按钮操作 */
const handleAdd = () => {
currentProcess.value = {}
@@ -535,44 +537,62 @@ const handleDetail = (row: EnrollmentProcessVO) => {
detailDialogVisible.value = true
}
/** 信息审核 */
const handleInfoAudit = (row: EnrollmentProcessVO) => {
currentProcess.value = row
infoAuditDialogVisible.value = true
}
/** 录取处理 */
const handleAdmissionProcess = (row: EnrollmentProcessVO) => {
currentProcess.value = row
admissionProcessDialogVisible.value = true
// 验证缴费状态
if ((row.payment_status !== '1' && row.paymentStatus !== '1')) {
ElMessage.warning('请先完成缴费确认,再进行录取处理');
return;
}
currentProcess.value = row;
admissionProcessDialogVisible.value = true;
}
/** 最终审核 */
const handleFinalAudit = (row: EnrollmentProcessVO) => {
currentProcess.value = row
finalAuditDialogVisible.value = true
/** 缴费确认按钮点击处理 */
const handlePaymentConfirm = async (row: EnrollmentProcessVO) => {
try {
await handleConfirmPayment(row)
} catch (error) {
console.error('缴费确认失败:', error)
}
}
/** 更多操作命令处理 */
const handleCommand = async (command: string, row: EnrollmentProcessVO) => {
currentProcess.value = row
switch (command) {
case 'payment':
await handleConfirmPayment(row)
break
case 'complete':
await handleCompleteEnrollment(row)
break
case 'reject':
rejectDialogVisible.value = true
break
case 'reset':
await handleResetProcess(row)
break
case 'delete':
await handleDelete(row)
break
/** 完成录取按钮点击处理 */
const handleComplete = async (row: EnrollmentProcessVO) => {
// 验证缴费状态
if ((row.payment_status !== '1' && row.paymentStatus !== '1')) {
ElMessage.warning('请先完成缴费确认,再进行完成录取操作');
return;
}
// 验证录取处理状态
if ((row.admissionStatus !== '1' && row.admission_status !== '1')) {
ElMessage.warning('请先完成录取处理,再进行完成录取操作');
return;
}
// 验证是否已经完成
if (row.is_completed === 1 || row.is_completed === 'Y') {
ElMessage.warning('该录取流程已完成,无需重复操作');
return;
}
try {
await ElMessageBox.confirm('确认完成该学生的录取流程?', '完成录取', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning'
})
await completeProcess(row.id as number)
ElMessage.success('录取完成')
await getList()
await getStatistics()
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('完成录取失败')
}
}
}
@@ -584,7 +604,7 @@ const handleConfirmPayment = async (row: EnrollmentProcessVO) => {
cancelButtonText: '取消',
type: 'warning'
})
await confirmPayment(row.id as number)
ElMessage.success('缴费确认成功')
await getList()
@@ -604,8 +624,8 @@ const handleCompleteEnrollment = async (row: EnrollmentProcessVO) => {
cancelButtonText: '取消',
type: 'warning'
})
await completeEnrollment(row.id as number)
await completeProcess(row.id as number)
ElMessage.success('录取完成')
await getList()
await getStatistics()
@@ -624,7 +644,7 @@ const handleResetProcess = async (row: EnrollmentProcessVO) => {
cancelButtonText: '取消',
type: 'warning'
})
await resetProcess(row.id as number)
ElMessage.success('流程重置成功')
await getList()
@@ -645,7 +665,7 @@ const handleDelete = async (row?: EnrollmentProcessVO) => {
cancelButtonText: '取消',
type: 'warning'
})
await delEnrollmentProcess(processIds)
ElMessage.success('删除成功')
await getList()
@@ -676,10 +696,50 @@ const handleDialogSuccess = async () => {
await getStatistics()
}
// 初始化
/** 获取完成录取按钮的提示内容 */
const getCompleteButtonTooltip = (row: any): string => {
if (row.is_completed === 1 || row.is_completed === 'Y') {
return '录取已完成,无需重复操作';
}
if (row.payment_status !== '1' && row.paymentStatus !== '1') {
return '请先完成缴费确认';
}
if (row.admissionStatus !== '1' && row.admission_status !== '1') {
return '请先完成录取处理';
}
return '完成录取';
}
/** 初始化 */
onMounted(async () => {
await getList()
await getStatistics()
try {
// 处理路由参数
const route = useRoute();
if (route && route.query) {
if (route.query.enrollmentId) {
queryParams.value.enrollmentId = Number(route.query.enrollmentId);
}
if (route.query.studentName) {
queryParams.value.studentName = route.query.studentName as string;
}
}
// 先获取列表数据,再基于列表数据计算统计
try {
await getList();
// 列表数据获取成功后,计算统计数据
await getStatistics();
} catch (error) {
console.error('获取数据失败:', error);
proxy?.$modal.msgError('获取数据失败,请检查网络连接或联系管理员');
}
} catch (error) {
console.error('页面初始化失败:', error);
proxy?.$modal.msgError('页面初始化失败,请刷新页面重试');
}
})
</script>
@@ -822,6 +882,7 @@ onMounted(async () => {
/* 表格卡片样式 */
.table-card {
margin-top: 0;
border-radius: 12px;
border: 1px solid #e4e7ed;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
@@ -831,15 +892,92 @@ onMounted(async () => {
padding: 0px 20px 20px 20px !important;
}
/* 表格样式 */
.scroll-table :deep(.el-table__header-wrapper .el-table__header th) {
background-color: #f8f9fa;
color: #333;
font-weight: 600;
/* 表格容器 - 支持横向滚动 */
.table-container {
width: 100%;
margin-bottom: 0;
}
.scroll-table :deep(.el-table__body-wrapper .el-table__row:hover) {
background-color: #f5f7fa;
/* 横向滚动表格样式 */
.scroll-table {
width: 100%;
}
/* 表格横向滚动条样式优化 */
.scroll-table :deep(.el-table__body-wrapper) {
overflow-x: auto !important; /* 自动显示横向滚动条 */
overflow-y: auto;
}
.scroll-table :deep(.el-table__header-wrapper) {
overflow-x: hidden;
}
/* 强制显示表格横向滚动条 - 适用于所有浏览器 */
.scroll-table :deep(.el-table__body-wrapper) {
scrollbar-width: thin; /* Firefox */
scrollbar-color: #409eff #f0f0f0; /* Firefox - thumb track */
}
/* Webkit 浏览器的横向滚动条样式 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar {
height: 12px !important; /* 增加滚动条高度 */
width: 12px !important;
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-track {
background: #f0f0f0 !important;
border-radius: 6px !important;
border: 1px solid #e4e7ed !important; /* 添加边框使其更明显 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb {
background: #409eff !important; /* 使用主题色,更明显 */
border-radius: 6px !important;
border: 2px solid #f0f0f0 !important; /* 添加白色边框 */
min-height: 20px !important; /* 确保最小高度 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:hover {
background: #337ecc !important; /* 悬停时更深的蓝色 */
}
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-thumb:active {
background: #2b6cb0 !important; /* 点击时的颜色 */
}
/* 确保滚动条始终可见的额外设置 */
.scroll-table :deep(.el-table__body-wrapper)::-webkit-scrollbar-corner {
background: #f0f0f0;
}
/* 固定列样式优化 */
.scroll-table :deep(.el-table__fixed-right) {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
}
.scroll-table :deep(.el-table__fixed-left) {
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
}
/* 确保固定列在滚动时有正确的背景色 */
.scroll-table :deep(.el-table__fixed-right .el-table__cell),
.scroll-table :deep(.el-table__fixed-left .el-table__cell) {
background-color: #fff;
}
/* 表格行悬停效果在固定列上的同步 */
.scroll-table :deep(.el-table__row:hover .el-table__cell) {
background-color: #f5f7fa !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 操作按钮样式 */
@@ -851,6 +989,7 @@ onMounted(async () => {
white-space: nowrap;
}
/* 确保操作按钮宽度一致 */
.action-buttons .el-button {
width: 27px !important;
height: 27px !important;
@@ -860,12 +999,40 @@ onMounted(async () => {
justify-content: center;
}
/* 分页容器样式 */
.pagination-container {
/* 批量操作表单样式 */
.form-tips {
font-size: 12px;
color: #909399;
line-height: 1.2;
padding: 2px 0 0 0;
}
/* 搜索区域样式优化 */
.search-card {
margin-bottom: 20px;
padding: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.search-card :deep(.el-select) {
width: 240px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
margin-top: 10px;
}
</style>
.search-btn-container .el-form-item {
margin-bottom: 0 !important;
}
/* 确保文本不会被截断 */
:deep(.el-select-dropdown__item span) {
white-space: normal !important;
word-break: break-word !important;
}
</style>
@@ -0,0 +1,411 @@
<template>
<div class="page-container">
<!-- 搜索区域 -->
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div ref="searchRef" v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<!-- 第一行:搜索元素 -->
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="身份证号" prop="idCard">
<el-input v-model="queryParams.idCard" placeholder="请输入身份证号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
</el-row>
<!-- 操作按钮行 -->
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<!-- 表格操作区域 -->
<el-card shadow="never" class="table-card">
<template #header>
<el-row ref="toolbarRef" :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="success" plain icon="Check" :disabled="single" @click="handleApprove()" v-hasPermi="['zsjy:enrollment:process:audit']">
通过
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Close" :disabled="single" @click="handleReject()" v-hasPermi="['zsjy:enrollment:process:audit']">
驳回
</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="primary" plain icon="View" :disabled="single" @click="handleView()" v-hasPermi="['zsjy:enrollment:process:query']">
查看
</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['zsjy:enrollment:process:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<!-- 表格区域 -->
<el-table
v-loading="loading"
border
:data="processList"
@selection-change="handleSelectionChange"
@row-dblclick="handleRowClick"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="学生姓名" align="center" prop="studentName" />
<el-table-column label="身份证号" align="center" prop="idCard" :show-overflow-tooltip="true" />
<el-table-column label="当前状态" align="center" prop="currentStatus">
<template #default="scope">
<dict-tag :options="zsjy_enrollment_status" :value="scope.row.currentStatus" />
</template>
</el-table-column>
<el-table-column label="信息审核状态" align="center" prop="infoAuditStatus">
<template #default="scope">
<dict-tag :options="zsjy_audit_status" :value="scope.row.infoAuditStatus" />
</template>
</el-table-column>
<el-table-column label="录取处理状态" align="center" prop="admissionStatus">
<template #default="scope">
<dict-tag :options="zsjy_process_status" :value="scope.row.admissionStatus" />
</template>
</el-table-column>
<el-table-column label="最终审核状态" align="center" prop="finalAuditStatus">
<template #default="scope">
<dict-tag :options="zsjy_audit_status" :value="scope.row.finalAuditStatus" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="180" class-name="small-padding fixed-width">
<template #default="scope">
<el-button
type="primary"
link
icon="View"
@click="handleView(scope.row)"
v-hasPermi="['zsjy:enrollment:process:query']"
>查看</el-button>
<el-button
v-if="canAudit(scope.row)"
type="success"
link
icon="Check"
@click="handleApprove(scope.row)"
v-hasPermi="['zsjy:enrollment:process:audit']"
>通过</el-button>
<el-button
v-if="canAudit(scope.row)"
type="danger"
link
icon="Close"
@click="handleReject(scope.row)"
v-hasPermi="['zsjy:enrollment:process:audit']"
>驳回</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<pagination
v-if="total > 0"
:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</el-card>
<!-- 审核对话框 -->
<el-dialog :title="auditDialogTitle" v-model="auditDialogVisible" width="500px" append-to-body>
<el-form ref="auditFormRef" :model="auditForm" :rules="auditRules" label-width="100px">
<el-form-item label="审核意见" prop="auditOpinion">
<el-input
v-model="auditForm.auditOpinion"
type="textarea"
placeholder="请输入审核意见"
:rows="4"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitAudit">确 定</el-button>
<el-button @click="auditDialogVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup name="PendingAudit" lang="ts">
import { ref, reactive, onMounted, computed, getCurrentInstance, toRefs } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { useRouter } from 'vue-router';
import { listMyAuditProcess, infoAudit, admissionProcess, finalAudit, exportEnrollmentProcess } from '@/api/zsjy/enrollmentProcess';
import { parseTime } from '@/utils/ruoyi';
import { download } from '@/utils/request';
import type { EnrollmentProcessVO, EnrollmentProcessQuery } from '@/api/zsjy/enrollmentProcess';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
// 字典数据
const { zsjy_enrollment_status, zsjy_audit_status, zsjy_process_status, sys_yes_no } = toRefs<any>(
proxy?.useDict('zsjy_enrollment_status', 'zsjy_audit_status', 'zsjy_process_status', 'sys_yes_no')
);
// 遮罩层
const loading = ref(true);
// 选中数组
const ids = ref<Array<string | number>>([]);
// 非单个禁用
const single = computed(() => ids.value.length !== 1);
// 非多个禁用
const multiple = computed(() => !ids.value.length);
// 显示搜索条件
const showSearch = ref(true);
// 总条数
const total = ref(0);
// 录取流程状态表格数据
const processList = ref<EnrollmentProcessVO[]>([]);
// 弹出层标题
const auditDialogTitle = ref("");
// 是否显示弹出层
const auditDialogVisible = ref(false);
// 当前操作类型
const actionType = ref('');
// 当前选中的记录
const currentRow = ref<EnrollmentProcessVO | null>(null);
// 审核表单
const auditForm = reactive({
processId: 0,
auditResult: '',
auditOpinion: ''
});
// 审核表单校验规则
const auditRules = {
auditOpinion: [{ required: true, message: "请输入审核意见", trigger: "blur" }]
};
// 查询参数
const queryParams = reactive<EnrollmentProcessQuery>({
pageNum: 1,
pageSize: 10,
studentName: '',
idCard: '',
params: {}
});
// 表单引用
const queryFormRef = ref();
const auditFormRef = ref();
/** 查询录取流程状态列表 */
function getList() {
loading.value = true;
listMyAuditProcess(queryParams).then(response => {
processList.value = response.rows || [];
total.value = response.total || 0;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
queryFormRef.value?.resetFields();
handleQuery();
}
/** 多选框选中数据 */
function handleSelectionChange(selection: EnrollmentProcessVO[]) {
ids.value = selection.map(item => item.id);
if (selection.length === 1) {
currentRow.value = selection[0];
} else {
currentRow.value = null;
}
}
/** 行点击事件 */
function handleRowClick(row: EnrollmentProcessVO) {
handleView(row);
}
/** 查看按钮操作 */
function handleView(row?: EnrollmentProcessVO) {
const id = row?.id || (ids.value.length > 0 ? ids.value[0] : null);
if (id) {
// 跳转到详情页面
const router = useRouter();
router.push({ path: '/zsjy/enrollmentProcess/detail', query: { id: id.toString() } });
}
}
/** 导出按钮操作 */
function handleExport() {
ElMessageBox.confirm('是否确认导出所有待我审核的数据项?', "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
loading.value = true;
exportEnrollmentProcess(queryParams).then(response => {
download(response.msg, '待我审核列表', 'xlsx');
loading.value = false;
}).catch(() => {
loading.value = false;
});
}).catch(() => {});
}
/** 判断是否可以审核 */
function canAudit(row: EnrollmentProcessVO) {
// 根据当前用户角色和流程状态判断是否可以审核
// 这里简单实现,实际应该根据用户角色和流程状态判断
return true;
}
/** 通过按钮操作 */
function handleApprove(row?: EnrollmentProcessVO) {
const selectedRow = row || currentRow.value;
if (!selectedRow) {
ElMessage.warning('请选择要审核的记录');
return;
}
auditForm.processId = selectedRow.id as number;
auditForm.auditResult = '1'; // 1-通过
auditForm.auditOpinion = '';
auditDialogTitle.value = '审核通过';
actionType.value = 'approve';
auditDialogVisible.value = true;
}
/** 驳回按钮操作 */
function handleReject(row?: EnrollmentProcessVO) {
const selectedRow = row || currentRow.value;
if (!selectedRow) {
ElMessage.warning('请选择要审核的记录');
return;
}
auditForm.processId = selectedRow.id as number;
auditForm.auditResult = '9'; // 9-驳回
auditForm.auditOpinion = '';
auditDialogTitle.value = '审核驳回';
actionType.value = 'reject';
auditDialogVisible.value = true;
}
/** 提交审核 */
function submitAudit() {
auditFormRef.value?.validate((valid: boolean) => {
if (valid) {
const selectedRow = currentRow.value;
if (!selectedRow) {
ElMessage.warning('请选择要审核的记录');
return;
}
const params = {
processId: auditForm.processId,
auditOpinion: auditForm.auditOpinion
};
// 根据当前流程状态和用户角色调用不同的审核API
// 这里简单实现,实际应该根据用户角色和流程状态调用不同的API
if (selectedRow.currentStatus === '0' || selectedRow.currentStatus === '1') {
// 信息审核
infoAudit({
processId: auditForm.processId,
auditResult: auditForm.auditResult,
auditOpinion: auditForm.auditOpinion
}).then(() => {
ElMessage.success('审核操作成功');
auditDialogVisible.value = false;
getList();
});
} else if (selectedRow.currentStatus === '2') {
// 录取处理
admissionProcess({
processId: auditForm.processId,
admissionResult: auditForm.auditResult,
admissionOpinion: auditForm.auditOpinion
}).then(() => {
ElMessage.success('审核操作成功');
auditDialogVisible.value = false;
getList();
});
} else if (selectedRow.currentStatus === '3') {
// 最终确认
finalAudit({
processId: auditForm.processId,
finalResult: auditForm.auditResult,
finalOpinion: auditForm.auditOpinion
}).then(() => {
ElMessage.success('审核操作成功');
auditDialogVisible.value = false;
getList();
});
}
}
});
}
onMounted(() => {
getList();
});
</script>
<style scoped>
.page-container {
padding: 16px;
}
.search-card {
margin-bottom: 16px;
}
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-right: 10px;
}
.table-card {
margin-bottom: 16px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
}
</style>
+847
View File
@@ -0,0 +1,847 @@
<template>
<div class="p-2 dashboard-container">
<!-- 头部统计卡片 -->
<el-row :gutter="20">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-content">
<div class="stat-icon primary-icon">
<el-icon><UserFilled /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statisticsData?.totalEnrollments || 0 }}</div>
<div class="stat-label">总报名人数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-content">
<div class="stat-icon success-icon">
<el-icon><Calendar /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statisticsData?.todayEnrollments || 0 }}</div>
<div class="stat-label">今日报名人数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-content">
<div class="stat-icon warning-icon">
<el-icon><Money /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statisticsData?.paidCount || 0 }}</div>
<div class="stat-label">已缴费人数</div>
<div class="stat-sub-label">缴费率: {{ statisticsData?.paymentRate || '0%' }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-card-content">
<div class="stat-icon info-icon">
<el-icon><Medal /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ statisticsData?.admittedCount || 0 }}</div>
<div class="stat-label">已录取人数</div>
<div class="stat-sub-label">录取率: {{ statisticsData?.admissionRate || '0%' }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" class="chart-row">
<!-- 近7天报名趋势 -->
<el-col :span="16">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="chart-header">
<span class="chart-title">近7天报名趋势</span>
<el-radio-group v-model="trendDays" size="small" @change="handleTrendDaysChange">
<el-radio-button label="7">7天</el-radio-button>
<el-radio-button label="15">15天</el-radio-button>
<el-radio-button label="30">30天</el-radio-button>
</el-radio-group>
</div>
</template>
<div class="chart-container" v-loading="trendLoading">
<div id="trendChart" class="chart"></div>
</div>
</el-card>
</el-col>
<!-- 性别比例 -->
<el-col :span="8">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="chart-header">
<span class="chart-title">性别比例</span>
</div>
</template>
<div class="chart-container" v-loading="genderLoading">
<div id="genderChart" class="chart"></div>
</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="20" class="chart-row">
<!-- 专业分布 -->
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="chart-header">
<span class="chart-title">专业分布</span>
</div>
</template>
<div class="chart-container" v-loading="majorLoading">
<div id="majorChart" class="chart"></div>
</div>
</el-card>
</el-col>
<!-- 地区分布 -->
<el-col :span="12">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="chart-header">
<span class="chart-title">地区分布</span>
</div>
</template>
<div class="chart-container" v-loading="regionLoading">
<div id="regionChart" class="chart"></div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 状态分布 -->
<el-row>
<el-col :span="24">
<el-card shadow="hover" class="chart-card">
<template #header>
<div class="chart-header">
<span class="chart-title">录取状态分布</span>
</div>
</template>
<div class="chart-container" v-loading="statusLoading">
<div id="statusChart" class="chart"></div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup name="EnrollmentStatistics" lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { UserFilled, Calendar, Money, Medal } from '@element-plus/icons-vue';
import * as echarts from 'echarts';
import { getStatisticsOverview, getEnrollmentTrend, getGenderRatio, getMajorDistribution, getRegionDistribution, getStatusDistribution, StatisticsOverview } from '@/api/zsjy/statistics';
import { ElMessage } from 'element-plus'; // Added ElMessage import
// 图表实例
let trendChartInstance: echarts.ECharts | null = null;
let genderChartInstance: echarts.ECharts | null = null;
let majorChartInstance: echarts.ECharts | null = null;
let regionChartInstance: echarts.ECharts | null = null;
let statusChartInstance: echarts.ECharts | null = null;
// 数据加载状态
const loading = ref(true);
const trendLoading = ref(false);
const genderLoading = ref(false);
const majorLoading = ref(false);
const regionLoading = ref(false);
const statusLoading = ref(false);
// 统计数据
const statisticsData = ref<StatisticsOverview | null>(null);
// 趋势图天数选择
const trendDays = ref<number>(7);
// 获取统计概览数据
const loadStatisticsData = async () => {
loading.value = true;
try {
console.log('开始请求统计概览数据...');
const res = await getStatisticsOverview();
console.log('统计概览数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('统计概览数据为空或格式不正确:', res);
ElMessage.error('获取统计数据失败,请联系管理员');
return;
}
statisticsData.value = res.data;
// 加载所有图表
loadAllCharts();
} catch (error) {
console.error('获取统计概览数据失败', error);
ElMessage.error('获取统计数据失败,请联系管理员');
} finally {
loading.value = false;
}
};
// 加载所有图表
const loadAllCharts = () => {
loadTrendChart();
loadGenderChart();
loadMajorChart();
loadRegionChart();
loadStatusChart();
};
// 加载趋势图
const loadTrendChart = async () => {
trendLoading.value = true;
try {
console.log('开始请求报名趋势数据...');
const res = await getEnrollmentTrend(trendDays.value);
console.log('报名趋势数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('报名趋势数据为空或格式不正确:', res);
return;
}
const trendData = res.data;
// 初始化趋势图
if (!trendChartInstance) {
const chartDom = document.getElementById('trendChart');
if (!chartDom) {
console.error('找不到趋势图DOM元素!');
return;
}
trendChartInstance = echarts.init(chartDom);
}
const dates: string[] = [];
const counts: number[] = [];
if (Array.isArray(trendData)) {
trendData.forEach(item => {
dates.push(item.date as string);
counts.push(item.count as number);
});
} else {
console.error('趋势数据不是数组格式:', trendData);
return;
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: dates,
axisLabel: {
rotate: dates.length > 10 ? 45 : 0
}
},
yAxis: {
type: 'value',
name: '报名人数',
minInterval: 1
},
series: [
{
name: '报名人数',
type: 'line',
smooth: true,
data: counts,
areaStyle: {
opacity: 0.3,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#5d87ff' },
{ offset: 1, color: 'rgba(93, 135, 255, 0.1)' }
])
},
lineStyle: {
width: 3,
color: '#5d87ff'
},
itemStyle: {
color: '#5d87ff'
}
}
]
};
trendChartInstance.setOption(option);
} catch (error) {
console.error('获取报名趋势数据失败', error);
ElMessage.warning('获取报名趋势数据失败');
} finally {
trendLoading.value = false;
}
};
// 加载性别比例图
const loadGenderChart = async () => {
genderLoading.value = true;
try {
console.log('开始请求性别比例数据...');
const res = await getGenderRatio();
console.log('性别比例数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('性别比例数据为空或格式不正确:', res);
return;
}
const genderData = res.data;
// 初始化性别比例图
if (!genderChartInstance) {
const chartDom = document.getElementById('genderChart');
if (!chartDom) {
console.error('找不到性别比例图DOM元素!');
return;
}
genderChartInstance = echarts.init(chartDom);
}
const pieData = [];
let total = 0;
// 计算总数
Object.keys(genderData).forEach(key => {
total += genderData[key];
});
// 转换数据格式
Object.keys(genderData).forEach(key => {
const name = key === 'M' ? '男' : key === 'F' ? '女' : '未知';
const value = genderData[key];
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0.0';
pieData.push({
name: `${name}: ${percentage}%`,
value
});
});
const option = {
tooltip: {
trigger: 'item',
formatter: '{b}'
},
legend: {
orient: 'vertical',
right: '5%',
top: 'center'
},
series: [
{
name: '性别比例',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false
},
emphasis: {
label: {
show: true,
fontSize: '12',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: pieData,
color: ['#5d87ff', '#ff5da0', '#909399']
}
]
};
genderChartInstance.setOption(option);
} catch (error) {
console.error('获取性别比例数据失败', error);
ElMessage.warning('获取性别比例数据失败');
} finally {
genderLoading.value = false;
}
};
// 加载专业分布图
const loadMajorChart = async () => {
majorLoading.value = true;
try {
console.log('开始请求专业分布数据...');
const res = await getMajorDistribution();
console.log('专业分布数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('专业分布数据为空或格式不正确:', res);
return;
}
const majorData = res.data;
// 初始化专业分布图
if (!majorChartInstance) {
const chartDom = document.getElementById('majorChart');
if (!chartDom) {
console.error('找不到专业分布图DOM元素!');
return;
}
majorChartInstance = echarts.init(chartDom);
}
const majorNames: string[] = [];
const majorCounts: number[] = [];
if (Array.isArray(majorData)) {
majorData.forEach(item => {
majorNames.push(item.name as string);
majorCounts.push(item.value as number);
});
} else {
console.error('专业分布数据不是数组格式:', majorData);
return;
}
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: majorNames,
axisLabel: {
interval: 0,
rotate: majorNames.length > 5 ? 45 : 0,
textStyle: {
fontSize: 12
}
}
},
yAxis: {
type: 'value',
name: '报名人数',
minInterval: 1
},
series: [
{
name: '报名人数',
type: 'bar',
data: majorCounts,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#5d87ff' },
{ offset: 1, color: '#83bff6' }
])
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#2378f7' },
{ offset: 0.7, color: '#2378f7' },
{ offset: 1, color: '#83bff6' }
])
}
}
}
]
};
majorChartInstance.setOption(option);
} catch (error) {
console.error('获取专业分布数据失败', error);
ElMessage.warning('获取专业分布数据失败');
} finally {
majorLoading.value = false;
}
};
// 加载地区分布图
const loadRegionChart = async () => {
regionLoading.value = true;
try {
console.log('开始请求地区分布数据...');
const res = await getRegionDistribution();
console.log('地区分布数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('地区分布数据为空或格式不正确:', res);
return;
}
const regionData = res.data;
// 初始化地区分布图
if (!regionChartInstance) {
const chartDom = document.getElementById('regionChart');
if (!chartDom) {
console.error('找不到地区分布图DOM元素!');
return;
}
regionChartInstance = echarts.init(chartDom);
}
const pieData: { name: string; value: number }[] = [];
if (Array.isArray(regionData)) {
regionData.forEach(item => {
pieData.push({
name: item.name as string,
value: item.value as number
});
});
} else {
console.error('地区分布数据不是数组格式:', regionData);
return;
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
type: 'scroll',
orient: 'vertical',
right: 10,
top: 20,
bottom: 20
},
series: [
{
name: '地区分布',
type: 'pie',
radius: '55%',
center: ['40%', '50%'],
data: pieData,
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}
]
};
regionChartInstance.setOption(option);
} catch (error) {
console.error('获取地区分布数据失败', error);
ElMessage.warning('获取地区分布数据失败');
} finally {
regionLoading.value = false;
}
};
// 加载状态分布图
const loadStatusChart = async () => {
statusLoading.value = true;
try {
console.log('开始请求状态分布数据...');
const res = await getStatusDistribution();
console.log('状态分布数据响应:', res);
// 检查响应数据结构
if (!res || !res.data) {
console.error('状态分布数据为空或格式不正确:', res);
return;
}
const statusData = res.data;
// 初始化状态分布图
if (!statusChartInstance) {
const chartDom = document.getElementById('statusChart');
if (!chartDom) {
console.error('找不到状态分布图DOM元素!');
return;
}
statusChartInstance = echarts.init(chartDom);
}
const statusMap: Record<string, string> = {
'0': '未开始',
'1': '信息审核中',
'2': '缴费确认中',
'3': '录取处理中',
'4': '最终审核中',
'5': '完成录取',
'6': '退学',
'7': '其他'
};
// 转换为可视化数据
const chartData = [];
// 如果是原始格式 (pending, infoAuditing, processing 等)
if (statusData.pending !== undefined) {
if (statusData.pending > 0) chartData.push({ name: '未开始', value: statusData.pending });
if (statusData.infoAuditing > 0) chartData.push({ name: '信息审核中', value: statusData.infoAuditing });
if (statusData.processing > 0) chartData.push({ name: '缴费确认中', value: statusData.processing });
if (statusData.finalAuditing > 0) chartData.push({ name: '最终审核中', value: statusData.finalAuditing });
if (statusData.completed > 0) chartData.push({ name: '完成录取', value: statusData.completed });
if (statusData.rejected > 0) chartData.push({ name: '退学', value: statusData.rejected });
}
// 如果是数字键值对格式
else {
Object.keys(statusData).forEach(key => {
chartData.push({
name: statusMap[key] || `状态${key}`,
value: statusData[key]
});
});
}
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 'bottom'
},
series: [
{
name: '录取状态',
type: 'pie',
radius: ['30%', '50%'],
center: ['50%', '40%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: true,
formatter: '{b}: {c} ({d}%)'
},
emphasis: {
label: {
show: true,
fontSize: '12',
fontWeight: 'bold'
}
},
labelLine: {
show: true
},
data: chartData
}
]
};
statusChartInstance.setOption(option);
} catch (error) {
console.error('获取状态分布数据失败', error);
ElMessage.warning('获取状态分布数据失败');
} finally {
statusLoading.value = false;
}
};
// 处理趋势图天数变化
const handleTrendDaysChange = () => {
loadTrendChart();
};
// 窗口大小变化时重新调整图表大小
const handleResize = () => {
trendChartInstance?.resize();
genderChartInstance?.resize();
majorChartInstance?.resize();
regionChartInstance?.resize();
statusChartInstance?.resize();
};
onMounted(() => {
console.log('统计页面已挂载');
try {
// 加载后台真实数据
loadStatisticsData();
window.addEventListener('resize', handleResize);
console.log('添加窗口resize事件监听');
} catch (e) {
console.error('页面初始化错误:', e);
}
});
onUnmounted(() => {
// 销毁图表实例
trendChartInstance?.dispose();
genderChartInstance?.dispose();
majorChartInstance?.dispose();
regionChartInstance?.dispose();
statusChartInstance?.dispose();
// 移除事件监听
window.removeEventListener('resize', handleResize);
});
</script>
<style scoped>
.dashboard-container {
padding: 8px;
}
.stat-card {
height: 120px;
margin-bottom: 20px;
border-radius: 10px;
overflow: hidden;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
}
.stat-card-content {
height: 100%;
display: flex;
align-items: center;
padding: 0 20px;
}
.stat-icon {
width: 64px;
height: 64px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
}
.stat-icon .el-icon {
font-size: 28px;
color: #fff;
}
.primary-icon {
background: linear-gradient(135deg, #5d87ff 0%, #5d87ff 100%);
}
.success-icon {
background: linear-gradient(135deg, #13deab 0%, #13de95 100%);
}
.warning-icon {
background: linear-gradient(135deg, #ffae1f 0%, #ffae1f 100%);
}
.info-icon {
background: linear-gradient(135deg, #539bff 0%, #539bff 100%);
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #303133;
margin-bottom: 4px;
line-height: 1;
}
.stat-label {
font-size: 14px;
color: #909399;
margin-bottom: 4px;
}
.stat-sub-label {
font-size: 12px;
color: #67c23a;
background-color: #f0f9eb;
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
}
.chart-row {
margin-bottom: 20px;
}
.chart-card {
border-radius: 10px;
overflow: hidden;
height: 100%;
transition: all 0.3s ease;
}
.chart-card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.chart-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.chart-container {
height: 300px;
width: 100%;
}
.chart {
height: 100%;
width: 100%;
}
</style>