12312
This commit is contained in:
commit
239fa7b92a
249 files changed
+43765
-999
No files matched your search
@@ -21,6 +21,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "2.3.1",
|
||||
"@fullcalendar/core": "^6.1.18",
|
||||
"@fullcalendar/daygrid": "^6.1.18",
|
||||
"@fullcalendar/interaction": "^6.1.18",
|
||||
"@fullcalendar/timegrid": "^6.1.18",
|
||||
"@fullcalendar/vue3": "^6.1.18",
|
||||
"@highlightjs/vue-plugin": "2.1.0",
|
||||
"@vueup/vue-quill": "1.2.0",
|
||||
"@vueuse/core": "13.1.0",
|
||||
|
||||
@@ -7,7 +7,7 @@ import request from '@/utils/request'
|
||||
* @param expireSeconds 过期时间(秒),默认300秒
|
||||
* @returns 临时访问URL
|
||||
*/
|
||||
export function getFileAccessUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
export function getFileAccessUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
return request({
|
||||
url: `/resource/file/access/${ossId}`,
|
||||
method: 'get',
|
||||
@@ -44,9 +44,9 @@ export function getBatchFileAccessUrl(ossIds: number[], bucketPassword?: string,
|
||||
* @param expireSeconds 过期时间(秒),默认300秒
|
||||
* @returns 下载URL
|
||||
*/
|
||||
export function getFileDownloadUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
export function getFileDownloadUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
return request({
|
||||
url: `/resource/file/download/${ossId}`,
|
||||
url: `/resource/oss/download/${ossId}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
bucketPassword,
|
||||
@@ -62,7 +62,7 @@ export function getFileDownloadUrl(ossId: number, bucketPassword?: string, expir
|
||||
* @param expireSeconds 过期时间(秒),默认300秒
|
||||
* @returns 预览URL
|
||||
*/
|
||||
export function getFilePreviewUrl(ossId: number, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
export function getFilePreviewUrl(ossId: number | string | bigint, bucketPassword?: string, expireSeconds: number = 300) {
|
||||
return request({
|
||||
url: `/resource/file/preview/${ossId}`,
|
||||
method: 'get',
|
||||
@@ -80,7 +80,7 @@ export function getFilePreviewUrl(ossId: number, bucketPassword?: string, expire
|
||||
* @param bucketPassword bucket访问密码
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
export async function secureDownloadFile(ossId: number, fileName: string, bucketPassword?: string) {
|
||||
export async function secureDownloadFile(ossId: number | string | bigint, fileName: string, bucketPassword?: string) {
|
||||
try {
|
||||
// 获取临时下载URL
|
||||
const { data: downloadUrl } = await getFileDownloadUrl(ossId, bucketPassword)
|
||||
@@ -119,7 +119,7 @@ export async function secureDownloadFile(ossId: number, fileName: string, bucket
|
||||
* @param bucketPassword bucket访问密码
|
||||
* @returns Promise<string> 预览URL
|
||||
*/
|
||||
export async function securePreviewFile(ossId: number, contentType?: string, bucketPassword?: string) {
|
||||
export async function securePreviewFile(ossId: number | string | bigint, contentType?: string, bucketPassword?: string) {
|
||||
try {
|
||||
// 获取临时预览URL
|
||||
const { data: previewUrl } = await getFilePreviewUrl(ossId, bucketPassword)
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { StudentLeaveVO, StudentLeaveForm, StudentLeaveQuery } from '@/api/workflow/studentLeave/studentLeave/types';
|
||||
|
||||
/**
|
||||
* 查询学生请假 - 记录学生的请假申请、审批和销假信息列表
|
||||
* @param query
|
||||
* @returns {*}
|
||||
*/
|
||||
|
||||
export const listStudentLeave = (query?: StudentLeaveQuery): AxiosPromise<StudentLeaveVO[]> => {
|
||||
return request({
|
||||
url: '/studentLeave/studentLeave/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询学生请假 - 记录学生的请假申请、审批和销假信息详细
|
||||
* @param id
|
||||
*/
|
||||
export const getStudentLeave = (id: string | number): AxiosPromise<StudentLeaveVO> => {
|
||||
return request({
|
||||
url: '/studentLeave/studentLeave/' + id,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增学生请假 - 记录学生的请假申请、审批和销假信息
|
||||
* @param data
|
||||
*/
|
||||
export const addStudentLeave = (data: StudentLeaveForm) => {
|
||||
return request({
|
||||
url: '/studentLeave/studentLeave',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改学生请假 - 记录学生的请假申请、审批和销假信息
|
||||
* @param data
|
||||
*/
|
||||
export const updateStudentLeave = (data: StudentLeaveForm) => {
|
||||
return request({
|
||||
url: '/studentLeave/studentLeave',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除学生请假 - 记录学生的请假申请、审批和销假信息
|
||||
* @param id
|
||||
*/
|
||||
export const delStudentLeave = (id: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: '/studentLeave/studentLeave/' + id,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,511 @@
|
||||
export interface StudentLeaveVO {
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 1代表删除)
|
||||
*/
|
||||
deleted: number;
|
||||
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId: string | number;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo: string;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
classId: string | number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
className: string;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
majorId: string | number;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
majorName: string;
|
||||
|
||||
/**
|
||||
* 学院ID
|
||||
*/
|
||||
collegeId: string | number;
|
||||
|
||||
/**
|
||||
* 学院名称
|
||||
*/
|
||||
collegeName: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode: string;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName: string;
|
||||
|
||||
/**
|
||||
* 开始时间(年月日时分秒)
|
||||
*/
|
||||
startTime: string;
|
||||
|
||||
/**
|
||||
* 结束时间(年月日时分秒)
|
||||
*/
|
||||
endTime: string;
|
||||
|
||||
/**
|
||||
* 请假时长(天)
|
||||
*/
|
||||
leaveDuration: number;
|
||||
|
||||
/**
|
||||
* 是否离校:0-否,1-是
|
||||
*/
|
||||
isLeaveSchool: string;
|
||||
|
||||
/**
|
||||
* 是否需要销假:0-否,1-是
|
||||
*/
|
||||
needCancel: string;
|
||||
|
||||
/**
|
||||
* 紧急联系人
|
||||
*/
|
||||
emergencyContact: string;
|
||||
|
||||
/**
|
||||
* 紧急联系电话
|
||||
*/
|
||||
emergencyPhone: string;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
leaveReason: string;
|
||||
|
||||
/**
|
||||
* 请假时地理位置
|
||||
*/
|
||||
leaveLocation: string;
|
||||
|
||||
/**
|
||||
* 附件OSS文件ID(逗号分隔)
|
||||
*/
|
||||
attachmentOssIds: string | number;
|
||||
|
||||
/**
|
||||
* 审批状态:0-待审批,1-已通过,2-已拒绝,3-已取消
|
||||
*/
|
||||
approvalStatus: string;
|
||||
|
||||
/**
|
||||
* 审批意见
|
||||
*/
|
||||
approvalOpinion: string;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
approvalTime: string;
|
||||
|
||||
/**
|
||||
* 审批人ID
|
||||
*/
|
||||
approvalBy: number;
|
||||
|
||||
/**
|
||||
* 审批人姓名
|
||||
*/
|
||||
approvalByName: string;
|
||||
|
||||
/**
|
||||
* 销假状态:0-未销假,1-已销假,2-逾期未销假
|
||||
*/
|
||||
cancelStatus: string;
|
||||
|
||||
/**
|
||||
* 销假时间
|
||||
*/
|
||||
cancelTime: string;
|
||||
|
||||
/**
|
||||
* 销假地理位置
|
||||
*/
|
||||
cancelLocation: string;
|
||||
|
||||
/**
|
||||
* 销假照片OSS文件ID(逗号分隔)
|
||||
*/
|
||||
cancelPhotoOssIds: string | number;
|
||||
|
||||
/**
|
||||
* 销假备注
|
||||
*/
|
||||
cancelRemark: string;
|
||||
|
||||
}
|
||||
|
||||
export interface StudentLeaveForm extends BaseEntity {
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 1代表删除)
|
||||
*/
|
||||
deleted?: number;
|
||||
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId?: string | number;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
classId?: string | number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
majorId?: string | number;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
majorName?: string;
|
||||
|
||||
/**
|
||||
* 学院ID
|
||||
*/
|
||||
collegeId?: string | number;
|
||||
|
||||
/**
|
||||
* 学院名称
|
||||
*/
|
||||
collegeName?: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode?: string;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName?: string;
|
||||
|
||||
/**
|
||||
* 开始时间(年月日时分秒)
|
||||
*/
|
||||
startTime?: string;
|
||||
|
||||
/**
|
||||
* 结束时间(年月日时分秒)
|
||||
*/
|
||||
endTime?: string;
|
||||
|
||||
/**
|
||||
* 请假时长(天)
|
||||
*/
|
||||
leaveDuration?: number;
|
||||
|
||||
/**
|
||||
* 是否离校:0-否,1-是
|
||||
*/
|
||||
isLeaveSchool?: string;
|
||||
|
||||
/**
|
||||
* 是否需要销假:0-否,1-是
|
||||
*/
|
||||
needCancel?: string;
|
||||
|
||||
/**
|
||||
* 紧急联系人
|
||||
*/
|
||||
emergencyContact?: string;
|
||||
|
||||
/**
|
||||
* 紧急联系电话
|
||||
*/
|
||||
emergencyPhone?: string;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
leaveReason?: string;
|
||||
|
||||
/**
|
||||
* 请假时地理位置
|
||||
*/
|
||||
leaveLocation?: string;
|
||||
|
||||
/**
|
||||
* 附件OSS文件ID(逗号分隔)
|
||||
*/
|
||||
attachmentOssIds?: string | number;
|
||||
|
||||
/**
|
||||
* 审批状态:0-待审批,1-已通过,2-已拒绝,3-已取消
|
||||
*/
|
||||
approvalStatus?: string;
|
||||
|
||||
/**
|
||||
* 审批意见
|
||||
*/
|
||||
approvalOpinion?: string;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
approvalTime?: string;
|
||||
|
||||
/**
|
||||
* 审批人ID
|
||||
*/
|
||||
approvalBy?: number;
|
||||
|
||||
/**
|
||||
* 审批人姓名
|
||||
*/
|
||||
approvalByName?: string;
|
||||
|
||||
/**
|
||||
* 销假状态:0-未销假,1-已销假,2-逾期未销假
|
||||
*/
|
||||
cancelStatus?: string;
|
||||
|
||||
/**
|
||||
* 销假时间
|
||||
*/
|
||||
cancelTime?: string;
|
||||
|
||||
/**
|
||||
* 销假地理位置
|
||||
*/
|
||||
cancelLocation?: string;
|
||||
|
||||
/**
|
||||
* 销假照片OSS文件ID(逗号分隔)
|
||||
*/
|
||||
cancelPhotoOssIds?: string | number;
|
||||
|
||||
/**
|
||||
* 销假备注
|
||||
*/
|
||||
cancelRemark?: string;
|
||||
|
||||
/**
|
||||
* 操作类型:add-新增,update-修改,view-查看
|
||||
*/
|
||||
type?: string;
|
||||
|
||||
}
|
||||
|
||||
export interface StudentLeaveQuery extends PageQuery {
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 1代表删除)
|
||||
*/
|
||||
deleted?: number;
|
||||
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId?: string | number;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
classId?: string | number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
majorId?: string | number;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
majorName?: string;
|
||||
|
||||
/**
|
||||
* 学院ID
|
||||
*/
|
||||
collegeId?: string | number;
|
||||
|
||||
/**
|
||||
* 学院名称
|
||||
*/
|
||||
collegeName?: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode?: string;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName?: string;
|
||||
|
||||
/**
|
||||
* 开始时间(年月日时分秒)
|
||||
*/
|
||||
startTime?: string;
|
||||
|
||||
/**
|
||||
* 结束时间(年月日时分秒)
|
||||
*/
|
||||
endTime?: string;
|
||||
|
||||
/**
|
||||
* 请假时长(天)
|
||||
*/
|
||||
leaveDuration?: number;
|
||||
|
||||
/**
|
||||
* 是否离校:0-否,1-是
|
||||
*/
|
||||
isLeaveSchool?: string;
|
||||
|
||||
/**
|
||||
* 是否需要销假:0-否,1-是
|
||||
*/
|
||||
needCancel?: string;
|
||||
|
||||
/**
|
||||
* 紧急联系人
|
||||
*/
|
||||
emergencyContact?: string;
|
||||
|
||||
/**
|
||||
* 紧急联系电话
|
||||
*/
|
||||
emergencyPhone?: string;
|
||||
|
||||
/**
|
||||
* 请假原因
|
||||
*/
|
||||
leaveReason?: string;
|
||||
|
||||
/**
|
||||
* 请假时地理位置
|
||||
*/
|
||||
leaveLocation?: string;
|
||||
|
||||
/**
|
||||
* 附件OSS文件ID(逗号分隔)
|
||||
*/
|
||||
attachmentOssIds?: string | number;
|
||||
|
||||
/**
|
||||
* 审批状态:0-待审批,1-已通过,2-已拒绝,3-已取消
|
||||
*/
|
||||
approvalStatus?: string;
|
||||
|
||||
/**
|
||||
* 审批意见
|
||||
*/
|
||||
approvalOpinion?: string;
|
||||
|
||||
/**
|
||||
* 审批时间
|
||||
*/
|
||||
approvalTime?: string;
|
||||
|
||||
/**
|
||||
* 审批人ID
|
||||
*/
|
||||
approvalBy?: number;
|
||||
|
||||
/**
|
||||
* 审批人姓名
|
||||
*/
|
||||
approvalByName?: string;
|
||||
|
||||
/**
|
||||
* 销假状态:0-未销假,1-已销假,2-逾期未销假
|
||||
*/
|
||||
cancelStatus?: string;
|
||||
|
||||
/**
|
||||
* 销假时间
|
||||
*/
|
||||
cancelTime?: string;
|
||||
|
||||
/**
|
||||
* 销假地理位置
|
||||
*/
|
||||
cancelLocation?: string;
|
||||
|
||||
/**
|
||||
* 销假照片OSS文件ID(逗号分隔)
|
||||
*/
|
||||
cancelPhotoOssIds?: string | number;
|
||||
|
||||
/**
|
||||
* 销假备注
|
||||
*/
|
||||
cancelRemark?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import request from '@/utils/request';
|
||||
import { AxiosPromise } from 'axios';
|
||||
import { XgLeaveTypeConfigVO, XgLeaveTypeConfigForm, XgLeaveTypeConfigQuery } from '@/api/xg/xsjcxx/xgLeaveTypeConfig/xgLeaveTypeConfig/types';
|
||||
|
||||
/**
|
||||
* 查询学生请假类型配置列表
|
||||
* @param query
|
||||
* @returns {*}
|
||||
*/
|
||||
|
||||
export const listXgLeaveTypeConfig = (query?: XgLeaveTypeConfigQuery): AxiosPromise<XgLeaveTypeConfigVO[]> => {
|
||||
return request({
|
||||
url: '/xgLeaveTypeConfig/xgLeaveTypeConfig/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查询学生请假类型配置详细
|
||||
* @param id
|
||||
*/
|
||||
export const getXgLeaveTypeConfig = (id: string | number): AxiosPromise<XgLeaveTypeConfigVO> => {
|
||||
return request({
|
||||
url: '/xgLeaveTypeConfig/xgLeaveTypeConfig/' + id,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 新增学生请假类型配置
|
||||
* @param data
|
||||
*/
|
||||
export const addXgLeaveTypeConfig = (data: XgLeaveTypeConfigForm) => {
|
||||
return request({
|
||||
url: '/xgLeaveTypeConfig/xgLeaveTypeConfig',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 修改学生请假类型配置
|
||||
* @param data
|
||||
*/
|
||||
export const updateXgLeaveTypeConfig = (data: XgLeaveTypeConfigForm) => {
|
||||
return request({
|
||||
url: '/xgLeaveTypeConfig/xgLeaveTypeConfig',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除学生请假类型配置
|
||||
* @param id
|
||||
*/
|
||||
export const delXgLeaveTypeConfig = (id: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: '/xgLeaveTypeConfig/xgLeaveTypeConfig/' + id,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,166 @@
|
||||
export interface XgLeaveTypeConfigVO {
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode: string;
|
||||
|
||||
/**
|
||||
* 请假类型描述
|
||||
*/
|
||||
leaveTypeDesc: string;
|
||||
|
||||
/**
|
||||
* 排序号
|
||||
*/
|
||||
sortOrder: number;
|
||||
|
||||
/**
|
||||
* 请假是否需要上传地理位置:0-否,1-是
|
||||
*/
|
||||
needLocationOnApply: string;
|
||||
|
||||
/**
|
||||
* 销假是否必须上传照片:0-否,1-是
|
||||
*/
|
||||
needPhotoOnCancel: string;
|
||||
|
||||
/**
|
||||
* 销假是否需要上传地理位置:0-否,1-是
|
||||
*/
|
||||
needLocationOnCancel: string;
|
||||
|
||||
/**
|
||||
* 非离校请假是否需要销假:0-否,1-是
|
||||
*/
|
||||
needCancelForNonLeave: string;
|
||||
|
||||
/**
|
||||
* 是否可以提前请假:0-否,1-是
|
||||
*/
|
||||
canApplyInAdvance: string;
|
||||
|
||||
/**
|
||||
* 是否开启:0-关闭,1-开启
|
||||
*/
|
||||
isEnabled: string;
|
||||
|
||||
/**
|
||||
* 请假最大申请时长(天)
|
||||
*/
|
||||
maxApplyDuration: number;
|
||||
|
||||
/**
|
||||
* 销假逾期多少时长给予提醒(天)
|
||||
*/
|
||||
cancelOverdueReminder: number;
|
||||
|
||||
}
|
||||
|
||||
export interface XgLeaveTypeConfigForm extends BaseEntity {
|
||||
/**
|
||||
* 主键ID(雪花ID)
|
||||
*/
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName?: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode?: string;
|
||||
|
||||
/**
|
||||
* 请假类型描述
|
||||
*/
|
||||
leaveTypeDesc?: string;
|
||||
|
||||
/**
|
||||
* 排序号
|
||||
*/
|
||||
sortOrder?: number;
|
||||
|
||||
/**
|
||||
* 请假是否需要上传地理位置:0-否,1-是
|
||||
*/
|
||||
needLocationOnApply?: string;
|
||||
|
||||
/**
|
||||
* 销假是否必须上传照片:0-否,1-是
|
||||
*/
|
||||
needPhotoOnCancel?: string;
|
||||
|
||||
/**
|
||||
* 销假是否需要上传地理位置:0-否,1-是
|
||||
*/
|
||||
needLocationOnCancel?: string;
|
||||
|
||||
/**
|
||||
* 非离校请假是否需要销假:0-否,1-是
|
||||
*/
|
||||
needCancelForNonLeave?: string;
|
||||
|
||||
/**
|
||||
* 是否可以提前请假:0-否,1-是
|
||||
*/
|
||||
canApplyInAdvance?: string;
|
||||
|
||||
/**
|
||||
* 是否开启:0-关闭,1-开启
|
||||
*/
|
||||
isEnabled?: string;
|
||||
|
||||
/**
|
||||
* 请假最大申请时长(天)
|
||||
*/
|
||||
maxApplyDuration?: number;
|
||||
|
||||
/**
|
||||
* 销假逾期多少时长给予提醒(天)
|
||||
*/
|
||||
cancelOverdueReminder?: number;
|
||||
|
||||
}
|
||||
|
||||
export interface XgLeaveTypeConfigQuery extends PageQuery {
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 1代表删除)
|
||||
*/
|
||||
deleted?: number;
|
||||
|
||||
/**
|
||||
* 请假类型名称
|
||||
*/
|
||||
leaveTypeName?: string;
|
||||
|
||||
/**
|
||||
* 请假类型编码
|
||||
*/
|
||||
leaveTypeCode?: string;
|
||||
|
||||
/**
|
||||
* 是否开启:0-关闭,1-开启
|
||||
*/
|
||||
isEnabled?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
import request from '@/utils/request';
|
||||
import {
|
||||
XljkConsultationScheduleVO,
|
||||
XljkConsultationScheduleForm,
|
||||
XljkConsultationScheduleQuery,
|
||||
XljkConsultationAppointmentVO,
|
||||
XljkConsultationAppointmentForm,
|
||||
XljkConsultationAppointmentQuery,
|
||||
AvailableTimeSlot,
|
||||
TimeSlotDetail,
|
||||
CounselorOption
|
||||
} from './types';
|
||||
|
||||
// ===== 预约时段管理 API =====
|
||||
|
||||
// 查询预约时段列表
|
||||
export const listSchedule = (query: XljkConsultationScheduleQuery) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/schedule/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
// 查询预约时段详细信息
|
||||
export const getSchedule = (id: string | number) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/schedule/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 新增预约时段
|
||||
export const addSchedule = (data: any) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/schedule',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
// 修改预约时段
|
||||
export const updateSchedule = (data: XljkConsultationScheduleForm) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/schedule',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
// 删除预约时段
|
||||
export const delSchedule = (ids: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/schedule/${typeof ids === 'object' ? ids.join(',') : ids}`,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
|
||||
// ===== 预约记录管理 API =====
|
||||
|
||||
// 查询预约记录列表
|
||||
export const listAppointment = (query: XljkConsultationAppointmentQuery) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
};
|
||||
|
||||
// 查询预约记录详细信息
|
||||
export const getAppointment = (id: string | number) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/appointment/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 修改预约记录
|
||||
export const updateAppointment = (data: XljkConsultationAppointmentForm) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
// 删除预约记录
|
||||
export const delAppointment = (ids: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/appointment/${typeof ids === 'object' ? ids.join(',') : ids}`,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
|
||||
// 获取可预约时段列表(用于日历视图)
|
||||
export const getAvailableTimeSlots = (startDate: string, endDate: string) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/available-slots',
|
||||
method: 'get',
|
||||
params: { startDate, endDate }
|
||||
});
|
||||
};
|
||||
|
||||
// 获取可用的详细时段列表(30分钟一个时段)
|
||||
export const getAvailableDetailSlots = (date: string, timeSlot: string) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/available-detail-slots',
|
||||
method: 'get',
|
||||
params: { date, timeSlot }
|
||||
});
|
||||
};
|
||||
|
||||
// 学生提交预约
|
||||
export const submitAppointment = async (data: XljkConsultationAppointmentForm) => {
|
||||
// 检查是否被限制预约
|
||||
try {
|
||||
const { checkLimitation } = await import('./noshow');
|
||||
if (data.studentId) {
|
||||
const res = await checkLimitation(data.studentId);
|
||||
if (res.code === 200 && res.data && res.data.limited) {
|
||||
return {
|
||||
code: 400,
|
||||
msg: `由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDateStr} 自动解除。如有特殊情况,请联系心理咨询中心。`,
|
||||
data: null
|
||||
};
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查预约限制失败', error);
|
||||
// 检查失败不阻止预约,继续提交
|
||||
}
|
||||
|
||||
// 提交预约申请
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/submit',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
// 获取我的预约记录(学生用)
|
||||
export const getMyAppointments = (studentId: string | number, forceRefresh: boolean = false) => {
|
||||
let url = `/api/xljk/consultation/appointment/my-appointments/${studentId}`;
|
||||
|
||||
// 强制添加时间戳参数,确保绕过浏览器缓存
|
||||
url += `?_t=${new Date().getTime()}`;
|
||||
|
||||
// 如果需要额外强制刷新(如用户手动点击刷新按钮)
|
||||
if (forceRefresh) {
|
||||
url += `&force=true`;
|
||||
}
|
||||
|
||||
return request({
|
||||
url,
|
||||
method: 'get',
|
||||
headers: {
|
||||
// 禁止所有缓存
|
||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||
'Pragma': 'no-cache',
|
||||
'Expires': '0'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 记录咨询结果
|
||||
export const recordConsultation = (id: string | number, data: any) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/appointment/record-consultation/${id}`,
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
};
|
||||
|
||||
// 获取咨询师选项
|
||||
export const getCounselorOptions = () => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/schedule/options/counselors',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 获取学生信息(通过学号)
|
||||
export const getStudentByNo = (studentNo: string) => {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/student/${studentNo}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 获取院系选项
|
||||
export const getCollegeOptions = () => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/options/colleges',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 获取专业选项
|
||||
export const getMajorOptions = (collegeName?: string) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/options/majors',
|
||||
method: 'get',
|
||||
params: { collegeName }
|
||||
});
|
||||
};
|
||||
|
||||
// 获取班级选项
|
||||
export const getClassOptions = (collegeName?: string, majorName?: string) => {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/appointment/options/classes',
|
||||
method: 'get',
|
||||
params: { collegeName, majorName }
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import request from '@/utils/request';
|
||||
import { NoShowQuery, NoShowVO } from './types';
|
||||
|
||||
/**
|
||||
* 查询未到访记录列表
|
||||
* @param query 查询参数
|
||||
* @returns 未到访记录列表
|
||||
*/
|
||||
export function listNoShow(query: NoShowQuery) {
|
||||
// 创建一个新的查询对象,避免修改原始对象
|
||||
const params = { ...query };
|
||||
|
||||
// 如果状态是'all',表示"全部",则从参数中移除
|
||||
if (params.status === 'all') {
|
||||
delete params.status;
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/api/xljk/consultation/no-show/list',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未到访记录详情
|
||||
* @param id 未到访记录ID
|
||||
* @returns 未到访记录详情
|
||||
*/
|
||||
export function getNoShow(id: string | number) {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/no-show/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解除预约限制
|
||||
* @param id 未到访记录ID
|
||||
* @returns 操作结果
|
||||
*/
|
||||
export function releaseNoShowLimitation(id: string | number) {
|
||||
return request({
|
||||
url: `/api/xljk/consultation/no-show/${id}/release`,
|
||||
method: 'put'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查学生是否被限制预约
|
||||
* @param studentId 学生ID
|
||||
* @returns 限制状态
|
||||
*/
|
||||
export function checkLimitation(studentId: string | number) {
|
||||
return request({
|
||||
url: '/api/xljk/consultation/no-show/check-limitation',
|
||||
method: 'get',
|
||||
params: { studentId }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
export interface XljkConsultationScheduleVO {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId: number;
|
||||
|
||||
/**
|
||||
* 咨询师姓名(关联展示字段)
|
||||
*/
|
||||
counselorName?: string;
|
||||
|
||||
/**
|
||||
* 排班日期
|
||||
*/
|
||||
scheduleDate: string;
|
||||
|
||||
/**
|
||||
* 时段类型(0上午 1下午)
|
||||
*/
|
||||
timeSlot: string;
|
||||
|
||||
/**
|
||||
* 开始时间(HH:mm)
|
||||
*/
|
||||
startTime: string;
|
||||
|
||||
/**
|
||||
* 结束时间(HH:mm)
|
||||
*/
|
||||
endTime: string;
|
||||
|
||||
/**
|
||||
* 状态(0正常 1停用)
|
||||
*/
|
||||
status: string;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
createBy: number;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createTime: string;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
updateBy: number;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
updateTime: string;
|
||||
}
|
||||
|
||||
export interface XljkConsultationScheduleForm extends BaseEntity {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
id?: string | number;
|
||||
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId?: number;
|
||||
|
||||
/**
|
||||
* 排班日期
|
||||
*/
|
||||
scheduleDate?: string;
|
||||
|
||||
/**
|
||||
* 时段类型(0上午 1下午)
|
||||
*/
|
||||
timeSlot?: string;
|
||||
|
||||
/**
|
||||
* 开始时间(HH:mm)
|
||||
*/
|
||||
startTime?: string;
|
||||
|
||||
/**
|
||||
* 结束时间(HH:mm)
|
||||
*/
|
||||
endTime?: string;
|
||||
|
||||
/**
|
||||
* 状态(0正常 1停用)
|
||||
*/
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface XljkConsultationScheduleQuery extends PageQuery {
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId?: number;
|
||||
|
||||
/**
|
||||
* 排班日期开始
|
||||
*/
|
||||
beginScheduleDate?: string;
|
||||
|
||||
/**
|
||||
* 排班日期结束
|
||||
*/
|
||||
endScheduleDate?: string;
|
||||
|
||||
/**
|
||||
* 时段类型(0上午 1下午)
|
||||
*/
|
||||
timeSlot?: string;
|
||||
|
||||
/**
|
||||
* 状态(0正常 1停用)
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export interface XljkConsultationAppointmentVO extends BaseEntity {
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId?: number;
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId?: number;
|
||||
/**
|
||||
* 排班ID
|
||||
*/
|
||||
scheduleId?: number;
|
||||
/**
|
||||
* 预约日期
|
||||
*/
|
||||
appointmentDate?: string;
|
||||
/**
|
||||
* 预约时段
|
||||
*/
|
||||
appointmentTime?: string;
|
||||
/**
|
||||
* 具体预约时间段,如"08:30-09:00"
|
||||
*/
|
||||
appointmentDetailTime?: string;
|
||||
/**
|
||||
* 咨询原因
|
||||
*/
|
||||
reason?: string;
|
||||
/**
|
||||
* 状态(0待咨询 1已完成 2已取消 3未到访)
|
||||
*/
|
||||
status?: number;
|
||||
/**
|
||||
* 实际到访时间
|
||||
*/
|
||||
actualVisitTime?: any;
|
||||
/**
|
||||
* 沟通时长(分钟)
|
||||
*/
|
||||
communicationDuration?: number;
|
||||
/**
|
||||
* 咨询内容
|
||||
*/
|
||||
consultationContent?: string;
|
||||
/**
|
||||
* 咨询师建议
|
||||
*/
|
||||
counselorSuggestion?: string;
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
/**
|
||||
* 班级
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* 咨询师姓名
|
||||
*/
|
||||
counselorName?: string;
|
||||
/**
|
||||
* 院系
|
||||
*/
|
||||
collegeName?: string;
|
||||
/**
|
||||
* 专业
|
||||
*/
|
||||
majorName?: string;
|
||||
}
|
||||
|
||||
export interface XljkConsultationAppointmentForm {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
id?: string | number;
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId?: string | number;
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId?: number;
|
||||
/**
|
||||
* 排班ID
|
||||
*/
|
||||
scheduleId?: number;
|
||||
/**
|
||||
* 预约日期
|
||||
*/
|
||||
appointmentDate?: string;
|
||||
/**
|
||||
* 预约时段
|
||||
*/
|
||||
appointmentTime?: string;
|
||||
/**
|
||||
* 具体预约时间段,如"08:30-09:00"
|
||||
*/
|
||||
appointmentDetailTime?: string;
|
||||
/**
|
||||
* 咨询原因
|
||||
*/
|
||||
reason?: string;
|
||||
/**
|
||||
* 状态(0待咨询 1已完成 2已取消 3未到访)
|
||||
*/
|
||||
status?: number;
|
||||
/**
|
||||
* 实际到访时间
|
||||
*/
|
||||
actualVisitTime?: any;
|
||||
/**
|
||||
* 沟通时长(分钟)
|
||||
*/
|
||||
communicationDuration?: number;
|
||||
/**
|
||||
* 咨询内容
|
||||
*/
|
||||
consultationContent?: string;
|
||||
/**
|
||||
* 咨询师建议
|
||||
*/
|
||||
counselorSuggestion?: string;
|
||||
}
|
||||
|
||||
export interface XljkConsultationAppointmentQuery extends PageQuery {
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId?: number;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 院系
|
||||
*/
|
||||
collegeName?: string;
|
||||
|
||||
/**
|
||||
* 专业
|
||||
*/
|
||||
majorName?: string;
|
||||
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId?: number;
|
||||
|
||||
/**
|
||||
* 预约日期开始
|
||||
*/
|
||||
beginAppointmentDate?: string;
|
||||
|
||||
/**
|
||||
* 预约日期结束
|
||||
*/
|
||||
endAppointmentDate?: string;
|
||||
|
||||
/**
|
||||
* 状态(0待咨询 1已完成 2已取消 3未到访)
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
export interface AvailableTimeSlot {
|
||||
/**
|
||||
* 预约时段ID
|
||||
*/
|
||||
scheduleId: number;
|
||||
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
counselorId: number;
|
||||
|
||||
/**
|
||||
* 咨询师姓名
|
||||
*/
|
||||
counselorName?: string;
|
||||
|
||||
/**
|
||||
* 日期
|
||||
*/
|
||||
date: string;
|
||||
|
||||
/**
|
||||
* 时段类型(0上午 1下午)
|
||||
*/
|
||||
timeSlot: string;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
startTime: string;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
endTime: string;
|
||||
|
||||
/**
|
||||
* 是否全部约满
|
||||
*/
|
||||
allBooked?: boolean;
|
||||
|
||||
/**
|
||||
* 可用数量
|
||||
*/
|
||||
availableCount?: number;
|
||||
|
||||
/**
|
||||
* 是否已过期
|
||||
*/
|
||||
isExpired?: boolean;
|
||||
|
||||
/**
|
||||
* 日历事件标题
|
||||
*/
|
||||
title?: string;
|
||||
|
||||
/**
|
||||
* 日历事件开始时间
|
||||
*/
|
||||
start?: string;
|
||||
|
||||
/**
|
||||
* 日历事件结束时间
|
||||
*/
|
||||
end?: string;
|
||||
}
|
||||
|
||||
export interface TimeSlotDetail {
|
||||
/**
|
||||
* 开始时间(HH:mm)
|
||||
*/
|
||||
startTime: string;
|
||||
|
||||
/**
|
||||
* 结束时间(HH:mm)
|
||||
*/
|
||||
endTime: string;
|
||||
|
||||
/**
|
||||
* 是否已预约
|
||||
*/
|
||||
isBooked: boolean;
|
||||
|
||||
/**
|
||||
* 是否已过期
|
||||
*/
|
||||
isExpired: boolean;
|
||||
|
||||
/**
|
||||
* 完整时段表示(如"08:30-09:00")
|
||||
*/
|
||||
timeSlot: string;
|
||||
}
|
||||
|
||||
export interface CounselorOption {
|
||||
/**
|
||||
* 咨询师ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 咨询师姓名
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 预约时段汇总视图对象
|
||||
export interface XljkConsultationScheduleSummaryVO {
|
||||
id: number;
|
||||
tenantId: string;
|
||||
createDept: number;
|
||||
createBy: number;
|
||||
createTime: string;
|
||||
updateBy: number;
|
||||
updateTime: string;
|
||||
deleted: number;
|
||||
counselorId: number;
|
||||
counselorName: string;
|
||||
dateRange: string;
|
||||
morningEnabled: boolean;
|
||||
morningTime: string;
|
||||
afternoonEnabled: boolean;
|
||||
afternoonTime: string;
|
||||
status: string;
|
||||
scheduleCount: number;
|
||||
batchId: string;
|
||||
}
|
||||
|
||||
// 院系选项
|
||||
export interface CollegeOption {
|
||||
/**
|
||||
* 院系ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 院系名称
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 专业选项
|
||||
export interface MajorOption {
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 班级选项
|
||||
export interface ClassOption {
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 未到访记录查询参数
|
||||
export interface NoShowQuery extends PageQuery {
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 状态(0生效中 1已过期 2已解除 'all'全部)
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 未到访日期开始
|
||||
*/
|
||||
startDate?: string;
|
||||
|
||||
/**
|
||||
* 未到访日期结束
|
||||
*/
|
||||
endDate?: string;
|
||||
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
// 未到访记录VO
|
||||
export interface NoShowVO {
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
id: string | number;
|
||||
|
||||
/**
|
||||
* 学生ID
|
||||
*/
|
||||
studentId: string | number;
|
||||
|
||||
/**
|
||||
* 学生姓名
|
||||
*/
|
||||
studentName?: string;
|
||||
|
||||
/**
|
||||
* 学号
|
||||
*/
|
||||
studentNo?: string;
|
||||
|
||||
/**
|
||||
* 院系
|
||||
*/
|
||||
collegeName?: string;
|
||||
|
||||
/**
|
||||
* 专业
|
||||
*/
|
||||
majorName?: string;
|
||||
|
||||
/**
|
||||
* 班级
|
||||
*/
|
||||
className?: string;
|
||||
|
||||
/**
|
||||
* 咨询师名称
|
||||
*/
|
||||
counselorName?: string;
|
||||
|
||||
/**
|
||||
* 预约日期
|
||||
*/
|
||||
appointmentDate?: string;
|
||||
|
||||
/**
|
||||
* 预约时段
|
||||
*/
|
||||
appointmentDetailTime?: string;
|
||||
|
||||
/**
|
||||
* 咨询原因
|
||||
*/
|
||||
reason?: string;
|
||||
|
||||
/**
|
||||
* 预约记录ID
|
||||
*/
|
||||
appointmentId: string | number;
|
||||
|
||||
/**
|
||||
* 未到访日期
|
||||
*/
|
||||
noShowDate: string;
|
||||
|
||||
/**
|
||||
* 限制到期日
|
||||
*/
|
||||
expireDate?: string;
|
||||
|
||||
/**
|
||||
* 状态(0生效中 1已过期 2已解除)
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
createTime?: string;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
updateTime?: string;
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
// 查询心理健康测评列表
|
||||
export const listEvaluation = (query: XljkEvaluationQuery) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/list',
|
||||
url: '/api/xljk/evaluation/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
@@ -24,7 +24,7 @@ export const listEvaluation = (query: XljkEvaluationQuery) => {
|
||||
// 查询心理健康测评详细信息
|
||||
export const getEvaluation = (id: string | number) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/${id}`,
|
||||
url: `/api/xljk/evaluation/${id}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
@@ -45,7 +45,7 @@ export const addEvaluation = (data: XljkEvaluationForm) => {
|
||||
}
|
||||
|
||||
return request({
|
||||
url: '/xljk/evaluation',
|
||||
url: '/api/xljk/evaluation',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
@@ -54,7 +54,7 @@ export const addEvaluation = (data: XljkEvaluationForm) => {
|
||||
// 修改心理健康测评
|
||||
export const updateEvaluation = (data: XljkEvaluationForm) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation',
|
||||
url: '/api/xljk/evaluation',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
@@ -63,7 +63,7 @@ export const updateEvaluation = (data: XljkEvaluationForm) => {
|
||||
// 删除心理健康测评
|
||||
export const delEvaluation = (ids: string | number | Array<string | number>) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/${typeof ids === 'object' ? ids.join(',') : ids}`,
|
||||
url: `/api/xljk/evaluation/${typeof ids === 'object' ? ids.join(',') : ids}`,
|
||||
method: 'delete'
|
||||
});
|
||||
};
|
||||
@@ -71,7 +71,7 @@ export const delEvaluation = (ids: string | number | Array<string | number>) =>
|
||||
// 获取院系选项
|
||||
export const getDepartmentOptions = () => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/options/department',
|
||||
url: '/api/xljk/evaluation/options/department',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
@@ -79,7 +79,7 @@ export const getDepartmentOptions = () => {
|
||||
// 获取院系选项(新接口)
|
||||
export const getCollegeOptions = () => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/options/colleges',
|
||||
url: '/api/xljk/evaluation/options/colleges',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
@@ -87,7 +87,7 @@ export const getCollegeOptions = () => {
|
||||
// 获取专业选项
|
||||
export const getMajorOptions = (collegeName?: string) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/options/majors',
|
||||
url: '/api/xljk/evaluation/options/majors',
|
||||
method: 'get',
|
||||
params: { collegeName }
|
||||
});
|
||||
@@ -96,7 +96,7 @@ export const getMajorOptions = (collegeName?: string) => {
|
||||
// 获取班级选项
|
||||
export const getClassOptions = (collegeName?: string, majorName?: string) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/options/classes',
|
||||
url: '/api/xljk/evaluation/options/classes',
|
||||
method: 'get',
|
||||
params: { collegeName, majorName }
|
||||
});
|
||||
@@ -105,7 +105,7 @@ export const getClassOptions = (collegeName?: string, majorName?: string) => {
|
||||
// 获取班级学生列表
|
||||
export const getStudentsByClass = (collegeName?: string, majorName?: string, className?: string) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/students',
|
||||
url: '/api/xljk/evaluation/students',
|
||||
method: 'get',
|
||||
params: { collegeName, majorName, className }
|
||||
});
|
||||
@@ -114,23 +114,23 @@ export const getStudentsByClass = (collegeName?: string, majorName?: string, cla
|
||||
// 获取学期选项
|
||||
export const getSemesterOptions = () => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/options/semester',
|
||||
url: '/api/xljk/evaluation/options/semester',
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
|
||||
// 发布测评
|
||||
// 发布测评(已废弃,改为提交功能)
|
||||
export const publishEvaluation = (id: string | number) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/${id}/publish`,
|
||||
url: `/api/xljk/evaluation/${id}/publish`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
|
||||
// 完成测评
|
||||
// 提交测评
|
||||
export const completeEvaluation = (id: string | number) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/${id}/complete`,
|
||||
url: `/api/xljk/evaluation/${id}/complete`,
|
||||
method: 'put'
|
||||
});
|
||||
};
|
||||
@@ -138,7 +138,7 @@ export const completeEvaluation = (id: string | number) => {
|
||||
// 获取测评统计数据
|
||||
export const getEvaluationStatistics = (id: string | number) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/${id}/statistics`,
|
||||
url: `/api/xljk/evaluation/${id}/statistics`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
@@ -146,7 +146,7 @@ export const getEvaluationStatistics = (id: string | number) => {
|
||||
// 获取重点关注学生统计
|
||||
export const getFocusStudentsStatistics = (query: XljkEvaluationQuery) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/statistics/focus',
|
||||
url: '/api/xljk/evaluation/statistics/focus',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
@@ -155,7 +155,7 @@ export const getFocusStudentsStatistics = (query: XljkEvaluationQuery) => {
|
||||
// 查询测评明细列表
|
||||
export const listEvaluationDetail = (query: XljkEvaluationDetailQuery) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/detail/list',
|
||||
url: '/api/xljk/evaluation/detail/list',
|
||||
method: 'get',
|
||||
params: query
|
||||
});
|
||||
@@ -164,7 +164,7 @@ export const listEvaluationDetail = (query: XljkEvaluationDetailQuery) => {
|
||||
// 获取测评明细列表
|
||||
export const getEvaluationDetailsByEvaluationId = (evaluationId: string | number) => {
|
||||
return request({
|
||||
url: `/xljk/evaluation/detail/evaluation/${evaluationId}`,
|
||||
url: `/api/xljk/evaluation/detail/evaluation/${evaluationId}`,
|
||||
method: 'get'
|
||||
});
|
||||
};
|
||||
@@ -172,7 +172,7 @@ export const getEvaluationDetailsByEvaluationId = (evaluationId: string | number
|
||||
// 批量新增测评明细
|
||||
export const batchAddEvaluationDetails = (data: XljkEvaluationDetailForm[]) => {
|
||||
return request({
|
||||
url: '/xljk/evaluation/detail/batch',
|
||||
url: '/api/xljk/evaluation/detail/batch',
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
@@ -184,7 +184,7 @@ export const batchUpdateMentalStatus = (data: XljkEvaluationDetailForm[]) => {
|
||||
console.log('批量更新数据 - 第一条记录evaluationId:', data[0]?.evaluationId);
|
||||
|
||||
return request({
|
||||
url: '/xljk/evaluation/detail/batch/mentalStatus',
|
||||
url: '/api/xljk/evaluation/detail/batch/mentalStatus',
|
||||
method: 'put',
|
||||
data: data
|
||||
});
|
||||
|
||||
@@ -45,7 +45,7 @@ export interface XljkEvaluationVO {
|
||||
semester: string;
|
||||
|
||||
/**
|
||||
* 状态(0-草稿,1-已发布,2-已完成)
|
||||
* 状态(0-草稿,2-已完成)
|
||||
*/
|
||||
status: string;
|
||||
|
||||
@@ -137,7 +137,7 @@ export interface XljkEvaluationForm extends BaseEntity {
|
||||
semester?: string;
|
||||
|
||||
/**
|
||||
* 状态(0-草稿,1-已发布,2-已完成)
|
||||
* 状态(0-草稿,2-已完成)
|
||||
*/
|
||||
status?: string;
|
||||
|
||||
@@ -443,6 +443,11 @@ export interface StudentInfo {
|
||||
}
|
||||
|
||||
export interface CollegeOption {
|
||||
/**
|
||||
* 院系ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 院系名称
|
||||
*/
|
||||
@@ -450,6 +455,11 @@ export interface CollegeOption {
|
||||
}
|
||||
|
||||
export interface MajorOption {
|
||||
/**
|
||||
* 专业ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 专业名称
|
||||
*/
|
||||
@@ -457,6 +467,11 @@ export interface MajorOption {
|
||||
}
|
||||
|
||||
export interface ClassOption {
|
||||
/**
|
||||
* 班级ID
|
||||
*/
|
||||
id: number;
|
||||
|
||||
/**
|
||||
* 班级名称
|
||||
*/
|
||||
|
||||
@@ -33,11 +33,12 @@
|
||||
<!-- 文件列表 -->
|
||||
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
|
||||
<li v-for="(file, index) in fileList" :key="file.uid" class="el-upload-list__item ele-upload-list__item-content">
|
||||
<el-link :href="`${file.url}`" :underline="false" target="_blank">
|
||||
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
||||
</el-link>
|
||||
<!-- <el-link :href="`${file.url}`" :underline="false" target="_blank"> -->
|
||||
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
|
||||
<!-- </el-link> -->
|
||||
<div class="ele-upload-list__item-content-action">
|
||||
<el-button type="danger" v-if="!disabled" link @click="handleDelete(index)">删除</el-button>
|
||||
<el-button type="danger" v-if="!disabled" link @click="handleDownloadAttachment(file)">下载</el-button>
|
||||
</div>
|
||||
</li>
|
||||
</transition-group>
|
||||
@@ -48,6 +49,8 @@
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { delOss, listByIds } from '@/api/system/oss';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
import request from '@/utils/request';
|
||||
import { vLoading, ElLoading } from 'element-plus';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -116,6 +119,64 @@ watch(
|
||||
{ deep: true, immediate: true }
|
||||
);
|
||||
|
||||
/** 下载附件 */
|
||||
const handleDownloadAttachment = async (file: any) => {
|
||||
let downloadUrl = file.ossId;
|
||||
let fileName = file.name || 'attachment';
|
||||
|
||||
if (!downloadUrl) {
|
||||
proxy?.$modal.msgError('无法获取文件下载地址');
|
||||
return;
|
||||
}
|
||||
|
||||
// 对于服务器文件,使用fetch获取内容强制下载
|
||||
try {
|
||||
const loading = ElLoading.service({
|
||||
text: '正在下载文件...',
|
||||
target: document.body
|
||||
});
|
||||
|
||||
// const response = await fetch(downloadUrl);
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// }
|
||||
// 通过fetch获取文件内容,强制下载
|
||||
// const response = await req('/resource/oss/download/' + attachment.filePath);
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + downloadUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
// const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileName;
|
||||
link.style.display = 'none';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
loading?.close();
|
||||
console.log('下载服务器文件成功:', fileName);
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
proxy?.$modal.msgError('下载文件失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 上传前校检格式和大小
|
||||
const handleBeforeUpload = (file: any) => {
|
||||
// 校检文件类型
|
||||
|
||||
@@ -6,19 +6,23 @@
|
||||
:preview-src-list="realSrcList"
|
||||
preview-teleported
|
||||
v-loading="loading"
|
||||
@error="handleImageError"
|
||||
@load="handleImageLoad"
|
||||
>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon><picture-filled /></el-icon>
|
||||
<div class="error-text">图片加载失败</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { computed, ref, watch, onBeforeUnmount } from 'vue';
|
||||
import { listByIds } from '@/api/system/oss';
|
||||
import type { OssVO } from '@/api/system/oss/types';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const props = defineProps({
|
||||
src: {
|
||||
@@ -42,28 +46,159 @@ const props = defineProps({
|
||||
|
||||
const loading = ref(false);
|
||||
const urlCache = ref<Map<string, string>>(new Map());
|
||||
const blobUrls = ref<string[]>([]);
|
||||
|
||||
// 图片加载成功事件
|
||||
const handleImageLoad = () => {
|
||||
// 图片加载成功
|
||||
};
|
||||
|
||||
// 图片加载失败事件
|
||||
const handleImageError = (error: any) => {
|
||||
// 图片加载失败处理
|
||||
};
|
||||
|
||||
// 清理blob URL,防止内存泄漏
|
||||
const cleanupBlobUrls = () => {
|
||||
blobUrls.value.forEach((url) => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
blobUrls.value = [];
|
||||
urlCache.value.clear();
|
||||
};
|
||||
|
||||
// 组件卸载时清理blob URLs
|
||||
onBeforeUnmount(() => {
|
||||
cleanupBlobUrls();
|
||||
});
|
||||
|
||||
// 验证blob数据是否为有效图片
|
||||
const validateImageBlob = (blob: Blob): boolean => {
|
||||
// 允许的图片类型,包括通用的octet-stream类型(OSS服务器常返回此类型)
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'application/octet-stream'];
|
||||
|
||||
// 基本验证:类型检查和大小检查
|
||||
const hasValidType = validTypes.includes(blob.type) || blob.type === '' || blob.type === 'application/octet-stream';
|
||||
const hasValidSize = blob.size > 0;
|
||||
|
||||
return hasValidType && hasValidSize;
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 检查是否已有缓存的blob URL
|
||||
const cachedUrl = urlCache.value.get(imageUrl);
|
||||
if (cachedUrl) {
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 检查blob内容类型
|
||||
// const contentType = blob.type;
|
||||
|
||||
// 如果Content-Type明确表明这不是图片数据且不是通用二进制格式,进行检查
|
||||
// if (
|
||||
// contentType &&
|
||||
// !contentType.toLowerCase().startsWith('image/') &&
|
||||
// !contentType.toLowerCase().includes('octet-stream') &&
|
||||
// (contentType.toLowerCase().includes('text/') || contentType.toLowerCase().includes('html'))
|
||||
// ) {
|
||||
// const text = await blob.text();
|
||||
// const preview = text.substring(0, 300);
|
||||
// throw new Error(`响应Content-Type不是图片格式: ${contentType}。响应内容预览: ${preview}`);
|
||||
// }
|
||||
|
||||
// 验证是否为有效图片
|
||||
// if (!validateImageBlob(blob)) {
|
||||
// throw new Error(`无效的图片格式: ${blob.type}, 大小: ${blob.size}`);
|
||||
// }
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 缓存blob URL
|
||||
// urlCache.value.set(imageUrl, blobUrl);
|
||||
// blobUrls.value.push(blobUrl);
|
||||
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 通过ossid获取真实URL
|
||||
const getUrlByOssId = async (ossId: string): Promise<string> => {
|
||||
// 检查缓存
|
||||
if (urlCache.value.has(ossId)) {
|
||||
return urlCache.value.get(ossId)!;
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await listByIds(ossId);
|
||||
const ossList: OssVO[] = response.data;
|
||||
|
||||
if (ossList && ossList.length > 0) {
|
||||
const url = ossList[0].url;
|
||||
// 缓存结果
|
||||
urlCache.value.set(ossId, url);
|
||||
return url;
|
||||
}
|
||||
return '';
|
||||
// const response = await listByIds(ossId);
|
||||
// // RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
|
||||
|
||||
// // 正确提取数据,考虑可能的响应格式
|
||||
// const ossList: OssVO[] = (response as any)?.data || response;
|
||||
|
||||
// if (!ossList || ossList.length === 0) {
|
||||
// throw new Error(`OSS ID ${ossId} 在数据库中不存在或已被删除。可能原因:1) OSS ID不正确 2) 文件已被删除 3) 权限不足`);
|
||||
// }
|
||||
|
||||
// const ossFile = ossList[0];
|
||||
|
||||
// // 额外验证ossFile对象的完整性
|
||||
// if (!ossFile || typeof ossFile !== 'object') {
|
||||
// throw new Error(`OSS数据格式异常:期望对象,实际获得 ${typeof ossFile}`);
|
||||
// }
|
||||
|
||||
// if (!ossFile.url) {
|
||||
// throw new Error(`OSS文件缺少url属性。完整数据:${JSON.stringify(ossFile)}`);
|
||||
// }
|
||||
// const imageUrl = ossFile.url;
|
||||
|
||||
// // 检查URL是否有效
|
||||
// if (!imageUrl || typeof imageUrl !== 'string') {
|
||||
// throw new Error(`OSS接口返回的URL无效。URL: ${imageUrl}, 类型: ${typeof imageUrl}`);
|
||||
// }
|
||||
|
||||
// // 如果URL不是以/开头的下载路径,可能是直接的OSS URL
|
||||
// if (!imageUrl.startsWith('/resource/oss/download/')) {
|
||||
// // 对于直接的OSS URL,我们仍然通过认证请求获取
|
||||
// }
|
||||
|
||||
// 获取认证的blob URL
|
||||
return await getImageBlobUrl('/resource/oss/download/' + ossId);
|
||||
} catch (error) {
|
||||
console.error('获取OSS URL失败:', error);
|
||||
return '';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -86,42 +221,75 @@ const getUrlsByOssIds = async (ossIds: string[]): Promise<string[]> => {
|
||||
return urls;
|
||||
};
|
||||
|
||||
const realSrc = ref('');
|
||||
// 处理普通URL模式(非ossId)
|
||||
const getUrlsByDirectUrls = async (urls: string[]): Promise<string[]> => {
|
||||
const blobUrls: string[] = [];
|
||||
|
||||
for (const url of urls) {
|
||||
if (url.trim()) {
|
||||
const blobUrl = await getImageBlobUrl(url.trim());
|
||||
if (blobUrl) {
|
||||
blobUrls.push(blobUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return blobUrls;
|
||||
};
|
||||
|
||||
const realSrc = ref('');
|
||||
const realSrcList = ref<string[]>([]);
|
||||
|
||||
// 处理src变化
|
||||
watch(
|
||||
() => props.src,
|
||||
async (newSrc) => {
|
||||
// 清理之前的blob URLs
|
||||
cleanupBlobUrls();
|
||||
|
||||
if (!newSrc) {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.isOssId) {
|
||||
// ossid模式:通过接口获取真实URL
|
||||
const ossIds = newSrc.split(',');
|
||||
const urls = await getUrlsByOssIds(ossIds);
|
||||
loading.value = true;
|
||||
|
||||
if (urls.length > 0) {
|
||||
realSrc.value = urls[0];
|
||||
realSrcList.value = urls;
|
||||
try {
|
||||
if (props.isOssId) {
|
||||
// console.log('ossid模式');
|
||||
// console.log(newSrc);
|
||||
// ossid模式:通过接口获取真实URL,然后转换为认证的blob URL
|
||||
const ossIds = newSrc.split(',').filter((id) => id.trim());
|
||||
|
||||
const urls = await getUrlsByOssIds(ossIds);
|
||||
|
||||
if (urls.length > 0) {
|
||||
realSrc.value = urls[0];
|
||||
realSrcList.value = urls;
|
||||
} else {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
}
|
||||
} else {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
}
|
||||
} else {
|
||||
// 普通模式:直接使用src作为URL
|
||||
const urls = newSrc.split(',').filter((url) => url.trim());
|
||||
if (urls.length > 0) {
|
||||
realSrc.value = urls[0];
|
||||
realSrcList.value = urls;
|
||||
} else {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
// 普通模式:直接使用src作为URL,但仍需要通过认证请求转换为blob URL
|
||||
const urls = newSrc.split(',').filter((url) => url.trim());
|
||||
|
||||
const blobUrls = await getUrlsByDirectUrls(urls);
|
||||
|
||||
if (blobUrls.length > 0) {
|
||||
realSrc.value = blobUrls[0];
|
||||
realSrcList.value = blobUrls;
|
||||
} else {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
realSrc.value = '';
|
||||
realSrcList.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -149,12 +317,19 @@ const realHeight = computed(() => (typeof props.height == 'string' ? props.heigh
|
||||
|
||||
:deep(.image-slot) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 30px;
|
||||
|
||||
.error-text {
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -35,18 +35,32 @@
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
|
||||
<img :src="dialogImageUrl" style="display: block; max-width: 100%; margin: 0 auto" />
|
||||
<div v-loading="previewLoading" style="text-align: center; min-height: 200px">
|
||||
<img
|
||||
v-if="dialogImageUrl"
|
||||
:src="dialogImageUrl"
|
||||
style="display: block; max-width: 100%; margin: 0 auto"
|
||||
@error="handlePreviewError"
|
||||
@load="handlePreviewLoad"
|
||||
/>
|
||||
<div v-if="previewError" class="preview-error">
|
||||
<el-icon size="48"><picture-filled /></el-icon>
|
||||
<div class="error-text">图片预览失败</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||
import { ref, computed, watch, nextTick, getCurrentInstance, onBeforeUnmount, type ComponentInternalInstance } from 'vue';
|
||||
import { listByIds, delOss } from '@/api/system/oss';
|
||||
import { OssVO } from '@/api/system/oss/types';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
import { compressAccurately } from 'image-conversion';
|
||||
import { PictureFilled, ZoomIn, Delete } from '@element-plus/icons-vue';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -80,8 +94,18 @@ const uploadList = ref<any[]>([]);
|
||||
const dialogImageUrl = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
// 使用环境变量,添加类型断言
|
||||
const baseUrl = (import.meta as any).env?.VITE_APP_BASE_API || '';
|
||||
// 预览相关状态
|
||||
const previewLoading = ref(false);
|
||||
const previewError = ref(false);
|
||||
const previewUrlCache = ref<Map<string, string>>(new Map());
|
||||
const previewBlobUrls = ref<string[]>([]);
|
||||
|
||||
// 异步加载相关状态
|
||||
const loadingStates = ref<Map<string, boolean>>(new Map());
|
||||
const loadErrorStates = ref<Map<string, boolean>>(new Map());
|
||||
|
||||
// 使用环境变量
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const uploadImgUrl = ref(baseUrl + '/resource/oss/upload'); // 上传的图片服务器地址
|
||||
const headers = ref(globalHeaders());
|
||||
|
||||
@@ -96,6 +120,241 @@ const isInternalUpdate = ref(false);
|
||||
// 监听 fileType 变化,更新 fileAccept
|
||||
const fileAccept = computed(() => props.fileType.map((type) => `.${type}`).join(','));
|
||||
|
||||
// 清理预览blob URL,防止内存泄漏
|
||||
const cleanupPreviewBlobUrls = () => {
|
||||
previewBlobUrls.value.forEach((url) => {
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
previewBlobUrls.value = [];
|
||||
previewUrlCache.value.clear();
|
||||
};
|
||||
|
||||
// 组件卸载时清理blob URLs
|
||||
onBeforeUnmount(() => {
|
||||
cleanupPreviewBlobUrls();
|
||||
});
|
||||
|
||||
// 验证blob数据是否为有效图片
|
||||
const validateImageBlob = (blob: Blob): boolean => {
|
||||
// 允许的图片类型,包括通用的octet-stream类型(OSS服务器常返回此类型)
|
||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'application/octet-stream'];
|
||||
|
||||
// 基本验证:类型检查和大小检查
|
||||
const hasValidType = validTypes.includes(blob.type) || blob.type === '' || blob.type === 'application/octet-stream';
|
||||
const hasValidSize = blob.size > 0;
|
||||
|
||||
return hasValidType && hasValidSize;
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getPreviewImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 检查是否已有缓存的blob URL
|
||||
const cachedUrl = previewUrlCache.value.get(imageUrl);
|
||||
if (cachedUrl) {
|
||||
return cachedUrl;
|
||||
}
|
||||
|
||||
// 通过request请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于request响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 检查blob内容类型
|
||||
// const contentType = blob.type;
|
||||
|
||||
// 如果Content-Type明确表明这不是图片数据且不是通用二进制格式,进行检查
|
||||
// if (
|
||||
// contentType &&
|
||||
// !contentType.toLowerCase().startsWith('image/') &&
|
||||
// !contentType.toLowerCase().includes('octet-stream') &&
|
||||
// (contentType.toLowerCase().includes('text/') || contentType.toLowerCase().includes('html'))
|
||||
// ) {
|
||||
// const text = await blob.text();
|
||||
// const preview = text.substring(0, 300);
|
||||
// throw new Error(`响应Content-Type不是图片格式: ${contentType}。响应内容预览: ${preview}`);
|
||||
// }
|
||||
|
||||
// 验证是否为有效图片
|
||||
// if (!validateImageBlob(blob)) {
|
||||
// throw new Error(`无效的图片格式: ${blob.type}, 大小: ${blob.size}`);
|
||||
// }
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 缓存blob URL
|
||||
previewUrlCache.value.set(imageUrl, blobUrl);
|
||||
previewBlobUrls.value.push(blobUrl);
|
||||
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
console.error('获取预览图片失败:', error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 通过ossid获取预览URL
|
||||
const getPreviewUrlByOssId = async (ossId: string): Promise<string> => {
|
||||
try {
|
||||
// const response = await listByIds(ossId);
|
||||
// // RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
|
||||
// const ossList: OssVO[] = (response as any)?.data || response;
|
||||
|
||||
// if (!ossList || ossList.length === 0) {
|
||||
// throw new Error(`OSS ID ${ossId} 在数据库中不存在或已被删除`);
|
||||
// }
|
||||
|
||||
// const ossFile = ossList[0];
|
||||
|
||||
// if (!ossFile || !ossFile.url) {
|
||||
// throw new Error(`OSS文件缺少url属性`);
|
||||
// }
|
||||
|
||||
// const imageUrl = ossFile.url;
|
||||
|
||||
// // 检查URL是否有效
|
||||
// if (!imageUrl || typeof imageUrl !== 'string') {
|
||||
// throw new Error(`OSS接口返回的URL无效`);
|
||||
// }
|
||||
|
||||
// 获取认证的blob URL
|
||||
return await getPreviewImageBlobUrl('/resource/oss/download/' + ossId);
|
||||
} catch (error) {
|
||||
console.error('通过ossId获取预览图片失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 异步加载单个文件的图片
|
||||
const loadFileImageAsync = async (item: any, index: number): Promise<void> => {
|
||||
const fileKey = item.ossId || item.url || `file_${index}`;
|
||||
|
||||
// 设置加载状态
|
||||
loadingStates.value.set(fileKey, true);
|
||||
loadErrorStates.value.set(fileKey, false);
|
||||
|
||||
try {
|
||||
let blobUrl = '';
|
||||
|
||||
if (item.ossId) {
|
||||
// 优先通过OSS ID获取预览URL
|
||||
console.log(`loadFileImageAsync: 通过OSS ID获取预览URL for ${item.ossId}`);
|
||||
blobUrl = await getPreviewUrlByOssId(item.ossId);
|
||||
} else if (typeof item === 'string') {
|
||||
// 直接URL情况
|
||||
console.log(`loadFileImageAsync: 直接URL情况 for ${item}`);
|
||||
blobUrl = await getPreviewImageBlobUrl(item);
|
||||
} else if (item.url) {
|
||||
// OSS对象情况(降级处理)
|
||||
console.log(`loadFileImageAsync: OSS对象情况 for ${item.url}`);
|
||||
blobUrl = await getPreviewImageBlobUrl(item.url);
|
||||
}
|
||||
|
||||
if (blobUrl) {
|
||||
// 更新文件列表中的对应项
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId || f.url === item.url || f.originalUrl === item.originalUrl);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
fileList.value[fileIndex].url = blobUrl;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`loadFileImageAsync: 成功更新文件 ${item.ossId || item.url}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('获取图片blob URL失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('异步加载图片失败:', error);
|
||||
loadErrorStates.value.set(fileKey, true);
|
||||
|
||||
// 降级处理:尝试通过OSS ID重新获取
|
||||
if (item.ossId) {
|
||||
try {
|
||||
console.log(`loadFileImageAsync: 降级处理,重新通过OSS ID获取 ${item.ossId}`);
|
||||
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
|
||||
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
||||
if (fileIndex !== -1 && fallbackBlobUrl) {
|
||||
fileList.value[fileIndex].url = fallbackBlobUrl;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`loadFileImageAsync: 降级处理成功 for ${item.ossId}`);
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('loadFileImageAsync: 降级处理也失败:', fallbackError);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loadingStates.value.set(fileKey, false);
|
||||
}
|
||||
};
|
||||
|
||||
// 批量异步加载图片
|
||||
const loadImagesAsync = async (items: any[]): Promise<void> => {
|
||||
// 先创建占位框,使用一个占位图片URL
|
||||
const placeholderItems = items.map((item, index) => {
|
||||
const name =
|
||||
item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId || `file_${index}`;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+', // 小圆圈浅灰色加载中占位图片
|
||||
originalUrl: item.url,
|
||||
ossId: item.ossId,
|
||||
isLoaded: false, // 标记为未加载
|
||||
isPlaceholder: true // 标记为占位符
|
||||
};
|
||||
});
|
||||
|
||||
// 立即更新文件列表显示占位框
|
||||
fileList.value = placeholderItems;
|
||||
|
||||
// 异步加载所有图片
|
||||
const loadPromises = items.map((item, index) => loadFileImageAsync(item, index));
|
||||
|
||||
// 等待所有图片加载完成(使用Promise.all替代allSettled以兼容旧版本)
|
||||
await Promise.all(loadPromises);
|
||||
};
|
||||
|
||||
// 图片预览加载成功
|
||||
const handlePreviewLoad = () => {
|
||||
previewError.value = false;
|
||||
};
|
||||
|
||||
// 图片预览加载失败
|
||||
const handlePreviewError = () => {
|
||||
previewError.value = true;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val: string) => {
|
||||
@@ -136,20 +395,26 @@ watch(
|
||||
}
|
||||
}
|
||||
}
|
||||
// 然后将数组转为对象数组
|
||||
fileList.value = list.map((item, index) => {
|
||||
// 字符串回显处理 如果此处存的是url可直接回显 如果存的是id需要调用接口查出来
|
||||
let itemData;
|
||||
if (typeof item === 'string') {
|
||||
itemData = { name: item, url: item };
|
||||
} else {
|
||||
// 此处name使用url 防止删除出现重名,对于URL格式使用url_index的格式
|
||||
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
|
||||
itemData = { name: name, url: item.url, ossId: item.ossId };
|
||||
}
|
||||
return itemData;
|
||||
});
|
||||
console.log('fileList', fileList.value);
|
||||
|
||||
// 使用异步加载方式处理文件列表
|
||||
try {
|
||||
await loadImagesAsync(list);
|
||||
console.log('fileList with async loaded images', fileList.value);
|
||||
} catch (error) {
|
||||
console.error('异步加载文件列表失败:', error);
|
||||
// 降级处理:使用原始URL
|
||||
fileList.value = list.map((item, index) => {
|
||||
let itemData;
|
||||
if (typeof item === 'string') {
|
||||
itemData = { name: item, url: item, originalUrl: item, isLoaded: true };
|
||||
} else {
|
||||
const name = item.url && (item.url.startsWith('http://') || item.url.startsWith('https://')) ? `${item.url}_${index}` : item.ossId;
|
||||
itemData = { name: name, url: item.url, originalUrl: item.url, ossId: item.ossId, isLoaded: true };
|
||||
}
|
||||
return itemData;
|
||||
});
|
||||
console.log('fileList (fallback)', fileList.value);
|
||||
}
|
||||
} else {
|
||||
fileList.value = [];
|
||||
return [];
|
||||
@@ -209,8 +474,16 @@ const handleExceed = () => {
|
||||
// 上传成功回调
|
||||
const handleUploadSuccess = (res: any, file: any) => {
|
||||
console.log('handleUploadSuccess:res', res);
|
||||
console.log('handleUploadSuccess:file', file);
|
||||
|
||||
if (res.code === 200) {
|
||||
uploadList.value.push({ name: res.data.fileName, url: res.data.url, ossId: res.data.ossId });
|
||||
const uploadItem = {
|
||||
name: res.data.fileName,
|
||||
url: res.data.url,
|
||||
ossId: res.data.ossId
|
||||
};
|
||||
console.log('添加上传项到uploadList:', uploadItem);
|
||||
uploadList.value.push(uploadItem);
|
||||
uploadedSuccessfully();
|
||||
} else {
|
||||
number.value--;
|
||||
@@ -226,7 +499,16 @@ const handleDelete = (file: any): boolean => {
|
||||
const findex = fileList.value.map((f) => f.name).indexOf(file.name);
|
||||
if (findex > -1 && uploadList.value.length === number.value) {
|
||||
const ossId = fileList.value[findex].ossId;
|
||||
delOss(ossId);
|
||||
if (ossId) {
|
||||
// delOss(ossId);
|
||||
}
|
||||
|
||||
// 清理对应的blob URL
|
||||
const fileToDelete = fileList.value[findex];
|
||||
if (fileToDelete.url && fileToDelete.url.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(fileToDelete.url);
|
||||
}
|
||||
|
||||
fileList.value.splice(findex, 1);
|
||||
// 设置内部更新标志
|
||||
isInternalUpdate.value = true;
|
||||
@@ -241,9 +523,126 @@ const handleDelete = (file: any): boolean => {
|
||||
};
|
||||
|
||||
// 上传结束处理
|
||||
const uploadedSuccessfully = () => {
|
||||
const uploadedSuccessfully = async () => {
|
||||
console.log('uploadedSuccessfully called, number:', number.value, 'uploadList length:', uploadList.value.length);
|
||||
|
||||
if (number.value > 0 && uploadList.value.length === number.value) {
|
||||
fileList.value = fileList.value.filter((f) => f.url !== undefined).concat(uploadList.value);
|
||||
console.log('开始处理新上传的文件:', uploadList.value);
|
||||
|
||||
// 对新上传的文件进行异步加载处理
|
||||
const newUploadedFiles = uploadList.value.map((item) => ({
|
||||
...item,
|
||||
isLoaded: false,
|
||||
isPlaceholder: false
|
||||
}));
|
||||
|
||||
console.log('处理后的新上传文件:', newUploadedFiles);
|
||||
|
||||
// 先显示新上传的文件(使用占位符)
|
||||
const placeholderItems = newUploadedFiles.map((item, index) => ({
|
||||
name: item.name,
|
||||
url: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjZjVmN2ZhIi8+CjxjaXJjbGUgY3g9IjUwIiBjeT0iNTAiIHI9IjE1IiBzdHJva2U9IiM5MDkzOTkiIHN0cm9rZS13aWR0aD0iMiIgZmlsbD0ibm9uZSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtZGFzaGFycmF5PSI2LCAzIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KCTxhbmltYXRlVHJhbnNmb3JtIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSIgdHlwZT0icm90YXRlIiB2YWx1ZXM9IjAgNTAgNTA7MzYwIDUwIDUwIiBkdXI9IjFzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIvPgo8L2NpcmNsZT4KPC9zdmc+',
|
||||
originalUrl: item.url,
|
||||
ossId: item.ossId,
|
||||
isLoaded: false,
|
||||
isPlaceholder: true
|
||||
}));
|
||||
|
||||
console.log('占位符项目:', placeholderItems);
|
||||
|
||||
// 合并现有文件和新上传的文件
|
||||
const existingFiles = fileList.value.filter((f) => f.url !== undefined);
|
||||
console.log('现有文件:', existingFiles);
|
||||
|
||||
fileList.value = existingFiles.concat(placeholderItems);
|
||||
console.log('合并后的文件列表:', fileList.value);
|
||||
|
||||
// 异步加载新上传的图片
|
||||
const loadPromises = newUploadedFiles.map((item, index) => {
|
||||
const fileKey = item.ossId || item.url || `file_${index}`;
|
||||
|
||||
console.log(`开始加载文件 ${index}:`, item);
|
||||
|
||||
// 设置加载状态
|
||||
loadingStates.value.set(fileKey, true);
|
||||
loadErrorStates.value.set(fileKey, false);
|
||||
|
||||
return (async () => {
|
||||
try {
|
||||
let blobUrl = '';
|
||||
|
||||
if (item.ossId) {
|
||||
console.log(`通过OSS ID获取预览URL for ${item.ossId}`);
|
||||
// 通过OSS ID获取预览URL
|
||||
blobUrl = await getPreviewUrlByOssId(item.ossId);
|
||||
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
|
||||
} else if (item.url) {
|
||||
console.log(`通过URL获取blob URL for ${item.ossId}:`, item.url);
|
||||
// 直接URL情况(降级处理)
|
||||
blobUrl = await getPreviewImageBlobUrl(item.url);
|
||||
console.log(`获取到的blob URL for ${item.ossId}:`, blobUrl);
|
||||
}
|
||||
|
||||
if (blobUrl) {
|
||||
// 更新文件列表中的对应项
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
||||
console.log(`更新文件列表索引 ${fileIndex} for ${item.ossId}`);
|
||||
|
||||
if (fileIndex !== -1) {
|
||||
fileList.value[fileIndex].url = blobUrl;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`成功更新文件 ${item.ossId}`);
|
||||
}
|
||||
} else {
|
||||
throw new Error('获取图片blob URL失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('异步加载新上传图片失败:', error);
|
||||
loadErrorStates.value.set(fileKey, true);
|
||||
|
||||
// 降级处理:通过OSS ID重新获取
|
||||
try {
|
||||
console.log(`降级处理:重新通过OSS ID获取 ${item.ossId}`);
|
||||
const fallbackBlobUrl = await getPreviewUrlByOssId(item.ossId);
|
||||
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
||||
if (fileIndex !== -1 && fallbackBlobUrl) {
|
||||
fileList.value[fileIndex].url = fallbackBlobUrl;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`降级处理成功,通过OSS ID重新获取 for ${item.ossId}`);
|
||||
} else {
|
||||
// 最后的降级:使用原始URL
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
||||
if (fileIndex !== -1) {
|
||||
fileList.value[fileIndex].url = item.url;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
|
||||
}
|
||||
}
|
||||
} catch (fallbackError) {
|
||||
console.error('降级处理也失败:', fallbackError);
|
||||
// 最后的降级:使用原始URL
|
||||
const fileIndex = fileList.value.findIndex((f) => f.ossId === item.ossId);
|
||||
if (fileIndex !== -1) {
|
||||
fileList.value[fileIndex].url = item.url;
|
||||
fileList.value[fileIndex].isLoaded = true;
|
||||
fileList.value[fileIndex].isPlaceholder = false;
|
||||
console.log(`最终降级处理,使用原始URL for ${item.ossId}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
loadingStates.value.set(fileKey, false);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
// 等待所有图片加载完成
|
||||
await Promise.all(loadPromises);
|
||||
console.log('所有图片加载完成,最终文件列表:', fileList.value);
|
||||
|
||||
uploadList.value = [];
|
||||
number.value = 0;
|
||||
// 设置内部更新标志
|
||||
@@ -264,10 +663,38 @@ const handleUploadError = () => {
|
||||
};
|
||||
|
||||
// 预览
|
||||
const handlePictureCardPreview = (file: any) => {
|
||||
console.log('file', file);
|
||||
dialogImageUrl.value = file.url;
|
||||
const handlePictureCardPreview = async (file: any) => {
|
||||
console.log('预览文件:', file);
|
||||
previewLoading.value = true;
|
||||
previewError.value = false;
|
||||
dialogVisible.value = true;
|
||||
|
||||
try {
|
||||
let previewUrl = '';
|
||||
|
||||
// 判断是否为ossId
|
||||
if (file.ossId) {
|
||||
// 通过ossId获取预览URL
|
||||
previewUrl = await getPreviewUrlByOssId(file.ossId);
|
||||
} else if (file.originalUrl) {
|
||||
// 使用原始URL进行预览(避免重复转换)
|
||||
previewUrl = await getPreviewImageBlobUrl(file.originalUrl);
|
||||
} else if (file.url) {
|
||||
// 直接URL预览
|
||||
previewUrl = await getPreviewImageBlobUrl(file.url);
|
||||
} else {
|
||||
throw new Error('文件缺少必要的URL或ossId信息');
|
||||
}
|
||||
|
||||
dialogImageUrl.value = previewUrl;
|
||||
} catch (error) {
|
||||
console.error('图片预览失败:', error);
|
||||
previewError.value = true;
|
||||
dialogImageUrl.value = '';
|
||||
(proxy as any)?.$modal.msgError('图片预览失败');
|
||||
} finally {
|
||||
previewLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 对象转成指定字符串分隔
|
||||
@@ -276,7 +703,7 @@ const listToString = (list: any[], separator?: string) => {
|
||||
separator = separator || ',';
|
||||
for (const i in list) {
|
||||
if (undefined !== list[i].ossId && list[i].ossId.indexOf('blob:') !== 0) {
|
||||
// 始终返回URL而不是ossId
|
||||
// 返回ossId用于数据存储
|
||||
strs += list[i].ossId + separator;
|
||||
}
|
||||
}
|
||||
@@ -289,4 +716,87 @@ const listToString = (list: any[], separator?: string) => {
|
||||
:deep(.hide .el-upload--picture-card) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// 预览错误状态样式
|
||||
.preview-error {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #909399;
|
||||
min-height: 200px;
|
||||
|
||||
.error-text {
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// 异步加载优化样式
|
||||
:deep(.el-upload-list--picture-card .el-upload-list__item) {
|
||||
// 占位图片加载时的样式
|
||||
.el-upload-list__item-thumbnail {
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
// 占位符图片样式优化
|
||||
&.placeholder-item {
|
||||
.el-upload-list__item-thumbnail {
|
||||
background-color: #f5f7fa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img {
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .el-upload-list__item-thumbnail img {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载完成后的样式
|
||||
&.loaded-item {
|
||||
.el-upload-list__item-thumbnail {
|
||||
img {
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 加载动画
|
||||
@keyframes loading-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// 占位符加载动画
|
||||
@keyframes placeholder-pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-loading {
|
||||
animation: placeholder-pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -7,9 +7,9 @@
|
||||
<el-button v-if="props.id && props.status !== 'draft'" type="primary" @click="handleApprovalRecord">流程进度</el-button>
|
||||
<slot />
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div>
|
||||
<el-button style="float: right" @click="goBack()">返回</el-button>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<el-table-column prop="originalName" width="202" :show-overflow-tooltip="true" label="附件名称"></el-table-column>
|
||||
<el-table-column prop="name" width="80" align="center" :show-overflow-tooltip="true" label="操作">
|
||||
<template #default="tool">
|
||||
<el-button type="text" @click="handleDownload(tool.row.ossId)">下载</el-button>
|
||||
<el-button type="text" @click="handleDownload(tool.row.ossId, tool.row.originalName)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -106,8 +106,9 @@ const getIds = async (ids: string | number) => {
|
||||
};
|
||||
|
||||
/** 下载按钮操作 */
|
||||
const handleDownload = (ossId: string) => {
|
||||
proxy?.$download.oss(ossId);
|
||||
const handleDownload = (ossId: string, originalName: string) => {
|
||||
console.log(ossId);
|
||||
proxy?.$download.secureOss(ossId, originalName);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<div class="right-menu flex align-center">
|
||||
<template v-if="appStore.device !== 'mobile'">
|
||||
<el-select
|
||||
<!-- <el-select
|
||||
v-if="userId === 1 && tenantEnabled"
|
||||
v-model="companyName"
|
||||
class="min-w-244px"
|
||||
@@ -19,7 +19,7 @@
|
||||
>
|
||||
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
|
||||
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
||||
</el-select>
|
||||
</el-select> -->
|
||||
|
||||
<search-menu ref="searchMenuRef" />
|
||||
<el-tooltip content="搜索" effect="dark" placement="bottom">
|
||||
|
||||
+33
-15
@@ -5,6 +5,7 @@ import { blobValidate } from '@/utils/ruoyi';
|
||||
import { LoadingInstance } from 'element-plus/es/components/loading/src/loading.mjs';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
import { getFileDownloadUrl } from '@/api/system/file';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const baseURL = import.meta.env.VITE_APP_BASE_API;
|
||||
let downloadLoadingInstance: LoadingInstance;
|
||||
@@ -12,16 +13,23 @@ export default {
|
||||
async oss(ossId: string | number) {
|
||||
downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
|
||||
try {
|
||||
// 使用新的安全下载方法
|
||||
const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId));
|
||||
// 使用新的安全下载方法,避免Number()转换导致的大整数精度丢失
|
||||
// const { data: downloadUrl } = await getFileDownloadUrl(String(ossId));
|
||||
|
||||
// 使用fetch下载文件
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
const response = await request({
|
||||
url: "/resource/oss/download/" + ossId,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const filename = response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`;
|
||||
|
||||
FileSaver.saveAs(blob, decodeURIComponent(filename));
|
||||
@@ -35,18 +43,27 @@ export default {
|
||||
|
||||
// 新增:安全下载文件方法
|
||||
async secureOss(ossId: string | number, fileName?: string) {
|
||||
console.log(ossId, fileName);
|
||||
downloadLoadingInstance = ElLoading.service({ text: '正在获取文件访问链接,请稍候', background: 'rgba(0, 0, 0, 0.7)' });
|
||||
try {
|
||||
// 获取临时下载URL
|
||||
const { data: downloadUrl } = await getFileDownloadUrl(Number(ossId));
|
||||
// 获取临时下载URL,避免Number()转换导致的大整数精度丢失
|
||||
// const { data: downloadUrl } = await getFileDownloadUrl(String(ossId));
|
||||
|
||||
// 使用fetch下载文件
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error('下载失败');
|
||||
const response = await request({
|
||||
url: "/resource/oss/download/" + ossId,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const finalFileName = fileName || response.headers.get('content-disposition')?.split('filename=')[1]?.replace(/"/g, '') || `file_${ossId}`;
|
||||
|
||||
FileSaver.saveAs(blob, decodeURIComponent(finalFileName));
|
||||
@@ -66,6 +83,7 @@ export default {
|
||||
method: 'get',
|
||||
url: url,
|
||||
responseType: 'blob',
|
||||
timeout: 300000 ,// 300秒超时
|
||||
headers: globalHeaders()
|
||||
});
|
||||
const isBlob = blobValidate(res.data);
|
||||
|
||||
Vendored
+10
@@ -87,6 +87,16 @@ declare global {
|
||||
declare interface PageQuery {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
orderByColumn?: string;
|
||||
|
||||
/**
|
||||
* 排序方式(asc/desc)
|
||||
*/
|
||||
isAsc?: string;
|
||||
}
|
||||
declare interface LayoutSetting {
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const errorCode: any = {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
'403': '当前操作没有权限',
|
||||
'403': '无访问权限',
|
||||
'404': '访问资源不存在',
|
||||
default: '系统未知错误,请反馈给管理员'
|
||||
};
|
||||
|
||||
@@ -177,6 +177,17 @@ service.interceptors.response.use(
|
||||
console.log('404错误被静默处理,不显示用户提示');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
// 对心理健康测评详情页面的403错误进行特殊处理
|
||||
if (statusCode === '403') {
|
||||
const url = error.config.url;
|
||||
// 如果是心理健康测评详情相关的API,不显示错误提示
|
||||
if (url && (url.includes('/xljk/evaluation/') || url.includes('/xljk/evaluation/detail/'))) {
|
||||
console.log('心理健康测评详情页面403错误被静默处理,不显示用户提示');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
message = '系统接口' + statusCode + '异常';
|
||||
}
|
||||
ElMessage({ message: message, type: 'error', duration: 5 * 1000 });
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="image-preview-test">
|
||||
<el-card header="图片预览组件测试">
|
||||
<el-form :model="testForm" label-width="120px">
|
||||
<el-form-item label="图片源:">
|
||||
<el-input v-model="testForm.src" placeholder="输入OSS ID或图片URL" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="模式:">
|
||||
<el-radio-group v-model="testForm.isOssId">
|
||||
<el-radio :label="true">OSS ID模式</el-radio>
|
||||
<el-radio :label="false">直接URL模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="调试模式:">
|
||||
<el-switch v-model="testForm.debug" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="尺寸:">
|
||||
<el-input v-model="testForm.width" placeholder="宽度" style="width: 100px" />
|
||||
<span style="margin: 0 10px">×</span>
|
||||
<el-input v-model="testForm.height" placeholder="高度" style="width: 100px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="testImage">测试图片</el-button>
|
||||
<el-button @click="checkOssId" :loading="checkingOssId">检查OSS ID</el-button>
|
||||
<el-button @click="clearTest">清除</el-button>
|
||||
<el-button @click="testDefaultOssId">测试默认OSS ID</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-divider content-position="left">预览结果</el-divider>
|
||||
|
||||
<div class="preview-container">
|
||||
<ImagePreview
|
||||
v-if="showPreview"
|
||||
:src="testForm.src"
|
||||
:is-oss-id="testForm.isOssId"
|
||||
:width="testForm.width || 200"
|
||||
:height="testForm.height || 200"
|
||||
:debug="testForm.debug"
|
||||
/>
|
||||
<div v-else class="no-preview">
|
||||
<el-icon><picture /></el-icon>
|
||||
<p>请输入图片源进行测试</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider content-position="left">调试指南</el-divider>
|
||||
|
||||
<div class="debug-guide">
|
||||
<el-alert title="调试步骤" type="info" show-icon :closable="false">
|
||||
<template #default>
|
||||
<p>1. 打开浏览器开发者工具 → Console 面板</p>
|
||||
<p>2. 开启"调试模式"可以看到详细的执行日志</p>
|
||||
<p>3. 观察 [ImagePreview Debug] 开头的日志信息</p>
|
||||
<p>4. 查看是否有错误信息(红色日志)</p>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<el-card style="margin-top: 15px" header="常见问题排查">
|
||||
<el-collapse>
|
||||
<el-collapse-item title="1. OSS接口调用失败" name="1">
|
||||
<p><strong>现象:</strong>控制台显示 "获取OSS URL失败"</p>
|
||||
<p><strong>排查:</strong>检查OSS ID是否正确,后端接口是否正常</p>
|
||||
<p><strong>解决:</strong>确认 /resource/oss/listByIds/{ossId} 接口可以正常访问</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="2. 图片请求权限问题" name="2">
|
||||
<p><strong>现象:</strong>控制台显示 401/403 错误</p>
|
||||
<p><strong>排查:</strong>检查token是否有效,clientId是否正确</p>
|
||||
<p><strong>解决:</strong>确认request拦截器正确添加了Authorization头</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="3. blob URL创建失败" name="3">
|
||||
<p><strong>现象:</strong>控制台显示 "无效的图片格式"</p>
|
||||
<p><strong>排查:</strong>检查图片URL返回的内容类型</p>
|
||||
<p><strong>解决:</strong>确认返回的是真实图片数据,不是HTML错误页面</p>
|
||||
</el-collapse-item>
|
||||
|
||||
<el-collapse-item title="4. CORS跨域问题" name="4">
|
||||
<p><strong>现象:</strong>控制台显示CORS相关错误</p>
|
||||
<p><strong>排查:</strong>检查图片服务器的CORS配置</p>
|
||||
<p><strong>解决:</strong>配置后端代理或修改CORS策略</p>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import { Picture } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { listByIds } from '@/api/system/oss';
|
||||
|
||||
const testForm = ref({
|
||||
src: '',
|
||||
isOssId: true,
|
||||
debug: true,
|
||||
width: '200',
|
||||
height: '200'
|
||||
});
|
||||
|
||||
const checkingOssId = ref(false);
|
||||
|
||||
const showPreview = computed(() => {
|
||||
return testForm.value.src && testForm.value.src.trim() !== '';
|
||||
});
|
||||
|
||||
const testImage = () => {
|
||||
if (!testForm.value.src) {
|
||||
ElMessage.warning('请输入图片源');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('='.repeat(50));
|
||||
console.log('开始测试图片预览组件');
|
||||
console.log('测试参数:', {
|
||||
src: testForm.value.src,
|
||||
isOssId: testForm.value.isOssId,
|
||||
debug: testForm.value.debug,
|
||||
width: testForm.value.width,
|
||||
height: testForm.value.height
|
||||
});
|
||||
console.log('='.repeat(50));
|
||||
};
|
||||
|
||||
const checkOssId = async () => {
|
||||
if (!testForm.value.src) {
|
||||
ElMessage.warning('请输入OSS ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!testForm.value.isOssId) {
|
||||
ElMessage.warning('请切换到OSS ID模式进行检查');
|
||||
return;
|
||||
}
|
||||
|
||||
checkingOssId.value = true;
|
||||
|
||||
try {
|
||||
console.log('='.repeat(50));
|
||||
console.log('检查OSS ID:', testForm.value.src);
|
||||
|
||||
const response = await listByIds(testForm.value.src);
|
||||
// RuoYi框架的响应格式:{code, msg, data},实际数据在data字段中
|
||||
console.log('API响应原始数据:', response);
|
||||
const ossList = response.data || response;
|
||||
|
||||
console.log('OSS检查结果:', {
|
||||
dataLength: ossList?.length || 0,
|
||||
data: ossList
|
||||
});
|
||||
|
||||
if (!ossList || ossList.length === 0) {
|
||||
ElMessage.error(`OSS ID ${testForm.value.src} 在数据库中不存在`);
|
||||
console.error('OSS ID不存在:', testForm.value.src);
|
||||
} else {
|
||||
const ossFile = ossList[0];
|
||||
ElMessage.success(`OSS ID ${testForm.value.src} 存在`);
|
||||
console.log('OSS文件信息:', {
|
||||
ossId: ossFile.ossId,
|
||||
fileName: ossFile.fileName,
|
||||
originalName: ossFile.originalName,
|
||||
fileSuffix: ossFile.fileSuffix,
|
||||
url: ossFile.url,
|
||||
createTime: ossFile.createTime,
|
||||
service: ossFile.service
|
||||
});
|
||||
|
||||
// 检查文件类型
|
||||
if (ossFile.fileSuffix && !ossFile.fileSuffix.toLowerCase().match(/\.(jpg|jpeg|png|gif|webp|bmp)$/)) {
|
||||
ElMessage.warning(`文件类型 ${ossFile.fileSuffix} 可能不是图片格式`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('='.repeat(50));
|
||||
} catch (error) {
|
||||
console.error('检查OSS ID失败:', error);
|
||||
ElMessage.error(`检查OSS ID失败: ${error.message}`);
|
||||
} finally {
|
||||
checkingOssId.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const clearTest = () => {
|
||||
testForm.value.src = '';
|
||||
console.clear();
|
||||
ElMessage.success('已清除测试数据和控制台日志');
|
||||
};
|
||||
|
||||
const testDefaultOssId = () => {
|
||||
// 这里可以设置一个测试用的OSS ID
|
||||
testForm.value.src = '1'; // 请替换为实际存在的OSS ID
|
||||
testForm.value.isOssId = true;
|
||||
testForm.value.debug = true;
|
||||
|
||||
ElMessage.info('已设置默认测试OSS ID,请根据实际情况修改');
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-preview-test {
|
||||
padding: 20px;
|
||||
|
||||
.preview-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 250px;
|
||||
border: 2px dashed #dcdfe6;
|
||||
border-radius: 8px;
|
||||
background-color: #fafafa;
|
||||
|
||||
.no-preview {
|
||||
text-align: center;
|
||||
color: #909399;
|
||||
|
||||
.el-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.debug-guide {
|
||||
.el-alert {
|
||||
:deep(.el-alert__content) {
|
||||
p {
|
||||
margin: 5px 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-collapse {
|
||||
:deep(.el-collapse-item__content) {
|
||||
p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
|
||||
strong {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div class="image-upload-test">
|
||||
<el-card header="ImageUpload组件测试 - 认证图片回显和预览">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="测试数据1">
|
||||
<el-button @click="loadTestData1" type="primary">加载单个OSS ID</el-button>
|
||||
<span style="margin-left: 10px; color: #666;">测试单个OSS文件回显</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试数据2">
|
||||
<el-button @click="loadTestData2" type="primary">加载多个OSS ID</el-button>
|
||||
<span style="margin-left: 10px; color: #666;">测试多个OSS文件回显</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="测试数据3">
|
||||
<el-button @click="loadTestData3" type="primary">加载URL格式</el-button>
|
||||
<span style="margin-left: 10px; color: #666;">测试直接URL回显</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="清空数据">
|
||||
<el-button @click="clearData" type="danger">清空</el-button>
|
||||
<span style="margin-left: 10px; color: #666;">清空所有图片数据</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="当前值">
|
||||
<el-input v-model="form.images" type="textarea" :rows="3" readonly />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图片上传">
|
||||
<ImageUpload v-model="form.images" :limit="5" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="test-info">
|
||||
<h3>测试说明</h3>
|
||||
<ul>
|
||||
<li><strong>功能验证</strong>:
|
||||
<ol>
|
||||
<li>界面打开时历史图片是否正常显示(通过认证接口获取blob URL)</li>
|
||||
<li>点击图片预览是否正常工作</li>
|
||||
<li>上传新图片功能是否正常</li>
|
||||
<li>删除图片功能是否正常</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><strong>技术要点</strong>:
|
||||
<ul>
|
||||
<li>使用 Promise.all 并行处理多个文件的blob URL转换</li>
|
||||
<li>缓存机制避免重复请求相同图片的认证数据</li>
|
||||
<li>降级处理:认证失败时使用原始URL</li>
|
||||
<li>完整的blob URL生命周期管理,防止内存泄漏</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><strong>错误处理</strong>:如果图片无法显示,检查控制台错误信息</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import ImageUpload from '@/components/ImageUpload/index.vue';
|
||||
|
||||
const form = reactive({
|
||||
images: ''
|
||||
});
|
||||
|
||||
// 测试数据1:单个OSS ID
|
||||
const loadTestData1 = () => {
|
||||
form.images = '1951584287279034370'; // 示例OSS ID,实际使用时需要替换为有效的OSS ID
|
||||
};
|
||||
|
||||
// 测试数据2:多个OSS ID
|
||||
const loadTestData2 = () => {
|
||||
form.images = '1951584287279034370,1951584287279034371'; // 示例多个OSS ID
|
||||
};
|
||||
|
||||
// 测试数据3:URL格式
|
||||
const loadTestData3 = () => {
|
||||
form.images = '/resource/oss/download/1951584287279034370'; // 示例OSS下载URL
|
||||
};
|
||||
|
||||
// 清空数据
|
||||
const clearData = () => {
|
||||
form.images = '';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.image-upload-test {
|
||||
padding: 20px;
|
||||
|
||||
.test-info {
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin-left: 20px;
|
||||
|
||||
li {
|
||||
margin-bottom: 8px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #409EFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -216,11 +216,12 @@
|
||||
<el-form-item label="报告厅图片">
|
||||
<div class="view-hall-images">
|
||||
<div class="image-gallery" v-if="viewHallImages.length > 0">
|
||||
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
|
||||
<img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item">
|
||||
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div class="image-overlay">
|
||||
<el-icon class="preview-icon"><ZoomIn /></el-icon>
|
||||
</div>
|
||||
</div> -->
|
||||
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-images-placeholder">
|
||||
@@ -283,11 +284,12 @@
|
||||
</div>
|
||||
<div class="hall-images" v-if="getHallImagesByName(hallName).length > 0">
|
||||
<div class="image-gallery">
|
||||
<div v-for="(image, index) in getHallImagesByName(hallName)" :key="index" class="image-item" @click="previewImage(image, index)">
|
||||
<img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div v-for="(image, index) in getHallImagesByName(hallName)" :key="index" class="image-item">
|
||||
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div class="image-overlay">
|
||||
<el-icon class="preview-icon"><ZoomIn /></el-icon>
|
||||
</div>
|
||||
</div> -->
|
||||
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -560,14 +562,14 @@ const getHallImages = async (ossIds: string): Promise<string[]> => {
|
||||
console.log('处理OSS ID:', ossIds, 'ID数组:', idArray);
|
||||
|
||||
// 调用API获取真实的图片URL
|
||||
const response = await listByIds(idArray.join(','));
|
||||
const ossList: OssVO[] = response.data || [];
|
||||
// const response = await listByIds(idArray.join(','));
|
||||
// const ossList: OssVO[] = response.data || [];
|
||||
|
||||
// 提取URL列表
|
||||
const urls = ossList.map((oss) => oss.url).filter((url) => url);
|
||||
// // 提取URL列表
|
||||
// const urls = ossList.map((oss) => oss.url).filter((url) => url);
|
||||
|
||||
console.log('获取到的图片URL:', urls);
|
||||
return urls;
|
||||
// console.log('获取到的图片URL:', urls);
|
||||
return idArray;
|
||||
} catch (error) {
|
||||
console.error('获取图片URL失败:', error);
|
||||
return [];
|
||||
|
||||
@@ -210,15 +210,18 @@
|
||||
{{ displayHallDescription(hall.hallDescription) }}
|
||||
</div>
|
||||
<div class="hall-images">
|
||||
<template v-if="hall.imageUrls && hall.imageUrls.length > 0">
|
||||
<img
|
||||
<template v-if="hall.hallImage && hall.hallImage.length > 0">
|
||||
<!-- <img
|
||||
v-for="(image, index) in hall.imageUrls"
|
||||
:key="index"
|
||||
:src="image"
|
||||
class="hall-image"
|
||||
@error="handleImageError"
|
||||
@load="handleImageLoad"
|
||||
/>
|
||||
/> -->
|
||||
<div v-for="(image, index) in hall.hallImage" :key="index">
|
||||
<image-preview :src="image" :width="40" :height="40" :isOssId="true" />
|
||||
</div>
|
||||
</template>
|
||||
<span v-else class="no-image-text">暂无图片</span>
|
||||
</div>
|
||||
@@ -230,11 +233,12 @@
|
||||
<el-form-item label="报告厅图片">
|
||||
<div class="selected-hall-images">
|
||||
<div class="image-gallery" v-if="selectedHallImages.length > 0">
|
||||
<div v-for="(image, index) in selectedHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
|
||||
<img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div v-for="(image, index) in selectedHallImages" :key="index">
|
||||
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div class="image-overlay">
|
||||
<el-icon class="preview-icon"><ZoomIn /></el-icon>
|
||||
</div>
|
||||
</div> -->
|
||||
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-images-placeholder">
|
||||
@@ -298,11 +302,12 @@
|
||||
<el-form-item label="报告厅图片">
|
||||
<div class="view-hall-images">
|
||||
<div class="image-gallery" v-if="viewHallImages.length > 0">
|
||||
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item" @click="previewImage(image, index)">
|
||||
<img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div v-for="(image, index) in viewHallImages" :key="index" class="image-item">
|
||||
<!-- <img :src="image" class="gallery-image" @error="handleImageError" />
|
||||
<div class="image-overlay">
|
||||
<el-icon class="preview-icon"><ZoomIn /></el-icon>
|
||||
</div>
|
||||
</div> -->
|
||||
<image-preview :src="image" :width="120" :height="90" :isOssId="true" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-images-placeholder">
|
||||
@@ -525,21 +530,29 @@ const getHallList = async () => {
|
||||
hallStatus: '1'
|
||||
}); // 获取正常状态的报告厅
|
||||
hallList.value = res.rows || [];
|
||||
|
||||
// 为每个报告厅获取图片URL
|
||||
for (const hall of hallList.value) {
|
||||
// for (const hall of hallList.value) {
|
||||
// if (hall.hallImage) {
|
||||
// try {
|
||||
// const imageUrls = await getHallImages(hall.hallImage);
|
||||
// hall.imageUrls = imageUrls;
|
||||
// } catch (error) {
|
||||
// console.error(`获取报告厅 ${hall.hallName} 图片失败:`, error);
|
||||
// hall.imageUrls = [];
|
||||
// }
|
||||
// } else {
|
||||
// hall.imageUrls = [];
|
||||
// }
|
||||
// }
|
||||
//hall.hallImage.split(',')
|
||||
hallList.value.forEach((hall) => {
|
||||
//加一些判断
|
||||
if (hall.hallImage) {
|
||||
try {
|
||||
const imageUrls = await getHallImages(hall.hallImage);
|
||||
hall.imageUrls = imageUrls;
|
||||
} catch (error) {
|
||||
console.error(`获取报告厅 ${hall.hallName} 图片失败:`, error);
|
||||
hall.imageUrls = [];
|
||||
}
|
||||
hall.hallImage = hall.hallImage.split(',');
|
||||
} else {
|
||||
hall.imageUrls = [];
|
||||
hall.hallImage = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
// 为每个报告厅获取图片URL
|
||||
|
||||
// 调试信息:检查每个报告厅的图片信息
|
||||
console.log('报告厅列表数据:', hallList.value);
|
||||
@@ -562,7 +575,7 @@ const handleHallChange = (hallName: string) => {
|
||||
if (selectedHall) {
|
||||
form.value.hallId = selectedHall.id;
|
||||
// 更新选中的报告厅图片
|
||||
selectedHallImages.value = selectedHall.imageUrls || [];
|
||||
selectedHallImages.value = selectedHall.hallImage || [];
|
||||
} else {
|
||||
selectedHallImages.value = [];
|
||||
}
|
||||
@@ -796,8 +809,8 @@ const handleView = async (row: HallReservationVO) => {
|
||||
// 查找对应的报告厅并显示图片
|
||||
if (viewForm.value.hallName) {
|
||||
const selectedHall = hallList.value.find((hall) => hall.hallName === viewForm.value.hallName);
|
||||
if (selectedHall && selectedHall.imageUrls) {
|
||||
viewHallImages.value = selectedHall.imageUrls;
|
||||
if (selectedHall && selectedHall.hallImage) {
|
||||
viewHallImages.value = selectedHall.hallImage;
|
||||
} else {
|
||||
viewHallImages.value = [];
|
||||
}
|
||||
@@ -840,8 +853,9 @@ const handleUpdate = async (row?: HallReservationVO) => {
|
||||
// 如果有报告厅名称,找到对应的报告厅并显示图片
|
||||
if (form.value.hallName) {
|
||||
const selectedHall = hallList.value.find((hall) => hall.hallName === form.value.hallName);
|
||||
if (selectedHall && selectedHall.imageUrls) {
|
||||
selectedHallImages.value = selectedHall.imageUrls;
|
||||
console.log('selectedHall', selectedHall);
|
||||
if (selectedHall && selectedHall.hallImage) {
|
||||
selectedHallImages.value = selectedHall.hallImage;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="紧急程度" align="center" prop="priorityLevel" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getPriorityTagType(scope.row.priorityLevel) as any" size="small">
|
||||
<el-tag :type="getPriorityTagType(scope.row.priorityLevel)" size="small">
|
||||
{{ getPriorityText(scope.row.priorityLevel) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -252,7 +252,7 @@
|
||||
<el-table-column label="报修人电话" align="center" prop="reporterPhone" width="120" show-overflow-tooltip />
|
||||
<el-table-column label="工单状态" align="center" prop="orderStatus" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusTagType(scope.row.orderStatus) as any" size="small">
|
||||
<el-tag :type="getStatusTagType(scope.row.orderStatus)" size="small">
|
||||
{{ getStatusText(scope.row.orderStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
@@ -562,13 +562,15 @@
|
||||
<div class="upload-file-card">
|
||||
<!-- 缩略图区域 -->
|
||||
<div class="upload-file-thumb">
|
||||
<el-image
|
||||
<!-- {{ file.url }} -->
|
||||
<image-preview v-if="isImageFile(file) && file.url" :src="file.url" class="upload-thumb-image" :isOssId="true" />
|
||||
<!-- <el-image
|
||||
v-if="isImageFile(file) && file.url"
|
||||
:src="file.url"
|
||||
fit="cover"
|
||||
:preview-src-list="[file.url]"
|
||||
class="upload-thumb-image"
|
||||
/>
|
||||
/> -->
|
||||
<div v-else class="upload-file-placeholder">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span class="file-ext">{{ getFileExt(file.name) }}</span>
|
||||
@@ -582,7 +584,7 @@
|
||||
|
||||
<!-- 文件名称 -->
|
||||
<div class="upload-file-name" :title="file.name">
|
||||
{{ file.name }}
|
||||
{{ formatFileName(file.name) }}
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 (与预览界面样式一致) -->
|
||||
@@ -630,13 +632,13 @@
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>工单状态:</label>
|
||||
<el-tag :type="getStatusTagType(orderDetail.orderStatus) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
|
||||
<el-tag :type="getStatusTagType(orderDetail.orderStatus)" size="small">
|
||||
{{ getStatusText(orderDetail.orderStatus) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>紧急程度:</label>
|
||||
<el-tag :type="getPriorityTagType(orderDetail.priorityLevel) as 'success' | 'warning' | 'info' | 'primary' | 'danger'" size="small">
|
||||
<el-tag :type="getPriorityTagType(orderDetail.priorityLevel)" size="small">
|
||||
{{ getPriorityText(orderDetail.priorityLevel) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
@@ -822,14 +824,15 @@
|
||||
<div v-for="attachment in currentAttachments" :key="attachment.id" class="attachment-card">
|
||||
<!-- 附件缩略图 -->
|
||||
<div class="attachment-thumbnail-wrapper">
|
||||
<el-image
|
||||
<!-- <el-image
|
||||
v-if="attachment.fileType === 'image'"
|
||||
:src="attachment.filePath"
|
||||
:preview-src-list="getImagePreviewList()"
|
||||
fit="cover"
|
||||
class="attachment-image"
|
||||
lazy
|
||||
/>
|
||||
/> -->
|
||||
<image-preview v-if="attachment.fileType === 'image'" :src="attachment.filePath" class="attachment-image" :isOssId="true" />
|
||||
<div v-else class="attachment-file-icon">
|
||||
<el-icon :size="28">
|
||||
<Document v-if="attachment.fileType === 'document'" />
|
||||
@@ -843,7 +846,7 @@
|
||||
<!-- 附件信息 -->
|
||||
<div class="attachment-card-body">
|
||||
<div class="attachment-title" :title="attachment.fileName">
|
||||
{{ attachment.fileName }}
|
||||
{{ formatFileName(attachment.fileName) }}
|
||||
</div>
|
||||
<div class="attachment-meta">
|
||||
<span class="file-size">{{ formatFileSize(attachment.fileSize) }}</span>
|
||||
@@ -857,14 +860,14 @@
|
||||
<!-- 附件操作 -->
|
||||
<div class="attachment-card-actions">
|
||||
<el-button-group size="small">
|
||||
<el-button
|
||||
<!-- <el-button
|
||||
type="primary"
|
||||
@click="previewAttachment(attachment)"
|
||||
:disabled="!canPreview(attachment)"
|
||||
:title="canPreview(attachment) ? '预览' : '不支持预览'"
|
||||
>
|
||||
<el-icon><View /></el-icon>
|
||||
</el-button>
|
||||
</el-button> -->
|
||||
<el-button type="success" @click="downloadAttachment(attachment)" title="下载">
|
||||
<el-icon><Download /></el-icon>
|
||||
</el-button>
|
||||
@@ -1157,7 +1160,7 @@
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="attachment-details">
|
||||
<div class="file-name">{{ attachment.fileName }}</div>
|
||||
<div class="file-name">{{ formatFileName(attachment.fileName) }}</div>
|
||||
<div class="file-meta">
|
||||
<span>{{ formatFileSize(attachment.fileSize) }}</span>
|
||||
<span>{{ attachment.uploaderName }}</span>
|
||||
@@ -1230,6 +1233,7 @@ import { getInfo } from '@/api/login'; // 添加获取用户详细信息的API
|
||||
import { getToken } from '@/utils/auth'; // 修复:添加getToken导入
|
||||
import { vLoading, ElLoading } from 'element-plus';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
@@ -1271,7 +1275,7 @@ const workerOptions = ref<Array<{ value: string | number; label: string; userNam
|
||||
|
||||
// 附件相关数据
|
||||
const attachmentList = ref<any[]>([]);
|
||||
const uploadAction = ref('/dev-api/hq/pm/attachment/upload'); // 修复:使用正确的API路径
|
||||
const uploadAction = ref(import.meta.env.VITE_APP_BASE_API + '/hq/pm/attachment/upload'); // 修复:使用正确的API路径
|
||||
const uploadHeaders = ref({
|
||||
'Authorization': `Bearer ${getToken() || ''}`,
|
||||
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||
@@ -1826,7 +1830,10 @@ const submitForm = () => {
|
||||
buttonLoading.value = true;
|
||||
try {
|
||||
let orderId: string | number;
|
||||
|
||||
// todo form.attachementCount的数量等于附件栏中附件的数量
|
||||
console.log('form.value', form.value);
|
||||
console.log('attachmentList 长度:', attachmentList.value.length);
|
||||
form.value.attachmentCount = attachmentList.value.length;
|
||||
if (form.value.id) {
|
||||
await updateRepairOrder(form.value);
|
||||
orderId = form.value.id;
|
||||
@@ -2330,7 +2337,9 @@ const handleUploadSuccess = (response: any, file: any) => {
|
||||
|
||||
if (response.code === 200) {
|
||||
proxy?.$modal.msgSuccess(`文件 ${file.name} 上传成功`);
|
||||
|
||||
// todo
|
||||
console.log('orderDetail', orderDetail.value);
|
||||
// orderDetail.value.attachmentCount++;
|
||||
// 立即记录附件数据
|
||||
const attachmentData = response.data;
|
||||
if (attachmentData && attachmentData.id) {
|
||||
@@ -2415,12 +2424,28 @@ const handleDownloadAttachment = async (file: any) => {
|
||||
target: document.body
|
||||
});
|
||||
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// const response = await fetch(downloadUrl);
|
||||
// if (!response.ok) {
|
||||
// throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// }
|
||||
// 通过fetch获取文件内容,强制下载
|
||||
// const response = await req('/resource/oss/download/' + attachment.filePath);
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + downloadUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
// const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement('a');
|
||||
@@ -2713,12 +2738,23 @@ const downloadAttachment = async (attachment: any) => {
|
||||
});
|
||||
|
||||
// 通过fetch获取文件内容,强制下载
|
||||
const response = await fetch(attachment.filePath);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
// const response = await req('/resource/oss/download/' + attachment.filePath);
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + attachment.filePath,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
// const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// 创建下载链接
|
||||
@@ -2943,6 +2979,30 @@ const getFileExt = (fileName: string): string => {
|
||||
const idx = fileName.lastIndexOf('.');
|
||||
return idx !== -1 ? fileName.substring(idx + 1).toLowerCase() : '';
|
||||
};
|
||||
|
||||
/** 格式化文件名显示 - 长文件名用省略号缩写,保留后缀名 */
|
||||
const formatFileName = (fileName: string): string => {
|
||||
if (!fileName) return '';
|
||||
|
||||
const maxLength = 15; // 最大显示长度(包含后缀名)
|
||||
const idx = fileName.lastIndexOf('.');
|
||||
|
||||
if (idx === -1) {
|
||||
// 没有后缀名的情况
|
||||
return fileName.length > maxLength ? fileName.substring(0, maxLength - 3) + '...' : fileName;
|
||||
}
|
||||
|
||||
const name = fileName.substring(0, idx);
|
||||
const ext = fileName.substring(idx); // 包含点号的后缀名
|
||||
|
||||
// 如果文件名(不含后缀)长度超过限制
|
||||
if (name.length > maxLength - ext.length - 3) {
|
||||
const truncatedName = name.substring(0, maxLength - ext.length - 3);
|
||||
return truncatedName + '...' + ext;
|
||||
}
|
||||
|
||||
return fileName;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -266,6 +266,8 @@ const handleStartWorkFlow = async (data: LeaveForm) => {
|
||||
submitFormData.value.businessId = data.id;
|
||||
//流程变量
|
||||
taskVariables.value = {
|
||||
// hzdyjs-qj 使用的流程变量
|
||||
qjts: data.leaveDays,
|
||||
// leave2/6 使用的流程变量
|
||||
leaveDays: data.leaveDays,
|
||||
// leave4/5 使用的流程变量
|
||||
|
||||
@@ -99,7 +99,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -746,8 +750,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDetailDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// @ts-ignore
|
||||
import request from '@/utils/request';
|
||||
// 获取当前组件实例
|
||||
@@ -819,6 +824,8 @@ const loading = ref(false);
|
||||
const dialogLoading = ref(false); // 对话框整体加载状态
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 历史记录相关数据
|
||||
const historyLoading = ref(false);
|
||||
@@ -964,6 +971,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取用户历史数据 */
|
||||
const getUserHistoryData = async (batchId: number, userId?: number) => {
|
||||
if (!batchId) {
|
||||
@@ -1171,6 +1222,25 @@ const rules = reactive({
|
||||
gender: [{ required: true, message: '性别不能为空', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 属性变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -1666,4 +1736,30 @@ const handleClose = () => {
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -153,7 +153,11 @@
|
||||
:before-upload="beforePhotoUpload"
|
||||
:http-request="handlePhotoUpload"
|
||||
>
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
@@ -1433,6 +1437,8 @@ import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue'
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { ElMessageBox } from 'element-plus/es/components/message-box/index';
|
||||
import request from '@/utils/request';
|
||||
import service from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -1510,6 +1516,10 @@ const historyRecords = ref<any[]>([]);
|
||||
// 变更详情相关数据
|
||||
const changedFields = ref<any[]>([]); // 存储变更的字段信息
|
||||
|
||||
// 异步加载的图片URL
|
||||
const asyncPhotoUrl = ref<string>('');
|
||||
const photoLoading = ref<boolean>(false);
|
||||
|
||||
const studentFormRef = ref<any>();
|
||||
|
||||
/** 获取历史记录数据 */
|
||||
@@ -2369,8 +2379,110 @@ const handleApprove = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await service({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖定时器
|
||||
let photoLoadTimer: number | null = null;
|
||||
|
||||
// 监听photoPath变化,异步加载图片
|
||||
watch(
|
||||
() => form.photoPath,
|
||||
async (newPhotoPath, oldPhotoPath) => {
|
||||
// 如果路径没有变化,不重复加载
|
||||
if (newPhotoPath === oldPhotoPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
}
|
||||
|
||||
// 清理之前的blob URL
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
|
||||
// 防抖处理,避免频繁调用
|
||||
photoLoadTimer = window.setTimeout(async () => {
|
||||
if (newPhotoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(newPhotoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
}, 100); // 100ms 防抖延迟
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
|
||||
// 重置表单数据到默认值
|
||||
const resetFormToDefault = () => {
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
@@ -2788,6 +2900,27 @@ const handleClose = () => {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 照片加载样式 */
|
||||
.photo-loading {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-loading .is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
/* 变更字段样式 */
|
||||
.changed-field-label {
|
||||
color: #f56c6c !important;
|
||||
|
||||
@@ -99,7 +99,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -745,10 +749,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDetailDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
// @ts-ignore
|
||||
import request from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -818,6 +823,8 @@ const loading = ref(false);
|
||||
const dialogLoading = ref(false); // 对话框整体加载状态
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 历史记录相关数据
|
||||
const historyLoading = ref(false);
|
||||
@@ -963,6 +970,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取用户历史数据 */
|
||||
const getUserHistoryData = async (batchId: number) => {
|
||||
if (!batchId) {
|
||||
@@ -1167,6 +1218,25 @@ const rules = reactive({
|
||||
gender: [{ required: true, message: '性别不能为空', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 属性变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
@@ -1662,4 +1732,30 @@ const handleClose = () => {
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -105,7 +105,11 @@
|
||||
:before-upload="beforePhotoUpload"
|
||||
:http-request="handlePhotoUpload"
|
||||
>
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
@@ -751,9 +755,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuMsgDialog">
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, nextTick, watch, toRefs, watchEffect } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import request from '@/utils/request';
|
||||
import service from '@/utils/request';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -828,6 +834,10 @@ const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const historyLoading = ref(false);
|
||||
const historyRecords = ref<any[]>([]);
|
||||
|
||||
// 异步加载的图片URL
|
||||
const asyncPhotoUrl = ref<string>('');
|
||||
const photoLoading = ref<boolean>(false);
|
||||
|
||||
const studentFormRef = ref<any>();
|
||||
|
||||
/** 获取历史记录数据 */
|
||||
@@ -1339,6 +1349,19 @@ const submitForm = () => {
|
||||
|
||||
// 重置表单数据到默认值
|
||||
const resetFormToDefault = () => {
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置为默认值
|
||||
Object.assign(form, {
|
||||
id: undefined,
|
||||
@@ -1455,12 +1478,114 @@ const handleClose = () => {
|
||||
// 重置历史记录数据
|
||||
historyRecords.value = [];
|
||||
|
||||
// 清理定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
photoLoadTimer = null;
|
||||
}
|
||||
|
||||
// 清理blob URL,避免内存泄漏
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
photoLoading.value = false;
|
||||
|
||||
// 重置表单
|
||||
nextTick(() => {
|
||||
studentFormRef.value?.resetFields();
|
||||
resetFormToDefault(); // 重置表单数据到默认值
|
||||
});
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await service({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 防抖定时器
|
||||
let photoLoadTimer: number | null = null;
|
||||
|
||||
// 监听photoPath变化,异步加载图片
|
||||
watch(
|
||||
() => form.photoPath,
|
||||
async (newPhotoPath, oldPhotoPath) => {
|
||||
// 如果路径没有变化,不重复加载
|
||||
if (newPhotoPath === oldPhotoPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的定时器
|
||||
if (photoLoadTimer) {
|
||||
clearTimeout(photoLoadTimer);
|
||||
}
|
||||
|
||||
// 清理之前的blob URL
|
||||
if (asyncPhotoUrl.value) {
|
||||
URL.revokeObjectURL(asyncPhotoUrl.value);
|
||||
asyncPhotoUrl.value = '';
|
||||
}
|
||||
|
||||
// 防抖处理,避免频繁调用
|
||||
photoLoadTimer = setTimeout(async () => {
|
||||
if (newPhotoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(newPhotoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
}, 100); // 100ms 防抖延迟
|
||||
},
|
||||
{ immediate: true } // 立即执行一次
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -1742,4 +1867,25 @@ const handleClose = () => {
|
||||
cursor: help;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 照片加载样式 */
|
||||
.photo-loading {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8c939d;
|
||||
font-size: 14px;
|
||||
background-color: #fafafa;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.photo-loading .is-loading {
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
@@ -70,7 +70,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -725,9 +729,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuPersonalMsg">
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watchEffect } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
// 获取当前组件实例
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
@@ -779,6 +784,8 @@ const activeTab = ref('basic'); // 默认显示基础信息
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const photoLoading = ref(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref(''); // 异步加载的照片URL
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
@@ -999,6 +1006,69 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([getRegionTree(), getDormBuildingTree()]);
|
||||
@@ -1090,6 +1160,32 @@ onMounted(async () => {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Tab页样式优化 */
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 0;
|
||||
|
||||
@@ -66,7 +66,11 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="photo-display">
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -721,10 +725,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts" name="StuPersonalMsg">
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watch, computed } from 'vue';
|
||||
import { ref, reactive, getCurrentInstance, onMounted, toRefs, watch, computed, watchEffect } from 'vue';
|
||||
import request from '@/utils/request';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import api from '@/api/xg/xsjcxx/stuRoster/index';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
|
||||
// 定义props
|
||||
interface Props {
|
||||
@@ -798,6 +803,8 @@ const activeTab = ref('basic'); // 默认显示基础信息
|
||||
const regionOptions = ref([]); // 行政区划选项
|
||||
const dormBuildingOptions = ref([]); // 宿舍楼栋选项
|
||||
const loading = ref(false); // 加载状态
|
||||
const photoLoading = ref<boolean>(false); // 照片加载状态
|
||||
const asyncPhotoUrl = ref<string>(''); // 异步加载的照片URL
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
@@ -1021,6 +1028,50 @@ const getDormBuildingTree = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听学生ID变化,当弹窗打开且有学生ID时获取数据
|
||||
watch(
|
||||
() => [props.visible, props.studentId],
|
||||
@@ -1048,6 +1099,25 @@ watch(
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 监听form.photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 组件挂载时初始化基础数据
|
||||
onMounted(async () => {
|
||||
await Promise.all([getRegionTree(), getDormBuildingTree()]);
|
||||
@@ -1161,6 +1231,32 @@ defineOptions({
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Tab页样式优化 */
|
||||
:deep(.el-tabs__header .el-tabs__item.is-active) {
|
||||
color: var(--el-color-primary) !important;
|
||||
|
||||
@@ -167,15 +167,23 @@
|
||||
<div class="student-photo">
|
||||
<el-avatar
|
||||
:size="100"
|
||||
:src="item.photoUrl"
|
||||
:icon="!item.photoUrl ? (item.gender === '0' ? 'Male' : 'Female') : undefined"
|
||||
:src="getStudentPhotoUrl(item)"
|
||||
:icon="!getStudentPhotoUrl(item) ? (item.gender === '0' ? 'Male' : 'Female') : undefined"
|
||||
:class="{
|
||||
'male-placeholder': !item.photoUrl && item.gender === '0',
|
||||
'female-placeholder': !item.photoUrl && item.gender === '1'
|
||||
'male-placeholder': !getStudentPhotoUrl(item) && item.gender === '0',
|
||||
'female-placeholder': !getStudentPhotoUrl(item) && item.gender === '1'
|
||||
}"
|
||||
>
|
||||
<template v-if="!item.photoUrl">
|
||||
<template v-if="!getStudentPhotoUrl(item)">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="isPhotoLoading(item)" class="photo-loading-container">
|
||||
<el-icon class="is-loading" size="24" color="#409eff">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
</div>
|
||||
<!-- 无照片状态 -->
|
||||
<div
|
||||
v-else
|
||||
class="no-photo-placeholder"
|
||||
:class="{ 'male-placeholder': item.gender === '0', 'female-placeholder': item.gender === '1' }"
|
||||
>
|
||||
@@ -187,6 +195,15 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-avatar>
|
||||
<!-- 照片加载中状态文字 - 显示在头像底部 -->
|
||||
<div v-if="isPhotoLoading(item)" class="photo-loading-text">
|
||||
<el-text type="primary" size="small">
|
||||
<el-icon class="is-loading mr-1" size="12">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
图像加载中...
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="student-info">
|
||||
<h3 class="student-name">{{ item.name }}</h3>
|
||||
@@ -281,10 +298,11 @@
|
||||
import { ref, reactive, toRefs, watchEffect, onMounted, computed, getCurrentInstance } from 'vue';
|
||||
import ElMessage from 'element-plus/es/components/message/index';
|
||||
import ElMessageBox from 'element-plus/es/components/message-box/index';
|
||||
import { Search, Refresh, Setting, Download, List, Grid, View, Picture, Male, Female } from '@element-plus/icons-vue';
|
||||
import { Search, Refresh, Setting, Download, List, Grid, View, Picture, Male, Female, Loading } from '@element-plus/icons-vue';
|
||||
import AdvancedSearchDialog from './components/AdvancedSearchDialog.vue';
|
||||
import StudentDetailDialog from './components/StudentDetailDialog.vue';
|
||||
import api from '@/api/xg/xsjcxx/stuRoster/index';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
@@ -297,6 +315,10 @@ const deptName = ref('');
|
||||
const deptTreeRef = ref();
|
||||
const selectedDepts = ref<number[]>([]);
|
||||
|
||||
// 学生照片缓存
|
||||
const studentPhotoCache = ref<Map<string, string>>(new Map());
|
||||
const photoLoadingCache = ref<Map<string, boolean>>(new Map());
|
||||
|
||||
// 计算表格最大高度
|
||||
const tableMaxHeight = computed(() => {
|
||||
// 基础高度:视窗高度减去头部、搜索区域、工具栏、分页等区域的高度
|
||||
@@ -834,6 +856,99 @@ const getStudentStatusLabel = (status: string) => {
|
||||
return statusMap[status] || '未知';
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await request({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取学生照片URL(带缓存)
|
||||
const getStudentPhotoUrl = (student: any): string => {
|
||||
const photoPath = student.photoPath;
|
||||
if (!photoPath) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if (studentPhotoCache.value.has(photoPath)) {
|
||||
return studentPhotoCache.value.get(photoPath) || '';
|
||||
}
|
||||
|
||||
// 如果正在加载,返回空字符串
|
||||
if (photoLoadingCache.value.get(photoPath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 开始异步加载
|
||||
loadStudentPhoto(photoPath);
|
||||
return '';
|
||||
};
|
||||
|
||||
// 异步加载学生照片
|
||||
const loadStudentPhoto = async (photoPath: string) => {
|
||||
// 标记为正在加载
|
||||
photoLoadingCache.value.set(photoPath, true);
|
||||
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(photoPath);
|
||||
if (blobUrl) {
|
||||
studentPhotoCache.value.set(photoPath, blobUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载学生照片失败:', error);
|
||||
} finally {
|
||||
// 标记加载完成
|
||||
photoLoadingCache.value.set(photoPath, false);
|
||||
}
|
||||
};
|
||||
|
||||
// 检查照片是否正在加载
|
||||
const isPhotoLoading = (student: any): boolean => {
|
||||
const photoPath = student.photoPath;
|
||||
if (!photoPath) {
|
||||
return false;
|
||||
}
|
||||
return photoLoadingCache.value.get(photoPath) || false;
|
||||
};
|
||||
|
||||
// 监听部门名称变化
|
||||
watchEffect(() => {
|
||||
if (deptTreeRef.value) {
|
||||
@@ -1146,7 +1261,7 @@ onMounted(() => {
|
||||
|
||||
.student-card .card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
@@ -1154,6 +1269,39 @@ onMounted(() => {
|
||||
|
||||
.student-photo {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 照片加载中状态文字样式 */
|
||||
.photo-loading-text {
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
background-color: #f0f9ff;
|
||||
transition: all 0.3s ease;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.photo-loading-text .el-text {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.photo-loading-text .el-icon {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.student-info {
|
||||
@@ -1225,6 +1373,40 @@ onMounted(() => {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #f5f7fa !important;
|
||||
border-radius: 50%;
|
||||
color: #8c8d91;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.photo-loading-container .el-icon {
|
||||
margin-bottom: 4px;
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.photo-loading-container .loading-text {
|
||||
font-size: 10px;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 男生占位符样式 */
|
||||
.male-placeholder {
|
||||
background: #e6f3ff !important;
|
||||
@@ -1370,6 +1552,17 @@ onMounted(() => {
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
/* 移动端照片加载中文字样式调整 */
|
||||
.photo-loading-text {
|
||||
margin-top: 6px;
|
||||
padding: 3px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.photo-loading-text .el-icon {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -236,7 +236,11 @@
|
||||
:before-upload="beforePhotoUpload"
|
||||
:http-request="handlePhotoUpload"
|
||||
>
|
||||
<img v-if="form.photoUrl" :src="form.photoUrl" class="photo" />
|
||||
<div v-if="photoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncPhotoUrl" :src="asyncPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
@@ -912,7 +916,11 @@
|
||||
<el-col :span="8">
|
||||
<el-form-item label="学生照片">
|
||||
<div class="photo-display">
|
||||
<img v-if="detailForm.photoUrl" :src="detailForm.photoUrl" class="photo" />
|
||||
<div v-if="detailPhotoLoading" class="photo-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<img v-else-if="asyncDetailPhotoUrl" :src="asyncDetailPhotoUrl" class="photo" />
|
||||
<div v-else class="photo-placeholder">暂无照片</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -1474,7 +1482,7 @@ import { to } from 'await-to-js';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
// import type { UploadInstance } from 'element-plus';
|
||||
// import type { FormInstance, TreeInstance } from 'element-plus';
|
||||
import { Plus, ArrowDown, InfoFilled } from '@element-plus/icons-vue';
|
||||
import { Plus, ArrowDown, InfoFilled, Loading } from '@element-plus/icons-vue';
|
||||
import service from '@/utils/request';
|
||||
interface ComponentInternalInstance {
|
||||
proxy?: any;
|
||||
@@ -1760,6 +1768,12 @@ const form = reactive<StudentMsgForm>({
|
||||
...initFormData
|
||||
});
|
||||
|
||||
// 异步加载的图片URL
|
||||
const asyncPhotoUrl = ref<string>('');
|
||||
const asyncDetailPhotoUrl = ref<string>('');
|
||||
const photoLoading = ref<boolean>(false);
|
||||
const detailPhotoLoading = ref<boolean>(false);
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
||||
studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
|
||||
@@ -2513,6 +2527,88 @@ const closeDialog = () => {
|
||||
reset();
|
||||
};
|
||||
|
||||
// 通过认证请求获取图片数据并转换为blob URL
|
||||
const getImageBlobUrl = async (imageUrl: string): Promise<string> => {
|
||||
try {
|
||||
// 通过axios请求图片数据,这样会自动带上token和clientId
|
||||
const response = await service({
|
||||
url: '/resource/oss/download/' + imageUrl,
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 300000 // 300秒超时
|
||||
});
|
||||
|
||||
// 由于axios响应拦截器对blob类型响应直接返回blob数据
|
||||
// 这里response直接就是blob对象,而不是包含data属性的响应对象
|
||||
const blob = response;
|
||||
|
||||
// 验证响应数据
|
||||
if (!blob) {
|
||||
throw new Error('响应数据为空');
|
||||
}
|
||||
|
||||
if (!(blob instanceof Blob)) {
|
||||
// 如果不是Blob,尝试获取实际内容用于调试
|
||||
let actualContent = 'Unknown';
|
||||
try {
|
||||
if (typeof blob === 'string') {
|
||||
actualContent = (blob as string).substring(0, 200);
|
||||
} else if (blob && typeof blob.toString === 'function') {
|
||||
actualContent = blob.toString().substring(0, 200);
|
||||
}
|
||||
} catch (e) {
|
||||
actualContent = 'Cannot convert to string';
|
||||
}
|
||||
|
||||
throw new Error(`响应数据不是有效的Blob对象。实际类型: ${typeof blob}, 内容: ${actualContent}`);
|
||||
}
|
||||
|
||||
// 创建blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
return blobUrl;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// 监听photoPath变化,异步加载图片
|
||||
watchEffect(async () => {
|
||||
if (form.photoPath) {
|
||||
photoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(form.photoPath);
|
||||
asyncPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载图片失败:', error);
|
||||
asyncPhotoUrl.value = '';
|
||||
} finally {
|
||||
photoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncPhotoUrl.value = '';
|
||||
photoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听detailForm.photoPath变化,异步加载详情图片
|
||||
watchEffect(async () => {
|
||||
if (detailForm.photoPath) {
|
||||
detailPhotoLoading.value = true;
|
||||
try {
|
||||
const blobUrl = await getImageBlobUrl(detailForm.photoPath);
|
||||
asyncDetailPhotoUrl.value = blobUrl;
|
||||
} catch (error) {
|
||||
console.error('加载详情图片失败:', error);
|
||||
asyncDetailPhotoUrl.value = '';
|
||||
} finally {
|
||||
detailPhotoLoading.value = false;
|
||||
}
|
||||
} else {
|
||||
asyncDetailPhotoUrl.value = '';
|
||||
detailPhotoLoading.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getDeptTree(); // 初始化部门数据(会在完成后自动调用getList)
|
||||
getRegionTree(); // 初始化行政区划数据
|
||||
@@ -2665,6 +2761,32 @@ onMounted(() => {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
|
||||
/* 照片加载状态样式 */
|
||||
.photo-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border: 2px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fafafa;
|
||||
color: #8c8d91;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.photo-loading .el-icon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
color: #409eff;
|
||||
}
|
||||
|
||||
.photo-loading span {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 照片上传样式 */
|
||||
.photo-uploader .photo {
|
||||
width: 130px;
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" label-width="100px" :inline="true">
|
||||
<!-- <el-form-item label="删除标志" prop="deleted">
|
||||
<el-input v-model="queryParams.deleted" placeholder="请输入删除标志" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="请假类型名称" prop="leaveTypeName">
|
||||
<el-input v-model="queryParams.leaveTypeName" placeholder="请输入请假类型名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假类型编码" prop="leaveTypeCode">
|
||||
<el-input v-model="queryParams.leaveTypeCode" placeholder="请输入请假类型编码" clearable @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>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
icon="Edit"
|
||||
:disabled="single"
|
||||
@click="handleUpdate()"
|
||||
v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig:edit']"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete()"
|
||||
v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig:remove']"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-col> -->
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig: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="xgLeaveTypeConfigList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="主键ID" align="center" prop="id" v-if="true" />
|
||||
<el-table-column label="请假类型名称" align="center" prop="leaveTypeName" />
|
||||
<el-table-column label="请假类型编码" align="center" prop="leaveTypeCode" />
|
||||
<el-table-column label="请假类型描述" align="center" prop="leaveTypeDesc" />
|
||||
<el-table-column label="排序号" align="center" prop="sortOrder" />
|
||||
<el-table-column label="请假是否需要上传地理位置" align="center" prop="needLocationOnApply">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.needLocationOnApply" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销假是否必须上传照片" align="center" prop="needPhotoOnCancel">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.needPhotoOnCancel" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销假是否需要上传地理位置" align="center" prop="needLocationOnCancel">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.needLocationOnCancel" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="非离校请假是否需要销假" align="center" prop="needCancelForNonLeave">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.needCancelForNonLeave" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否可以提前请假" align="center" prop="canApplyInAdvance">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.canApplyInAdvance" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否开启" align="center" prop="isEnabled">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.isEnabled" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请假最大申请时长(天)" align="center" prop="maxApplyDuration" />
|
||||
<el-table-column label="销假逾期多少时长给予提醒(天)" align="center" prop="cancelOverdueReminder" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig:edit']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
<!-- <el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['xgLeaveTypeConfig:xgLeaveTypeConfig:remove']"
|
||||
></el-button>
|
||||
</el-tooltip> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改学生请假类型配置对话框 -->
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="600px" append-to-body>
|
||||
<el-form ref="xgLeaveTypeConfigFormRef" :model="form" :rules="rules" label-position="top">
|
||||
<el-form-item label="请假类型名称" prop="leaveTypeName">
|
||||
<el-input v-model="form.leaveTypeName" placeholder="请输入请假类型名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假类型编码" prop="leaveTypeCode">
|
||||
<el-input v-model="form.leaveTypeCode" placeholder="请输入请假类型编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假类型描述" prop="leaveTypeDesc">
|
||||
<el-input v-model="form.leaveTypeDesc" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="sortOrder">
|
||||
<el-input v-model="form.sortOrder" placeholder="请输入排序号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假是否需要上传地理位置" prop="needLocationOnApply">
|
||||
<el-select v-model="form.needLocationOnApply" placeholder="请选择请假是否需要上传地理位置">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="销假是否必须上传照片" prop="needPhotoOnCancel">
|
||||
<el-select v-model="form.needPhotoOnCancel" placeholder="请选择销假是否必须上传照片">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="销假是否需要上传地理位置" prop="needLocationOnCancel">
|
||||
<el-select v-model="form.needLocationOnCancel" placeholder="请选择销假是否需要上传地理位置">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="非离校请假是否需要销假" prop="needCancelForNonLeave">
|
||||
<el-select v-model="form.needCancelForNonLeave" placeholder="请选择非离校请假是否需要销假">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否可以提前请假" prop="canApplyInAdvance">
|
||||
<el-select v-model="form.canApplyInAdvance" placeholder="请选择是否可以提前请假">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否开启" prop="isEnabled">
|
||||
<el-select v-model="form.isEnabled" placeholder="请选择是否开启">
|
||||
<el-option v-for="dict in sys_yesno" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假最大申请时长(天)" prop="maxApplyDuration">
|
||||
<el-input v-model="form.maxApplyDuration" placeholder="请输入请假最大申请时长" />
|
||||
</el-form-item>
|
||||
<el-form-item label="销假逾期多少时长给予提醒(天)" prop="cancelOverdueReminder">
|
||||
<el-input v-model="form.cancelOverdueReminder" placeholder="请输入销假逾期多少时长给予提醒" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="XgLeaveTypeConfig" lang="ts">
|
||||
import {
|
||||
listXgLeaveTypeConfig,
|
||||
getXgLeaveTypeConfig,
|
||||
delXgLeaveTypeConfig,
|
||||
addXgLeaveTypeConfig,
|
||||
updateXgLeaveTypeConfig
|
||||
} from '@/api/xg/xsjcxx/xgLeaveTypeConfig/xgLeaveTypeConfig';
|
||||
import { XgLeaveTypeConfigVO, XgLeaveTypeConfigQuery, XgLeaveTypeConfigForm } from '@/api/xg/xsjcxx/xgLeaveTypeConfig/xgLeaveTypeConfig/types';
|
||||
import { getCurrentInstance, ref, reactive, toRefs, onMounted } from 'vue';
|
||||
import type { FormInstance as ElFormInstance } from 'element-plus';
|
||||
|
||||
interface ComponentInternalInstance {
|
||||
proxy?: any;
|
||||
}
|
||||
|
||||
interface DialogOption {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface PageData<T, Q> {
|
||||
form: T;
|
||||
queryParams: Q;
|
||||
rules: Record<string, any[]>;
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
// 加载字典数据
|
||||
const { sys_yesno } = toRefs<any>(proxy?.useDict('sys_yesno'));
|
||||
|
||||
const xgLeaveTypeConfigList = ref<XgLeaveTypeConfigVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const xgLeaveTypeConfigFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: XgLeaveTypeConfigForm = {
|
||||
id: undefined,
|
||||
leaveTypeName: undefined,
|
||||
leaveTypeCode: undefined,
|
||||
leaveTypeDesc: undefined,
|
||||
sortOrder: undefined,
|
||||
needLocationOnApply: '1',
|
||||
needPhotoOnCancel: '1',
|
||||
needLocationOnCancel: '1',
|
||||
needCancelForNonLeave: '1',
|
||||
canApplyInAdvance: '1',
|
||||
isEnabled: '1',
|
||||
maxApplyDuration: undefined,
|
||||
cancelOverdueReminder: undefined
|
||||
};
|
||||
const data = reactive<PageData<XgLeaveTypeConfigForm, XgLeaveTypeConfigQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
deleted: undefined,
|
||||
leaveTypeName: undefined,
|
||||
leaveTypeCode: undefined,
|
||||
params: {}
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
|
||||
leaveTypeName: [{ required: true, message: '请假类型名称不能为空', trigger: 'blur' }],
|
||||
leaveTypeCode: [{ required: true, message: '请假类型编码不能为空', trigger: 'blur' }],
|
||||
needLocationOnApply: [{ required: true, message: '请假是否需要上传地理位置不能为空', trigger: 'blur' }],
|
||||
needPhotoOnCancel: [{ required: true, message: '销假是否必须上传照片不能为空', trigger: 'blur' }],
|
||||
needLocationOnCancel: [{ required: true, message: '销假是否需要上传地理位置不能为空', trigger: 'blur' }],
|
||||
needCancelForNonLeave: [{ required: true, message: '非离校请假是否需要销假不能为空', trigger: 'blur' }],
|
||||
canApplyInAdvance: [{ required: true, message: '是否可以提前请假不能为空', trigger: 'blur' }],
|
||||
isEnabled: [{ required: true, message: '是否开启不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询学生请假类型配置列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listXgLeaveTypeConfig(queryParams.value);
|
||||
xgLeaveTypeConfigList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
xgLeaveTypeConfigFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: XgLeaveTypeConfigVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加学生请假类型配置';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: XgLeaveTypeConfigVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getXgLeaveTypeConfig(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改学生请假类型配置';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
xgLeaveTypeConfigFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateXgLeaveTypeConfig(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addXgLeaveTypeConfig(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: XgLeaveTypeConfigVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除学生请假类型配置编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await delXgLeaveTypeConfig(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'xgLeaveTypeConfig/xgLeaveTypeConfig/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`xgLeaveTypeConfig_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="请假天数" prop="startLeaveDays">
|
||||
<el-input v-model="queryParams.startLeaveDays" placeholder="请输入请假天数" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="endLeaveDays"> 至 </el-form-item>
|
||||
<el-form-item prop="endLeaveDays">
|
||||
<el-input v-model="queryParams.endLeaveDays" placeholder="请输入请假天数" clearable @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>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:leave:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:leave:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="leaveList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="主键" align="center" prop="id" />
|
||||
<el-table-column label="请假类型" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ options.find((e) => e.value === scope.row.leaveType)?.label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" align="center" prop="startDate">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.startDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" align="center" prop="endDate">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.endDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请假天数" align="center" prop="leaveDays" />
|
||||
<el-table-column label="请假原因" align="center" prop="remark" />
|
||||
<el-table-column align="center" label="流程状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.status"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="162">
|
||||
<template #default="scope">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
|
||||
<el-button v-hasPermi="['workflow:leave:edit']" size="small" type="primary" icon="Edit" @click="handleUpdate(scope.row)"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
|
||||
<el-button v-hasPermi="['workflow:leave:remove']" size="small" type="primary" icon="Delete" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'waiting'">
|
||||
<el-button size="small" type="primary" icon="Notification" @click="handleCancelProcessApply(scope.row.id)">撤销</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Leave" lang="ts">
|
||||
import { delLeave, listLeave } from '@/api/workflow/leave';
|
||||
import { cancelProcessApply } from '@/api/workflow/instance';
|
||||
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const leaveList = ref<LeaveVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '事假'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '调休'
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '病假'
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '婚假'
|
||||
}
|
||||
];
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startLeaveDays: undefined,
|
||||
endLeaveDays: undefined
|
||||
},
|
||||
rules: {}
|
||||
});
|
||||
|
||||
const { queryParams } = toRefs(data);
|
||||
|
||||
/** 查询请假列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listLeave(queryParams.value);
|
||||
leaveList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: LeaveVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
|
||||
query: {
|
||||
type: 'add'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = (row?: LeaveVO) => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'update'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row?: LeaveVO) => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/xg/xsjcxx/xsqj/leaveEdit/index`,
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'view'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: LeaveVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除请假编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await delLeave(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'workflow/leave/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`leave_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
/** 撤销按钮操作 */
|
||||
const handleCancelProcessApply = async (id: string) => {
|
||||
await proxy?.$modal.confirm('是否确认撤销当前单据?');
|
||||
loading.value = true;
|
||||
const data = {
|
||||
businessId: id,
|
||||
message: '申请人撤销流程!'
|
||||
};
|
||||
await cancelProcessApply(data).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('撤销成功');
|
||||
};
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,525 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div 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="deleted">
|
||||
<el-input v-model="queryParams.deleted" placeholder="请输入删除标志" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="学生ID" prop="studentId">
|
||||
<el-input v-model="queryParams.studentId" placeholder="请输入学生ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<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="studentNo">
|
||||
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<!-- <el-form-item label="学院ID" prop="collegeId">
|
||||
<el-input v-model="queryParams.collegeId" placeholder="请输入学院ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="学院名称" prop="collegeName">
|
||||
<el-input v-model="queryParams.collegeName" placeholder="请输入学院名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<!-- <el-form-item label="专业ID" prop="majorId">
|
||||
<el-input v-model="queryParams.majorId" placeholder="请输入专业ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="专业名称" prop="majorName">
|
||||
<el-input v-model="queryParams.majorName" placeholder="请输入专业名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<!-- <el-form-item label="班级ID" prop="classId">
|
||||
<el-input v-model="queryParams.classId" placeholder="请输入班级ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="班级名称" prop="className">
|
||||
<el-input v-model="queryParams.className" placeholder="请输入班级名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<!-- <el-form-item label="请假类型编码" prop="leaveTypeCode">
|
||||
<el-input v-model="queryParams.leaveTypeCode" placeholder="请输入请假类型编码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<el-form-item label="请假类型名称" prop="leaveTypeName">
|
||||
<el-input v-model="queryParams.leaveTypeName" placeholder="请输入请假类型名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="queryParams.startTime"
|
||||
type="date"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择开始时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker
|
||||
clearable
|
||||
v-model="queryParams.endTime"
|
||||
type="date"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择结束时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- <el-form-item label="请假时长" prop="leaveDuration">
|
||||
<el-input v-model="queryParams.leaveDuration" placeholder="请输入请假时长" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否离校:0-否,1-是" prop="isLeaveSchool">
|
||||
<el-input v-model="queryParams.isLeaveSchool" placeholder="请输入是否离校:0-否,1-是" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否需要销假:0-否,1-是" prop="needCancel">
|
||||
<el-input v-model="queryParams.needCancel" placeholder="请输入是否需要销假:0-否,1-是" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="紧急联系人" prop="emergencyContact">
|
||||
<el-input v-model="queryParams.emergencyContact" placeholder="请输入紧急联系人" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="紧急联系电话" prop="emergencyPhone">
|
||||
<el-input v-model="queryParams.emergencyPhone" placeholder="请输入紧急联系电话" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" prop="leaveReason">
|
||||
<el-input v-model="queryParams.leaveReason" placeholder="请输入请假原因" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假时地理位置" prop="leaveLocation">
|
||||
<el-input v-model="queryParams.leaveLocation" placeholder="请输入请假时地理位置" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="附件OSS文件ID" prop="attachmentOssIds">
|
||||
<el-input v-model="queryParams.attachmentOssIds" placeholder="请输入附件OSS文件ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批意见" prop="approvalOpinion">
|
||||
<el-input v-model="queryParams.approvalOpinion" placeholder="请输入审批意见" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批时间" prop="approvalTime">
|
||||
<el-date-picker clearable v-model="queryParams.approvalTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择审批时间" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人ID" prop="approvalBy">
|
||||
<el-input v-model="queryParams.approvalBy" placeholder="请输入审批人ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="审批人姓名" prop="approvalByName">
|
||||
<el-input v-model="queryParams.approvalByName" placeholder="请输入审批人姓名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="销假时间" prop="cancelTime">
|
||||
<el-date-picker clearable v-model="queryParams.cancelTime" type="date" value-format="YYYY-MM-DD" placeholder="请选择销假时间" />
|
||||
</el-form-item>
|
||||
<el-form-item label="销假地理位置" prop="cancelLocation">
|
||||
<el-input v-model="queryParams.cancelLocation" placeholder="请输入销假地理位置" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="销假照片OSS文件ID" prop="cancelPhotoOssIds">
|
||||
<el-input v-model="queryParams.cancelPhotoOssIds" placeholder="请输入销假照片OSS文件ID" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="销假备注" prop="cancelRemark">
|
||||
<el-input v-model="queryParams.cancelRemark" placeholder="请输入销假备注" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item> -->
|
||||
<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">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['studentLeave:studentLeave:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['studentLeave:studentLeave:edit']"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<!-- <el-col :span="1.5">
|
||||
<el-button
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="multiple"
|
||||
@click="handleDelete()"
|
||||
v-hasPermi="['studentLeave:studentLeave:remove']"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['studentLeave:studentLeave: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="studentLeaveList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="主键ID" align="center" prop="id" v-if="true" />
|
||||
<!-- <el-table-column label="删除标志" align="center" prop="deleted" /> -->
|
||||
<!-- <el-table-column label="学生ID" align="center" prop="studentId" /> -->
|
||||
<el-table-column label="学生姓名" align="center" prop="studentName" />
|
||||
<el-table-column label="学号" align="center" prop="studentNo" />
|
||||
<!-- <el-table-column label="班级ID" align="center" prop="classId" /> -->
|
||||
<el-table-column label="班级名称" align="center" prop="className" />
|
||||
<!-- <el-table-column label="专业ID" align="center" prop="majorId" /> -->
|
||||
<el-table-column label="专业名称" align="center" prop="majorName" />
|
||||
<!-- <el-table-column label="学院ID" align="center" prop="collegeId" /> -->
|
||||
<el-table-column label="学院名称" align="center" prop="collegeName" />
|
||||
<!-- <el-table-column label="请假类型编码" align="center" prop="leaveTypeCode" /> -->
|
||||
<el-table-column label="请假类型名称" align="center" prop="leaveTypeName" />
|
||||
<el-table-column label="开始时间" align="center" prop="startTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.startTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" align="center" prop="endTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.endTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请假时长(天)" align="center" prop="leaveDuration" />
|
||||
<el-table-column label="是否离校" align="center" prop="isLeaveSchool">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.isLeaveSchool" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否需要销假" align="center" prop="needCancel">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yesno" :value="scope.row.needCancel" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="紧急联系人" align="center" prop="emergencyContact" />
|
||||
<el-table-column label="紧急联系电话" align="center" prop="emergencyPhone" />
|
||||
<el-table-column label="请假原因" align="center" prop="leaveReason" />
|
||||
<!-- <el-table-column label="请假时地理位置" align="center" prop="leaveLocation" /> -->
|
||||
<el-table-column label="附件OSS文件ID" align="center" prop="attachmentOssIds" />
|
||||
<!-- <el-table-column label="审批状态" align="center" prop="approvalStatus" />
|
||||
<el-table-column label="审批意见" align="center" prop="approvalOpinion" />
|
||||
<el-table-column label="审批时间" align="center" prop="approvalTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.approvalTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column> -->
|
||||
<!-- <el-table-column label="审批人ID" align="center" prop="approvalBy" />
|
||||
<el-table-column label="审批人姓名" align="center" prop="approvalByName" />
|
||||
<el-table-column label="销假状态:0-未销假,1-已销假,2-逾期未销假" align="center" prop="cancelStatus" />
|
||||
<el-table-column label="销假时间" align="center" prop="cancelTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.cancelTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="销假地理位置" align="center" prop="cancelLocation" />
|
||||
<el-table-column label="销假照片OSS文件ID" align="center" prop="cancelPhotoOssIds" />
|
||||
<el-table-column label="销假备注" align="center" prop="cancelRemark" /> -->
|
||||
<el-table-column label="操作" align="center" width="100px" class-name="small-padding fixed-width" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['studentLeave:studentLeave:edit']"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['studentLeave:studentLeave:remove']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 学生请假对话框组件 -->
|
||||
<StudentLeaveDialog
|
||||
v-model:dialog="dialog"
|
||||
:form="form"
|
||||
:rules="rules"
|
||||
:button-loading="buttonLoading"
|
||||
:type="form.type"
|
||||
@submit="submitForm"
|
||||
@cancel="cancel"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="StudentLeave" lang="ts">
|
||||
import { ref, reactive, toRefs, onMounted, getCurrentInstance, type ComponentInternalInstance } from 'vue';
|
||||
import { listStudentLeave, getStudentLeave, delStudentLeave, addStudentLeave, updateStudentLeave } from '@/api/workflow/studentLeave/studentLeave';
|
||||
import { StudentLeaveVO, StudentLeaveQuery, StudentLeaveForm } from '@/api/workflow/studentLeave/studentLeave/types';
|
||||
import StudentLeaveDialog from './dialog.vue';
|
||||
|
||||
// 类型定义
|
||||
interface DialogOption {
|
||||
visible: boolean;
|
||||
title: string;
|
||||
}
|
||||
|
||||
interface ElFormInstance {
|
||||
validate: (callback?: (valid: boolean) => void) => Promise<boolean>;
|
||||
resetFields: () => void;
|
||||
}
|
||||
|
||||
interface PageData<T, Q> {
|
||||
form: T;
|
||||
queryParams: Q;
|
||||
rules: Record<string, any>;
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
// 加载字典数据
|
||||
const { sys_yesno } = toRefs<any>(proxy?.useDict('sys_yesno'));
|
||||
|
||||
const studentLeaveList = ref<StudentLeaveVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const studentLeaveFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: StudentLeaveForm = {
|
||||
id: undefined,
|
||||
deleted: undefined,
|
||||
studentId: undefined,
|
||||
studentName: undefined,
|
||||
studentNo: undefined,
|
||||
classId: undefined,
|
||||
className: undefined,
|
||||
majorId: undefined,
|
||||
majorName: undefined,
|
||||
collegeId: undefined,
|
||||
collegeName: undefined,
|
||||
leaveTypeCode: undefined,
|
||||
leaveTypeName: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveDuration: undefined,
|
||||
isLeaveSchool: undefined,
|
||||
needCancel: undefined,
|
||||
emergencyContact: undefined,
|
||||
emergencyPhone: undefined,
|
||||
leaveReason: undefined,
|
||||
leaveLocation: undefined,
|
||||
attachmentOssIds: undefined,
|
||||
approvalStatus: undefined,
|
||||
approvalOpinion: undefined,
|
||||
approvalTime: undefined,
|
||||
approvalBy: undefined,
|
||||
approvalByName: undefined,
|
||||
cancelStatus: undefined,
|
||||
cancelTime: undefined,
|
||||
cancelLocation: undefined,
|
||||
cancelPhotoOssIds: undefined,
|
||||
cancelRemark: undefined
|
||||
};
|
||||
const data = reactive<PageData<StudentLeaveForm, StudentLeaveQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
deleted: undefined,
|
||||
studentId: undefined,
|
||||
studentName: undefined,
|
||||
studentNo: undefined,
|
||||
classId: undefined,
|
||||
className: undefined,
|
||||
majorId: undefined,
|
||||
majorName: undefined,
|
||||
collegeId: undefined,
|
||||
collegeName: undefined,
|
||||
leaveTypeCode: undefined,
|
||||
leaveTypeName: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
leaveDuration: undefined,
|
||||
isLeaveSchool: undefined,
|
||||
needCancel: undefined,
|
||||
emergencyContact: undefined,
|
||||
emergencyPhone: undefined,
|
||||
leaveReason: undefined,
|
||||
leaveLocation: undefined,
|
||||
attachmentOssIds: undefined,
|
||||
approvalStatus: undefined,
|
||||
approvalOpinion: undefined,
|
||||
approvalTime: undefined,
|
||||
approvalBy: undefined,
|
||||
approvalByName: undefined,
|
||||
cancelStatus: undefined,
|
||||
cancelTime: undefined,
|
||||
cancelLocation: undefined,
|
||||
cancelPhotoOssIds: undefined,
|
||||
cancelRemark: undefined,
|
||||
params: {}
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键ID不能为空', trigger: 'blur' }],
|
||||
studentId: [{ required: true, message: '学生ID不能为空', trigger: 'blur' }],
|
||||
studentName: [{ required: true, message: '学生姓名不能为空', trigger: 'blur' }],
|
||||
studentNo: [{ required: true, message: '学号不能为空', trigger: 'blur' }],
|
||||
leaveTypeCode: [{ required: true, message: '请假类型编码不能为空', trigger: 'blur' }],
|
||||
leaveTypeName: [{ required: true, message: '请假类型名称不能为空', trigger: 'blur' }],
|
||||
startTime: [{ required: true, message: '开始时间不能为空', trigger: 'blur' }],
|
||||
endTime: [{ required: true, message: '结束时间不能为空', trigger: 'blur' }],
|
||||
leaveDuration: [{ required: true, message: '请假时长不能为空', trigger: 'blur' }],
|
||||
isLeaveSchool: [{ required: true, message: '是否离校:0-否,1-是不能为空', trigger: 'blur' }],
|
||||
needCancel: [{ required: true, message: '是否需要销假:0-否,1-是不能为空', trigger: 'blur' }],
|
||||
leaveReason: [{ required: true, message: '请假原因不能为空', trigger: 'blur' }],
|
||||
approvalStatus: [{ required: true, message: '审批状态:0-待审批,1-已通过,2-已拒绝,3-已取消不能为空', trigger: 'change' }],
|
||||
cancelStatus: [{ required: true, message: '销假状态:0-未销假,1-已销假,2-逾期未销假不能为空', trigger: 'change' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询学生请假 - 记录学生的请假申请、审批和销假信息列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listStudentLeave(queryParams.value);
|
||||
studentLeaveList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
studentLeaveFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: StudentLeaveVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加学生请假';
|
||||
// 传递type参数,标识为新增操作
|
||||
form.value.type = 'add';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: StudentLeaveVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getStudentLeave(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
// 传递type参数,标识为修改操作
|
||||
form.value.type = 'update';
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改学生请假';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
studentLeaveFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateStudentLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addStudentLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: StudentLeaveVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal
|
||||
.confirm('是否确认删除学生请假 - 记录学生的请假申请、审批和销假信息编号为"' + _ids + '"的数据项?')
|
||||
.finally(() => (loading.value = false));
|
||||
await delStudentLeave(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'studentLeave/studentLeave/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`studentLeave_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 搜索按钮容器样式 */
|
||||
.search-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-card shadow="never">
|
||||
<approvalButton
|
||||
@submitForm="submitForm"
|
||||
@approvalVerifyOpen="approvalVerifyOpen"
|
||||
@handleApprovalRecord="handleApprovalRecord"
|
||||
:buttonLoading="buttonLoading"
|
||||
:id="form.id"
|
||||
:status="form.status"
|
||||
:pageType="routeParams.type"
|
||||
/>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="height: 78vh; overflow-y: auto">
|
||||
<el-form ref="leaveFormRef" v-loading="loading" :disabled="routeParams.type === 'view'" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="请假类型" prop="leaveType">
|
||||
<el-select v-model="form.leaveType" placeholder="请选择请假类型" style="width: 100%">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假时间" required>
|
||||
<el-date-picker
|
||||
v-model="leaveTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="To"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
@change="changeLeaveTime()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假天数" prop="leaveDays">
|
||||
<el-input v-model="form.leaveDays" disabled type="number" placeholder="请输入请假天数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入请假原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<!-- 提交组件 -->
|
||||
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
|
||||
<!-- 审批记录 -->
|
||||
<approvalRecord ref="approvalRecordRef" />
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" :before-close="handleClose" width="500">
|
||||
<el-select v-model="flowCode" placeholder="Select" style="width: 240px">
|
||||
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitFlow()"> 确认 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Leave" lang="ts">
|
||||
import { addLeave, getLeave, updateLeave } from '@/api/workflow/leave';
|
||||
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
|
||||
import { startWorkFlow } from '@/api/workflow/task';
|
||||
import SubmitVerify from '@/components/Process/submitVerify.vue';
|
||||
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
|
||||
import ApprovalButton from '@/components/Process/approvalButton.vue';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const leaveTime = ref<Array<string>>([]);
|
||||
//路由参数
|
||||
const routeParams = ref<Record<string, any>>({});
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '事假'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '调休'
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '病假'
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '婚假'
|
||||
}
|
||||
];
|
||||
const flowCodeOptions = [
|
||||
{
|
||||
value: 'hzdyjs-qj',
|
||||
label: '学生请假流程-杭州第一技师'
|
||||
},
|
||||
{
|
||||
value: 'leave1',
|
||||
label: '请假申请-普通'
|
||||
},
|
||||
{
|
||||
value: 'leave2',
|
||||
label: '请假申请-排他网关'
|
||||
},
|
||||
{
|
||||
value: 'leave3',
|
||||
label: '请假申请-并行网关'
|
||||
},
|
||||
{
|
||||
value: 'leave4',
|
||||
label: '请假申请-会签'
|
||||
},
|
||||
{
|
||||
value: 'leave5',
|
||||
label: '请假申请-并行会签网关'
|
||||
},
|
||||
{
|
||||
value: 'leave6',
|
||||
label: '请假申请-排他并行会签'
|
||||
}
|
||||
];
|
||||
|
||||
const flowCode = ref<string>('');
|
||||
|
||||
const dialogVisible = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '流程定义'
|
||||
});
|
||||
//提交组件
|
||||
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
|
||||
//审批记录组件
|
||||
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
|
||||
//按钮组件
|
||||
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
|
||||
|
||||
const leaveFormRef = ref<ElFormInstance>();
|
||||
|
||||
const submitFormData = ref<StartProcessBo>({
|
||||
businessId: '',
|
||||
flowCode: '',
|
||||
variables: {}
|
||||
});
|
||||
const taskVariables = ref<Record<string, any>>({});
|
||||
|
||||
const initFormData: LeaveForm = {
|
||||
id: undefined,
|
||||
leaveType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
leaveDays: undefined,
|
||||
remark: undefined,
|
||||
status: undefined
|
||||
};
|
||||
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startLeaveDays: undefined,
|
||||
endLeaveDays: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
|
||||
leaveTime: [{ required: true, message: '请假时间不能为空', trigger: 'blur' }],
|
||||
leaveDays: [{ required: true, message: '请假天数不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.visible = false;
|
||||
flowCode.value = '';
|
||||
buttonLoading.value = false;
|
||||
};
|
||||
const { form, rules } = toRefs(data);
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
leaveTime.value = [];
|
||||
leaveFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const changeLeaveTime = () => {
|
||||
const startDate = new Date(leaveTime.value[0]).getTime();
|
||||
const endDate = new Date(leaveTime.value[1]).getTime();
|
||||
const diffInMilliseconds = endDate - startDate;
|
||||
form.value.leaveDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1;
|
||||
};
|
||||
/** 获取详情 */
|
||||
const getInfo = () => {
|
||||
loading.value = true;
|
||||
buttonLoading.value = false;
|
||||
nextTick(async () => {
|
||||
const res = await getLeave(routeParams.value.id);
|
||||
Object.assign(form.value, res.data);
|
||||
console.log('res.data:', res.data);
|
||||
console.log('form.value:', form.value);
|
||||
// 为了确保 Element Plus 日期区间组件能够正确渲染,需要直接替换绑定数组引用
|
||||
// 同时将后端返回的日期统一格式化为组件要求的 "YYYY-MM-DD HH:mm:ss"
|
||||
// 后端仅返回 "YYYY-MM-DD",需要为日期补齐时间,符合 value-format="YYYY-MM-DD HH:mm:ss"
|
||||
const ensureDateTime = (dateStr: string | undefined, endFlag = false) => {
|
||||
if (!dateStr) return '';
|
||||
// 如果已包含时间段,直接返回
|
||||
if (/\d{2}:\d{2}:\d{2}$/.test(dateStr)) {
|
||||
return dateStr;
|
||||
}
|
||||
return endFlag ? `${dateStr} 23:59:59` : `${dateStr} 00:00:00`;
|
||||
};
|
||||
const start = ensureDateTime(form.value.startDate, false);
|
||||
const end = ensureDateTime(form.value.endDate, true);
|
||||
leaveTime.value = [start, end];
|
||||
loading.value = false;
|
||||
buttonLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = (status: string) => {
|
||||
if (leaveTime.value.length === 0) {
|
||||
proxy?.$modal.msgError('请假时间不能为空');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
leaveFormRef.value?.validate(async (valid: boolean) => {
|
||||
form.value.startDate = leaveTime.value[0];
|
||||
form.value.endDate = leaveTime.value[1];
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
let res: AxiosResponse<LeaveVO>;
|
||||
if (form.value.id) {
|
||||
res = await updateLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
res = await addLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
form.value = res.data;
|
||||
if (status === 'draft') {
|
||||
buttonLoading.value = false;
|
||||
proxy?.$modal.msgSuccess('暂存成功');
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
} else {
|
||||
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
|
||||
flowCode.value = flowCodeOptions[0].value;
|
||||
dialogVisible.visible = true;
|
||||
return;
|
||||
}
|
||||
//说明启动过先随意穿个参数
|
||||
if (flowCode.value === '' || flowCode.value === null) {
|
||||
flowCode.value = 'xx';
|
||||
}
|
||||
await handleStartWorkFlow(res.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitFlow = async () => {
|
||||
handleStartWorkFlow(form.value);
|
||||
dialogVisible.visible = false;
|
||||
};
|
||||
//提交申请
|
||||
const handleStartWorkFlow = async (data: LeaveForm) => {
|
||||
try {
|
||||
submitFormData.value.flowCode = flowCode.value;
|
||||
submitFormData.value.businessId = data.id;
|
||||
//流程变量
|
||||
taskVariables.value = {
|
||||
// hzdyjs-qj 使用的流程变量
|
||||
qjts: data.leaveDays,
|
||||
// leave2/6 使用的流程变量
|
||||
leaveDays: data.leaveDays,
|
||||
// leave4/5 使用的流程变量
|
||||
userList: ['1', '3', '4']
|
||||
};
|
||||
submitFormData.value.variables = taskVariables.value;
|
||||
const resp = await startWorkFlow(submitFormData.value);
|
||||
if (submitVerifyRef.value) {
|
||||
buttonLoading.value = false;
|
||||
submitVerifyRef.value.openDialog(resp.data.taskId);
|
||||
}
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
//审批记录
|
||||
const handleApprovalRecord = () => {
|
||||
approvalRecordRef.value.init(form.value.id);
|
||||
};
|
||||
//提交回调
|
||||
const submitCallback = async () => {
|
||||
await proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
};
|
||||
//审批
|
||||
const approvalVerifyOpen = async () => {
|
||||
submitVerifyRef.value.openDialog(routeParams.value.taskId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(async () => {
|
||||
routeParams.value = proxy.$route.query;
|
||||
reset();
|
||||
loading.value = false;
|
||||
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
|
||||
getInfo();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-card shadow="never">
|
||||
<approvalButton
|
||||
@submitForm="submitForm"
|
||||
@approvalVerifyOpen="approvalVerifyOpen"
|
||||
@handleApprovalRecord="handleApprovalRecord"
|
||||
:buttonLoading="buttonLoading"
|
||||
:id="form.id"
|
||||
:status="form.status"
|
||||
:pageType="routeParams.type"
|
||||
/>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="height: 78vh; overflow-y: auto">
|
||||
<el-form ref="leaveFormRef" v-loading="loading" :disabled="routeParams.type === 'view'" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="请假类型" prop="leaveType">
|
||||
<el-select v-model="form.leaveType" placeholder="请选择请假类型" style="width: 100%">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假时间" required>
|
||||
<el-date-picker
|
||||
v-model="leaveTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="To"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
@change="changeLeaveTime()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假天数" prop="leaveDays">
|
||||
<el-input v-model="form.leaveDays" disabled type="number" placeholder="请输入请假天数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入请假原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<!-- 提交组件 -->
|
||||
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
|
||||
<!-- 审批记录 -->
|
||||
<approvalRecord ref="approvalRecordRef" />
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" :before-close="handleClose" width="500">
|
||||
<el-select v-model="flowCode" placeholder="Select" style="width: 240px">
|
||||
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitFlow()"> 确认 </el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Leave" lang="ts">
|
||||
import { addLeave, getLeave, updateLeave } from '@/api/workflow/leave';
|
||||
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
|
||||
import { startWorkFlow } from '@/api/workflow/task';
|
||||
import SubmitVerify from '@/components/Process/submitVerify.vue';
|
||||
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
|
||||
import ApprovalButton from '@/components/Process/approvalButton.vue';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const leaveTime = ref<Array<string>>([]);
|
||||
//路由参数
|
||||
const routeParams = ref<Record<string, any>>({});
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '事假'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '调休'
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '病假'
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '婚假'
|
||||
}
|
||||
];
|
||||
const flowCodeOptions = [
|
||||
{
|
||||
value: 'hzdyjs-qj',
|
||||
label: '学生请假流程-杭州第一技师'
|
||||
},
|
||||
{
|
||||
value: 'leave1',
|
||||
label: '请假申请-普通'
|
||||
},
|
||||
{
|
||||
value: 'leave2',
|
||||
label: '请假申请-排他网关'
|
||||
},
|
||||
{
|
||||
value: 'leave3',
|
||||
label: '请假申请-并行网关'
|
||||
},
|
||||
{
|
||||
value: 'leave4',
|
||||
label: '请假申请-会签'
|
||||
},
|
||||
{
|
||||
value: 'leave5',
|
||||
label: '请假申请-并行会签网关'
|
||||
},
|
||||
{
|
||||
value: 'leave6',
|
||||
label: '请假申请-排他并行会签'
|
||||
}
|
||||
];
|
||||
|
||||
const flowCode = ref<string>('');
|
||||
|
||||
const dialogVisible = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '流程定义'
|
||||
});
|
||||
//提交组件
|
||||
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
|
||||
//审批记录组件
|
||||
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
|
||||
//按钮组件
|
||||
const approvalButtonRef = ref<InstanceType<typeof ApprovalButton>>();
|
||||
|
||||
const leaveFormRef = ref<ElFormInstance>();
|
||||
|
||||
const submitFormData = ref<StartProcessBo>({
|
||||
businessId: '',
|
||||
flowCode: '',
|
||||
variables: {}
|
||||
});
|
||||
const taskVariables = ref<Record<string, any>>({});
|
||||
|
||||
const initFormData: LeaveForm = {
|
||||
id: undefined,
|
||||
leaveType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
leaveDays: undefined,
|
||||
remark: undefined,
|
||||
status: undefined
|
||||
};
|
||||
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startLeaveDays: undefined,
|
||||
endLeaveDays: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
|
||||
leaveTime: [{ required: true, message: '请假时间不能为空', trigger: 'blur' }],
|
||||
leaveDays: [{ required: true, message: '请假天数不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
dialogVisible.visible = false;
|
||||
flowCode.value = '';
|
||||
buttonLoading.value = false;
|
||||
};
|
||||
const { form, rules } = toRefs(data);
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
leaveTime.value = [];
|
||||
leaveFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const changeLeaveTime = () => {
|
||||
const startDate = new Date(leaveTime.value[0]).getTime();
|
||||
const endDate = new Date(leaveTime.value[1]).getTime();
|
||||
const diffInMilliseconds = endDate - startDate;
|
||||
form.value.leaveDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1;
|
||||
};
|
||||
/** 获取详情 */
|
||||
const getInfo = () => {
|
||||
loading.value = true;
|
||||
buttonLoading.value = false;
|
||||
nextTick(async () => {
|
||||
const res = await getLeave(routeParams.value.id);
|
||||
Object.assign(form.value, res.data);
|
||||
console.log('res.data:', res.data);
|
||||
console.log('form.value:', form.value);
|
||||
// 为了确保 Element Plus 日期区间组件能够正确渲染,需要直接替换绑定数组引用
|
||||
// 同时将后端返回的日期统一格式化为组件要求的 "YYYY-MM-DD HH:mm:ss"
|
||||
// 后端仅返回 "YYYY-MM-DD",需要为日期补齐时间,符合 value-format="YYYY-MM-DD HH:mm:ss"
|
||||
const ensureDateTime = (dateStr: string | undefined, endFlag = false) => {
|
||||
if (!dateStr) return '';
|
||||
// 如果已包含时间段,直接返回
|
||||
if (/\d{2}:\d{2}:\d{2}$/.test(dateStr)) {
|
||||
return dateStr;
|
||||
}
|
||||
return endFlag ? `${dateStr} 23:59:59` : `${dateStr} 00:00:00`;
|
||||
};
|
||||
const start = ensureDateTime(form.value.startDate, false);
|
||||
const end = ensureDateTime(form.value.endDate, true);
|
||||
leaveTime.value = [start, end];
|
||||
loading.value = false;
|
||||
buttonLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = (status: string) => {
|
||||
if (leaveTime.value.length === 0) {
|
||||
proxy?.$modal.msgError('请假时间不能为空');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
leaveFormRef.value?.validate(async (valid: boolean) => {
|
||||
form.value.startDate = leaveTime.value[0];
|
||||
form.value.endDate = leaveTime.value[1];
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
let res: AxiosResponse<LeaveVO>;
|
||||
if (form.value.id) {
|
||||
res = await updateLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
res = await addLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
form.value = res.data;
|
||||
if (status === 'draft') {
|
||||
buttonLoading.value = false;
|
||||
proxy?.$modal.msgSuccess('暂存成功');
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
} else {
|
||||
if ((form.value.status === 'draft' && (flowCode.value === '' || flowCode.value === null)) || routeParams.value.type === 'add') {
|
||||
flowCode.value = flowCodeOptions[0].value;
|
||||
dialogVisible.visible = true;
|
||||
return;
|
||||
}
|
||||
//说明启动过先随意穿个参数
|
||||
if (flowCode.value === '' || flowCode.value === null) {
|
||||
flowCode.value = 'xx';
|
||||
}
|
||||
await handleStartWorkFlow(res.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitFlow = async () => {
|
||||
handleStartWorkFlow(form.value);
|
||||
dialogVisible.visible = false;
|
||||
};
|
||||
//提交申请
|
||||
const handleStartWorkFlow = async (data: LeaveForm) => {
|
||||
try {
|
||||
submitFormData.value.flowCode = flowCode.value;
|
||||
submitFormData.value.businessId = data.id;
|
||||
//流程变量
|
||||
taskVariables.value = {
|
||||
// hzdyjs-qj 使用的流程变量
|
||||
qjts: data.leaveDays,
|
||||
// leave2/6 使用的流程变量
|
||||
leaveDays: data.leaveDays,
|
||||
// leave4/5 使用的流程变量
|
||||
userList: ['1', '3', '4']
|
||||
};
|
||||
submitFormData.value.variables = taskVariables.value;
|
||||
const resp = await startWorkFlow(submitFormData.value);
|
||||
if (submitVerifyRef.value) {
|
||||
buttonLoading.value = false;
|
||||
submitVerifyRef.value.openDialog(resp.data.taskId);
|
||||
}
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
//审批记录
|
||||
const handleApprovalRecord = () => {
|
||||
approvalRecordRef.value.init(form.value.id);
|
||||
};
|
||||
//提交回调
|
||||
const submitCallback = async () => {
|
||||
await proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
};
|
||||
//审批
|
||||
const approvalVerifyOpen = async () => {
|
||||
submitVerifyRef.value.openDialog(routeParams.value.taskId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(async () => {
|
||||
routeParams.value = proxy.$route.query;
|
||||
reset();
|
||||
loading.value = false;
|
||||
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
|
||||
getInfo();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,903 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div 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="studentNo">
|
||||
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="预约日期" prop="dateRange">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<el-form-item label="院系" prop="collegeName">
|
||||
<el-select v-model="queryParams.collegeName" placeholder="请选择院系" clearable style="width: 100%" @change="handleCollegeChange">
|
||||
<el-option
|
||||
v-for="college in collegeOptions"
|
||||
:key="college.name"
|
||||
:label="college.name"
|
||||
:value="college.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="专业" prop="majorName">
|
||||
<el-select v-model="queryParams.majorName" placeholder="请选择专业" clearable style="width: 100%" @change="handleMajorChange">
|
||||
<el-option
|
||||
v-for="major in majorOptions"
|
||||
:key="major.name"
|
||||
:label="major.name"
|
||||
:value="major.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="班级" prop="className">
|
||||
<el-select v-model="queryParams.className" placeholder="请选择班级" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="classItem in classOptions"
|
||||
:key="classItem.name"
|
||||
:label="classItem.name"
|
||||
:value="classItem.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
|
||||
<el-option v-for="dict in appointment_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</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="button-card table-card">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button icon="Download" @click="handleExport" v-hasPermi="['xljk:consultationappointment: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="appointmentList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||||
<el-table-column label="学生姓名" align="center" prop="studentName" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="学号" align="center" prop="studentNo" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="院系" align="center" prop="collegeName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="专业" align="center" prop="majorName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="班级" align="center" prop="className" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="咨询原因" align="center" prop="reason" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="咨询师" align="center" prop="counselorName" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="预约日期" align="center" prop="appointmentDate" min-width="100">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.appointmentDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时段" align="center" prop="appointmentDetailTime" width="120" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-tag v-if="scope.row.status === 0" type="primary">待咨询</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" type="success">已到访</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 2" type="danger">已取消</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 3" type="warning">未到访</el-tag>
|
||||
<el-tag v-else type="info">未知状态</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="查看" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
icon="View"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handleView(scope.row)"
|
||||
v-hasPermi="['xljk:consultationappointment:query']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 待咨询状态可记录咨询结果 -->
|
||||
<el-tooltip content="记录结果" placement="top" v-if="scope.row.status === 0">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handleRecord(scope.row)"
|
||||
v-hasPermi="['xljk:consultationappointment:edit']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 查看预约详情对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="700px" append-to-body>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="学生姓名">{{ form.studentName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学号">{{ form.studentNo }}</el-descriptions-item>
|
||||
<el-descriptions-item label="院系">{{ form.collegeName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="专业">{{ form.majorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="班级">{{ form.className }}</el-descriptions-item>
|
||||
<el-descriptions-item label="咨询师">{{ form.counselorName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约日期">{{ parseTime(form.appointmentDate, '{y}-{m}-{d}') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时段">{{ form.appointmentDetailTime }}</el-descriptions-item>
|
||||
<el-descriptions-item label="咨询原因" :span="2">{{ form.reason }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag v-if="form.status === 0" type="primary">待咨询</el-tag>
|
||||
<el-tag v-else-if="form.status === 1" type="success">已到访</el-tag>
|
||||
<el-tag v-else-if="form.status === 2" type="danger">已取消</el-tag>
|
||||
<el-tag v-else-if="form.status === 3" type="warning">未到访</el-tag>
|
||||
<el-tag v-else type="info">未知状态({{ form.status }})</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ parseTime(form.createTime) }}</el-descriptions-item>
|
||||
|
||||
<!-- 只有在已到访状态时才显示咨询结果相关字段 -->
|
||||
<template v-if="form.status == 1">
|
||||
<el-descriptions-item label="实际到访时间">
|
||||
{{ form.actualVisitTime ? parseTime(form.actualVisitTime, '{y}-{m}-{d} {h}:{i}:{s}') : '未记录' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="沟通时长">
|
||||
{{ form.communicationDuration ? form.communicationDuration + ' 分钟' : '未记录' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="咨询内容" :span="2">
|
||||
{{ form.consultationContent || '未记录' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="咨询师建议" :span="2">
|
||||
{{ form.counselorSuggestion || '未记录' }}
|
||||
</el-descriptions-item>
|
||||
</template>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="open = false">关 闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 记录咨询结果对话框 -->
|
||||
<el-dialog title="记录咨询结果" v-model="recordDialogVisible" width="600px" append-to-body>
|
||||
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="100px">
|
||||
<el-form-item label="学生姓名">
|
||||
<el-input v-model="recordForm.studentName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约日期">
|
||||
<el-input :value="parseTime(recordForm.appointmentDate, '{y}-{m}-{d}')" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约时段">
|
||||
<el-input v-model="recordForm.appointmentDetailTime" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="咨询结果" prop="status">
|
||||
<el-radio-group v-model="recordForm.status">
|
||||
<el-radio :label="1">已到访</el-radio>
|
||||
<el-radio :label="3">未到访</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- 只有在已到访状态时才显示以下字段 -->
|
||||
<template v-if="recordForm.status === 1">
|
||||
<el-form-item label="实际到访时间" prop="actualVisitTime" class="no-wrap-label">
|
||||
<el-date-picker
|
||||
v-model="recordForm.actualVisitTime"
|
||||
type="datetime"
|
||||
placeholder="选择日期时间"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="沟通时长" prop="communicationDuration">
|
||||
<div class="flex-row align-center">
|
||||
<el-input-number v-model="recordForm.communicationDuration" :min="1" :max="180" />
|
||||
<span class="ml-2">分钟</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="咨询内容" prop="consultationContent">
|
||||
<el-input
|
||||
v-model="recordForm.consultationContent"
|
||||
type="textarea"
|
||||
placeholder="请输入咨询内容"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="咨询师建议" prop="counselorSuggestion">
|
||||
<el-input
|
||||
v-model="recordForm.counselorSuggestion"
|
||||
type="textarea"
|
||||
placeholder="请输入咨询师建议"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="recordDialogVisible = false">取 消</el-button>
|
||||
<el-button type="primary" @click="submitRecordForm" :loading="submitLoading">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { listAppointment, getAppointment, delAppointment, updateAppointment, recordConsultation, getCounselorOptions, getCollegeOptions, getMajorOptions, getClassOptions } from '@/api/xljk/consultation';
|
||||
import { XljkConsultationAppointmentVO, XljkConsultationAppointmentForm, CounselorOption, CollegeOption, MajorOption, ClassOption } from '@/api/xljk/consultation/types';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import { download } from '@/utils/request';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 使用本地字典数据,确保状态过滤条件有选项
|
||||
const appointment_status = ref([
|
||||
{ label: '待咨询', value: 0 },
|
||||
{ label: '已到访', value: 1 },
|
||||
{ label: '已取消', value: 2 },
|
||||
{ label: '未到访', value: 3 }
|
||||
]);
|
||||
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 预约记录表格数据
|
||||
const appointmentList = ref<XljkConsultationAppointmentVO[]>([]);
|
||||
// 弹出层标题
|
||||
const title = ref('');
|
||||
// 是否显示弹出层
|
||||
const open = ref(false);
|
||||
// 日期范围
|
||||
const dateRange = ref<string[]>([]);
|
||||
// 咨询师选项
|
||||
const counselorOptions = ref<CounselorOption[]>([]);
|
||||
// 院系选项
|
||||
const collegeOptions = ref<CollegeOption[]>([]);
|
||||
// 专业选项
|
||||
const majorOptions = ref<MajorOption[]>([]);
|
||||
// 班级选项
|
||||
const classOptions = ref<ClassOption[]>([]);
|
||||
// 是否显示记录对话框
|
||||
const recordDialogVisible = ref(false);
|
||||
// 提交按钮加载状态
|
||||
const submitLoading = ref(false);
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
studentName: undefined,
|
||||
studentNo: undefined,
|
||||
collegeName: undefined,
|
||||
majorName: undefined,
|
||||
className: undefined,
|
||||
status: undefined,
|
||||
params: {}
|
||||
});
|
||||
|
||||
// 表单参数
|
||||
const form = reactive<XljkConsultationAppointmentVO>({} as XljkConsultationAppointmentVO);
|
||||
|
||||
// 记录表单参数
|
||||
const recordForm = reactive<XljkConsultationAppointmentForm>({
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
counselorId: undefined,
|
||||
scheduleId: undefined,
|
||||
appointmentDate: '',
|
||||
appointmentTime: '',
|
||||
reason: '',
|
||||
status: 1, // 默认已到访,使用数值类型
|
||||
actualVisitTime: undefined,
|
||||
communicationDuration: 30, // 默认30分钟
|
||||
consultationContent: '',
|
||||
counselorSuggestion: ''
|
||||
});
|
||||
|
||||
// 记录表单校验
|
||||
const recordRules = ref({
|
||||
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
|
||||
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
|
||||
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
|
||||
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
|
||||
});
|
||||
|
||||
// 监听状态变化,动态调整表单验证规则
|
||||
watch(() => recordForm.status, (newStatus) => {
|
||||
if (newStatus === 3) { // 未到访
|
||||
recordRules.value = {
|
||||
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }]
|
||||
};
|
||||
} else { // 已到访
|
||||
recordRules.value = {
|
||||
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
|
||||
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
|
||||
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
|
||||
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const queryFormRef = ref();
|
||||
const recordFormRef = ref();
|
||||
|
||||
/** 查询预约记录列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 处理日期范围
|
||||
queryParams.params = {};
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
queryParams.params.beginAppointmentDate = dateRange.value[0];
|
||||
queryParams.params.endAppointmentDate = dateRange.value[1];
|
||||
}
|
||||
|
||||
const res = await listAppointment(queryParams);
|
||||
appointmentList.value = res.rows || [];
|
||||
total.value = res.total || 0;
|
||||
|
||||
// 添加日志输出
|
||||
console.log('appointment_status字典数据:', appointment_status.value);
|
||||
console.log('appointmentList数据:', appointmentList.value);
|
||||
if (appointmentList.value.length > 0) {
|
||||
console.log('第一条记录status值:', appointmentList.value[0].status);
|
||||
console.log('第一条记录status类型:', typeof appointmentList.value[0].status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预约记录列表失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取咨询师选项 */
|
||||
const getCounselors = async () => {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
counselorOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取咨询师选项失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取院系选项 */
|
||||
const getColleges = async () => {
|
||||
try {
|
||||
const res = await getCollegeOptions();
|
||||
collegeOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取院系选项失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取专业选项 */
|
||||
const getMajors = async () => {
|
||||
try {
|
||||
const res = await getMajorOptions();
|
||||
majorOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取专业选项失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取班级选项 */
|
||||
const getClasses = async () => {
|
||||
try {
|
||||
const res = await getClassOptions();
|
||||
classOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取班级选项失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = [];
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: XljkConsultationAppointmentVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
|
||||
// 判断是否有非待咨询状态
|
||||
const hasNonPending = selection.some(item => item.status !== 0);
|
||||
multiple.value = !selection.length || hasNonPending;
|
||||
};
|
||||
|
||||
/** 查看按钮操作 */
|
||||
const handleView = async (row: XljkConsultationAppointmentVO) => {
|
||||
try {
|
||||
console.log('查看预约详情,行数据:', row);
|
||||
console.log('行数据status类型:', typeof row.status, '值:', row.status);
|
||||
|
||||
const res = await getAppointment(row.id);
|
||||
console.log('获取预约详情API响应:', res);
|
||||
|
||||
// 清空表单,确保没有残留数据
|
||||
Object.keys(form).forEach(key => {
|
||||
form[key] = undefined;
|
||||
});
|
||||
|
||||
// 检查API响应格式
|
||||
if (res.code === 200 && res.data) {
|
||||
// 将API响应数据复制到表单
|
||||
Object.assign(form, res.data);
|
||||
|
||||
// 确保状态是数值类型
|
||||
console.log('API返回的status类型:', typeof form.status, '值:', form.status);
|
||||
|
||||
if (form.status === null || form.status === undefined) {
|
||||
// 如果API返回的status为空,使用行数据的status
|
||||
form.status = row.status;
|
||||
console.log('使用行数据的status:', form.status);
|
||||
}
|
||||
|
||||
if (typeof form.status === 'string') {
|
||||
form.status = parseInt(form.status);
|
||||
console.log('转换后的status:', form.status);
|
||||
}
|
||||
|
||||
// 检查实际到访时间
|
||||
console.log('实际到访时间:', form.actualVisitTime);
|
||||
if (form.status === 1 && !form.actualVisitTime) {
|
||||
console.warn('已到访状态但实际到访时间为空,尝试使用行数据');
|
||||
if (row.actualVisitTime) {
|
||||
form.actualVisitTime = row.actualVisitTime;
|
||||
console.log('使用行数据的实际到访时间:', form.actualVisitTime);
|
||||
} else {
|
||||
console.warn('行数据也没有实际到访时间');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('复制后的表单数据:', form);
|
||||
|
||||
// 如果是已到访状态,确保咨询结果相关字段有值
|
||||
if (form.status === 1) {
|
||||
console.log('已到访状态,咨询结果相关字段:', {
|
||||
actualVisitTime: form.actualVisitTime,
|
||||
communicationDuration: form.communicationDuration,
|
||||
consultationContent: form.consultationContent,
|
||||
counselorSuggestion: form.counselorSuggestion
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.warn('API响应格式不正确或无数据:', res);
|
||||
// 使用行数据填充表单
|
||||
Object.assign(form, row);
|
||||
|
||||
// 确保状态是数值类型
|
||||
console.log('使用行数据,status类型:', typeof form.status, '值:', form.status);
|
||||
|
||||
if (typeof form.status === 'string') {
|
||||
form.status = parseInt(form.status);
|
||||
console.log('转换后的status:', form.status);
|
||||
}
|
||||
|
||||
console.log('使用行数据填充表单:', form);
|
||||
}
|
||||
|
||||
open.value = true;
|
||||
title.value = '查看预约详情';
|
||||
} catch (error) {
|
||||
console.error('获取预约详情失败', error);
|
||||
ElMessage.error('获取预约详情失败');
|
||||
|
||||
// 出错时也使用行数据填充
|
||||
Object.assign(form, row);
|
||||
|
||||
// 确保状态是数值类型
|
||||
if (typeof form.status === 'string') {
|
||||
form.status = parseInt(form.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 记录咨询结果操作 */
|
||||
const handleRecord = async (row: XljkConsultationAppointmentVO) => {
|
||||
try {
|
||||
console.log('记录咨询结果,行数据:', row);
|
||||
const res = await getAppointment(row.id);
|
||||
console.log('获取预约详情API响应:', res);
|
||||
|
||||
// 重置记录表单
|
||||
const defaultValues = {
|
||||
id: undefined,
|
||||
studentId: undefined,
|
||||
counselorId: undefined,
|
||||
scheduleId: undefined,
|
||||
appointmentDate: '',
|
||||
appointmentDetailTime: '',
|
||||
reason: '',
|
||||
status: 1, // 默认已完成
|
||||
actualVisitTime: new Date(),
|
||||
communicationDuration: 30,
|
||||
consultationContent: '',
|
||||
counselorSuggestion: ''
|
||||
};
|
||||
|
||||
Object.assign(recordForm, defaultValues);
|
||||
|
||||
// 检查API响应格式
|
||||
if (res.code === 200 && res.data) {
|
||||
// 填充API返回的数据
|
||||
recordForm.id = res.data.id;
|
||||
recordForm.studentId = res.data.studentId;
|
||||
recordForm.counselorId = res.data.counselorId;
|
||||
recordForm.scheduleId = res.data.scheduleId;
|
||||
recordForm.appointmentDate = res.data.appointmentDate;
|
||||
recordForm.appointmentDetailTime = res.data.appointmentDetailTime;
|
||||
recordForm.reason = res.data.reason;
|
||||
recordForm.studentName = res.data.studentName;
|
||||
} else {
|
||||
console.warn('API响应格式不正确或无数据:', res);
|
||||
// 使用行数据填充
|
||||
recordForm.id = row.id;
|
||||
recordForm.studentId = row.studentId;
|
||||
recordForm.counselorId = row.counselorId;
|
||||
recordForm.scheduleId = row.scheduleId;
|
||||
recordForm.appointmentDate = row.appointmentDate;
|
||||
recordForm.appointmentDetailTime = row.appointmentDetailTime;
|
||||
recordForm.reason = row.reason;
|
||||
recordForm.studentName = row.studentName;
|
||||
}
|
||||
|
||||
console.log('填充后的记录表单:', recordForm);
|
||||
recordDialogVisible.value = true;
|
||||
} catch (error) {
|
||||
console.error('获取预约详情失败', error);
|
||||
ElMessage.error('获取预约详情失败');
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交记录表单 */
|
||||
const submitRecordForm = async () => {
|
||||
if (!recordFormRef.value) return;
|
||||
|
||||
await recordFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: any = {
|
||||
id: recordForm.id,
|
||||
status: recordForm.status
|
||||
};
|
||||
|
||||
// 如果是已到访状态,添加其他字段
|
||||
if (recordForm.status === 1) {
|
||||
// 确保日期格式正确
|
||||
if (recordForm.actualVisitTime) {
|
||||
// 如果是Date对象,转换为ISO字符串格式
|
||||
if (recordForm.actualVisitTime instanceof Date) {
|
||||
const year = recordForm.actualVisitTime.getFullYear();
|
||||
const month = String(recordForm.actualVisitTime.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(recordForm.actualVisitTime.getDate()).padStart(2, '0');
|
||||
const hours = String(recordForm.actualVisitTime.getHours()).padStart(2, '0');
|
||||
const minutes = String(recordForm.actualVisitTime.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(recordForm.actualVisitTime.getSeconds()).padStart(2, '0');
|
||||
|
||||
submitData.actualVisitTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
} else {
|
||||
submitData.actualVisitTime = recordForm.actualVisitTime;
|
||||
}
|
||||
console.log('提交的实际到访时间:', submitData.actualVisitTime);
|
||||
}
|
||||
|
||||
submitData.communicationDuration = recordForm.communicationDuration;
|
||||
submitData.consultationContent = recordForm.consultationContent;
|
||||
submitData.counselorSuggestion = recordForm.counselorSuggestion;
|
||||
} else {
|
||||
// 未到访状态,设置默认值
|
||||
submitData.actualVisitTime = null;
|
||||
submitData.communicationDuration = 0;
|
||||
submitData.consultationContent = '';
|
||||
submitData.counselorSuggestion = '';
|
||||
}
|
||||
|
||||
await recordConsultation(recordForm.id as number, submitData);
|
||||
ElMessage.success('记录成功');
|
||||
recordDialogVisible.value = false;
|
||||
getList();
|
||||
} catch (error) {
|
||||
console.error('提交记录失败', error);
|
||||
ElMessage.error('提交记录失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: XljkConsultationAppointmentVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
|
||||
// 检查是否有非待咨询状态的记录
|
||||
if (!row) {
|
||||
const selectedRecords = appointmentList.value.filter(item => ids.value.includes(item.id));
|
||||
if (selectedRecords.some(item => item.status !== 0)) {
|
||||
ElMessage.error('只能删除待咨询状态的预约记录');
|
||||
return;
|
||||
}
|
||||
} else if (row.status !== 0) {
|
||||
ElMessage.error('只能删除待咨询状态的预约记录');
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmMessage = row
|
||||
? `是否确认删除该预约记录?`
|
||||
: `是否确认删除选中的${ids.value.length}条数据?`;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmMessage, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
await delAppointment(_ids);
|
||||
ElMessage.success('删除成功');
|
||||
getList();
|
||||
} catch (error) {
|
||||
console.error('删除失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
// 构建查询参数,处理日期范围和params对象
|
||||
const exportParams = { ...queryParams };
|
||||
delete exportParams.params; // 移除params对象
|
||||
|
||||
// 将日期范围添加到参数中
|
||||
if (dateRange.value && dateRange.value.length === 2) {
|
||||
exportParams.beginAppointmentDate = dateRange.value[0];
|
||||
exportParams.endAppointmentDate = dateRange.value[1];
|
||||
}
|
||||
|
||||
// 移除undefined值
|
||||
Object.keys(exportParams).forEach(key => {
|
||||
if (exportParams[key] === undefined) {
|
||||
delete exportParams[key];
|
||||
}
|
||||
});
|
||||
|
||||
// 构建URL参数
|
||||
const params = new URLSearchParams();
|
||||
Object.keys(exportParams).forEach(key => {
|
||||
params.append(key, exportParams[key]);
|
||||
});
|
||||
|
||||
// 获取token
|
||||
const token = getToken();
|
||||
|
||||
try {
|
||||
// 使用fetch API发送请求,正确设置Authorization头
|
||||
const response = await fetch(`http://localhost:8080/api/xljk/consultation/appointment/list/export?${params.toString()}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`导出失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
// 获取文件名
|
||||
const contentDisposition = response.headers.get('Content-Disposition');
|
||||
let filename = '预约记录.xlsx';
|
||||
if (contentDisposition) {
|
||||
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
|
||||
if (filenameMatch && filenameMatch[1]) {
|
||||
filename = filenameMatch[1].replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
ElMessage.error(`导出失败: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
/** 院系选择变化时更新专业选项 */
|
||||
const handleCollegeChange = async (collegeName: string) => {
|
||||
// 清空专业和班级选择
|
||||
queryParams.majorName = undefined;
|
||||
queryParams.className = undefined;
|
||||
majorOptions.value = [];
|
||||
classOptions.value = [];
|
||||
|
||||
if (collegeName) {
|
||||
// 获取该院系下的专业
|
||||
try {
|
||||
const res = await getMajorOptions(collegeName);
|
||||
majorOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取专业选项失败', error);
|
||||
}
|
||||
} else {
|
||||
// 如果清空院系,则获取所有专业
|
||||
await getMajors();
|
||||
}
|
||||
};
|
||||
|
||||
/** 专业选择变化时更新班级选项 */
|
||||
const handleMajorChange = async (majorName: string) => {
|
||||
// 清空班级选择
|
||||
queryParams.className = undefined;
|
||||
classOptions.value = [];
|
||||
|
||||
if (majorName && queryParams.collegeName) {
|
||||
// 获取专业下的班级
|
||||
try {
|
||||
const res = await getClassOptions(queryParams.collegeName, majorName);
|
||||
classOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取班级选项失败', error);
|
||||
}
|
||||
} else if (queryParams.collegeName) {
|
||||
// 如果清空专业但有院系,则获取院系下所有班级
|
||||
try {
|
||||
const res = await getClassOptions(queryParams.collegeName);
|
||||
classOptions.value = res.data || [];
|
||||
} catch (error) {
|
||||
console.error('获取班级选项失败', error);
|
||||
}
|
||||
} else {
|
||||
// 如果都为空,则获取所有班级
|
||||
await getClasses();
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
console.log('初始化appointment_status字典数据:', appointment_status.value);
|
||||
getCounselors();
|
||||
getColleges();
|
||||
getMajors();
|
||||
getClasses();
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 表格卡片布局 */
|
||||
.table-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-card__body) {
|
||||
padding: 0px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
/* 移除按钮栏底部边框 */
|
||||
.button-card :deep(.el-card__header) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* 分页容器样式 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px 0 0 !important;
|
||||
background-color: #fff;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
/* 搜索按钮容器样式 */
|
||||
.search-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.no-wrap-label :deep(.el-form-item__label) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,673 @@
|
||||
<template>
|
||||
<div class="week-view-container">
|
||||
<!-- 日期导航和视图切换 -->
|
||||
<div class="calendar-header">
|
||||
<div class="nav-buttons">
|
||||
<el-button type="primary" plain @click="prevPeriod">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
</el-button>
|
||||
<span class="period-title">{{ periodTitle }}</span>
|
||||
<el-button type="primary" plain @click="nextPeriod">
|
||||
<el-icon><ArrowRight /></el-icon>
|
||||
</el-button>
|
||||
<el-button type="primary" plain @click="goToday">今天</el-button>
|
||||
</div>
|
||||
<div class="view-switch">
|
||||
<el-radio-group v-model="currentView" size="small" @change="handleViewChange">
|
||||
<el-radio-button label="month">月视图</el-radio-button>
|
||||
<el-radio-button label="week">周视图</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 周视图 -->
|
||||
<div v-if="currentView === 'week'" class="week-grid">
|
||||
<!-- 表头:显示日期和星期 -->
|
||||
<div class="week-header">
|
||||
<div class="time-label-cell">
|
||||
<span>时段</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
:class="{ 'today': day.isToday }"
|
||||
>
|
||||
<div class="day-number">{{ day.dayNumber }}</div>
|
||||
<div class="day-name">{{ day.dayName }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格内容:显示全天和时间段 -->
|
||||
<div class="week-body">
|
||||
<div class="time-row">
|
||||
<div class="time-label-cell">
|
||||
<span>全天</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
>
|
||||
<!-- 这里可以放全天事件,目前为空 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 时间段行 -->
|
||||
<div class="time-slots-container">
|
||||
<!-- 上午时段 -->
|
||||
<div class="time-row">
|
||||
<div class="time-label-cell">
|
||||
<span>上午</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
>
|
||||
<div
|
||||
v-if="getMorningSlot(day.date)"
|
||||
class="time-slot-card morning"
|
||||
:class="{
|
||||
'fully-booked': getMorningSlot(day.date).allBooked || getMorningSlot(day.date).isExpired,
|
||||
}"
|
||||
@click="handleSlotClick(getMorningSlot(day.date))"
|
||||
>
|
||||
{{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下午时段 -->
|
||||
<div class="time-row">
|
||||
<div class="time-label-cell">
|
||||
<span>下午</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="day in weekDays"
|
||||
:key="day.date"
|
||||
class="day-cell"
|
||||
>
|
||||
<div
|
||||
v-if="getAfternoonSlot(day.date)"
|
||||
class="time-slot-card afternoon"
|
||||
:class="{
|
||||
'fully-booked': getAfternoonSlot(day.date).allBooked || getAfternoonSlot(day.date).isExpired,
|
||||
}"
|
||||
@click="handleSlotClick(getAfternoonSlot(day.date))"
|
||||
>
|
||||
{{ getAfternoonSlot(day.date).startTime }}-{{ getAfternoonSlot(day.date).endTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 月视图 -->
|
||||
<div v-else class="month-grid">
|
||||
<!-- 表头:星期几 -->
|
||||
<div class="month-header">
|
||||
<div v-for="day in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="day" class="month-header-cell">
|
||||
{{ day }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日期单元格 -->
|
||||
<div class="month-body">
|
||||
<div v-for="week in monthDays" :key="week[0].date" class="month-row">
|
||||
<div
|
||||
v-for="day in week"
|
||||
:key="day.date"
|
||||
class="month-cell"
|
||||
:class="{
|
||||
'today': day.isToday,
|
||||
'other-month': day.isOtherMonth
|
||||
}"
|
||||
@click="day.isOtherMonth ? null : handleDateClick(day.date)"
|
||||
>
|
||||
<div class="month-day-number">{{ day.dayNumber }}</div>
|
||||
|
||||
<!-- 上午时段 -->
|
||||
<div
|
||||
v-if="getMorningSlot(day.date)"
|
||||
class="month-slot morning"
|
||||
:class="{ 'fully-booked': getMorningSlot(day.date).allBooked || getMorningSlot(day.date).isExpired }"
|
||||
@click.stop="handleSlotClick(getMorningSlot(day.date))"
|
||||
>
|
||||
上午 {{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
|
||||
</div>
|
||||
|
||||
<!-- 下午时段 -->
|
||||
<div
|
||||
v-if="getAfternoonSlot(day.date)"
|
||||
class="month-slot afternoon"
|
||||
:class="{ 'fully-booked': getAfternoonSlot(day.date).allBooked || getAfternoonSlot(day.date).isExpired }"
|
||||
@click.stop="handleSlotClick(getAfternoonSlot(day.date))"
|
||||
>
|
||||
下午 {{ getAfternoonSlot(day.date).startTime }}-{{ getAfternoonSlot(day.date).endTime }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { AvailableTimeSlot } from '@/api/xljk/consultation/types';
|
||||
|
||||
// 定义组件的Props
|
||||
const props = defineProps({
|
||||
availableSlots: {
|
||||
type: Array as () => AvailableTimeSlot[],
|
||||
default: () => []
|
||||
},
|
||||
defaultView: {
|
||||
type: String,
|
||||
default: 'week',
|
||||
validator: (value: string) => ['week', 'month'].includes(value)
|
||||
}
|
||||
});
|
||||
|
||||
// 定义组件的事件
|
||||
const emit = defineEmits(['dateRangeChange', 'slotClick', 'viewChange']);
|
||||
|
||||
// 当前日期和周期的开始日期
|
||||
const today = ref(new Date());
|
||||
const periodStart = ref(new Date());
|
||||
const currentView = ref(props.defaultView);
|
||||
|
||||
// 初始化周期的开始日期
|
||||
onMounted(() => {
|
||||
goToday();
|
||||
});
|
||||
|
||||
// 设置当前日期为今天
|
||||
function goToday() {
|
||||
const currentDate = new Date();
|
||||
today.value = currentDate;
|
||||
|
||||
if (currentView.value === 'week') {
|
||||
// 计算本周的周一
|
||||
const day = currentDate.getDay() || 7; // 周日是0,转换为7
|
||||
const diff = day - 1; // 计算与周一的天数差
|
||||
periodStart.value = new Date(currentDate.getTime() - diff * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
// 计算本月的1号
|
||||
periodStart.value = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
||||
}
|
||||
|
||||
// 触发日期范围变化事件
|
||||
emitDateRangeChange();
|
||||
}
|
||||
|
||||
// 上一个周期(周或月)
|
||||
function prevPeriod() {
|
||||
if (currentView.value === 'week') {
|
||||
// 上一周
|
||||
periodStart.value = new Date(periodStart.value.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
// 上一月
|
||||
periodStart.value = new Date(periodStart.value.getFullYear(), periodStart.value.getMonth() - 1, 1);
|
||||
}
|
||||
emitDateRangeChange();
|
||||
}
|
||||
|
||||
// 下一个周期(周或月)
|
||||
function nextPeriod() {
|
||||
if (currentView.value === 'week') {
|
||||
// 下一周
|
||||
periodStart.value = new Date(periodStart.value.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
// 下一月
|
||||
periodStart.value = new Date(periodStart.value.getFullYear(), periodStart.value.getMonth() + 1, 1);
|
||||
}
|
||||
emitDateRangeChange();
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
function handleViewChange(view: string) {
|
||||
// 重新计算周期开始日期
|
||||
if (view === 'week') {
|
||||
// 计算当前日期所在周的周一
|
||||
const currentDate = new Date();
|
||||
const day = currentDate.getDay() || 7;
|
||||
const diff = day - 1;
|
||||
periodStart.value = new Date(currentDate.getTime() - diff * 24 * 60 * 60 * 1000);
|
||||
} else {
|
||||
// 计算当前月的1号
|
||||
periodStart.value = new Date(today.value.getFullYear(), today.value.getMonth(), 1);
|
||||
}
|
||||
|
||||
// 触发视图变化事件
|
||||
emit('viewChange', view);
|
||||
|
||||
// 触发日期范围变化事件
|
||||
emitDateRangeChange();
|
||||
}
|
||||
|
||||
// 处理月视图中的日期点击
|
||||
function handleDateClick(date: string) {
|
||||
// 可以添加日期点击的逻辑,比如跳转到该日期的详细视图
|
||||
console.log('点击日期:', date);
|
||||
}
|
||||
|
||||
// 触发日期范围变化事件
|
||||
function emitDateRangeChange() {
|
||||
let startDate, endDate;
|
||||
|
||||
if (currentView.value === 'week') {
|
||||
// 周视图:周一到周日
|
||||
startDate = formatDate(periodStart.value);
|
||||
endDate = formatDate(new Date(periodStart.value.getTime() + 6 * 24 * 60 * 60 * 1000));
|
||||
} else {
|
||||
// 月视图:需要包含日历中显示的所有日期,包括上月和下月的日期
|
||||
// 计算本月第一天
|
||||
const year = periodStart.value.getFullYear();
|
||||
const month = periodStart.value.getMonth();
|
||||
|
||||
// 本月第一天
|
||||
const firstDay = new Date(year, month, 1);
|
||||
// 本月第一天是星期几(0-6,0是星期日)
|
||||
let firstDayOfWeek = firstDay.getDay() || 7; // 转换为1-7,1是星期一
|
||||
|
||||
// 计算本月第一周的第一天(可能是上个月的日期)
|
||||
const firstDayOfCalendar = new Date(firstDay);
|
||||
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
|
||||
|
||||
// 计算本月最后一天
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
// 本月最后一天是星期几(0-6,0是星期日)
|
||||
let lastDayOfWeek = lastDay.getDay() || 7; // 转换为1-7,1是星期一
|
||||
|
||||
// 计算本月最后一周的最后一天(可能是下个月的日期)
|
||||
const lastDayOfCalendar = new Date(lastDay);
|
||||
lastDayOfCalendar.setDate(lastDayOfCalendar.getDate() + (7 - lastDayOfWeek));
|
||||
|
||||
// 确保我们覆盖整个日历视图中显示的所有日期,包括6行的完整数据
|
||||
// 这意味着我们至少需要包含42天的数据(6行 x 7天)
|
||||
const minEndDate = new Date(firstDayOfCalendar);
|
||||
minEndDate.setDate(minEndDate.getDate() + 41); // 41 = 6*7-1
|
||||
|
||||
// 使用日历的第一天和至少覆盖6周的结束日期(或最后一周的最后一天,取较大者)
|
||||
startDate = formatDate(firstDayOfCalendar);
|
||||
|
||||
// 确保结束日期至少覆盖6行
|
||||
if (lastDayOfCalendar < minEndDate) {
|
||||
endDate = formatDate(minEndDate);
|
||||
} else {
|
||||
endDate = formatDate(lastDayOfCalendar);
|
||||
}
|
||||
|
||||
console.log(`日历日期范围:${startDate} 到 ${endDate},确保覆盖所有6行`);
|
||||
}
|
||||
|
||||
emit('dateRangeChange', { startDate, endDate });
|
||||
}
|
||||
|
||||
// 处理时段点击
|
||||
function handleSlotClick(slot: AvailableTimeSlot | undefined) {
|
||||
if (!slot) return;
|
||||
|
||||
// 如果该时段已过期,则不允许预约
|
||||
if (slot.isExpired) {
|
||||
ElMessage.warning('该时段已过期,请选择其他时段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果该时段全部约满,则不允许预约
|
||||
if (slot.allBooked) {
|
||||
ElMessage.warning('该时段已全部约满,请选择其他时段');
|
||||
return;
|
||||
}
|
||||
|
||||
emit('slotClick', slot);
|
||||
}
|
||||
|
||||
// 计算周期标题
|
||||
const periodTitle = computed(() => {
|
||||
if (currentView.value === 'week') {
|
||||
// 周视图:显示 "2025-07-29 至 2025-08-04"
|
||||
const start = formatDate(periodStart.value);
|
||||
const end = formatDate(new Date(periodStart.value.getTime() + 6 * 24 * 60 * 60 * 1000));
|
||||
return `${start} 至 ${end}`;
|
||||
} else {
|
||||
// 月视图:显示 "2025年7月"
|
||||
const year = periodStart.value.getFullYear();
|
||||
const month = periodStart.value.getMonth() + 1;
|
||||
return `${year}年${month}月`;
|
||||
}
|
||||
});
|
||||
|
||||
// 计算周内的日期
|
||||
const weekDays = computed(() => {
|
||||
const days = [];
|
||||
const dayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const date = new Date(periodStart.value.getTime() + i * 24 * 60 * 60 * 1000);
|
||||
const formattedDate = formatDate(date);
|
||||
const isToday = isSameDay(date, today.value);
|
||||
|
||||
days.push({
|
||||
date: formattedDate,
|
||||
dayNumber: date.getDate(), // 日期数字
|
||||
dayName: dayNames[i], // 星期几
|
||||
isToday
|
||||
});
|
||||
}
|
||||
|
||||
return days;
|
||||
});
|
||||
|
||||
// 计算月视图的日期
|
||||
const monthDays = computed(() => {
|
||||
const weeks = [];
|
||||
const year = periodStart.value.getFullYear();
|
||||
const month = periodStart.value.getMonth();
|
||||
|
||||
// 本月第一天
|
||||
const firstDay = new Date(year, month, 1);
|
||||
// 本月第一天是星期几(0-6,0是星期日)
|
||||
let firstDayOfWeek = firstDay.getDay() || 7; // 转换为1-7,1是星期一
|
||||
|
||||
// 计算本月第一周的第一天(可能是上个月的日期)
|
||||
const firstDayOfCalendar = new Date(firstDay);
|
||||
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
|
||||
|
||||
// 生成6周的日期(确保能显示完整月份)
|
||||
for (let week = 0; week < 6; week++) {
|
||||
const weekDays = [];
|
||||
|
||||
for (let day = 0; day < 7; day++) {
|
||||
const date = new Date(firstDayOfCalendar);
|
||||
date.setDate(date.getDate() + week * 7 + day);
|
||||
|
||||
const formattedDate = formatDate(date);
|
||||
const isToday = isSameDay(date, today.value);
|
||||
const isOtherMonth = date.getMonth() !== month;
|
||||
|
||||
// 调试信息,查看是否有对应的时段数据
|
||||
const morningSlot = getMorningSlot(formattedDate);
|
||||
const afternoonSlot = getAfternoonSlot(formattedDate);
|
||||
|
||||
// 如果是最后一行,添加额外日志
|
||||
if (week === 5) {
|
||||
console.log(`日期 ${formattedDate} (${date.toLocaleDateString()}) 数据状态:`, {
|
||||
上午时段: morningSlot ? '有数据' : '无数据',
|
||||
下午时段: afternoonSlot ? '有数据' : '无数据',
|
||||
isOtherMonth
|
||||
});
|
||||
}
|
||||
|
||||
weekDays.push({
|
||||
date: formattedDate,
|
||||
dayNumber: date.getDate(),
|
||||
isToday,
|
||||
isOtherMonth
|
||||
});
|
||||
}
|
||||
|
||||
weeks.push(weekDays);
|
||||
}
|
||||
|
||||
return weeks;
|
||||
});
|
||||
|
||||
// 判断两个日期是否是同一天
|
||||
function isSameDay(date1: Date, date2: Date) {
|
||||
return date1.getFullYear() === date2.getFullYear() &&
|
||||
date1.getMonth() === date2.getMonth() &&
|
||||
date1.getDate() === date2.getDate();
|
||||
}
|
||||
|
||||
// 格式化日期为 yyyy-MM-dd 格式
|
||||
function formatDate(date: Date) {
|
||||
return parseTime(date, '{y}-{m}-{d}');
|
||||
}
|
||||
|
||||
// 获取指定日期的上午时段
|
||||
function getMorningSlot(dateStr: string) {
|
||||
return props.availableSlots.find(slot =>
|
||||
slot.date === dateStr && slot.timeSlot === '0'
|
||||
);
|
||||
}
|
||||
|
||||
// 获取指定日期的下午时段
|
||||
function getAfternoonSlot(dateStr: string) {
|
||||
return props.availableSlots.find(slot =>
|
||||
slot.date === dateStr && slot.timeSlot === '1'
|
||||
);
|
||||
}
|
||||
|
||||
// 监听视图变化
|
||||
watch(() => props.defaultView, (newView) => {
|
||||
currentView.value = newView;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.week-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.period-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin: 0 16px;
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 周视图样式 */
|
||||
.week-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.week-header {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.time-label-cell {
|
||||
width: 80px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
border-right: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.day-cell {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border-right: 1px solid #ebeef5;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.day-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.day-name {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.today {
|
||||
background-color: #ecf5ff;
|
||||
}
|
||||
|
||||
.week-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-row {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.time-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.time-slots-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.time-slot-card {
|
||||
padding: 8px;
|
||||
margin: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.time-slot-card.morning {
|
||||
background-color: #409EFF;
|
||||
}
|
||||
|
||||
.time-slot-card.afternoon {
|
||||
background-color: #67C23A;
|
||||
}
|
||||
|
||||
.time-slot-card.fully-booked {
|
||||
background-color: #C0C0C0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* 月视图样式 */
|
||||
.month-grid {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto; /* 添加垂直滚动条 */
|
||||
max-height: calc(100vh - 180px); /* 设置最大高度,确保可以滚动 */
|
||||
}
|
||||
|
||||
.month-header {
|
||||
display: flex;
|
||||
background-color: #f5f7fa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
position: sticky; /* 表头固定 */
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.month-header-cell {
|
||||
flex: 1;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.month-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.month-row {
|
||||
display: flex;
|
||||
min-height: 120px; /* 增加行高,确保有足够空间显示预约时段 */
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.month-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.month-cell {
|
||||
flex: 1;
|
||||
min-height: 120px; /* 增加单元格高度 */
|
||||
border-right: 1px solid #ebeef5;
|
||||
padding: 5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.month-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.month-day-number {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.other-month {
|
||||
background-color: #f9f9f9;
|
||||
color: #c0c4cc;
|
||||
}
|
||||
|
||||
.month-slot {
|
||||
margin: 2px 0;
|
||||
padding: 4px;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.month-slot.morning {
|
||||
background-color: #409EFF;
|
||||
}
|
||||
|
||||
.month-slot.afternoon {
|
||||
background-color: #67C23A;
|
||||
}
|
||||
|
||||
.month-slot.fully-booked {
|
||||
background-color: #C0C0C0;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,48 @@
|
||||
// 读取原始文件
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, 'index.vue');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 添加导入语句
|
||||
content = content.replace(
|
||||
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';",
|
||||
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';\nimport { checkLimitation } from '@/api/xljk/consultation/noShow';"
|
||||
);
|
||||
|
||||
// 修改submitAppointmentForm函数
|
||||
content = content.replace(
|
||||
"function submitAppointmentForm() {",
|
||||
"async function submitAppointmentForm() {"
|
||||
);
|
||||
|
||||
// 添加预约限制检查
|
||||
content = content.replace(
|
||||
"async function submitAppointmentForm() {",
|
||||
`async function submitAppointmentForm() {
|
||||
// 表单验证
|
||||
if (!form.value.studentId) {
|
||||
ElMessage.warning('请选择学生');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查学生是否被限制预约
|
||||
try {
|
||||
const res = await checkLimitation(form.value.studentId);
|
||||
if (res.data && res.data.limited) {
|
||||
ElMessageBox.alert(
|
||||
\`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 \${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。\`,
|
||||
'预约受限',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查预约限制失败', error);
|
||||
}
|
||||
`);
|
||||
|
||||
// 写回文件
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log('文件修改成功');
|
||||
@@ -0,0 +1,48 @@
|
||||
// 读取原始文件
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = path.join(__dirname, 'index.vue');
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 添加导入语句
|
||||
content = content.replace(
|
||||
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';",
|
||||
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';\nimport { checkLimitation } from '@/api/xljk/consultation/noShow';"
|
||||
);
|
||||
|
||||
// 修改submitAppointmentForm函数
|
||||
content = content.replace(
|
||||
"function submitAppointmentForm() {",
|
||||
"async function submitAppointmentForm() {"
|
||||
);
|
||||
|
||||
// 添加预约限制检查
|
||||
content = content.replace(
|
||||
"function submitAppointmentForm() {",
|
||||
`async function submitAppointmentForm() {
|
||||
// 表单验证
|
||||
if (!form.value.studentId) {
|
||||
ElMessage.warning('请选择学生');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查学生是否被限制预约
|
||||
try {
|
||||
const res = await checkLimitation(form.value.studentId);
|
||||
if (res.data && res.data.limited) {
|
||||
ElMessageBox.alert(
|
||||
\`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 \${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。\`,
|
||||
'预约受限',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查预约限制失败', error);
|
||||
}
|
||||
`);
|
||||
|
||||
// 写回文件
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log('文件修改成功');
|
||||
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<el-card shadow="never" class="debug-card">
|
||||
<template #header>
|
||||
<h3>心理咨询预约系统调试工具</h3>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form :model="form" label-width="120px">
|
||||
<el-form-item label="学号">
|
||||
<el-input v-model="form.studentNo" placeholder="请输入学号,例如:2025WRJ010"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getStudentInfo">查询学生信息</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">学生信息结果</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<pre>{{ studentInfo }}</pre>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">预约记录查询</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form :model="appointmentForm" label-width="120px">
|
||||
<el-form-item label="学生ID">
|
||||
<el-input v-model="appointmentForm.studentId" placeholder="请输入学生ID"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getAppointments">查询预约记录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">预约记录结果</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<pre>{{ appointmentList }}</pre>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">直接查询数据库</el-divider>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form :model="sqlForm" label-width="120px">
|
||||
<el-form-item label="SQL语句">
|
||||
<el-input
|
||||
v-model="sqlForm.sql"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="SELECT * FROM xljk_consultation_appointment WHERE student_id = ?"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="executeSql">执行SQL</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">SQL执行结果</el-divider>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<pre>{{ sqlResult }}</pre>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue';
|
||||
import { getStudentByNo, getMyAppointments } from '@/api/xljk/consultation';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import request from '@/utils/request';
|
||||
|
||||
const form = reactive({
|
||||
studentNo: '2025WRJ010'
|
||||
});
|
||||
|
||||
const appointmentForm = reactive({
|
||||
studentId: ''
|
||||
});
|
||||
|
||||
const sqlForm = reactive({
|
||||
sql: 'SELECT * FROM xljk_consultation_appointment WHERE student_id = ?'
|
||||
});
|
||||
|
||||
const studentInfo = ref<any>(null);
|
||||
const appointmentList = ref<any[]>([]);
|
||||
const sqlResult = ref<any>(null);
|
||||
|
||||
// 查询学生信息
|
||||
async function getStudentInfo() {
|
||||
try {
|
||||
const res = await getStudentByNo(form.studentNo);
|
||||
studentInfo.value = res.data;
|
||||
|
||||
if (res.data && res.data.id) {
|
||||
appointmentForm.studentId = res.data.id;
|
||||
ElMessage.success('查询成功,已自动填充学生ID');
|
||||
} else {
|
||||
ElMessage.warning('未找到该学号对应的学生信息');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学生信息失败', error);
|
||||
ElMessage.error('获取学生信息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 查询预约记录
|
||||
async function getAppointments() {
|
||||
if (!appointmentForm.studentId) {
|
||||
ElMessage.warning('请先输入学生ID');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getMyAppointments(appointmentForm.studentId);
|
||||
appointmentList.value = res.data || [];
|
||||
|
||||
if (appointmentList.value.length === 0) {
|
||||
ElMessage.info('未找到该学生的预约记录');
|
||||
} else {
|
||||
ElMessage.success(`查询到${appointmentList.value.length}条预约记录`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预约记录失败', error);
|
||||
ElMessage.error('获取预约记录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行SQL
|
||||
async function executeSql() {
|
||||
if (!sqlForm.sql) {
|
||||
ElMessage.warning('请输入SQL语句');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request({
|
||||
url: '/api/xljk/consultation/debug/execute-sql',
|
||||
method: 'post',
|
||||
data: { sql: sqlForm.sql }
|
||||
});
|
||||
|
||||
sqlResult.value = res.data;
|
||||
ElMessage.success('SQL执行成功');
|
||||
} catch (error) {
|
||||
console.error('SQL执行失败', error);
|
||||
ElMessage.error('SQL执行失败,可能是权限问题或SQL语法错误');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.debug-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.debug-card :deep(.el-card__body) {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f7fa;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,864 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<el-card shadow="never" class="calendar-card">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="12">
|
||||
<h3 class="calendar-title">心理咨询预约日历</h3>
|
||||
</el-col>
|
||||
<el-col :span="12" class="text-right">
|
||||
<el-button type="primary" plain @click="refreshCalendar">刷新日历</el-button>
|
||||
<el-button type="success" plain @click="viewMyAppointments">我的预约</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="calendar-container">
|
||||
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
|
||||
<WeekView
|
||||
:available-slots="availableSlots"
|
||||
:default-view="calendarView"
|
||||
@date-range-change="handleDateRangeChange"
|
||||
@slot-click="handleSlotClick"
|
||||
@view-change="handleViewChange"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 预约表单弹窗 -->
|
||||
<el-dialog
|
||||
v-model="appointmentFormVisible"
|
||||
title="预约咨询"
|
||||
width="500px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="appointmentFormRef"
|
||||
:model="appointmentForm"
|
||||
:rules="appointmentRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="咨询师" prop="counselorId">
|
||||
<el-input v-model="counselorDisplayName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约日期" prop="appointmentDate">
|
||||
<el-input v-model="formattedDate" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约时段" prop="appointmentDetailTime">
|
||||
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
|
||||
<el-option
|
||||
v-for="slot in availableDetailSlots"
|
||||
:key="slot.timeSlot"
|
||||
:label="slot.timeSlot"
|
||||
:value="slot.timeSlot"
|
||||
:disabled="slot.isBooked || slot.isExpired"
|
||||
:style="getTimeSlotStyle(slot)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="学号" prop="studentNo" v-if="!hasStudentInfo">
|
||||
<el-input v-model="studentNo" placeholder="请输入学号" @blur="handleStudentNoBlur" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="studentName">
|
||||
<el-input v-model="studentName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="咨询原因" prop="reason">
|
||||
<el-input
|
||||
v-model="appointmentForm.reason"
|
||||
type="textarea"
|
||||
placeholder="请简要描述咨询原因"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitAppointmentForm" :loading="submitLoading">确 定</el-button>
|
||||
<el-button @click="appointmentFormVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 我的预约列表弹窗 -->
|
||||
<el-dialog
|
||||
v-model="myAppointmentsVisible"
|
||||
title="我的预约记录"
|
||||
width="800px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-table :data="myAppointmentsList" border style="width: 100%">
|
||||
<el-table-column label="预约日期" prop="appointmentDate" width="120" />
|
||||
<el-table-column label="预约时段" prop="appointmentDetailTime" width="120" />
|
||||
<el-table-column label="咨询师" prop="counselorName" width="100" />
|
||||
<el-table-column label="咨询原因" prop="reason" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<!-- 添加调试信息 -->
|
||||
<div style="display: none;">
|
||||
状态值: {{ scope.row.status }}
|
||||
类型: {{ typeof scope.row.status }}
|
||||
字典数据: {{ appointment_status }}
|
||||
</div>
|
||||
|
||||
<!-- 根据预约记录管理页面统一样式 -->
|
||||
<el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="primary">待咨询</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1 || scope.row.status === '1'" type="success">已到访</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="danger">已取消</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="warning">未到访</el-tag>
|
||||
<el-tag v-else type="info">未知({{ scope.row.status }})</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<!-- 添加调试信息 -->
|
||||
<div style="display: none;">
|
||||
状态值: {{ scope.row.status }}
|
||||
类型: {{ typeof scope.row.status }}
|
||||
条件结果: {{ scope.row.status === 0 || scope.row.status === '0' }}
|
||||
</div>
|
||||
|
||||
<!-- 使用更直接的方式显示取消按钮 -->
|
||||
<template v-if="scope.row.status === 0 || scope.row.status === '0'">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="cancelAppointment(scope.row)"
|
||||
>
|
||||
取消预约
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else>--</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="refreshMyAppointments" :loading="refreshLoading">刷 新</el-button>
|
||||
<el-button @click="myAppointmentsVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { getAvailableTimeSlots, submitAppointment, getMyAppointments, updateAppointment, getStudentByNo, getCounselorOptions, getAvailableDetailSlots } from '@/api/xljk/consultation';
|
||||
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import WeekView from './WeekView.vue';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 不再使用后端字典数据
|
||||
// const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
|
||||
|
||||
// 本地定义状态字典(与预约记录管理页面保持一致)
|
||||
const appointment_status = ref([
|
||||
{ label: '待咨询', value: 0, listClass: 'primary' },
|
||||
{ label: '已到访', value: 1, listClass: 'success' },
|
||||
{ label: '已取消', value: 2, listClass: 'danger' },
|
||||
{ label: '未到访', value: 3, listClass: 'warning' }
|
||||
]);
|
||||
|
||||
// 用户信息
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => userStore.userId);
|
||||
const nickname = computed(() => userStore.nickname);
|
||||
const hasStudentInfo = computed(() => Boolean(studentId.value));
|
||||
|
||||
// 日历相关
|
||||
const availableSlots = ref<AvailableTimeSlot[]>([]);
|
||||
const calendarView = ref('month'); // 默认使用月视图
|
||||
|
||||
// 预约表单相关
|
||||
const appointmentFormVisible = ref(false);
|
||||
const appointmentFormRef = ref();
|
||||
const submitLoading = ref(false);
|
||||
const selectedSlot = ref<AvailableTimeSlot>({} as AvailableTimeSlot);
|
||||
const studentNo = ref('');
|
||||
const studentName = ref('');
|
||||
const studentId = ref<string | number | null>(null);
|
||||
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
|
||||
|
||||
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
|
||||
counselorId: undefined,
|
||||
scheduleId: undefined,
|
||||
appointmentDate: '',
|
||||
appointmentTime: '',
|
||||
appointmentDetailTime: '',
|
||||
reason: '',
|
||||
status: 0 // 默认待咨询状态,使用数值类型
|
||||
});
|
||||
|
||||
const appointmentRules = {
|
||||
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
|
||||
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
|
||||
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
|
||||
};
|
||||
|
||||
// 我的预约相关
|
||||
const myAppointmentsVisible = ref(false);
|
||||
const myAppointmentsList = ref<any[]>([]);
|
||||
const refreshLoading = ref(false);
|
||||
|
||||
// 计算属性
|
||||
const formattedDate = computed(() => {
|
||||
if (!selectedSlot.value.date) return '';
|
||||
return selectedSlot.value.date;
|
||||
});
|
||||
|
||||
const formattedTimeSlot = computed(() => {
|
||||
if (!selectedSlot.value.startTime || !selectedSlot.value.endTime) return '';
|
||||
return `${selectedSlot.value.startTime} - ${selectedSlot.value.endTime}`;
|
||||
});
|
||||
|
||||
// 计算属性:咨询师名称
|
||||
const counselorDisplayName = computed(() => {
|
||||
return selectedSlot.value?.counselorName || '正在加载...';
|
||||
});
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange({ startDate, endDate }: { startDate: string, endDate: string }) {
|
||||
console.log(`日期范围变化:${startDate} 到 ${endDate},立即加载数据`);
|
||||
|
||||
// 确保日期范围足够大,覆盖整个日历视图
|
||||
const startDateObj = new Date(startDate.replace(/-/g, '/'));
|
||||
const endDateObj = new Date(endDate.replace(/-/g, '/'));
|
||||
|
||||
// 计算日期差(天数)
|
||||
const diffTime = endDateObj.getTime() - startDateObj.getTime();
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
// 如果日期范围小于42天(6周),则扩展结束日期
|
||||
if (diffDays < 42) {
|
||||
const expandedEndDate = new Date(startDateObj);
|
||||
expandedEndDate.setDate(expandedEndDate.getDate() + 42);
|
||||
endDate = parseTime(expandedEndDate, '{y}-{m}-{d}');
|
||||
console.log(`日期范围不足42天,扩展至:${startDate} 到 ${endDate}`);
|
||||
}
|
||||
|
||||
loadAvailableSlots(startDate, endDate);
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
function handleViewChange(view: string) {
|
||||
calendarView.value = view;
|
||||
}
|
||||
|
||||
// 加载可用预约时段
|
||||
async function loadAvailableSlots(startDate: string, endDate: string) {
|
||||
try {
|
||||
const res = await getAvailableTimeSlots(startDate, endDate);
|
||||
if (res.code === 200) {
|
||||
availableSlots.value = res.data || [];
|
||||
|
||||
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
|
||||
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
|
||||
try {
|
||||
const counselorRes = await getCounselorOptions();
|
||||
if (counselorRes.code === 200 && counselorRes.data) {
|
||||
const counselors = counselorRes.data;
|
||||
// 为每个时段添加咨询师姓名
|
||||
availableSlots.value = availableSlots.value.map(slot => {
|
||||
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
return { ...slot, counselorName: counselor.name };
|
||||
}
|
||||
return slot;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取可预约时段失败', error);
|
||||
ElMessage.error('获取可预约时段失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时段点击
|
||||
async function handleSlotClick(slot: AvailableTimeSlot) {
|
||||
selectedSlot.value = slot;
|
||||
|
||||
// 如果没有咨询师姓名,尝试获取咨询师信息
|
||||
if (!slot.counselorName && slot.counselorId) {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
if (res.code === 200 && res.data) {
|
||||
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
slot.counselorName = counselor.name;
|
||||
selectedSlot.value = { ...slot };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 填充预约表单
|
||||
appointmentForm.counselorId = slot.counselorId;
|
||||
appointmentForm.scheduleId = slot.scheduleId;
|
||||
appointmentForm.appointmentDate = slot.date;
|
||||
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
|
||||
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
|
||||
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
|
||||
|
||||
// 加载详细时段
|
||||
await loadDetailTimeSlots(slot.date, slot.timeSlot);
|
||||
|
||||
// 如果已登录且有用户ID,初始化学生信息
|
||||
if (userId.value) {
|
||||
// 初始设置为当前用户ID
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
}
|
||||
|
||||
appointmentFormVisible.value = true;
|
||||
}
|
||||
|
||||
// 加载详细时段
|
||||
async function loadDetailTimeSlots(date: string, timeSlot: string) {
|
||||
try {
|
||||
const res = await getAvailableDetailSlots(date, timeSlot);
|
||||
if (res.code === 200) {
|
||||
availableDetailSlots.value = res.data || [];
|
||||
} else {
|
||||
availableDetailSlots.value = [];
|
||||
ElMessage.warning('获取可用时段失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详细时段失败', error);
|
||||
ElMessage.error('获取详细时段失败');
|
||||
availableDetailSlots.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间段样式
|
||||
function getTimeSlotStyle(slot: TimeSlotDetail) {
|
||||
if (slot.isBooked) {
|
||||
// 已预约:灰色+删除线
|
||||
return {
|
||||
color: '#999',
|
||||
textDecoration: 'line-through',
|
||||
backgroundColor: '#f5f7fa'
|
||||
};
|
||||
} else if (slot.isExpired) {
|
||||
// 已过期:灰色,无删除线
|
||||
return {
|
||||
color: '#999',
|
||||
backgroundColor: '#f5f7fa'
|
||||
};
|
||||
} else {
|
||||
// 可预约:正常样式
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string | number) {
|
||||
console.log('getStatusText 被调用,status:', status, '类型:', typeof status);
|
||||
const statusMap: Record<string, string> = {
|
||||
'0': '待咨询',
|
||||
'1': '已到访',
|
||||
'2': '已取消',
|
||||
'3': '未到访'
|
||||
};
|
||||
|
||||
// 将数值类型转为字符串
|
||||
if (typeof status === 'number') {
|
||||
status = status.toString();
|
||||
}
|
||||
|
||||
return statusMap[status] || `未知(${status})`;
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status: string | number) {
|
||||
console.log('getStatusClass 被调用,status:', status, '类型:', typeof status);
|
||||
const classMap: Record<string, string> = {
|
||||
'0': 'text-warning',
|
||||
'1': 'text-success',
|
||||
'2': 'text-info',
|
||||
'3': 'text-danger'
|
||||
};
|
||||
|
||||
// 将数值类型转为字符串
|
||||
if (typeof status === 'number') {
|
||||
status = status.toString();
|
||||
}
|
||||
|
||||
return classMap[status] || '';
|
||||
}
|
||||
|
||||
// 提交预约表单
|
||||
function submitAppointmentForm() {
|
||||
appointmentFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
|
||||
if (!studentId.value) {
|
||||
ElMessage.warning('请先输入有效的学号');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appointmentForm.appointmentDetailTime) {
|
||||
ElMessage.warning('请选择具体预约时段');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: XljkConsultationAppointmentForm = {
|
||||
...appointmentForm,
|
||||
studentId: studentId.value, // 不进行Number转换,保持原始值
|
||||
studentNo: studentNo.value // 添加学号
|
||||
};
|
||||
|
||||
console.log('提交预约的studentId:', submitData.studentId);
|
||||
|
||||
const res = await submitAppointment(submitData);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('预约成功');
|
||||
appointmentFormVisible.value = false;
|
||||
refreshCalendar(); // 刷新日历
|
||||
|
||||
// 预约成功后,自动打开"我的预约"弹窗
|
||||
setTimeout(() => {
|
||||
viewMyAppointments();
|
||||
}, 500);
|
||||
} else {
|
||||
ElMessage.error(res.msg || '预约失败,该时段可能已被预约');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交预约失败', error);
|
||||
ElMessage.error('提交预约失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 查看我的预约
|
||||
async function viewMyAppointments() {
|
||||
if (!userId.value) {
|
||||
ElMessage.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!studentId.value) {
|
||||
ElMessage.warning('请先在学号输入框中输入学号并查询学生信息,或刷新页面让系统自动获取');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('查询预约记录的studentId:', studentId.value, '类型:', typeof studentId.value);
|
||||
|
||||
// 添加一个时间戳参数,强制绕过浏览器缓存
|
||||
const timestamp = new Date().getTime();
|
||||
console.log(`获取预约记录,添加时间戳${timestamp}强制刷新数据`);
|
||||
|
||||
// 尝试多种类型转换,确保能正确查询到记录
|
||||
let id = studentId.value;
|
||||
|
||||
// 尝试使用原始studentId查询,强制刷新数据
|
||||
console.log('尝试使用原始studentId查询:', id);
|
||||
let res = await getMyAppointments(id, true); // 强制刷新数据
|
||||
console.log('查询结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
// 处理数据,确保状态字段是数值类型
|
||||
myAppointmentsList.value = res.data.map((item: any) => {
|
||||
console.log('原始记录项:', item);
|
||||
|
||||
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
|
||||
|
||||
// 如果status是undefined或null,设置默认值为0
|
||||
if (item.status === undefined || item.status === null) {
|
||||
item.status = 0; // 默认为待咨询
|
||||
console.log('状态为空,设置默认值0');
|
||||
}
|
||||
// 如果状态是字符串类型,尝试转换为数值
|
||||
else if (typeof item.status === 'string') {
|
||||
const statusNum = parseInt(item.status);
|
||||
if (!isNaN(statusNum)) {
|
||||
item.status = statusNum;
|
||||
console.log('状态从字符串转换为数值:', statusNum);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
|
||||
return item;
|
||||
});
|
||||
|
||||
console.log('预约记录数据:', myAppointmentsList.value);
|
||||
console.log('字典数据:', appointment_status.value);
|
||||
myAppointmentsVisible.value = true;
|
||||
} else {
|
||||
console.log('原始ID查询无结果,尝试转换为数字类型');
|
||||
|
||||
// 尝试转换为数字类型
|
||||
const numericId = Number(id);
|
||||
if (!isNaN(numericId)) {
|
||||
console.log('尝试使用数字类型studentId查询:', numericId);
|
||||
res = await getMyAppointments(numericId, true); // 强制刷新数据
|
||||
console.log('数字类型查询结果:', res);
|
||||
|
||||
if (res.code === 200) {
|
||||
// 处理数据,确保状态字段是数值类型
|
||||
myAppointmentsList.value = (res.data || []).map((item: any) => {
|
||||
console.log('原始记录项:', item);
|
||||
|
||||
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
|
||||
|
||||
// 如果status是undefined或null,设置默认值为0
|
||||
if (item.status === undefined || item.status === null) {
|
||||
item.status = 0; // 默认为待咨询
|
||||
console.log('状态为空,设置默认值0');
|
||||
}
|
||||
// 如果状态是字符串类型,尝试转换为数值
|
||||
else if (typeof item.status === 'string') {
|
||||
const statusNum = parseInt(item.status);
|
||||
if (!isNaN(statusNum)) {
|
||||
item.status = statusNum;
|
||||
console.log('状态从字符串转换为数值:', statusNum);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
|
||||
return item;
|
||||
});
|
||||
|
||||
console.log('预约记录数据:', myAppointmentsList.value);
|
||||
console.log('字典数据:', appointment_status.value);
|
||||
myAppointmentsVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 无论是否找到记录,都显示弹窗
|
||||
myAppointmentsVisible.value = true;
|
||||
|
||||
// 如果没有预约记录,显示提示
|
||||
if (myAppointmentsList.value.length === 0) {
|
||||
ElMessage.info('暂无预约记录');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预约记录失败', error);
|
||||
ElMessage.error('获取预约记录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消预约
|
||||
async function cancelAppointment(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该预约吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
// 记录原始行数据,用于调试
|
||||
console.log('取消预约的原始行数据:', row);
|
||||
|
||||
const updateData = {
|
||||
id: row.id,
|
||||
status: 2 // 已取消,使用数值类型
|
||||
};
|
||||
|
||||
console.log('发送取消预约请求:', updateData);
|
||||
|
||||
const res = await updateAppointment(updateData);
|
||||
console.log('取消预约响应:', res);
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('取消预约成功');
|
||||
|
||||
// 直接更新本地数据,避免重新请求
|
||||
if (myAppointmentsList.value && myAppointmentsList.value.length > 0) {
|
||||
const index = myAppointmentsList.value.findIndex(item => item.id === row.id);
|
||||
if (index !== -1) {
|
||||
myAppointmentsList.value[index].status = 2;
|
||||
console.log('本地数据已更新,新状态:', myAppointmentsList.value[index].status);
|
||||
}
|
||||
}
|
||||
|
||||
// 不再调用viewMyAppointments(),避免重新请求导致状态被覆盖
|
||||
// 只刷新日历,不刷新预约列表
|
||||
refreshCalendar();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '取消预约失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消预约失败', error);
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('取消预约失败,请稍后重试');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化咨询师信息
|
||||
async function initCounselorInfo() {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
if (res.code === 200 && res.data) {
|
||||
const counselors = res.data;
|
||||
// 存储咨询师信息,以备后用
|
||||
if (availableSlots.value.length > 0) {
|
||||
availableSlots.value = availableSlots.value.map(slot => {
|
||||
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
return { ...slot, counselorName: counselor.name };
|
||||
}
|
||||
return slot;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新"我的预约"数据
|
||||
async function refreshMyAppointments() {
|
||||
if (!studentId.value) {
|
||||
ElMessage.warning('无法获取学生ID');
|
||||
return;
|
||||
}
|
||||
|
||||
refreshLoading.value = true;
|
||||
try {
|
||||
// 强制刷新数据
|
||||
const res = await getMyAppointments(studentId.value, true);
|
||||
if (res.code === 200) {
|
||||
// 处理数据,确保状态字段是数值类型
|
||||
myAppointmentsList.value = (res.data || []).map((item: any) => {
|
||||
// 如果status是undefined或null,设置默认值为0
|
||||
if (item.status === undefined || item.status === null) {
|
||||
item.status = 0; // 默认为待咨询
|
||||
}
|
||||
// 如果状态是字符串类型,尝试转换为数值
|
||||
else if (typeof item.status === 'string') {
|
||||
const statusNum = parseInt(item.status);
|
||||
if (!isNaN(statusNum)) {
|
||||
item.status = statusNum;
|
||||
}
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
ElMessage.success('刷新成功');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '刷新失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('刷新预约记录失败', error);
|
||||
ElMessage.error('刷新预约记录失败');
|
||||
} finally {
|
||||
refreshLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日历
|
||||
function refreshCalendar() {
|
||||
// 获取当前日期
|
||||
const now = new Date();
|
||||
// 获取当前月的第一天
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// 获取当前月第一天所在周的周一(以确保完整覆盖当前月份)
|
||||
const firstDayOfWeek = firstDayOfMonth.getDay() || 7; // 转换为1-7,1是周一
|
||||
const firstDayOfCalendar = new Date(firstDayOfMonth);
|
||||
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
|
||||
|
||||
// 计算显示6周需要的结束日期(从日历第一天开始算的42天后)
|
||||
const sixWeeksLater = new Date(firstDayOfCalendar);
|
||||
sixWeeksLater.setDate(sixWeeksLater.getDate() + 42); // 显示6周
|
||||
|
||||
// 获取30天前的日期,确保能看到历史记录
|
||||
const thirtyDaysBefore = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
// 使用较早的日期作为开始日期
|
||||
const startDate = parseTime(thirtyDaysBefore < firstDayOfCalendar ? thirtyDaysBefore : firstDayOfCalendar, '{y}-{m}-{d}');
|
||||
|
||||
// 结束日期取下月底和sixWeeksLater的较大者,确保完整覆盖日历视图
|
||||
const nextMonthEnd = new Date(now.getFullYear(), now.getMonth() + 2, 0); // 下个月的最后一天
|
||||
const endDateValue = new Date(Math.max(sixWeeksLater.getTime(), nextMonthEnd.getTime(), now.getTime() + 30 * 24 * 60 * 60 * 1000));
|
||||
const endDate = parseTime(endDateValue, '{y}-{m}-{d}');
|
||||
|
||||
console.log(`刷新日历,日期范围:${startDate} 到 ${endDate},确保包含完整的6周显示`);
|
||||
loadAvailableSlots(startDate, endDate);
|
||||
}
|
||||
|
||||
// 学号输入框失去焦点时查询学生信息
|
||||
async function handleStudentNoBlur() {
|
||||
if (!studentNo.value) return;
|
||||
|
||||
try {
|
||||
console.log('查询学生信息,学号:', studentNo.value);
|
||||
const res = await getStudentByNo(studentNo.value);
|
||||
console.log('查询学生信息结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
studentId.value = res.data.id;
|
||||
studentName.value = res.data.name || '';
|
||||
console.log('获取到学生ID:', studentId.value, '类型:', typeof studentId.value);
|
||||
|
||||
// 确保studentId是数字类型
|
||||
if (typeof studentId.value === 'string') {
|
||||
studentId.value = Number(studentId.value);
|
||||
console.log('转换为数字类型后的studentId:', studentId.value);
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('未找到该学号对应的学生信息');
|
||||
studentId.value = null;
|
||||
studentName.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学生信息失败', error);
|
||||
ElMessage.error('获取学生信息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(async () => {
|
||||
// 初始化咨询师信息
|
||||
initCounselorInfo();
|
||||
|
||||
// 刷新日历,确保初次加载就包含完整的数据
|
||||
refreshCalendar();
|
||||
|
||||
// 额外请求一次下个月的数据
|
||||
const now = new Date();
|
||||
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
|
||||
const nextMonthEnd = new Date(now.getFullYear(), now.getMonth() + 2, 0);
|
||||
const startDate = parseTime(nextMonth, '{y}-{m}-{d}');
|
||||
const endDate = parseTime(nextMonthEnd, '{y}-{m}-{d}');
|
||||
console.log(`额外请求下个月数据:${startDate} 到 ${endDate}`);
|
||||
loadAvailableSlots(startDate, endDate);
|
||||
|
||||
// 自动获取学生信息(如果用户已登录且用户名为学号格式)
|
||||
if (userId.value && nickname.value) {
|
||||
console.log('当前用户信息:', { userId: userId.value, nickname: nickname.value });
|
||||
|
||||
// 尝试直接使用nickname作为学号查询
|
||||
try {
|
||||
console.log('尝试使用nickname查询学生信息:', nickname.value);
|
||||
const res = await getStudentByNo(nickname.value);
|
||||
console.log('查询学生信息结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
studentId.value = res.data.id;
|
||||
studentName.value = res.data.name || '';
|
||||
studentNo.value = res.data.studentNo || nickname.value;
|
||||
console.log('自动获取学生信息成功:', res.data);
|
||||
|
||||
// 查询是否有预约记录
|
||||
try {
|
||||
const appointmentRes = await getMyAppointments(studentId.value);
|
||||
console.log('学生预约记录:', appointmentRes.data);
|
||||
} catch (e) {
|
||||
console.error('查询预约记录失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('用户可能不是学生或学号格式不正确,尝试其他方式');
|
||||
|
||||
// 尝试特定格式处理,例如"2025WRJ010林伟"格式可能需要提取前面的学号部分
|
||||
const studentNoMatch = nickname.value.match(/^(\d{4}[A-Za-z]{3}\d{3})/);
|
||||
if (studentNoMatch && studentNoMatch[1]) {
|
||||
const extractedStudentNo = studentNoMatch[1];
|
||||
console.log('尝试提取学号:', extractedStudentNo);
|
||||
|
||||
try {
|
||||
const res2 = await getStudentByNo(extractedStudentNo);
|
||||
if (res2.code === 200 && res2.data) {
|
||||
studentId.value = res2.data.id;
|
||||
studentName.value = res2.data.name || '';
|
||||
studentNo.value = res2.data.studentNo || extractedStudentNo;
|
||||
console.log('使用提取的学号获取学生信息成功:', res2.data);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('使用提取的学号查询失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果上述方法都失败,直接使用userId作为studentId(临时解决方案)
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
console.log('使用userId作为studentId:', userId.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动获取学生信息失败,使用userId作为studentId', error);
|
||||
// 如果自动获取失败,直接使用userId作为studentId(临时解决方案)
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
console.log('使用userId作为studentId:', userId.value);
|
||||
}
|
||||
} else {
|
||||
console.log('用户未登录或没有nickname');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.calendar-card :deep(.el-card__body) {
|
||||
padding: 0px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
.calendar-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
height: calc(100vh - 240px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* 状态文本样式 */
|
||||
.text-warning {
|
||||
color: #E6A23C;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,683 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<el-card shadow="never" class="calendar-card">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="12">
|
||||
<h3 class="calendar-title">心理咨询预约日历</h3>
|
||||
</el-col>
|
||||
<el-col :span="12" class="text-right">
|
||||
<el-button type="primary" plain @click="refreshCalendar">刷新日历</el-button>
|
||||
<el-button type="success" plain @click="viewMyAppointments">我的预约</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="calendar-container">
|
||||
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
|
||||
<WeekView
|
||||
:available-slots="availableSlots"
|
||||
:default-view="calendarView"
|
||||
@date-range-change="handleDateRangeChange"
|
||||
@slot-click="handleSlotClick"
|
||||
@view-change="handleViewChange"
|
||||
/>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
|
||||
<!-- 预约表单弹窗 -->
|
||||
<el-dialog
|
||||
v-model="appointmentFormVisible"
|
||||
title="预约咨询"
|
||||
width="500px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-form
|
||||
ref="appointmentFormRef"
|
||||
:model="appointmentForm"
|
||||
:rules="appointmentRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="咨询师" prop="counselorId">
|
||||
<el-input v-model="counselorDisplayName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约日期" prop="appointmentDate">
|
||||
<el-input v-model="formattedDate" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="预约时段" prop="appointmentDetailTime">
|
||||
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
|
||||
<el-option
|
||||
v-for="slot in availableDetailSlots"
|
||||
:key="slot.timeSlot"
|
||||
:label="slot.timeSlot"
|
||||
:value="slot.timeSlot"
|
||||
:disabled="slot.isBooked || slot.isExpired"
|
||||
:style="getTimeSlotStyle(slot)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="学号" prop="studentNo" v-if="!hasStudentInfo">
|
||||
<el-input v-model="studentNo" placeholder="请输入学号" @blur="handleStudentNoBlur" />
|
||||
</el-form-item>
|
||||
<el-form-item label="姓名" prop="studentName">
|
||||
<el-input v-model="studentName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="咨询原因" prop="reason">
|
||||
<el-input
|
||||
v-model="appointmentForm.reason"
|
||||
type="textarea"
|
||||
placeholder="请简要描述咨询原因"
|
||||
:rows="4"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitAppointmentForm" :loading="submitLoading">确 定</el-button>
|
||||
<el-button @click="appointmentFormVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 我的预约列表弹窗 -->
|
||||
<el-dialog
|
||||
v-model="myAppointmentsVisible"
|
||||
title="我的预约记录"
|
||||
width="800px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
>
|
||||
<el-table :data="myAppointmentsList" border style="width: 100%">
|
||||
<el-table-column label="预约日期" prop="appointmentDate" width="120" />
|
||||
<el-table-column label="预约时段" prop="appointmentDetailTime" width="120" />
|
||||
<el-table-column label="咨询师" prop="counselorName" width="100" />
|
||||
<el-table-column label="咨询原因" prop="reason" show-overflow-tooltip />
|
||||
<el-table-column label="状态" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="appointment_status" :value="scope.row.status" />
|
||||
<!-- 备用显示方案,如果字典数据没有加载,显示原始值 -->
|
||||
<span v-if="!appointment_status || appointment_status.length === 0" :class="getStatusClass(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="cancelAppointment(scope.row)"
|
||||
v-if="scope.row.status === '0'"
|
||||
>
|
||||
取消预约
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="myAppointmentsVisible = false">关 闭</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { getAvailableTimeSlots, submitAppointment, getMyAppointments, updateAppointment, getStudentByNo, getCounselorOptions, getAvailableDetailSlots } from '@/api/xljk/consultation';
|
||||
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import WeekView from './WeekView.vue';
|
||||
import { checkLimitation } from '@/api/xljk/consultation/noShow';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
|
||||
|
||||
// 用户信息
|
||||
const userStore = useUserStore();
|
||||
const userId = computed(() => userStore.userId);
|
||||
const nickname = computed(() => userStore.nickname);
|
||||
const hasStudentInfo = computed(() => Boolean(studentId.value));
|
||||
|
||||
// 日历相关
|
||||
const availableSlots = ref<AvailableTimeSlot[]>([]);
|
||||
const calendarView = ref('month'); // 默认使用月视图
|
||||
|
||||
// 预约表单相关
|
||||
const appointmentFormVisible = ref(false);
|
||||
const appointmentFormRef = ref();
|
||||
const submitLoading = ref(false);
|
||||
const selectedSlot = ref<AvailableTimeSlot>({} as AvailableTimeSlot);
|
||||
const studentNo = ref('');
|
||||
const studentName = ref('');
|
||||
const studentId = ref<string | number | null>(null);
|
||||
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
|
||||
|
||||
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
|
||||
counselorId: undefined,
|
||||
scheduleId: undefined,
|
||||
appointmentDate: '',
|
||||
appointmentTime: '',
|
||||
appointmentDetailTime: '',
|
||||
reason: '',
|
||||
status: '0' // 默认待咨询状态
|
||||
});
|
||||
|
||||
const appointmentRules = {
|
||||
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
|
||||
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
|
||||
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
|
||||
};
|
||||
|
||||
// 我的预约相关
|
||||
const myAppointmentsVisible = ref(false);
|
||||
const myAppointmentsList = ref<any[]>([]);
|
||||
|
||||
// 计算属性
|
||||
const formattedDate = computed(() => {
|
||||
if (!selectedSlot.value.date) return '';
|
||||
return selectedSlot.value.date;
|
||||
});
|
||||
|
||||
const formattedTimeSlot = computed(() => {
|
||||
if (!selectedSlot.value.startTime || !selectedSlot.value.endTime) return '';
|
||||
return `${selectedSlot.value.startTime} - ${selectedSlot.value.endTime}`;
|
||||
});
|
||||
|
||||
// 计算属性:咨询师名称
|
||||
const counselorDisplayName = computed(() => {
|
||||
return selectedSlot.value?.counselorName || '正在加载...';
|
||||
});
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange({ startDate, endDate }: { startDate: string, endDate: string }) {
|
||||
loadAvailableSlots(startDate, endDate);
|
||||
}
|
||||
|
||||
// 处理视图切换
|
||||
function handleViewChange(view: string) {
|
||||
calendarView.value = view;
|
||||
}
|
||||
|
||||
// 加载可用预约时段
|
||||
async function loadAvailableSlots(startDate: string, endDate: string) {
|
||||
try {
|
||||
const res = await getAvailableTimeSlots(startDate, endDate);
|
||||
if (res.code === 200) {
|
||||
availableSlots.value = res.data || [];
|
||||
|
||||
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
|
||||
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
|
||||
try {
|
||||
const counselorRes = await getCounselorOptions();
|
||||
if (counselorRes.code === 200 && counselorRes.data) {
|
||||
const counselors = counselorRes.data;
|
||||
// 为每个时段添加咨询师姓名
|
||||
availableSlots.value = availableSlots.value.map(slot => {
|
||||
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
return { ...slot, counselorName: counselor.name };
|
||||
}
|
||||
return slot;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取可预约时段失败', error);
|
||||
ElMessage.error('获取可预约时段失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 处理时段点击
|
||||
async function handleSlotClick(slot: AvailableTimeSlot) {
|
||||
selectedSlot.value = slot;
|
||||
|
||||
// 如果没有咨询师姓名,尝试获取咨询师信息
|
||||
if (!slot.counselorName && slot.counselorId) {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
if (res.code === 200 && res.data) {
|
||||
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
slot.counselorName = counselor.name;
|
||||
selectedSlot.value = { ...slot };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 填充预约表单
|
||||
appointmentForm.counselorId = slot.counselorId;
|
||||
appointmentForm.scheduleId = slot.scheduleId;
|
||||
appointmentForm.appointmentDate = slot.date;
|
||||
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
|
||||
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
|
||||
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
|
||||
|
||||
// 加载详细时段
|
||||
await loadDetailTimeSlots(slot.date, slot.timeSlot);
|
||||
|
||||
// 如果已登录且有用户ID,初始化学生信息
|
||||
if (userId.value) {
|
||||
// 初始设置为当前用户ID
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
}
|
||||
|
||||
appointmentFormVisible.value = true;
|
||||
}
|
||||
|
||||
// 加载详细时段
|
||||
async function loadDetailTimeSlots(date: string, timeSlot: string) {
|
||||
try {
|
||||
const res = await getAvailableDetailSlots(date, timeSlot);
|
||||
if (res.code === 200) {
|
||||
availableDetailSlots.value = res.data || [];
|
||||
} else {
|
||||
availableDetailSlots.value = [];
|
||||
ElMessage.warning('获取可用时段失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详细时段失败', error);
|
||||
ElMessage.error('获取详细时段失败');
|
||||
availableDetailSlots.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间段样式
|
||||
function getTimeSlotStyle(slot: TimeSlotDetail) {
|
||||
if (slot.isBooked) {
|
||||
// 已预约:灰色+删除线
|
||||
return {
|
||||
color: '#999',
|
||||
textDecoration: 'line-through',
|
||||
backgroundColor: '#f5f7fa'
|
||||
};
|
||||
} else if (slot.isExpired) {
|
||||
// 已过期:灰色,无删除线
|
||||
return {
|
||||
color: '#999',
|
||||
backgroundColor: '#f5f7fa'
|
||||
};
|
||||
} else {
|
||||
// 可预约:正常样式
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
function getStatusText(status: string) {
|
||||
const statusMap: Record<string, string> = {
|
||||
'0': '待咨询',
|
||||
'1': '已完成',
|
||||
'2': '已取消',
|
||||
'3': '未到访'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
}
|
||||
|
||||
// 获取状态样式类
|
||||
function getStatusClass(status: string) {
|
||||
const classMap: Record<string, string> = {
|
||||
'0': 'text-warning',
|
||||
'1': 'text-success',
|
||||
'2': 'text-info',
|
||||
'3': 'text-danger'
|
||||
};
|
||||
return classMap[status] || '';
|
||||
}
|
||||
|
||||
// 提交预约表单
|
||||
function submitAppointmentForm() {
|
||||
appointmentFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
|
||||
if (!studentId.value) {
|
||||
ElMessage.warning('请先输入有效的学号');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!appointmentForm.appointmentDetailTime) {
|
||||
ElMessage.warning('请选择具体预约时段');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查学生是否被限制预约
|
||||
try {
|
||||
const res = await checkLimitation(studentId.value);
|
||||
if (res.data.limited) {
|
||||
ElMessageBox.alert(
|
||||
`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。`,
|
||||
'预约受限',
|
||||
{ type: 'warning' }
|
||||
);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查预约限制失败', error);
|
||||
}
|
||||
|
||||
try {
|
||||
submitLoading.value = true;
|
||||
|
||||
// 构建提交数据
|
||||
const submitData: XljkConsultationAppointmentForm = {
|
||||
...appointmentForm,
|
||||
studentId: studentId.value, // 不进行Number转换,保持原始值
|
||||
studentNo: studentNo.value // 添加学号
|
||||
};
|
||||
|
||||
console.log('提交预约的studentId:', submitData.studentId);
|
||||
|
||||
const res = await submitAppointment(submitData);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('预约成功');
|
||||
appointmentFormVisible.value = false;
|
||||
refreshCalendar(); // 刷新日历
|
||||
|
||||
// 预约成功后,自动打开"我的预约"弹窗
|
||||
setTimeout(() => {
|
||||
viewMyAppointments();
|
||||
}, 500);
|
||||
} else {
|
||||
ElMessage.error(res.msg || '预约失败,该时段可能已被预约');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交预约失败', error);
|
||||
ElMessage.error('提交预约失败');
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 查看我的预约
|
||||
async function viewMyAppointments() {
|
||||
if (!userId.value) {
|
||||
ElMessage.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!studentId.value) {
|
||||
ElMessage.warning('请先在学号输入框中输入学号并查询学生信息,或刷新页面让系统自动获取');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('查询预约记录的studentId:', studentId.value, '类型:', typeof studentId.value);
|
||||
|
||||
// 尝试多种类型转换,确保能正确查询到记录
|
||||
let id = studentId.value;
|
||||
|
||||
// 尝试使用原始studentId查询
|
||||
console.log('尝试使用原始studentId查询:', id);
|
||||
let res = await getMyAppointments(id);
|
||||
console.log('查询结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data && res.data.length > 0) {
|
||||
myAppointmentsList.value = res.data;
|
||||
console.log('预约记录数据:', myAppointmentsList.value);
|
||||
console.log('字典数据:', appointment_status.value);
|
||||
myAppointmentsVisible.value = true;
|
||||
} else {
|
||||
console.log('原始ID查询无结果,尝试转换为数字类型');
|
||||
|
||||
// 尝试转换为数字类型
|
||||
const numericId = Number(id);
|
||||
if (!isNaN(numericId)) {
|
||||
console.log('尝试使用数字类型studentId查询:', numericId);
|
||||
res = await getMyAppointments(numericId);
|
||||
console.log('数字类型查询结果:', res);
|
||||
|
||||
if (res.code === 200) {
|
||||
myAppointmentsList.value = res.data || [];
|
||||
console.log('预约记录数据:', myAppointmentsList.value);
|
||||
console.log('字典数据:', appointment_status.value);
|
||||
myAppointmentsVisible.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 无论是否找到记录,都显示弹窗
|
||||
myAppointmentsVisible.value = true;
|
||||
|
||||
// 如果没有预约记录,显示提示
|
||||
if (myAppointmentsList.value.length === 0) {
|
||||
ElMessage.info('暂无预约记录');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取预约记录失败', error);
|
||||
ElMessage.error('获取预约记录失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 取消预约
|
||||
async function cancelAppointment(row: any) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要取消该预约吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const updateData = {
|
||||
id: row.id,
|
||||
status: '2' // 已取消
|
||||
};
|
||||
|
||||
const res = await updateAppointment(updateData);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('取消预约成功');
|
||||
viewMyAppointments(); // 刷新预约列表
|
||||
refreshCalendar(); // 刷新日历
|
||||
} else {
|
||||
ElMessage.error(res.msg || '取消预约失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('取消预约失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化咨询师信息
|
||||
async function initCounselorInfo() {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
if (res.code === 200 && res.data) {
|
||||
const counselors = res.data;
|
||||
// 存储咨询师信息,以备后用
|
||||
if (availableSlots.value.length > 0) {
|
||||
availableSlots.value = availableSlots.value.map(slot => {
|
||||
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
|
||||
if (counselor) {
|
||||
return { ...slot, counselorName: counselor.name };
|
||||
}
|
||||
return slot;
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取咨询师信息失败', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新日历
|
||||
function refreshCalendar() {
|
||||
// 获取当前日期
|
||||
const now = new Date();
|
||||
// 获取当前月的第一天
|
||||
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// 获取30天前的日期,确保能看到历史记录
|
||||
const thirtyDaysBefore = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
||||
// 使用较早的日期作为开始日期
|
||||
const startDate = parseTime(thirtyDaysBefore < firstDayOfMonth ? thirtyDaysBefore : firstDayOfMonth, '{y}-{m}-{d}');
|
||||
// 结束日期保持为30天后
|
||||
const endDate = parseTime(new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), '{y}-{m}-{d}');
|
||||
loadAvailableSlots(startDate, endDate);
|
||||
}
|
||||
|
||||
// 学号输入框失去焦点时查询学生信息
|
||||
async function handleStudentNoBlur() {
|
||||
if (!studentNo.value) return;
|
||||
|
||||
try {
|
||||
console.log('查询学生信息,学号:', studentNo.value);
|
||||
const res = await getStudentByNo(studentNo.value);
|
||||
console.log('查询学生信息结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
studentId.value = res.data.id;
|
||||
studentName.value = res.data.name || '';
|
||||
console.log('获取到学生ID:', studentId.value, '类型:', typeof studentId.value);
|
||||
|
||||
// 确保studentId是数字类型
|
||||
if (typeof studentId.value === 'string') {
|
||||
studentId.value = Number(studentId.value);
|
||||
console.log('转换为数字类型后的studentId:', studentId.value);
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('未找到该学号对应的学生信息');
|
||||
studentId.value = null;
|
||||
studentName.value = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取学生信息失败', error);
|
||||
ElMessage.error('获取学生信息失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(async () => {
|
||||
// 初始化咨询师信息
|
||||
initCounselorInfo();
|
||||
|
||||
// 刷新日历
|
||||
refreshCalendar();
|
||||
|
||||
// 自动获取学生信息(如果用户已登录且用户名为学号格式)
|
||||
if (userId.value && nickname.value) {
|
||||
console.log('当前用户信息:', { userId: userId.value, nickname: nickname.value });
|
||||
|
||||
// 尝试直接使用nickname作为学号查询
|
||||
try {
|
||||
console.log('尝试使用nickname查询学生信息:', nickname.value);
|
||||
const res = await getStudentByNo(nickname.value);
|
||||
console.log('查询学生信息结果:', res);
|
||||
|
||||
if (res.code === 200 && res.data) {
|
||||
studentId.value = res.data.id;
|
||||
studentName.value = res.data.name || '';
|
||||
studentNo.value = res.data.studentNo || nickname.value;
|
||||
console.log('自动获取学生信息成功:', res.data);
|
||||
|
||||
// 查询是否有预约记录
|
||||
try {
|
||||
const appointmentRes = await getMyAppointments(studentId.value);
|
||||
console.log('学生预约记录:', appointmentRes.data);
|
||||
} catch (e) {
|
||||
console.error('查询预约记录失败:', e);
|
||||
}
|
||||
} else {
|
||||
console.log('用户可能不是学生或学号格式不正确,尝试其他方式');
|
||||
|
||||
// 尝试特定格式处理,例如"2025WRJ010林伟"格式可能需要提取前面的学号部分
|
||||
const studentNoMatch = nickname.value.match(/^(\d{4}[A-Za-z]{3}\d{3})/);
|
||||
if (studentNoMatch && studentNoMatch[1]) {
|
||||
const extractedStudentNo = studentNoMatch[1];
|
||||
console.log('尝试提取学号:', extractedStudentNo);
|
||||
|
||||
try {
|
||||
const res2 = await getStudentByNo(extractedStudentNo);
|
||||
if (res2.code === 200 && res2.data) {
|
||||
studentId.value = res2.data.id;
|
||||
studentName.value = res2.data.name || '';
|
||||
studentNo.value = res2.data.studentNo || extractedStudentNo;
|
||||
console.log('使用提取的学号获取学生信息成功:', res2.data);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('使用提取的学号查询失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果上述方法都失败,直接使用userId作为studentId(临时解决方案)
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
console.log('使用userId作为studentId:', userId.value);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('自动获取学生信息失败,使用userId作为studentId', error);
|
||||
// 如果自动获取失败,直接使用userId作为studentId(临时解决方案)
|
||||
studentId.value = userId.value;
|
||||
studentName.value = nickname.value || '';
|
||||
studentNo.value = nickname.value || '';
|
||||
console.log('使用userId作为studentId:', userId.value);
|
||||
}
|
||||
} else {
|
||||
console.log('用户未登录或没有nickname');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.calendar-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.calendar-card :deep(.el-card__body) {
|
||||
padding: 0px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
.calendar-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calendar-container {
|
||||
height: calc(100vh - 240px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
/* 状态文本样式 */
|
||||
.text-warning {
|
||||
color: #E6A23C;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #67C23A;
|
||||
}
|
||||
|
||||
.text-info {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #F56C6C;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,40 @@
|
||||
--- index.vue.orig 2025-07-30 11:30:00
|
||||
+++ index.vue 2025-07-30 11:30:00
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted, watch, getCurrentInstance } from 'vue';
|
||||
import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';
|
||||
+import { checkLimitation } from '@/api/xljk/consultation/noShow';
|
||||
import { getStudentOptions, getCounselorOptions } from '@/api/xljk/consultation/options';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
@@ -344,7 +345,24 @@
|
||||
}
|
||||
|
||||
// 提交预约
|
||||
-function submitAppointmentForm() {
|
||||
+async function submitAppointmentForm() {
|
||||
+ // 表单验证
|
||||
+ if (!form.value.studentId) {
|
||||
+ ElMessage.warning('请选择学生');
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ // 检查学生是否被限制预约
|
||||
+ try {
|
||||
+ const res = await checkLimitation(form.value.studentId);
|
||||
+ if (res.data && res.data.limited) {
|
||||
+ ElMessageBox.alert(
|
||||
+ `由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。`,
|
||||
+ '预约受限',
|
||||
+ { type: 'warning' }
|
||||
+ );
|
||||
+ return;
|
||||
+ }
|
||||
+ } catch (error) {
|
||||
+ console.error('检查预约限制失败', error);
|
||||
+ }
|
||||
+
|
||||
// 表单验证
|
||||
if (!form.value.studentId) {
|
||||
ElMessage.warning('请选择学生');
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 主入口文件,用于路由跳转
|
||||
</script>
|
||||
@@ -0,0 +1,446 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div 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="studentNo">
|
||||
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" style="width: 100%" popper-class="noshow-status-popper">
|
||||
<el-option
|
||||
v-for="dict in limitationStatusOptions"
|
||||
: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="dateRange">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</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="button-card table-card">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="noShowList" @row-dblclick="handleView">
|
||||
<el-table-column label="序号" type="index" width="55" align="center" />
|
||||
<el-table-column label="学生姓名" align="center" prop="studentName" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="学号" align="center" prop="studentNo" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="院系" align="center" prop="collegeName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="专业" align="center" prop="majorName" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="班级" align="center" prop="className" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="未到访日期" align="center" prop="noShowDate" min-width="100">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.noShowDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="限制到期日" align="center" prop="expireDate" min-width="100">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.expireDate ? parseTime(scope.row.expireDate, '{y}-{m}-{d}') : '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="120">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-tag v-if="scope.row.status === '0'" type="danger">生效中</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === '1'" type="info">已过期</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === '2'" type="success">已解除</el-tag>
|
||||
<span v-else>-</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="查看" placement="top">
|
||||
<el-button
|
||||
link
|
||||
type="info"
|
||||
icon="View"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handleView(scope.row)"
|
||||
v-hasPermi="['xljk:consultationnoshow:query']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 生效中状态可解除限制 -->
|
||||
<el-tooltip content="解除限制" placement="top" v-if="scope.row.status === '0'">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Key"
|
||||
size="default"
|
||||
style="font-size: 16px"
|
||||
@click="handleRelease(scope.row)"
|
||||
v-hasPermi="['xljk:consultationnoshow:release']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 查看详情对话框 -->
|
||||
<el-dialog title="查看未到访记录" v-model="viewOpen" width="700px" append-to-body>
|
||||
<el-descriptions :column="2" border>
|
||||
<!-- 第一行:学生姓名、学号 -->
|
||||
<el-descriptions-item label="学生姓名">{{ form.studentName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学号">{{ form.studentNo || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 第二行:院系、专业 -->
|
||||
<el-descriptions-item label="院系">{{ form.collegeName || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="专业">{{ form.majorName || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 第三行:班级、咨询师 -->
|
||||
<el-descriptions-item label="班级">{{ form.className || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="咨询师">{{ form.counselorName || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 第四行:预约日期、预约时段 -->
|
||||
<el-descriptions-item label="预约日期">{{ form.appointmentDate ? parseTime(form.appointmentDate, '{y}-{m}-{d}') : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="预约时段">{{ form.appointmentDetailTime || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 第五行:咨询原因(占两列) -->
|
||||
<el-descriptions-item label="咨询原因" :span="2">{{ form.reason || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 第六行:未到访日期(占两列) -->
|
||||
<el-descriptions-item label="未到访日期" :span="2">{{ parseTime(form.noShowDate, '{y}-{m}-{d}') || '-' }}</el-descriptions-item>
|
||||
|
||||
<!-- 最后一行:限制到期日、状态 -->
|
||||
<el-descriptions-item label="限制到期日">{{ form.expireDate ? parseTime(form.expireDate, '{y}-{m}-{d}') : '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag v-if="form.status === '0'" type="danger">生效中</el-tag>
|
||||
<el-tag v-else-if="form.status === '1'" type="info">已过期</el-tag>
|
||||
<el-tag v-else-if="form.status === '2'" type="success">已解除</el-tag>
|
||||
<span v-else>-</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="viewOpen = false">关 闭</el-button>
|
||||
<el-button
|
||||
v-if="form.status === '0'"
|
||||
type="primary"
|
||||
@click="handleRelease(form)"
|
||||
v-hasPermi="['xljk:consultationnoshow:release']"
|
||||
>解除限制</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, Refresh, View, Key } from '@element-plus/icons-vue';
|
||||
import { listNoShow, getNoShow, releaseNoShowLimitation } from '@/api/xljk/consultation/noshow';
|
||||
import { NoShowVO, NoShowQuery } from '@/api/xljk/consultation/types';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
// 遮罩层
|
||||
const loading = ref(false);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 未到访记录表格数据
|
||||
const noShowList = ref<NoShowVO[]>([]);
|
||||
// 查看对话框
|
||||
const viewOpen = ref(false);
|
||||
// 日期范围
|
||||
const dateRange = ref<[string, string]>();
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive<NoShowQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
studentName: undefined,
|
||||
studentNo: undefined,
|
||||
status: '0', // 默认查询"生效中"状态
|
||||
params: {
|
||||
startDate: undefined,
|
||||
endDate: undefined
|
||||
}
|
||||
});
|
||||
|
||||
// 表单参数
|
||||
const form = reactive<NoShowVO>({
|
||||
id: '',
|
||||
studentId: '',
|
||||
studentName: '',
|
||||
studentNo: '',
|
||||
collegeName: '',
|
||||
majorName: '',
|
||||
className: '',
|
||||
counselorName: '',
|
||||
appointmentDate: '',
|
||||
appointmentDetailTime: '',
|
||||
reason: '',
|
||||
appointmentId: '',
|
||||
noShowDate: '',
|
||||
expireDate: '',
|
||||
status: '',
|
||||
createTime: '',
|
||||
updateTime: ''
|
||||
});
|
||||
|
||||
// 限制状态选项
|
||||
const limitationStatusOptions = ref([
|
||||
{ value: 'all', label: '全部' }, // 使用'all'代替空字符串
|
||||
{ value: '0', label: '生效中' },
|
||||
{ value: '1', label: '已过期' },
|
||||
{ value: '2', label: '已解除' }
|
||||
]);
|
||||
|
||||
const queryFormRef = ref();
|
||||
|
||||
/** 查询未到访记录列表 */
|
||||
function getList() {
|
||||
loading.value = true;
|
||||
|
||||
// 确保params对象存在
|
||||
if (!queryParams.params) {
|
||||
queryParams.params = {};
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (dateRange.value) {
|
||||
queryParams.params.startDate = dateRange.value[0];
|
||||
queryParams.params.endDate = dateRange.value[1];
|
||||
} else {
|
||||
queryParams.params.startDate = undefined;
|
||||
queryParams.params.endDate = undefined;
|
||||
}
|
||||
|
||||
// 添加排序参数,实现倒序排列,按未到访日期排序
|
||||
queryParams.orderByColumn = 'noShowDate';
|
||||
queryParams.isAsc = 'desc';
|
||||
|
||||
// 创建一个新的查询参数对象,避免修改原始对象
|
||||
const query = { ...queryParams };
|
||||
|
||||
listNoShow(query).then(response => {
|
||||
if (response.code === 200) {
|
||||
// 记录响应数据格式以便调试
|
||||
console.log('API响应数据:', response);
|
||||
|
||||
// 处理不同的数据结构可能性
|
||||
if (response.rows) {
|
||||
// 直接包含rows的情况
|
||||
noShowList.value = response.rows || [];
|
||||
total.value = response.total || 0;
|
||||
} else if (response.data) {
|
||||
if (Array.isArray(response.data)) {
|
||||
// 数据是数组的情况
|
||||
noShowList.value = response.data;
|
||||
total.value = response.data.length;
|
||||
} else if (response.data.rows) {
|
||||
// 标准分页格式
|
||||
noShowList.value = response.data.rows || [];
|
||||
total.value = response.data.total || 0;
|
||||
} else {
|
||||
// 其他情况,可能data本身就是数据
|
||||
noShowList.value = [].concat(response.data || []);
|
||||
total.value = [].concat(response.data || []).length;
|
||||
}
|
||||
} else {
|
||||
// 没有找到数据
|
||||
noShowList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.msg || '获取未到访记录列表失败');
|
||||
noShowList.value = [];
|
||||
total.value = 0;
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
console.error('获取未到访记录列表失败', error);
|
||||
ElMessage.error('获取未到访记录列表失败,请稍后重试');
|
||||
noShowList.value = [];
|
||||
total.value = 0;
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
dateRange.value = undefined;
|
||||
queryFormRef.value?.resetFields();
|
||||
|
||||
// 确保params对象存在并重置日期范围
|
||||
if (!queryParams.params) {
|
||||
queryParams.params = {};
|
||||
}
|
||||
queryParams.params.startDate = undefined;
|
||||
queryParams.params.endDate = undefined;
|
||||
|
||||
// 确保重置后状态仍为"生效中"
|
||||
queryParams.status = '0';
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 查看按钮操作 */
|
||||
function handleView(row: NoShowVO) {
|
||||
const id = row.id;
|
||||
loading.value = true;
|
||||
getNoShow(id).then(response => {
|
||||
if (response.code === 200) {
|
||||
console.log('获取详情响应:', response);
|
||||
// 处理不同的数据结构可能性
|
||||
if (response.data) {
|
||||
Object.assign(form, response.data);
|
||||
} else {
|
||||
// 可能直接返回了数据对象
|
||||
const data = {...response};
|
||||
delete data.code;
|
||||
delete data.msg;
|
||||
Object.assign(form, data);
|
||||
}
|
||||
viewOpen.value = true;
|
||||
} else {
|
||||
ElMessage.error(response.msg || '获取未到访记录详情失败');
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
console.error('获取未到访记录详情失败', error);
|
||||
ElMessage.error('获取未到访记录详情失败,请稍后重试');
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** 解除限制按钮操作 */
|
||||
function handleRelease(row: NoShowVO) {
|
||||
const id = row.id;
|
||||
ElMessageBox.confirm('确认解除该学生的预约限制吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
loading.value = true;
|
||||
releaseNoShowLimitation(id).then(response => {
|
||||
if (response.code === 200) {
|
||||
ElMessage.success('解除限制成功');
|
||||
getList();
|
||||
if (viewOpen.value) {
|
||||
viewOpen.value = false;
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.msg || '解除限制失败');
|
||||
}
|
||||
loading.value = false;
|
||||
}).catch(error => {
|
||||
console.error('解除限制失败', error);
|
||||
ElMessage.error('解除限制失败,请稍后重试');
|
||||
loading.value = false;
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 表格卡片布局 */
|
||||
.table-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-card__body) {
|
||||
padding: 0px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
/* 移除按钮栏底部边框 */
|
||||
.button-card :deep(.el-card__header) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* 分页容器样式 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px 0 0 !important;
|
||||
background-color: #fff;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
/* 搜索按钮容器样式 */
|
||||
.search-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,677 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div 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="counselorId">
|
||||
<el-select v-model="queryParams.counselorId" placeholder="请选择咨询师" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in counselorOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-form-item label="排班日期" prop="dateRange">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="时段类型" prop="timeSlot">
|
||||
<el-select v-model="queryParams.timeSlot" placeholder="请选择时段类型" clearable style="width: 100%">
|
||||
<el-option label="上午" value="0" />
|
||||
<el-option label="下午" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
|
||||
<el-option label="正常" value="0" />
|
||||
<el-option label="停用" value="1" />
|
||||
</el-select>
|
||||
</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="button-card table-card">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xljk:consultationschedule:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['xljk:consultationschedule:edit']">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['xljk:consultationschedule:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="scheduleList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="咨询师" align="center" prop="counselorName" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column label="日期范围" align="center" prop="dateRange" min-width="180" />
|
||||
<el-table-column label="上午时段" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.morningEnabled">{{ scope.row.morningTime }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="下午时段" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.afternoonEnabled">{{ scope.row.afternoonTime }}</span>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="预约时段数" align="center" prop="scheduleCount" width="100" />
|
||||
<el-table-column label="状态" align="center" prop="status" width="80">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="schedule_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
|
||||
<template #default="scope">
|
||||
<span>{{ parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
icon="Edit"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handleUpdate(scope.row)"
|
||||
v-hasPermi="['xljk:consultationschedule:edit']"
|
||||
></el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
icon="Delete"
|
||||
size="default"
|
||||
style="font-size: 16px"
|
||||
@click="handleDelete(scope.row)"
|
||||
v-hasPermi="['xljk:consultationschedule:remove']"
|
||||
></el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<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>
|
||||
|
||||
<!-- 添加或修改预约时段对话框 -->
|
||||
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
|
||||
<el-form ref="scheduleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="咨询师" prop="counselorId">
|
||||
<el-select v-model="form.counselorId" placeholder="请选择咨询师" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in counselorOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排班日期" prop="dateRange" v-if="!form.id">
|
||||
<el-date-picker
|
||||
v-model="form.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排班日期" prop="scheduleDate" v-else>
|
||||
<el-date-picker
|
||||
v-model="form.scheduleDate"
|
||||
type="date"
|
||||
placeholder="选择日期"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 上午时段设置 -->
|
||||
<el-divider content-position="left">上午时段</el-divider>
|
||||
<el-form-item label="启用上午" prop="morningEnabled">
|
||||
<el-switch v-model="form.morningEnabled" />
|
||||
</el-form-item>
|
||||
<template v-if="form.morningEnabled">
|
||||
<el-form-item label="开始时间" prop="morningStartTime">
|
||||
<el-time-picker
|
||||
v-model="morningStartTimeTemp"
|
||||
format="HH:mm"
|
||||
placeholder="选择时间"
|
||||
style="width: 100%"
|
||||
:picker-options="morningTimePickerOptions"
|
||||
@change="handleMorningStartTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="morningEndTime">
|
||||
<el-time-picker
|
||||
v-model="morningEndTimeTemp"
|
||||
format="HH:mm"
|
||||
placeholder="选择时间"
|
||||
style="width: 100%"
|
||||
:picker-options="morningTimePickerOptions"
|
||||
@change="handleMorningEndTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<!-- 下午时段设置 -->
|
||||
<el-divider content-position="left">下午时段</el-divider>
|
||||
<el-form-item label="启用下午" prop="afternoonEnabled">
|
||||
<el-switch v-model="form.afternoonEnabled" />
|
||||
</el-form-item>
|
||||
<template v-if="form.afternoonEnabled">
|
||||
<el-form-item label="开始时间" prop="afternoonStartTime">
|
||||
<el-time-picker
|
||||
v-model="afternoonStartTimeTemp"
|
||||
format="HH:mm"
|
||||
placeholder="选择时间"
|
||||
style="width: 100%"
|
||||
:picker-options="afternoonTimePickerOptions"
|
||||
@change="handleAfternoonStartTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="结束时间" prop="afternoonEndTime">
|
||||
<el-time-picker
|
||||
v-model="afternoonEndTimeTemp"
|
||||
format="HH:mm"
|
||||
placeholder="选择时间"
|
||||
style="width: 100%"
|
||||
:picker-options="afternoonTimePickerOptions"
|
||||
@change="handleAfternoonEndTimeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio label="0">正常</el-radio>
|
||||
<el-radio label="1">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed, watch } from 'vue';
|
||||
import { listSchedule, getSchedule, delSchedule, addSchedule, updateSchedule, getCounselorOptions } from '@/api/xljk/consultation';
|
||||
import { XljkConsultationScheduleVO, XljkConsultationScheduleForm, CounselorOption, XljkConsultationScheduleSummaryVO } from '@/api/xljk/consultation/types';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
const { schedule_status } = toRefs(proxy?.useDict('schedule_status'));
|
||||
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 预约时段表格数据
|
||||
const scheduleList = ref<XljkConsultationScheduleSummaryVO[]>([]);
|
||||
// 弹出层标题
|
||||
const title = ref('');
|
||||
// 是否显示弹出层
|
||||
const open = ref(false);
|
||||
// 日期范围
|
||||
const dateRange = ref<string[]>([]);
|
||||
// 咨询师选项
|
||||
const counselorOptions = ref<CounselorOption[]>([]);
|
||||
// 时间选择器临时变量
|
||||
const startTimeTemp = ref<Date | null>(null);
|
||||
const endTimeTemp = ref<Date | null>(null);
|
||||
const morningStartTimeTemp = ref<Date | null>(null);
|
||||
const morningEndTimeTemp = ref<Date | null>(null);
|
||||
const afternoonStartTimeTemp = ref<Date | null>(null);
|
||||
const afternoonEndTimeTemp = ref<Date | null>(null);
|
||||
|
||||
// 时间选择器配置
|
||||
const morningTimePickerOptions = {
|
||||
start: '08:00',
|
||||
step: '00:30',
|
||||
end: '12:00',
|
||||
format: 'HH:mm'
|
||||
};
|
||||
|
||||
const afternoonTimePickerOptions = {
|
||||
start: '14:00',
|
||||
step: '00:30',
|
||||
end: '17:00',
|
||||
format: 'HH:mm'
|
||||
};
|
||||
|
||||
// 查询参数
|
||||
const queryParams = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
counselorId: undefined,
|
||||
timeSlot: undefined,
|
||||
status: undefined,
|
||||
params: {} as any
|
||||
});
|
||||
|
||||
// 表单参数
|
||||
const form = reactive<any>({
|
||||
id: undefined,
|
||||
counselorId: undefined,
|
||||
scheduleDate: '',
|
||||
dateRange: [],
|
||||
morningEnabled: true,
|
||||
morningStartTime: '08:30',
|
||||
morningEndTime: '12:00',
|
||||
afternoonEnabled: true,
|
||||
afternoonStartTime: '14:30',
|
||||
afternoonEndTime: '17:00',
|
||||
timeSlot: '0',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
status: '0'
|
||||
});
|
||||
|
||||
// 表单校验
|
||||
const rules = {
|
||||
counselorId: [{ required: true, message: '咨询师不能为空', trigger: 'change' }],
|
||||
scheduleDate: [{ required: false, message: '排班日期不能为空', trigger: 'change' }],
|
||||
dateRange: [{ required: true, message: '排班日期范围不能为空', trigger: 'change' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
|
||||
};
|
||||
|
||||
const queryFormRef = ref();
|
||||
const scheduleFormRef = ref();
|
||||
|
||||
// 处理日期范围
|
||||
watch(dateRange, (val) => {
|
||||
if (val && val.length === 2) {
|
||||
queryParams.params.beginScheduleDate = val[0];
|
||||
queryParams.params.endScheduleDate = val[1];
|
||||
} else {
|
||||
queryParams.params.beginScheduleDate = undefined;
|
||||
queryParams.params.endScheduleDate = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
/** 查询预约时段列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await listSchedule(queryParams);
|
||||
scheduleList.value = res.rows || [];
|
||||
total.value = res.total || 0;
|
||||
} catch (error) {
|
||||
console.error('获取预约时段列表失败', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取咨询师选项 */
|
||||
const getCounselors = async () => {
|
||||
try {
|
||||
const res = await getCounselorOptions();
|
||||
console.log('咨询师选项API返回数据:', res);
|
||||
counselorOptions.value = res.data || [];
|
||||
console.log('处理后的咨询师选项:', counselorOptions.value);
|
||||
} catch (error) {
|
||||
console.error('获取咨询师选项失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
open.value = false;
|
||||
reset();
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.id = undefined;
|
||||
form.counselorId = undefined;
|
||||
form.scheduleDate = '';
|
||||
form.dateRange = [];
|
||||
form.morningEnabled = true;
|
||||
form.afternoonEnabled = true;
|
||||
form.morningStartTime = '08:30';
|
||||
form.morningEndTime = '12:00';
|
||||
form.afternoonStartTime = '14:30';
|
||||
form.afternoonEndTime = '17:00';
|
||||
form.timeSlot = '0';
|
||||
form.startTime = '';
|
||||
form.endTime = '';
|
||||
form.status = '0';
|
||||
|
||||
startTimeTemp.value = null;
|
||||
endTimeTemp.value = null;
|
||||
|
||||
// 设置默认时间
|
||||
const morningStart = new Date();
|
||||
morningStart.setHours(8, 30, 0, 0);
|
||||
morningStartTimeTemp.value = morningStart;
|
||||
|
||||
const morningEnd = new Date();
|
||||
morningEnd.setHours(12, 0, 0, 0);
|
||||
morningEndTimeTemp.value = morningEnd;
|
||||
|
||||
const afternoonStart = new Date();
|
||||
afternoonStart.setHours(14, 30, 0, 0);
|
||||
afternoonStartTimeTemp.value = afternoonStart;
|
||||
|
||||
const afternoonEnd = new Date();
|
||||
afternoonEnd.setHours(17, 0, 0, 0);
|
||||
afternoonEndTimeTemp.value = afternoonEnd;
|
||||
|
||||
scheduleFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = [];
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: XljkConsultationScheduleSummaryVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
open.value = true;
|
||||
title.value = '添加预约时段';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: XljkConsultationScheduleSummaryVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0];
|
||||
try {
|
||||
const res = await getSchedule(_id);
|
||||
Object.assign(form, res.data);
|
||||
|
||||
// 设置时间选择器的值
|
||||
if (form.startTime) {
|
||||
const [hours, minutes] = form.startTime.split(':').map(Number);
|
||||
startTimeTemp.value = new Date();
|
||||
startTimeTemp.value.setHours(hours, minutes, 0, 0);
|
||||
}
|
||||
|
||||
if (form.endTime) {
|
||||
const [hours, minutes] = form.endTime.split(':').map(Number);
|
||||
endTimeTemp.value = new Date();
|
||||
endTimeTemp.value.setHours(hours, minutes, 0, 0);
|
||||
}
|
||||
|
||||
open.value = true;
|
||||
title.value = '修改预约时段';
|
||||
} catch (error) {
|
||||
console.error('获取预约时段详情失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
if (!scheduleFormRef.value) return;
|
||||
|
||||
await scheduleFormRef.value.validate(async (valid: boolean) => {
|
||||
if (!valid) return;
|
||||
|
||||
try {
|
||||
if (form.id) {
|
||||
// 修改单个预约时段
|
||||
await updateSchedule({
|
||||
id: form.id,
|
||||
counselorId: form.counselorId,
|
||||
scheduleDate: form.scheduleDate,
|
||||
timeSlot: form.timeSlot,
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime,
|
||||
status: form.status
|
||||
});
|
||||
ElMessage.success('修改成功');
|
||||
} else {
|
||||
// 批量创建预约时段
|
||||
if (!form.dateRange || form.dateRange.length !== 2) {
|
||||
ElMessage.warning('请选择排班日期范围');
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date(form.dateRange[0]);
|
||||
const endDate = new Date(form.dateRange[1]);
|
||||
|
||||
if (startDate > endDate) {
|
||||
ElMessage.warning('开始日期不能大于结束日期');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否至少启用了一个时段
|
||||
if (!form.morningEnabled && !form.afternoonEnabled) {
|
||||
ElMessage.warning('请至少启用一个时段(上午或下午)');
|
||||
return;
|
||||
}
|
||||
|
||||
// 批量创建预约时段的请求数据
|
||||
const batchCreateData = {
|
||||
counselorId: form.counselorId,
|
||||
startDate: form.dateRange[0],
|
||||
endDate: form.dateRange[1],
|
||||
morningEnabled: form.morningEnabled,
|
||||
morningStartTime: form.morningStartTime,
|
||||
morningEndTime: form.morningEndTime,
|
||||
afternoonEnabled: form.afternoonEnabled,
|
||||
afternoonStartTime: form.afternoonStartTime,
|
||||
afternoonEndTime: form.afternoonEndTime,
|
||||
status: form.status,
|
||||
skipWeekends: true // 自动跳过周末
|
||||
};
|
||||
|
||||
// 调用批量创建API
|
||||
const res = await addSchedule(batchCreateData);
|
||||
ElMessage.success(`批量创建成功,共创建${res.data || 0}条预约时段`);
|
||||
}
|
||||
open.value = false;
|
||||
getList();
|
||||
} catch (error) {
|
||||
console.error('提交表单失败', error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: XljkConsultationScheduleSummaryVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
const confirmMessage = row
|
||||
? `是否确认删除该预约时段?${row.scheduleCount > 1 ? `(包含${row.scheduleCount}条记录)` : ''}`
|
||||
: `是否确认删除选中的${ids.value.length}条数据?`;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(confirmMessage, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
await delSchedule(_ids);
|
||||
ElMessage.success('删除成功');
|
||||
getList();
|
||||
} catch (error) {
|
||||
console.error('删除失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理开始时间变化 */
|
||||
const handleStartTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.startTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.startTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理结束时间变化 */
|
||||
const handleEndTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.endTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.endTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理上午开始时间变化 */
|
||||
const handleMorningStartTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.morningStartTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.morningStartTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理上午结束时间变化 */
|
||||
const handleMorningEndTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.morningEndTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.morningEndTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理下午开始时间变化 */
|
||||
const handleAfternoonStartTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.afternoonStartTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.afternoonStartTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
/** 处理下午结束时间变化 */
|
||||
const handleAfternoonEndTimeChange = (time: Date) => {
|
||||
if (time) {
|
||||
const hours = time.getHours().toString().padStart(2, '0');
|
||||
const minutes = time.getMinutes().toString().padStart(2, '0');
|
||||
form.afternoonEndTime = `${hours}:${minutes}`;
|
||||
} else {
|
||||
form.afternoonEndTime = '';
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getCounselors();
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 表格卡片布局 */
|
||||
.table-card {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.table-card :deep(.el-card__body) {
|
||||
padding: 0px 20px 20px 20px !important;
|
||||
}
|
||||
|
||||
/* 移除按钮栏底部边框 */
|
||||
.button-card :deep(.el-card__header) {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
/* 分页容器样式 */
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 16px 20px 0 0 !important;
|
||||
background-color: #fff;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
/* 搜索按钮容器样式 */
|
||||
.search-btn-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10px;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,93 +1,112 @@
|
||||
<template>
|
||||
<div class="p-2 page-container">
|
||||
<el-card shadow="hover" class="mb-[10px]">
|
||||
<el-card shadow="hover" class="mb-[10px]" v-loading="formLoading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="header-title">新增心理健康测评</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="测评名称" prop="evaluationName">
|
||||
<el-input
|
||||
v-model="form.evaluationName"
|
||||
placeholder="请输入测评名称"
|
||||
style="width: 500px"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="测评名称" prop="evaluationName">
|
||||
<el-input
|
||||
v-model="form.evaluationName"
|
||||
placeholder="请输入测评名称"
|
||||
style="width: 100%"
|
||||
maxlength="100"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="学期" prop="semester">
|
||||
<el-input
|
||||
v-model="form.semester"
|
||||
placeholder="请输入学期,如:2024上学期"
|
||||
style="width: 100%"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="院系" prop="departmentName">
|
||||
<el-select
|
||||
v-model="form.departmentName"
|
||||
placeholder="请选择院系"
|
||||
style="width: 500px"
|
||||
@change="handleCollegeChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="college in collegeOptions"
|
||||
:key="college.name"
|
||||
:label="college.name"
|
||||
:value="college.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="院系" prop="departmentName">
|
||||
<el-select
|
||||
v-model="form.departmentName"
|
||||
placeholder="请选择院系"
|
||||
style="width: 100%"
|
||||
@change="handleCollegeChange"
|
||||
:loading="collegeLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="college in collegeOptions"
|
||||
:key="college.id"
|
||||
:label="college.name"
|
||||
:value="college.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="专业" prop="majorName">
|
||||
<el-select
|
||||
v-model="form.majorName"
|
||||
placeholder="请选择专业"
|
||||
style="width: 100%"
|
||||
@change="handleMajorChange"
|
||||
:disabled="!form.departmentName"
|
||||
:loading="majorLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="major in majorOptions"
|
||||
:key="major.id"
|
||||
:label="major.name"
|
||||
:value="major.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item label="专业" prop="majorName">
|
||||
<el-select
|
||||
v-model="form.majorName"
|
||||
placeholder="请选择专业"
|
||||
style="width: 500px"
|
||||
@change="handleMajorChange"
|
||||
:disabled="!form.departmentName"
|
||||
>
|
||||
<el-option
|
||||
v-for="major in majorOptions"
|
||||
:key="major.name"
|
||||
:label="major.name"
|
||||
:value="major.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="班级" prop="className">
|
||||
<el-select
|
||||
v-model="form.className"
|
||||
placeholder="请选择班级"
|
||||
style="width: 500px"
|
||||
@change="handleClassChange"
|
||||
:disabled="!form.majorName"
|
||||
>
|
||||
<el-option
|
||||
v-for="cls in classOptions"
|
||||
:key="cls.name"
|
||||
:label="cls.name"
|
||||
:value="cls.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="学期" prop="semester">
|
||||
<el-input
|
||||
v-model="form.semester"
|
||||
placeholder="请输入学期,如:2024上学期"
|
||||
style="width: 500px"
|
||||
maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注信息"
|
||||
style="width: 500px"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="班级" prop="className">
|
||||
<el-select
|
||||
v-model="form.className"
|
||||
placeholder="请选择班级"
|
||||
style="width: 100%"
|
||||
@change="handleClassChange"
|
||||
:disabled="!form.majorName"
|
||||
:loading="classLoading"
|
||||
>
|
||||
<el-option
|
||||
v-for="cls in classOptions"
|
||||
:key="cls.id"
|
||||
:label="cls.name"
|
||||
:value="cls.name"
|
||||
:data-id="cls.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
placeholder="请输入备注信息"
|
||||
style="width: 100%"
|
||||
:rows="1"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item>
|
||||
<div class="form-buttons">
|
||||
@@ -117,14 +136,36 @@
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<!-- 批量操作区域 -->
|
||||
<div class="batch-actions" v-if="studentList.length > 0">
|
||||
<span class="batch-label">批量设置心理状态:</span>
|
||||
<el-radio-group v-model="batchMentalStatus">
|
||||
<el-radio-button label="1">健康</el-radio-button>
|
||||
<el-radio-button label="2">良好</el-radio-button>
|
||||
<el-radio-button label="3">重点关注</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" size="small" @click="applyBatchMentalStatus" :disabled="selectedStudents.length === 0">
|
||||
应用到所选学生
|
||||
</el-button>
|
||||
<el-button type="info" size="small" @click="applyBatchMentalStatusToAll">
|
||||
应用到全部学生
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="resetAllMentalStatus">
|
||||
重置所有状态
|
||||
</el-button>
|
||||
<span class="batch-hint">已选择 {{ selectedStudents.length }} 名学生</span>
|
||||
</div>
|
||||
|
||||
<!-- 学生列表表格 -->
|
||||
<el-table
|
||||
v-else
|
||||
v-if="studentList.length > 0"
|
||||
:data="studentList"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
|
||||
<el-table-column label="姓名" align="center" prop="studentName" width="100" />
|
||||
<el-table-column label="性别" align="center" prop="gender" width="80">
|
||||
@@ -195,13 +236,35 @@
|
||||
import { addEvaluation, getCollegeOptions, getMajorOptions, getClassOptions, getStudentsByClass, batchAddEvaluationDetails } from '@/api/xljk/evaluation';
|
||||
import { XljkEvaluationForm, StudentInfo, CollegeOption, MajorOption, ClassOption } from '@/api/xljk/evaluation/types';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { proxy } = getCurrentInstance() as any;
|
||||
|
||||
const router = useRouter();
|
||||
const formRef = ref<ElFormInstance>();
|
||||
const submitLoading = ref(false);
|
||||
const studentsLoading = ref(false);
|
||||
const formLoading = ref(false);
|
||||
const collegeLoading = ref(false);
|
||||
const majorLoading = ref(false);
|
||||
const classLoading = ref(false);
|
||||
const batchMentalStatus = ref('1'); // 默认批量设置为"健康"
|
||||
const selectedStudents = ref<StudentInfo[]>([]); // 选中的学生
|
||||
|
||||
/** 自动生成当前学期 */
|
||||
const generateCurrentSemester = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = now.getMonth() + 1; // 月份从0开始
|
||||
|
||||
// 上半年(1-7月)为下学期,下半年(8-12月)为上学期
|
||||
if (month >= 8) {
|
||||
return `${year}上学期`;
|
||||
} else if (month <= 7) {
|
||||
return `${year}下学期`;
|
||||
}
|
||||
return `${year}上学期`; // 默认返回上学期
|
||||
};
|
||||
|
||||
// 表单数据
|
||||
const form = ref<XljkEvaluationForm>({
|
||||
@@ -212,11 +275,29 @@ const form = ref<XljkEvaluationForm>({
|
||||
majorId: undefined, // 这里保留majorId字段,但实际使用majorName
|
||||
className: undefined,
|
||||
classId: undefined, // 这里保留classId字段,但实际使用className
|
||||
semester: undefined,
|
||||
semester: generateCurrentSemester(), // 自动生成当前学期
|
||||
status: '0', // 默认为草稿状态
|
||||
remark: undefined
|
||||
});
|
||||
|
||||
/** 自动生成测评名称 */
|
||||
const generateEvaluationName = () => {
|
||||
const semester = form.value.semester || '';
|
||||
const className = form.value.className || '';
|
||||
|
||||
if (semester && className) {
|
||||
return `${semester}${className}心理健康测评`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// 监听学期变化,自动更新测评名称
|
||||
watch(() => form.value.semester, (newSemester) => {
|
||||
if (newSemester && form.value.className) {
|
||||
form.value.evaluationName = generateEvaluationName();
|
||||
}
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
evaluationName: [{ required: true, message: '测评名称不能为空', trigger: 'blur' }],
|
||||
@@ -235,30 +316,72 @@ const studentList = ref<StudentInfo[]>([]);
|
||||
/** 获取院系选项 */
|
||||
const getColleges = async () => {
|
||||
try {
|
||||
collegeLoading.value = true;
|
||||
formLoading.value = true;
|
||||
const res = await getCollegeOptions();
|
||||
collegeOptions.value = res.data || [];
|
||||
|
||||
// 如果获取到院系选项,自动选择第一个
|
||||
if (collegeOptions.value.length > 0) {
|
||||
form.value.departmentName = collegeOptions.value[0].name;
|
||||
form.value.departmentId = collegeOptions.value[0].id;
|
||||
|
||||
// 获取对应的专业选项
|
||||
await getMajors(form.value.departmentName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取院系选项失败', error);
|
||||
} finally {
|
||||
collegeLoading.value = false;
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取专业选项 */
|
||||
const getMajors = async (collegeName: string) => {
|
||||
try {
|
||||
majorLoading.value = true;
|
||||
formLoading.value = true;
|
||||
const res = await getMajorOptions(collegeName);
|
||||
majorOptions.value = res.data || [];
|
||||
|
||||
// 如果获取到专业选项,自动选择第一个
|
||||
if (majorOptions.value.length > 0) {
|
||||
form.value.majorName = majorOptions.value[0].name;
|
||||
form.value.majorId = majorOptions.value[0].id;
|
||||
|
||||
// 获取对应的班级选项
|
||||
await getClasses(form.value.departmentName, form.value.majorName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取专业选项失败', error);
|
||||
} finally {
|
||||
majorLoading.value = false;
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 获取班级选项 */
|
||||
const getClasses = async (collegeName: string, majorName: string) => {
|
||||
try {
|
||||
classLoading.value = true;
|
||||
formLoading.value = true;
|
||||
const res = await getClassOptions(collegeName, majorName);
|
||||
classOptions.value = res.data || [];
|
||||
|
||||
// 如果获取到班级选项,自动选择第一个
|
||||
if (classOptions.value.length > 0) {
|
||||
form.value.className = classOptions.value[0].name;
|
||||
form.value.classId = classOptions.value[0].id;
|
||||
|
||||
// 获取班级学生列表
|
||||
await handleClassChange(form.value.className);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取班级选项失败', error);
|
||||
} finally {
|
||||
classLoading.value = false;
|
||||
formLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -297,11 +420,25 @@ const handleClassChange = async (className: string) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置班级ID
|
||||
const selectedClass = classOptions.value.find(c => c.name === className);
|
||||
if (selectedClass) {
|
||||
form.value.classId = selectedClass.id;
|
||||
}
|
||||
|
||||
// 自动生成测评名称
|
||||
form.value.evaluationName = generateEvaluationName();
|
||||
|
||||
// 获取班级学生列表
|
||||
try {
|
||||
studentsLoading.value = true;
|
||||
const res = await getStudentsByClass(form.value.departmentName, form.value.majorName, className);
|
||||
studentList.value = res.data || [];
|
||||
// 设置默认的心理状态为"健康"
|
||||
studentList.value = (res.data || []).map(student => ({
|
||||
...student,
|
||||
mentalStatus: '1', // 默认设置为"健康"
|
||||
remark: '' // 初始化备注为空字符串
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取班级学生列表失败', error);
|
||||
studentList.value = [];
|
||||
@@ -326,6 +463,60 @@ const countFocusStudents = () => {
|
||||
return studentList.value.filter(student => student.mentalStatus === '3').length;
|
||||
};
|
||||
|
||||
/** 批量设置心理状态 */
|
||||
const applyBatchMentalStatus = () => {
|
||||
const newStatus = batchMentalStatus.value;
|
||||
selectedStudents.value.forEach(student => {
|
||||
student.mentalStatus = newStatus;
|
||||
// 如果心理状态是"重点关注",设置默认分数
|
||||
if (newStatus === '3') {
|
||||
student.score = student.score || 70;
|
||||
} else {
|
||||
student.score = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 批量设置心理状态到全部学生 */
|
||||
const applyBatchMentalStatusToAll = () => {
|
||||
const newStatus = batchMentalStatus.value;
|
||||
studentList.value.forEach(student => {
|
||||
student.mentalStatus = newStatus;
|
||||
// 如果心理状态是"重点关注",设置默认分数
|
||||
if (newStatus === '3') {
|
||||
student.score = student.score || 70;
|
||||
} else {
|
||||
student.score = undefined;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 重置所有学生的心理状态 */
|
||||
const resetAllMentalStatus = () => {
|
||||
ElMessageBox.confirm('确认要将所有学生的心理状态重置为"健康"吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
studentList.value.forEach(student => {
|
||||
student.mentalStatus = '1'; // 重置为"健康"
|
||||
student.score = undefined; // 清空分数
|
||||
student.remark = ''; // 清空备注
|
||||
});
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '已重置所有学生的心理状态'
|
||||
});
|
||||
}).catch(() => {
|
||||
// 取消操作
|
||||
});
|
||||
};
|
||||
|
||||
/** 处理表格多选 */
|
||||
const handleSelectionChange = (selection: StudentInfo[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid: boolean) => {
|
||||
@@ -354,18 +545,22 @@ const submitForm = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保ID字段有值,即使是临时值
|
||||
if (form.value.departmentName && !form.value.departmentId) {
|
||||
form.value.departmentId = 0; // 临时ID,后端会基于departmentName处理
|
||||
// 确保ID字段有值,使用从选项中获取的ID
|
||||
if (form.value.departmentName) {
|
||||
const selectedCollege = collegeOptions.value.find(c => c.name === form.value.departmentName);
|
||||
if (selectedCollege) {
|
||||
form.value.departmentId = selectedCollege.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.value.majorName && !form.value.majorId) {
|
||||
form.value.majorId = 0; // 临时ID,后端会基于majorName处理
|
||||
if (form.value.majorName) {
|
||||
const selectedMajor = majorOptions.value.find(m => m.name === form.value.majorName);
|
||||
if (selectedMajor) {
|
||||
form.value.majorId = selectedMajor.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (form.value.className && !form.value.classId) {
|
||||
form.value.classId = 0; // 临时ID,后端会基于className处理
|
||||
}
|
||||
// 班级ID已在handleClassChange中设置
|
||||
|
||||
// 先创建测评
|
||||
const res = await addEvaluation(form.value);
|
||||
@@ -386,9 +581,9 @@ const submitForm = () => {
|
||||
gender: student.gender,
|
||||
birthDate: student.birthDate,
|
||||
phone: student.phone,
|
||||
mentalStatus: student.mentalStatus || '1', // 默认健康
|
||||
mentalStatus: student.mentalStatus || '1', // 确保有默认值"健康"
|
||||
score: student.mentalStatus === '3' ? student.score : undefined,
|
||||
remark: student.remark
|
||||
remark: student.remark || '' // 确保备注有默认值
|
||||
};
|
||||
});
|
||||
|
||||
@@ -399,7 +594,7 @@ const submitForm = () => {
|
||||
// 注:这部分逻辑可能需要在后端自动处理
|
||||
|
||||
proxy?.$modal.msgSuccess('新增成功');
|
||||
router.push('/xljk/evaluation');
|
||||
router.replace('/xljk/evaluation');
|
||||
} else {
|
||||
proxy?.$modal.msgError('新增测评失败');
|
||||
}
|
||||
@@ -414,7 +609,7 @@ const submitForm = () => {
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
router.push('/xljk/evaluation');
|
||||
router.replace('/xljk/evaluation');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -423,10 +618,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 页面容器样式 */
|
||||
.page-container {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
/* 表格卡片布局 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -445,7 +642,14 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.form-buttons {
|
||||
margin-top: 10px;
|
||||
margin-top: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 减小表单项之间的间距 */
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.student-count {
|
||||
@@ -454,4 +658,25 @@ onMounted(() => {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.batch-label {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.batch-hint {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,7 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 统计信息卡片 -->
|
||||
<el-card shadow="hover" class="mb-[10px]" v-loading="statisticsLoading">
|
||||
<el-card shadow="hover" class="mb-[10px] statistics-card" v-loading="statisticsLoading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="header-title">测评统计</span>
|
||||
@@ -35,29 +35,37 @@
|
||||
|
||||
<div class="statistics-container">
|
||||
<div class="statistics-row">
|
||||
<el-statistic title="学生总数" :value="statistics.totalStudents">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
<div class="statistic-item">
|
||||
<el-statistic title="学生总数" :value="statistics.totalStudents">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
|
||||
<el-statistic title="健康学生" :value="statistics.healthyCount">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人 ({{ formatPercent(statistics.healthyPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
<div class="statistic-item">
|
||||
<el-statistic title="健康学生" :value="statistics.healthyCount">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人 ({{ formatPercent(statistics.healthyPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
|
||||
<el-statistic title="良好学生" :value="statistics.goodCount">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人 ({{ formatPercent(statistics.goodPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
<div class="statistic-item">
|
||||
<el-statistic title="良好学生" :value="statistics.goodCount">
|
||||
<template #suffix>
|
||||
<span class="suffix-label">人 ({{ formatPercent(statistics.goodPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
|
||||
<el-statistic title="重点关注" :value="statistics.focusCount" value-style="color: #f56c6c;">
|
||||
<template #suffix>
|
||||
<span class="suffix-label" style="color: #f56c6c;">人 ({{ formatPercent(statistics.focusPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
<div class="statistic-item">
|
||||
<el-statistic title="重点关注" :value="statistics.focusCount" value-style="color: #f56c6c;">
|
||||
<template #suffix>
|
||||
<span class="suffix-label" style="color: #f56c6c;">人 ({{ formatPercent(statistics.focusPercent) }})</span>
|
||||
</template>
|
||||
</el-statistic>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表展示 -->
|
||||
@@ -92,6 +100,10 @@
|
||||
<el-icon class="el-input__icon" @click="handleSearch"><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-button type="info" plain size="small" style="margin-left: 10px" @click="resetFilter">
|
||||
清除筛选
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,6 +114,7 @@
|
||||
:data="detailList"
|
||||
stripe
|
||||
style="width: 100%"
|
||||
:empty-text="filterStatus || searchKeyword ? '没有符合条件的数据' : '暂无数据'"
|
||||
>
|
||||
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
|
||||
<el-table-column label="姓名" align="center" prop="studentName" width="100" />
|
||||
@@ -132,6 +145,11 @@
|
||||
</el-table>
|
||||
|
||||
<div class="pagination-container">
|
||||
<div class="filter-info">
|
||||
<span v-if="filterStatus">当前筛选:{{ filterStatus === '1' ? '健康' : filterStatus === '2' ? '良好' : '重点关注' }}</span>
|
||||
<span v-if="searchKeyword">搜索关键词:{{ searchKeyword }}</span>
|
||||
<span>共找到 {{ total }} 条记录</span>
|
||||
</div>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
@@ -152,6 +170,8 @@ import * as echarts from 'echarts/core';
|
||||
import { PieChart } from 'echarts/charts';
|
||||
import { TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components';
|
||||
import { CanvasRenderer } from 'echarts/renderers';
|
||||
import { Search } from '@element-plus/icons-vue';
|
||||
import { ElLoading, ElMessage } from 'element-plus';
|
||||
|
||||
// 注册必要的组件
|
||||
echarts.use([PieChart, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]);
|
||||
@@ -200,6 +220,16 @@ const getEvaluationInfo = async () => {
|
||||
evaluationInfo.value = res.data;
|
||||
} catch (error) {
|
||||
console.error('获取测评信息失败', error);
|
||||
// 显示友好的错误提示,而不是"无权限"
|
||||
if (error.response && error.response.status === 403) {
|
||||
proxy?.$modal.msgError('您没有查看该测评的权限');
|
||||
} else {
|
||||
proxy?.$modal.msgError('获取测评信息失败,请稍后再试');
|
||||
}
|
||||
// 返回列表页
|
||||
setTimeout(() => {
|
||||
router.push('/xljk/evaluation');
|
||||
}, 1500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -223,12 +253,49 @@ const getStatistics = async () => {
|
||||
const getDetailList = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
// 使用getEvaluationDetailsByEvaluationId替代listEvaluationDetail
|
||||
// 因为listEvaluationDetail需要分页参数,而我们需要的是获取指定测评ID的所有明细
|
||||
console.log('查询参数:', {
|
||||
filterStatus: filterStatus.value,
|
||||
searchKeyword: searchKeyword.value,
|
||||
studentNo: queryParams.value.studentNo,
|
||||
studentName: queryParams.value.studentName
|
||||
});
|
||||
|
||||
// 使用getEvaluationDetailsByEvaluationId获取所有数据
|
||||
const res = await getEvaluationDetailsByEvaluationId(evaluationId.value);
|
||||
if (res.data) {
|
||||
detailList.value = res.data;
|
||||
total.value = res.data.length;
|
||||
// 存储原始数据
|
||||
const allDetails = res.data;
|
||||
console.log('获取到的原始数据:', allDetails.length, '条');
|
||||
|
||||
// 根据过滤条件筛选数据
|
||||
let filteredData = [...allDetails];
|
||||
|
||||
// 按心理状态筛选
|
||||
if (filterStatus.value) {
|
||||
filteredData = filteredData.filter(item => item.mentalStatus === filterStatus.value);
|
||||
console.log('按心理状态筛选后:', filteredData.length, '条');
|
||||
}
|
||||
|
||||
// 按学号或姓名搜索
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase();
|
||||
filteredData = filteredData.filter(item =>
|
||||
(item.studentNo && item.studentNo.toLowerCase().includes(keyword)) ||
|
||||
(item.studentName && item.studentName.toLowerCase().includes(keyword))
|
||||
);
|
||||
console.log('按关键词筛选后:', filteredData.length, '条');
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
const pageSize = queryParams.value.pageSize;
|
||||
const pageNum = queryParams.value.pageNum;
|
||||
const startIndex = (pageNum - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
|
||||
// 设置总数和当前页数据
|
||||
total.value = filteredData.length;
|
||||
detailList.value = filteredData.slice(startIndex, endIndex);
|
||||
console.log('最终显示数据:', detailList.value.length, '条');
|
||||
} else {
|
||||
detailList.value = [];
|
||||
total.value = 0;
|
||||
@@ -257,15 +324,26 @@ const initPieChart = () => {
|
||||
const option = {
|
||||
title: {
|
||||
text: '心理状态分布',
|
||||
left: 'center'
|
||||
left: 'center',
|
||||
textStyle: {
|
||||
fontSize: 14
|
||||
},
|
||||
top: 5,
|
||||
padding: 5
|
||||
},
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)'
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
orient: 'horizontal',
|
||||
left: 'center',
|
||||
bottom: 0,
|
||||
itemWidth: 12,
|
||||
itemHeight: 8,
|
||||
textStyle: {
|
||||
fontSize: 12
|
||||
},
|
||||
data: ['健康', '良好', '重点关注']
|
||||
},
|
||||
color: ['#67C23A', '#E6A23C', '#F56C6C'],
|
||||
@@ -273,8 +351,23 @@ const initPieChart = () => {
|
||||
{
|
||||
name: '心理状态',
|
||||
type: 'pie',
|
||||
radius: '60%',
|
||||
radius: ['35%', '60%'], // 使用环形图,更节省空间
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: true,
|
||||
itemStyle: {
|
||||
borderRadius: 4,
|
||||
borderWidth: 1,
|
||||
borderColor: '#fff'
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
formatter: '{b}: {c} ({d}%)',
|
||||
fontSize: 12
|
||||
},
|
||||
labelLine: {
|
||||
length: 10,
|
||||
length2: 5
|
||||
},
|
||||
data: [
|
||||
{ value: statistics.value.healthyCount, name: '健康' },
|
||||
{ value: statistics.value.goodCount, name: '良好' },
|
||||
@@ -282,9 +375,9 @@ const initPieChart = () => {
|
||||
],
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowBlur: 5,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)'
|
||||
shadowColor: 'rgba(0, 0, 0, 0.3)'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -311,12 +404,13 @@ const goBack = () => {
|
||||
|
||||
/** 导出结果 */
|
||||
const handleExport = () => {
|
||||
console.log('导出测评明细,测评ID:', evaluationId.value);
|
||||
proxy?.download(
|
||||
'xljk/evaluationDetail/export',
|
||||
'xljk/evaluation/detail/export',
|
||||
{
|
||||
evaluationId: evaluationId.value
|
||||
},
|
||||
`evaluation_detail_${evaluationId.value}_${new Date().getTime()}.xlsx`
|
||||
`心理健康测评明细_${evaluationId.value}_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
@@ -330,18 +424,15 @@ const handleFilterChange = () => {
|
||||
/** 处理搜索 */
|
||||
const handleSearch = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
|
||||
// 判断输入是否为数字(学号)或文本(姓名)
|
||||
const isNumeric = /^\d+$/.test(searchKeyword.value);
|
||||
|
||||
if (isNumeric) {
|
||||
queryParams.value.studentNo = searchKeyword.value;
|
||||
queryParams.value.studentName = '';
|
||||
} else {
|
||||
queryParams.value.studentName = searchKeyword.value;
|
||||
queryParams.value.studentNo = '';
|
||||
}
|
||||
|
||||
// 不再需要设置queryParams,直接使用searchKeyword
|
||||
getDetailList();
|
||||
};
|
||||
|
||||
/** 重置过滤条件 */
|
||||
const resetFilter = () => {
|
||||
filterStatus.value = '';
|
||||
searchKeyword.value = '';
|
||||
queryParams.value.pageNum = 1;
|
||||
getDetailList();
|
||||
};
|
||||
|
||||
@@ -394,15 +485,22 @@ onBeforeUnmount(() => {
|
||||
|
||||
/* 统计样式 */
|
||||
.statistics-container {
|
||||
padding: 10px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.statistics-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-around;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.statistic-item {
|
||||
flex: 1; /* 让统计项占据相同宽度 */
|
||||
min-width: 150px; /* 确保统计项有最小宽度 */
|
||||
text-align: center;
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
.suffix-label {
|
||||
@@ -414,13 +512,28 @@ onBeforeUnmount(() => {
|
||||
.chart-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.pie-chart {
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
width: 500px;
|
||||
height: 280px;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.statistics-card :deep(.el-card__body) {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.statistics-card :deep(.el-statistic) {
|
||||
--el-statistic-content-font-size: 20px;
|
||||
--el-statistic-title-font-size: 14px;
|
||||
--el-statistic-title-margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.statistics-card :deep(.el-card__header) {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
/* 分页容器样式 */
|
||||
@@ -432,6 +545,17 @@ onBeforeUnmount(() => {
|
||||
margin-top: 10px !important;
|
||||
}
|
||||
|
||||
.filter-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding: 0 20px;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.statistics-row {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -82,13 +82,35 @@
|
||||
:closable="false"
|
||||
/>
|
||||
|
||||
<!-- 批量操作区域 -->
|
||||
<div class="batch-actions" v-if="studentList.length > 0">
|
||||
<span class="batch-label">批量设置心理状态:</span>
|
||||
<el-radio-group v-model="batchMentalStatus">
|
||||
<el-radio-button label="1">健康</el-radio-button>
|
||||
<el-radio-button label="2">良好</el-radio-button>
|
||||
<el-radio-button label="3">重点关注</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-button type="primary" size="small" @click="applyBatchMentalStatus" :disabled="selectedStudents.length === 0">
|
||||
应用到所选学生
|
||||
</el-button>
|
||||
<el-button type="info" size="small" @click="applyBatchMentalStatusToAll">
|
||||
应用到全部学生
|
||||
</el-button>
|
||||
<el-button type="warning" size="small" @click="resetAllMentalStatus">
|
||||
重置所有状态
|
||||
</el-button>
|
||||
<span class="batch-hint">已选择 {{ selectedStudents.length }} 名学生</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-else
|
||||
v-if="studentList.length > 0"
|
||||
:data="studentList"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column label="学号" align="center" prop="studentNo" width="120" />
|
||||
<el-table-column label="姓名" align="center" prop="studentName" width="100" />
|
||||
<el-table-column label="性别" align="center" prop="gender" width="80">
|
||||
@@ -157,6 +179,7 @@
|
||||
import { getEvaluation, updateEvaluation, getStudentsByClass, getEvaluationDetailsByEvaluationId, batchUpdateMentalStatus } from '@/api/xljk/evaluation';
|
||||
import { XljkEvaluationForm, StudentInfo, XljkEvaluationDetailForm } from '@/api/xljk/evaluation/types';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
|
||||
@@ -202,7 +225,7 @@ const getEvaluationInfo = async () => {
|
||||
|
||||
// 检查状态是否为草稿
|
||||
if (form.value.status !== '0') {
|
||||
proxy?.$modal.msgError('只能编辑草稿状态的测评');
|
||||
ElMessage.error('只能编辑草稿状态的测评');
|
||||
setTimeout(() => {
|
||||
router.push('/xljk/evaluation');
|
||||
}, 1500);
|
||||
@@ -234,12 +257,12 @@ const getStudents = async () => {
|
||||
// 打印返回结果
|
||||
console.log('获取学生列表的结果:', res);
|
||||
|
||||
studentList.value = res.data || [];
|
||||
|
||||
// 为每个学生初始化心理状态
|
||||
studentList.value.forEach(student => {
|
||||
student.mentalStatus = student.mentalStatus || '1'; // 默认健康
|
||||
});
|
||||
// 设置默认的心理状态为"健康"
|
||||
studentList.value = (res.data || []).map(student => ({
|
||||
...student,
|
||||
mentalStatus: '1', // 默认设置为"健康"
|
||||
remark: '' // 初始化备注为空字符串
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('获取班级学生列表失败', error);
|
||||
studentList.value = [];
|
||||
@@ -288,6 +311,99 @@ const countFocusStudents = () => {
|
||||
return studentList.value.filter(student => student.mentalStatus === '3').length;
|
||||
};
|
||||
|
||||
/** 批量设置心理状态 */
|
||||
const batchMentalStatus = ref('1'); // 默认健康
|
||||
const selectedStudents = ref<StudentInfo[]>([]); // 选中的学生列表
|
||||
|
||||
const handleSelectionChange = (selection: StudentInfo[]) => {
|
||||
selectedStudents.value = selection;
|
||||
};
|
||||
|
||||
const applyBatchMentalStatus = async () => {
|
||||
if (selectedStudents.value.length === 0) {
|
||||
ElMessage.error('请选择要批量设置的学生');
|
||||
return;
|
||||
}
|
||||
const status = batchMentalStatus.value;
|
||||
const detailsToUpdate = selectedStudents.value.map(student => {
|
||||
return {
|
||||
id: student.detailId,
|
||||
evaluationId: form.value.id,
|
||||
studentId: student.studentId,
|
||||
studentNo: student.studentNo,
|
||||
studentName: student.studentName,
|
||||
gender: student.gender,
|
||||
birthDate: student.birthDate,
|
||||
phone: student.phone,
|
||||
mentalStatus: status,
|
||||
score: status === '3' ? 70 : undefined, // 默认分数
|
||||
remark: student.remark
|
||||
} as XljkEvaluationDetailForm;
|
||||
});
|
||||
await batchUpdateMentalStatus(detailsToUpdate);
|
||||
ElMessage.success(`已批量设置 ${selectedStudents.value.length} 名学生的心理状态为 ${status}`);
|
||||
// 刷新学生列表以更新状态
|
||||
await getStudents();
|
||||
await getEvaluationDetails();
|
||||
};
|
||||
|
||||
const applyBatchMentalStatusToAll = async () => {
|
||||
const status = batchMentalStatus.value;
|
||||
const detailsToUpdate = studentList.value.map(student => {
|
||||
return {
|
||||
id: student.detailId,
|
||||
evaluationId: form.value.id,
|
||||
studentId: student.studentId,
|
||||
studentNo: student.studentNo,
|
||||
studentName: student.studentName,
|
||||
gender: student.gender,
|
||||
birthDate: student.birthDate,
|
||||
phone: student.phone,
|
||||
mentalStatus: status,
|
||||
score: status === '3' ? 70 : undefined, // 默认分数
|
||||
remark: student.remark
|
||||
} as XljkEvaluationDetailForm;
|
||||
});
|
||||
await batchUpdateMentalStatus(detailsToUpdate);
|
||||
ElMessage.success(`已批量设置 ${studentList.value.length} 名学生的心理状态为 ${status}`);
|
||||
// 刷新学生列表以更新状态
|
||||
await getStudents();
|
||||
await getEvaluationDetails();
|
||||
};
|
||||
|
||||
const resetAllMentalStatus = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认要将所有学生的心理状态重置为"健康"吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
});
|
||||
|
||||
const detailsToUpdate = studentList.value.map(student => {
|
||||
return {
|
||||
id: student.detailId,
|
||||
evaluationId: form.value.id,
|
||||
studentId: student.studentId,
|
||||
studentNo: student.studentNo,
|
||||
studentName: student.studentName,
|
||||
gender: student.gender,
|
||||
birthDate: student.birthDate,
|
||||
phone: student.phone,
|
||||
mentalStatus: '1', // 重置为健康
|
||||
score: undefined,
|
||||
remark: student.remark
|
||||
} as XljkEvaluationDetailForm;
|
||||
});
|
||||
await batchUpdateMentalStatus(detailsToUpdate);
|
||||
ElMessage.success('已重置所有学生的心理状态');
|
||||
// 刷新学生列表以更新状态
|
||||
await getStudents();
|
||||
await getEvaluationDetails();
|
||||
} catch (error) {
|
||||
// 用户取消操作
|
||||
}
|
||||
};
|
||||
|
||||
/** 提交表单 */
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid: boolean) => {
|
||||
@@ -317,8 +433,8 @@ const submitForm = () => {
|
||||
// 批量更新学生心理状态
|
||||
await batchUpdateMentalStatus(detailsToUpdate);
|
||||
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
router.push('/xljk/evaluation');
|
||||
ElMessage.success('修改成功');
|
||||
router.replace('/xljk/evaluation');
|
||||
} catch (error) {
|
||||
console.error('修改测评失败', error);
|
||||
} finally {
|
||||
@@ -330,7 +446,7 @@ const submitForm = () => {
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
router.push('/xljk/evaluation');
|
||||
router.replace('/xljk/evaluation');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@@ -370,4 +486,22 @@ onMounted(() => {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.batch-actions {
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.batch-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.batch-hint {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-left: 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -109,7 +109,7 @@
|
||||
</template>
|
||||
|
||||
<script setup name="XljkEvaluationFill" lang="ts">
|
||||
import { getEvaluation, listEvaluationDetail, batchUpdateEvaluationDetail } from '@/api/xljk/evaluation';
|
||||
import { getEvaluation, listEvaluationDetail, batchUpdateMentalStatus } from '@/api/xljk/evaluation';
|
||||
import { XljkEvaluationVO, XljkEvaluationDetailVO, XljkEvaluationDetailForm } from '@/api/xljk/evaluation/types';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { throttle } from 'lodash-es';
|
||||
@@ -143,7 +143,7 @@ const getEvaluationInfo = async () => {
|
||||
if (evaluationInfo.value.status !== '1') {
|
||||
proxy?.$modal.msgError('只能填写"已发布"状态的测评');
|
||||
setTimeout(() => {
|
||||
router.push('/xljk/evaluation');
|
||||
router.replace('/xljk/evaluation');
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -176,7 +176,7 @@ const filledCount = computed(() => {
|
||||
|
||||
/** 返回列表页 */
|
||||
const goBack = () => {
|
||||
router.push('/xljk/evaluation');
|
||||
router.replace('/xljk/evaluation');
|
||||
};
|
||||
|
||||
/** 处理心理状态变更 */
|
||||
@@ -248,7 +248,7 @@ const saveEvaluation = async () => {
|
||||
// 提交数据
|
||||
try {
|
||||
saveLoading.value = true;
|
||||
await batchUpdateEvaluationDetail(updateData);
|
||||
await batchUpdateMentalStatus(updateData);
|
||||
proxy?.$modal.msgSuccess('保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存失败', error);
|
||||
|
||||
@@ -169,41 +169,15 @@
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 草稿状态可发布 -->
|
||||
<el-tooltip content="发布" placement="top" v-if="scope.row.status === '0'">
|
||||
<!-- 草稿状态可提交 -->
|
||||
<el-tooltip content="提交" placement="top" v-if="scope.row.status === '0'">
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
icon="Check"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handlePublish(scope.row)"
|
||||
v-hasPermi="['xljk:evaluation:edit']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 已发布状态可填写 -->
|
||||
<el-tooltip content="填写" placement="top" v-if="scope.row.status === '1'">
|
||||
<el-button
|
||||
link
|
||||
type="warning"
|
||||
icon="Edit"
|
||||
size="default"
|
||||
style="font-size: 16px; margin-right: 8px"
|
||||
@click="handleEdit(scope.row)"
|
||||
v-hasPermi="['xljk:evaluation:edit']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<!-- 已发布状态可完成 -->
|
||||
<el-tooltip content="完成" placement="top" v-if="scope.row.status === '1'">
|
||||
<el-button
|
||||
link
|
||||
type="success"
|
||||
icon="CircleCheck"
|
||||
size="default"
|
||||
style="font-size: 16px"
|
||||
@click="handleComplete(scope.row)"
|
||||
@click="handleSubmit(scope.row)"
|
||||
v-hasPermi="['xljk:evaluation:edit']"
|
||||
></el-button>
|
||||
</el-tooltip>
|
||||
@@ -219,7 +193,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listEvaluation, getEvaluation, delEvaluation, addEvaluation, updateEvaluation, publishEvaluation, completeEvaluation, getCollegeOptions, getMajorOptions, getClassOptions, getSemesterOptions } from '@/api/xljk/evaluation';
|
||||
import { listEvaluation, getEvaluation, delEvaluation, addEvaluation, updateEvaluation, completeEvaluation, getCollegeOptions, getMajorOptions, getClassOptions, getSemesterOptions } from '@/api/xljk/evaluation';
|
||||
import { XljkEvaluationVO, XljkEvaluationForm, XljkEvaluationQuery, CollegeOption, MajorOption, ClassOption, SemesterOption } from '@/api/xljk/evaluation/types';
|
||||
|
||||
// 基本变量
|
||||
@@ -376,10 +350,7 @@ const handleUpdate = (row?: XljkEvaluationVO) => {
|
||||
proxy?.$router.push({ path: `/xljk/evaluation/edit/${id}` });
|
||||
};
|
||||
|
||||
/** 填写评测 */
|
||||
const handleEdit = (row: XljkEvaluationVO) => {
|
||||
proxy?.$router.push({ path: `/xljk/evaluation/fill/${row.id}` });
|
||||
};
|
||||
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: XljkEvaluationVO) => {
|
||||
@@ -412,26 +383,13 @@ const handleDelete = async (row?: XljkEvaluationVO) => {
|
||||
}
|
||||
};
|
||||
|
||||
/** 发布按钮操作 */
|
||||
const handlePublish = async (row: XljkEvaluationVO) => {
|
||||
await proxy?.$modal.confirm(`是否确认发布测评"${row.evaluationName}"?发布后将无法修改测评基本信息。`);
|
||||
try {
|
||||
loading.value = true;
|
||||
await publishEvaluation(row.id);
|
||||
proxy?.$modal.msgSuccess('发布成功');
|
||||
await getList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 完成按钮操作 */
|
||||
const handleComplete = async (row: XljkEvaluationVO) => {
|
||||
await proxy?.$modal.confirm(`是否确认完成测评"${row.evaluationName}"?完成后将无法再修改测评结果。`);
|
||||
/** 提交按钮操作 */
|
||||
const handleSubmit = async (row: XljkEvaluationVO) => {
|
||||
await proxy?.$modal.confirm(`是否确认提交测评"${row.evaluationName}"?提交后将无法再修改测评结果。`);
|
||||
try {
|
||||
loading.value = true;
|
||||
await completeEvaluation(row.id);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
proxy?.$modal.msgSuccess('提交成功');
|
||||
await getList();
|
||||
} finally {
|
||||
loading.value = false;
|
||||
@@ -449,11 +407,16 @@ const handleExport = () => {
|
||||
);
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getDepartments();
|
||||
getMajors();
|
||||
getSemesters();
|
||||
getList();
|
||||
});
|
||||
|
||||
// 监听路由变化,确保从其他页面返回时重新加载数据
|
||||
onActivated(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in new issue
Block a user