心理健康2.0

This commit is contained in:
李旭明 committed 2025-09-16 10:34:38 +08:00
1 parent 067957256d
commit 3b492479b5
63 files changed
+10278 -642

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
import request from '@/utils/request'
import { AxiosPromise } from 'axios'
import { StglAuditorVO, StglAuditorForm, StglAuditorQuery } from './types'
/**
* 查询社团管理审核人配置列表
* @param query
* @returns {*}
*/
export const listStglAuditor = (query?: StglAuditorQuery): AxiosPromise<StglAuditorVO[]> => {
return request({
url: '/stgl/auditor/list',
method: 'get',
params: query
})
}
/**
* 查询社团管理审核人配置详细
* @param id
*/
export const getStglAuditor = (id: string | number): AxiosPromise<StglAuditorVO> => {
return request({
url: '/stgl/auditor/' + id,
method: 'get'
})
}
/**
* 新增社团管理审核人配置
* @param data
*/
export const addStglAuditor = (data: StglAuditorForm) => {
return request({
url: '/stgl/auditor',
method: 'post',
data: data
})
}
/**
* 修改社团管理审核人配置
* @param data
*/
export const updateStglAuditor = (data: StglAuditorForm) => {
return request({
url: '/stgl/auditor',
method: 'put',
data: data
})
}
/**
* 删除社团管理审核人配置
* @param id
*/
export const delStglAuditor = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/auditor/' + id,
method: 'delete'
})
}
/**
* 导出社团管理审核人配置
* @param query
* @returns {*}
*/
export const exportStglAuditor = (query?: StglAuditorQuery) => {
return request({
url: '/stgl/auditor/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 批量添加审核人
* @param userIds 用户ID列表
* @param remark 备注
*/
export const batchAddAuditors = (userIds: number[], remark?: string) => {
return request({
url: '/stgl/auditor/batchAdd',
method: 'post',
data: {
userIds,
remark
}
})
}
/**
* 查询我配置的审核人列表
* @param query
*/
export const listMyConfigAuditors = (query?: StglAuditorQuery): AxiosPromise<StglAuditorVO[]> => {
return request({
url: '/stgl/auditor/myConfig',
method: 'get',
params: query
})
}
/**
* 批量启用审核人
* @param ids
*/
export const enableAuditors = (ids: number[]) => {
return request({
url: '/stgl/auditor/enable',
method: 'post',
data: ids
})
}
/**
* 批量停用审核人
* @param ids
*/
export const disableAuditors = (ids: number[]) => {
return request({
url: '/stgl/auditor/disable',
method: 'post',
data: ids
})
}
/**
* 根据部门ID获取用户列表
* @param deptId
*/
export const listUserByDeptId = (deptId: string | number) => {
return request({
url: '/stgl/auditor/users/' + deptId,
method: 'get'
})
}
+184
View File
@@ -0,0 +1,184 @@
import { PageQuery } from '@/api/types'
export interface StglAuditorVO {
/**
* 主键ID
*/
id: string | number;
/**
* 用户ID
*/
userId: number;
/**
* 用户账号
*/
userName: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 部门ID
*/
deptId: number;
/**
* 部门名称
*/
deptName: string;
/**
* 联系电话
*/
phoneNumber: string;
/**
* 邮箱地址
*/
email: string;
/**
* 状态(1正常 0停用)
*/
status: string;
/**
* 备注说明
*/
remark: string;
/**
* 配置人ID
*/
configBy: number;
/**
* 配置人姓名
*/
configByName: string;
/**
* 创建者
*/
createBy: number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: number;
/**
* 更新时间
*/
updateTime: string;
}
export interface StglAuditorForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 部门名称
*/
deptName?: string;
/**
* 联系电话
*/
phoneNumber?: string;
/**
* 邮箱地址
*/
email?: string;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 备注说明
*/
remark?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 配置人姓名
*/
configByName?: string;
}
export interface StglAuditorQuery extends PageQuery {
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 日期范围参数
*/
'params[beginCreateTime]'?: string;
'params[endCreateTime]'?: string;
}
@@ -0,0 +1,109 @@
import request from '@/utils/request';
import { AxiosPromise } from 'axios';
import { StglAnnualReviewAuditVO, StglAnnualReviewAuditForm, StglAnnualReviewAuditQuery } from './type';
/**
* 查询社团年审审批列表
* @param query
* @returns {*}
*/
export const listStglAnnualReviewAudit = (query?: StglAnnualReviewAuditQuery): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/list',
method: 'get',
params: query
});
};
/**
* 查询社团年审审批详细
* @param id
*/
export const getStglAnnualReviewAudit = (id: string | number): AxiosPromise<StglAnnualReviewAuditVO> => {
return request({
url: '/stgl/annualReviewAudit/' + id,
method: 'get'
});
};
/**
* 根据年审记录ID查询审批记录
* @param annualReviewId
*/
export const getAnnualReviewAuditsByReviewId = (annualReviewId: string | number): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/byAnnualReview/' + annualReviewId,
method: 'get'
});
};
/**
* 根据审批人ID查询待审批记录
* @param approverId
*/
export const getPendingAuditsByApproverId = (approverId: string | number): AxiosPromise<StglAnnualReviewAuditVO[]> => {
return request({
url: '/stgl/annualReviewAudit/pending/' + approverId,
method: 'get'
});
};
/**
* 新增社团年审审批
* @param data
*/
export const addStglAnnualReviewAudit = (data: StglAnnualReviewAuditForm) => {
return request({
url: '/stgl/annualReviewAudit',
method: 'post',
data: data
});
};
/**
* 修改社团年审审批
* @param data
*/
export const updateStglAnnualReviewAudit = (data: StglAnnualReviewAuditForm) => {
return request({
url: '/stgl/annualReviewAudit',
method: 'put',
data: data
});
};
/**
* 删除社团年审审批
* @param id
*/
export const delStglAnnualReviewAudit = (id: string | number | Array<string | number>) => {
return request({
url: '/stgl/annualReviewAudit/' + id,
method: 'delete'
});
};
/**
* 审批年审记录
* @param auditId 审批记录ID
* @param approveResult 审批结果
* @param approveOpinion 审批意见
* @param nextAuditors 下一步审核人列表(可选)
*/
export const approveAnnualReview = (
auditId: string | number,
approveResult: string,
approveOpinion: string,
nextAuditors?: Array<{userId: string | number; userName: string; nickName: string}>
) => {
return request({
url: '/stgl/annualReviewAudit/approve',
method: 'post',
data: {
auditId,
approveResult,
approveOpinion,
nextAuditors: nextAuditors || []
}
});
};
@@ -0,0 +1,182 @@
export interface StglAnnualReviewAuditVO {
/**
* 主键ID
*/
id: string | number;
/**
* 年审记录ID
*/
annualReviewId: string | number;
/**
* 节点代码(advisor_approve/college_approve/youth_league_approve)
*/
nodeCode: string;
/**
* 节点名称(指导老师审批/学院审批/校团委审批)
*/
nodeName: string;
/**
* 节点顺序(1.1,1.2,2,3)
*/
nodeOrder: number;
/**
* 节点状态(pending/waiting/approved)
*/
nodeStatus: string;
/**
* 审批人ID
*/
approverId: string | number;
/**
* 审批人姓名
*/
approverName: string;
/**
* 审批结果(通过/不通过)
*/
approveResult: string;
/**
* 审批意见
*/
approveOpinion: string;
/**
* 审批时间
*/
approveTime: string;
/**
* 创建者
*/
createBy: string | number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: string | number;
/**
* 更新时间
*/
updateTime: string;
}
export interface StglAnnualReviewAuditForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 年审记录ID
*/
annualReviewId?: string | number;
/**
* 节点代码(advisor_approve/college_approve/youth_league_approve)
*/
nodeCode?: string;
/**
* 节点名称(指导老师审批/学院审批/校团委审批)
*/
nodeName?: string;
/**
* 节点顺序(1.1,1.2,2,3)
*/
nodeOrder?: number;
/**
* 节点状态(pending/waiting/approved)
*/
nodeStatus?: string;
/**
* 审批人ID
*/
approverId?: string | number;
/**
* 审批人姓名
*/
approverName?: string;
/**
* 审批结果(通过/不通过)
*/
approveResult?: string;
/**
* 审批意见
*/
approveOpinion?: string;
/**
* 审批时间
*/
approveTime?: string;
}
export interface StglAnnualReviewAuditQuery extends PageQuery {
/**
* 年审记录ID
*/
annualReviewId?: string | number;
/**
* 节点代码
*/
nodeCode?: string;
/**
* 节点状态
*/
nodeStatus?: string;
/**
* 审批人ID
*/
approverId?: string | number;
/**
* 审批结果
*/
approveResult?: string;
/**
* 审批时间范围
*/
params?: any;
}
/**
* 审批节点状态选项
*/
export const AUDIT_NODE_STATUS_OPTIONS = [
{ label: '待审批', value: 'pending' },
{ label: '等待中', value: 'waiting' },
{ label: '已审批', value: 'approved' }
];
/**
* 审批结果选项
*/
export const APPROVE_RESULT_OPTIONS = [
{ label: '通过', value: '通过' },
{ label: '不通过', value: '不通过' }
];
+153 -2
View File
@@ -4,18 +4,23 @@ import {
XljkConsultationScheduleVO,
XljkConsultationScheduleForm,
XljkConsultationScheduleQuery,
XljkConsultationScheduleSummaryVO,
XljkConsultationAppointmentVO,
XljkConsultationAppointmentForm,
XljkConsultationAppointmentQuery,
AvailableTimeSlot,
TimeSlotDetail,
CounselorOption
CounselorOption,
ScheduleWithStatus,
XljkConsulterVO,
XljkConsulterForm,
XljkConsulterQuery
} from './types';
// ===== 预约时段管理 API =====
// 查询预约时段列表
export const listXljkSchedule = (query: XljkConsultationScheduleQuery): AxiosPromise<XljkConsultationScheduleVO[]> => {
export const listXljkSchedule = (query: XljkConsultationScheduleQuery): AxiosPromise<XljkConsultationScheduleSummaryVO[]> => {
return request({
url: '/xljk/consultation/schedule/list',
method: 'get',
@@ -102,6 +107,15 @@ export const getXljkAvailableTimeSlots = (startDate: string, endDate: string): A
});
};
// 获取日历视图的排班数据(带预约状态)
export const getXljkCalendarSchedules = (startDate: string, endDate: string): AxiosPromise<ScheduleWithStatus[]> => {
return request({
url: '/xljk/consultation/appointment/calendar-schedules',
method: 'get',
params: { startDate, endDate }
});
};
// 获取可用的详细时段列表(30分钟一个时段)
export const getXljkAvailableDetailSlots = (date: string, timeSlot: string): AxiosPromise<TimeSlotDetail[]> => {
return request({
@@ -213,3 +227,140 @@ export const getXljkConsultationClassOptions = (collegeName?: string, majorName?
params: { collegeName, majorName }
});
};
/**
* 查询心理咨询师配置列表
* @param query
* @returns {*}
*/
export const listXljkConsulter = (query?: XljkConsulterQuery): AxiosPromise<XljkConsulterVO[]> => {
return request({
url: '/xljk/consulter/list',
method: 'get',
params: query
})
}
/**
* 查询心理咨询师配置详细
* @param id
*/
export const getXljkConsulter = (id: string | number): AxiosPromise<XljkConsulterVO> => {
return request({
url: '/xljk/consulter/' + id,
method: 'get'
})
}
/**
* 新增心理咨询师配置
* @param data
*/
export const addXljkConsulter = (data: XljkConsulterForm) => {
return request({
url: '/xljk/consulter',
method: 'post',
data: data
})
}
/**
* 修改心理咨询师配置
* @param data
*/
export const updateXljkConsulter = (data: XljkConsulterForm) => {
return request({
url: '/xljk/consulter',
method: 'put',
data: data
})
}
/**
* 删除心理咨询师配置
* @param id
*/
export const delXljkConsulter = (id: string | number | Array<string | number>) => {
return request({
url: '/xljk/consulter/' + id,
method: 'delete'
})
}
/**
* 导出心理咨询师配置
* @param query
* @returns {*}
*/
export const exportXljkConsulter = (query?: XljkConsulterQuery) => {
return request({
url: '/xljk/consulter/export',
method: 'post',
data: query,
responseType: 'blob'
})
}
/**
* 批量添加咨询师
* @param userIds 用户ID列表
* @param remark 备注
*/
export const batchAddConsulters = (userIds: number[], remark?: string) => {
return request({
url: '/xljk/consulter/batchAdd',
method: 'post',
data: {
userIds,
remark
}
})
}
/**
* 查询我配置的咨询师列表
* @param query
*/
export const listMyConfigConsulters = (query?: XljkConsulterQuery): AxiosPromise<XljkConsulterVO[]> => {
return request({
url: '/xljk/consulter/myConfig',
method: 'get',
params: query
})
}
/**
* 批量启用咨询师
* @param ids
*/
export const enableConsulters = (ids: number[]) => {
return request({
url: '/xljk/consulter/enable',
method: 'post',
data: ids
})
}
/**
* 批量停用咨询师
* @param ids
*/
export const disableConsulters = (ids: number[]) => {
return request({
url: '/xljk/consulter/disable',
method: 'post',
data: ids
})
}
/**
* 根据部门ID获取用户列表
* @param deptId
*/
export const listUserByDeptId = (deptId: string | number) => {
return request({
url: '/xljk/consulter/users/' + deptId,
method: 'get'
})
}
+390 -9
View File
@@ -1,3 +1,5 @@
import { PageQuery } from '@/api/types'
export interface XljkConsultationScheduleVO {
/**
* 主键ID
@@ -14,6 +16,11 @@ export interface XljkConsultationScheduleVO {
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
@@ -71,6 +78,11 @@ export interface XljkConsultationScheduleForm extends BaseEntity {
*/
counselorId?: number;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
@@ -103,6 +115,11 @@ export interface XljkConsultationScheduleQuery extends PageQuery {
*/
counselorId?: number;
/**
* 办公室
*/
office?: string;
/**
* 排班日期开始
*/
@@ -130,6 +147,10 @@ export interface XljkConsultationScheduleQuery extends PageQuery {
}
export interface XljkConsultationAppointmentVO extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 学生ID
*/
@@ -163,13 +184,13 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
* 咨询主题
*/
status?: number;
consultTheme?: string;
/**
* 实际到访时间
* 状态(0待咨询 1已完成 2已取消 3未到访 4待审核 5已驳回)
*/
actualVisitTime?: any;
status?: number | string;
/**
* 沟通时长(分钟)
*/
@@ -186,6 +207,26 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 学生姓名
*/
studentName?: string;
/**
* 心理病史(0-是,1-否)
*/
psychologicalHistory?: number;
/**
* 心理病史详情
*/
psychologicalHistoryDetail?: string;
/**
* 基本情况
*/
basicSituation?: string;
/**
* 重大生活事件
*/
majorLifeEvents?: string;
/**
* 是否需要转介(0-是,1-否)
*/
needReferral?: number;
/**
* 班级
*/
@@ -194,6 +235,10 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 咨询师姓名
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 院系
*/
@@ -202,6 +247,10 @@ export interface XljkConsultationAppointmentVO extends BaseEntity {
* 专业
*/
majorName?: string;
/**
* 本学期到访次数
*/
semesterVisitCount?: number;
}
export interface XljkConsultationAppointmentForm {
@@ -242,13 +291,13 @@ export interface XljkConsultationAppointmentForm {
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
* 咨询主题
*/
status?: number;
consultTheme?: string;
/**
* 实际到访时间
* 状态(0待咨询 1已完成 2已取消 3未到访 4待审核 5已驳回)
*/
actualVisitTime?: any;
status?: number | string;
/**
* 沟通时长(分钟)
*/
@@ -261,6 +310,30 @@ export interface XljkConsultationAppointmentForm {
* 咨询师建议
*/
counselorSuggestion?: string;
/**
* 学生姓名
*/
studentName?: string;
/**
* 心理病史(0-是,1-否)
*/
psychologicalHistory?: number;
/**
* 心理病史详情
*/
psychologicalHistoryDetail?: string;
/**
* 基本情况
*/
basicSituation?: string;
/**
* 重大生活事件
*/
majorLifeEvents?: string;
/**
* 是否需要转介(0-是,1-否)
*/
needReferral?: number;
}
export interface XljkConsultationAppointmentQuery extends PageQuery {
@@ -289,6 +362,11 @@ export interface XljkConsultationAppointmentQuery extends PageQuery {
*/
majorName?: string;
/**
* 班级
*/
className?: string;
/**
* 咨询师ID
*/
@@ -312,7 +390,11 @@ export interface XljkConsultationAppointmentQuery extends PageQuery {
/**
* 日期范围参数
*/
params?: any;
params?: {
beginAppointmentDate?: string;
endAppointmentDate?: string;
[key: string]: any;
};
}
export interface AvailableTimeSlot {
@@ -331,6 +413,11 @@ export interface AvailableTimeSlot {
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 日期
*/
@@ -419,6 +506,11 @@ export interface CounselorOption {
* 咨询师姓名
*/
name: string;
/**
* 办公室
*/
office: string;
}
// 预约时段汇总视图对象
@@ -433,6 +525,7 @@ export interface XljkConsultationScheduleSummaryVO {
deleted: number;
counselorId: number;
counselorName: string;
office: string;
dateRange: string;
morningEnabled: boolean;
morningTime: string;
@@ -602,3 +695,291 @@ export interface NoShowVO {
*/
updateTime?: string;
}
/**
* 带预约状态的排班数据
*/
export interface ScheduleWithStatus {
/**
* 排班ID
*/
scheduleId: number;
/**
* 咨询师ID
*/
counselorId: number;
/**
* 咨询师姓名
*/
counselorName?: string;
/**
* 办公室
*/
office?: string;
/**
* 排班日期
*/
date: string;
/**
* 时段类型(0上午 1中午 2下午)
*/
timeSlot: string;
/**
* 开始时间(HH:mm)
*/
startTime: string;
/**
* 结束时间(HH:mm)
*/
endTime: string;
/**
* 时段显示文本(如:上午 08:30-12:00)
*/
timeDisplay: string;
/**
* 是否已预约满
*/
isFullyBooked: boolean;
/**
* 是否已过期
*/
isExpired: boolean;
/**
* 可预约数量
*/
availableSlots: number;
/**
* 总预约数量
*/
totalSlots: number;
/**
* 创建者
*/
createBy?: number;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新者
*/
updateBy?: number;
/**
* 更新时间
*/
updateTime?: string;
}
export interface XljkConsulterVO {
/**
* 主键ID
*/
id: string | number;
/**
* 用户ID
*/
userId: number;
/**
* 用户账号
*/
userName: string;
/**
* 用户昵称
*/
nickName: string;
/**
* 部门ID
*/
deptId: number;
/**
* 部门名称
*/
deptName: string;
/**
* 办公室
*/
office?: string;
/**
* 联系电话
*/
phoneNumber: string;
/**
* 邮箱地址
*/
email: string;
/**
* 状态(1正常 0停用)
*/
status: string;
/**
* 备注说明
*/
remark: string;
/**
* 配置人ID
*/
configBy: number;
/**
* 配置人姓名
*/
configByName: string;
/**
* 创建者
*/
createBy: number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: number;
/**
* 更新时间
*/
updateTime: string;
}
export interface XljkConsulterForm {
/**
* 主键ID
*/
id?: string | number;
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 部门名称
*/
deptName?: string;
/**
* 办公室
*/
office?: string;
/**
* 联系电话
*/
phoneNumber?: string;
/**
* 邮箱地址
*/
email?: string;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 备注说明
*/
remark?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 配置人姓名
*/
configByName?: string;
}
export interface XljkConsulterQuery extends PageQuery {
/**
* 用户ID
*/
userId?: number;
/**
* 用户账号
*/
userName?: string;
/**
* 用户昵称
*/
nickName?: string;
/**
* 部门ID
*/
deptId?: number;
/**
* 状态(1正常 0停用)
*/
status?: string;
/**
* 配置人ID
*/
configBy?: number;
/**
* 办公室
*/
office?: string;
/**
* 日期范围参数
*/
'params[beginCreateTime]'?: string;
'params[endCreateTime]'?: string;
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,350 @@
<template>
<el-dialog
title="年审审批"
v-model="visible"
width="600px"
append-to-body
:before-close="handleClose"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="社团名称:">
<span>{{ auditInfo?.clubName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="年审年度:">
<span>{{ auditInfo?.reviewYear || '加载中...' }}年</span>
</el-form-item>
<el-form-item label="审批节点:">
<span>{{ auditRecord?.nodeName || '加载中...' }}</span>
</el-form-item>
<el-form-item label="审批结果:" prop="approveResult">
<el-radio-group v-model="form.approveResult" @change="handleApproveResultChange">
<el-radio value="通过">通过</el-radio>
<el-radio value="不通过">不通过</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审批意见:" prop="approveOpinion">
<el-input
v-model="form.approveOpinion"
type="textarea"
:rows="4"
placeholder="请填写审批意见"
maxlength="500"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 审批流程选择对话框 -->
<ApprovalProcessDialog
v-model:visible="showApprovalProcessDialog"
:next-node-name="getNextNodeName()"
@confirm="handleApprovalProcessConfirm"
/>
</template>
<script setup lang="ts">
import { approveAnnualReview, getStglAnnualReviewAudit, getAnnualReviewAuditsByReviewId } from '@/api/stgl/club/annualReviewAudit';
import { getStglClubAnnualReview } from '@/api/stgl/club/annualReview';
import ApprovalProcessDialog from './auditorSelector/ApprovalProcessDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const emit = defineEmits<{
success: [];
}>();
const visible = ref(false);
const loading = ref(false);
const formRef = ref<ElFormInstance>();
const form = ref({
auditId: '',
approveResult: '',
approveOpinion: ''
});
const auditInfo = ref<any>(null);
const auditRecord = ref<any>(null);
const allAuditRecords = ref<any[]>([]);
const showApprovalProcessDialog = ref(false);
const rules = reactive<ElFormRules>({
approveResult: [
{ required: true, message: '请选择审批结果', trigger: 'change' }
],
approveOpinion: [
{ required: true, message: '请填写审批意见', trigger: 'blur' },
{ max: 500, message: '审批意见不能超过500个字符', trigger: 'blur' }
]
});
/** 打开弹窗 */
const open = async (auditId: string | number, annualReviewId: string | number) => {
visible.value = true;
form.value.auditId = auditId.toString();
// 获取年审记录信息用于显示
try {
const res = await getStglClubAnnualReview(annualReviewId);
auditInfo.value = res.data;
} catch (error) {
console.error('获取年审记录信息失败:', error);
}
// 获取审批记录信息用于显示节点名称
try {
const auditRes = await getStglAnnualReviewAudit(auditId);
auditRecord.value = auditRes.data;
} catch (error) {
console.error('获取审批记录信息失败:', error);
}
// 获取当前年审的所有审批记录,用于判断是否是本环节最后一个节点
try {
const allAuditsRes = await getAnnualReviewAuditsByReviewId(annualReviewId);
allAuditRecords.value = allAuditsRes.data || [];
} catch (error) {
console.error('获取所有审批记录失败:', error);
}
// 重置表单
resetForm();
};
/** 重置表单 */
const resetForm = () => {
form.value.approveResult = '';
form.value.approveOpinion = '';
formRef.value?.clearValidate();
};
/** 审批结果变化处理 */
const handleApproveResultChange = (value: string) => {
// 审批结果变化时的处理逻辑(如需要)
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
resetForm();
auditInfo.value = null;
auditRecord.value = null;
allAuditRecords.value = [];
};
/** 提交审批 */
const handleSubmit = async () => {
if (!formRef.value) return;
const valid = await formRef.value.validate();
if (!valid) return;
// 如果是校团委审批的最后一个节点,直接结束流程,不弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isSchoolCommitteeLastNode()) {
console.log('校团委最后节点,直接结束流程');
await submitApproval();
return;
}
// 如果审批通过,且当前节点是本环节的最后一个节点,弹出审批流程选择对话框
if (form.value.approveResult === '通过' && isLastNodeInCurrentPhase()) {
showApprovalProcessDialog.value = true;
return;
}
// 直接提交审批(审批不通过或不是本环节最后一个节点的情况)
await submitApproval();
};
/** 判断当前节点是否是本环节的最后一个节点 */
const isLastNodeInCurrentPhase = () => {
if (!auditRecord.value || !allAuditRecords.value.length) {
console.log('判断是否是最后节点: 数据不足', {
hasAuditRecord: !!auditRecord.value,
allAuditRecordsLength: allAuditRecords.value.length
});
return false;
}
const currentNodeCode = auditRecord.value.nodeCode;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
console.log('当前审批记录详情:', {
currentNodeCode,
currentNodeOrder,
currentNodeOrderType: typeof auditRecord.value.nodeOrder,
currentNodeOrderRaw: auditRecord.value.nodeOrder,
auditRecordKeys: Object.keys(auditRecord.value)
});
// 获取同一环节的所有节点
const samePhaseNodes = allAuditRecords.value.filter(record =>
record.nodeCode === currentNodeCode
);
console.log('所有审批记录:', allAuditRecords.value.map(record => ({
id: record.id,
nodeCode: record.nodeCode,
nodeName: record.nodeName,
nodeOrder: record.nodeOrder,
nodeOrderType: typeof record.nodeOrder,
approverName: record.approverName,
nodeStatus: record.nodeStatus
})));
console.log('同环节节点:', samePhaseNodes.map(n => ({
nodeOrder: n.nodeOrder,
nodeOrderType: typeof n.nodeOrder,
approverName: n.approverName,
nodeStatus: n.nodeStatus
})));
if (samePhaseNodes.length === 0) {
console.log('警告: 没有找到同环节的节点');
return false;
}
// 找出同一环节中最大的nodeOrder,使用Number转换确保数值比较
const nodeOrders = samePhaseNodes.map(node => Number(node.nodeOrder));
const maxNodeOrder = Math.max(...nodeOrders);
console.log('判断是否是最后节点:', {
currentNodeCode,
currentNodeOrder,
maxNodeOrder,
nodeOrders,
isLast: currentNodeOrder === maxNodeOrder,
comparison: `${currentNodeOrder} === ${maxNodeOrder}`
});
// 当前节点的nodeOrder等于同环节最大nodeOrder时,表示是最后一个节点
return currentNodeOrder === maxNodeOrder;
};
/** 判断当前是否是校团委审批的最后一个节点 */
const isSchoolCommitteeLastNode = () => {
if (!auditRecord.value) {
return false;
}
const currentNodeName = auditRecord.value.nodeName;
const currentNodeOrder = Number(auditRecord.value.nodeOrder);
// 检查是否是校团委审批节点且node_order是3.X格式
const isSchoolCommitteeNode = currentNodeName === '校团委审批';
const isNodeOrder3X = currentNodeOrder >= 3 && currentNodeOrder < 4;
console.log('校团委最后节点判断:', {
currentNodeName,
currentNodeOrder,
isSchoolCommitteeNode,
isNodeOrder3X,
isLastInPhase: isLastNodeInCurrentPhase()
});
// 校团委审批 + node_order=3.X + 是本环节最后一个节点
return isSchoolCommitteeNode && isNodeOrder3X && isLastNodeInCurrentPhase();
};
/** 获取下一步节点名称 */
const getNextNodeName = () => {
// 根据当前节点确定下一步节点
const currentNodeName = auditRecord.value?.nodeName;
if (currentNodeName === '指导老师审批') {
return '二级学院审批';
} else if (currentNodeName === '二级学院审批') {
return '校团委审批';
} else if (currentNodeName === '校团委审批') {
return '完成审批';
}
// 默认返回二级学院审批
return '二级学院审批';
};
/** 实际提交审批 */
const submitApproval = async (approvalProcessData?: any) => {
loading.value = true;
try {
// 判断是否是校团委最后节点
const isSchoolCommitteeFinalNode = isSchoolCommitteeLastNode();
// 准备审批数据
const approveData = {
auditId: form.value.auditId,
approveResult: form.value.approveResult,
approveOpinion: form.value.approveOpinion,
// 校团委最后节点时不设置下一步审核人,表示流程结束
// 其他情况:只有审批通过且是本环节最后一个节点时,才包含下一步审核人信息
nextAuditors: isSchoolCommitteeFinalNode ? [] :
(form.value.approveResult === '通过' && isLastNodeInCurrentPhase()) ?
(approvalProcessData?.auditors || []).map((auditor: any) => ({
userId: auditor.userId,
userName: auditor.userName,
nickName: auditor.nickName
})) : []
};
console.log('提交审批数据:', {
审批结果: approveData.approveResult,
是否最后节点: isLastNodeInCurrentPhase(),
是否校团委最后节点: isSchoolCommitteeFinalNode,
下一步审核人数量: approveData.nextAuditors.length,
流程状态: isSchoolCommitteeFinalNode ? '流程结束' : '继续流转'
});
await approveAnnualReview(
approveData.auditId,
approveData.approveResult,
approveData.approveOpinion,
approveData.nextAuditors
);
if (isSchoolCommitteeFinalNode) {
proxy?.$modal.msgSuccess('审批成功,年审流程已完成!');
} else {
proxy?.$modal.msgSuccess('审批成功');
}
handleClose();
emit('success');
} catch (error) {
console.error('审批失败:', error);
} finally {
loading.value = false;
}
};
/** 审批流程确认处理 */
const handleApprovalProcessConfirm = async (approvalProcessData: any) => {
console.log('审批流程确认,数据:', approvalProcessData);
await submitApproval(approvalProcessData);
};
defineExpose({
open
});
</script>
<style scoped>
.dialog-footer {
text-align: right;
}
</style>
@@ -0,0 +1,384 @@
<template>
<el-dialog
title="选择审批流程"
v-model="visible"
width="70%"
append-to-body
destroy-on-close
:close-on-click-modal="false"
:close-on-press-escape="false"
>
<div class="approval-dialog-content">
<el-alert
:title="`请选择${props.nextNodeName}审核人`"
description="社团年审审批提交后将按照您选择的审批人员顺序进行审批,请仔细选择审核人员。"
type="info"
:closable="false"
show-icon
style="margin-bottom: 20px"
/>
<!-- 下一步审批节点提示 -->
<div class="next-node-info">
<div class="node-label">
<el-icon><Document /></el-icon>
下一步审批节点:{{ props.nextNodeName }}
</div>
</div>
<!-- 当前已选择的审核人显示 -->
<div class="current-auditors" v-if="currentAuditors.length > 0">
<div class="section-title">
<el-icon><User /></el-icon>
当前已选择的审核人 ({{ currentAuditors.length }} 人)
</div>
<div class="auditor-tags">
<el-tag
v-for="auditor in currentAuditors"
:key="auditor.userId"
type="info"
style="margin: 2px 5px 2px 0;"
>
{{ auditor.nickName }}({{ auditor.userName }})
</el-tag>
</div>
</div>
<!-- 审核人选择区域 -->
<div class="auditor-selection-section">
<div class="custom-process-config">
<el-form-item label="选择审核人" required>
<div class="auditor-selector">
<div class="selector-header">
<el-button type="primary" icon="Plus" @click="openAuditorSelector">添加审核人</el-button>
<el-text v-if="selectedAuditors.length > 0" type="info" size="small">
已选择 {{ selectedAuditors.length }} 个审核人,将按添加顺序进行审批
</el-text>
</div>
<!-- 已选择的审核人列表 -->
<div v-if="selectedAuditors.length > 0" class="selected-auditors">
<div class="auditor-flow">
<div v-for="(auditor, index) in selectedAuditors" :key="auditor.userId" class="auditor-node">
<div class="node-content">
<span class="node-index">{{ index + 1 }}</span>
<div class="auditor-info">
<div class="auditor-name">{{ auditor.nickName || auditor.userName }}</div>
<div class="auditor-dept">{{ auditor.deptName || '未知部门' }}</div>
</div>
<el-button type="danger" icon="Delete" size="small" circle @click="removeAuditor(auditor)" />
</div>
<div v-if="index < selectedAuditors.length - 1" class="flow-arrow">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
<!-- 流程预览说明 -->
<div class="flow-description">
<el-alert type="info" :closable="false" show-icon>
<template #title>
<span>审批流程预览:{{ props.nextNodeName }}将按上述顺序进行逐级审批,每个节点审批通过后自动流转到下一节点</span>
</template>
</el-alert>
</div>
</div>
<!-- 空状态提示 -->
<div v-else class="empty-state">
<el-empty description="暂未选择审核人">
<el-button type="primary" @click="openAuditorSelector">选择审核人</el-button>
</el-empty>
</div>
</div>
</el-form-item>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="handleCancel">取消</el-button>
<el-button
type="primary"
@click="handleConfirm"
:loading="submitting"
:disabled="selectedAuditors.length === 0"
>
<el-icon><Check /></el-icon>
确认提交 ({{ selectedAuditors.length }} 个审核人)
</el-button>
</div>
</template>
</el-dialog>
<!-- 审核人选择器 -->
<AuditorSelector
ref="auditorSelectorRef"
@confirm="handleAuditorsSelected"
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Check, User, UserFilled, Document, Plus, ArrowRight, Delete } from '@element-plus/icons-vue';
import AuditorSelector from './index.vue';
interface Props {
visible: boolean;
currentAuditors?: any[]; // 当前已选择的审核人
nextNodeName?: string; // 下一步审批节点名称
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'confirm', approvalData: any): void;
}
const props = withDefaults(defineProps<Props>(), {
currentAuditors: () => [],
nextNodeName: '二级学院审批'
});
const emit = defineEmits<Emits>();
const visible = ref(props.visible);
const submitting = ref(false);
const selectedAuditors = ref<any[]>([]);
const currentAuditors = ref<any[]>([]);
const auditorSelectorRef = ref<InstanceType<typeof AuditorSelector>>();
// 监听visible变化
watch(() => props.visible, (newVal) => {
visible.value = newVal;
if (newVal) {
// 初始化当前审核人和选择的审核人
currentAuditors.value = [...(props.currentAuditors || [])];
selectedAuditors.value = [...(props.currentAuditors || [])];
}
});
// 监听内部visible变化
watch(visible, (newVal) => {
emit('update:visible', newVal);
});
// 打开审核人选择器
const openAuditorSelector = () => {
auditorSelectorRef.value?.open(selectedAuditors.value);
};
// 审核人选择确认
const handleAuditorsSelected = (auditors: any[]) => {
selectedAuditors.value = [...auditors];
};
// 移除审核人
const removeAuditor = (auditor: any) => {
const index = selectedAuditors.value.findIndex(item => item.userId === auditor.userId);
if (index > -1) {
selectedAuditors.value.splice(index, 1);
}
};
// 取消操作
const handleCancel = () => {
visible.value = false;
ElMessage.info('已取消提交');
};
// 确认提交
const handleConfirm = async () => {
try {
if (selectedAuditors.value.length === 0) {
ElMessage.warning('请至少选择一个审核人');
return;
}
submitting.value = true;
// 构建审批流程数据
const approvalData = {
auditorIds: selectedAuditors.value.map(auditor => auditor.userId),
processName: '年审审批流程',
auditors: selectedAuditors.value
};
console.log('确认审批流程,数据:', approvalData);
// 触发确认事件
emit('confirm', approvalData);
visible.value = false;
ElMessage.success('审批流程配置成功');
} catch (error) {
console.error('确认审批流程失败:', error);
ElMessage.error('确认审批流程失败');
} finally {
submitting.value = false;
}
};
// 暴露方法供父组件调用
defineExpose({
open: (preSelectedAuditors?: any[]) => {
visible.value = true;
currentAuditors.value = [...(preSelectedAuditors || [])];
selectedAuditors.value = [...(preSelectedAuditors || [])];
}
});
</script>
<style scoped>
.approval-dialog-content {
max-height: 500px;
}
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.next-node-info {
margin-bottom: 20px;
padding: 12px 16px;
background-color: #f0f9ff;
border: 1px solid #d1ecf1;
border-radius: 6px;
}
.node-label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #0c63e4;
font-size: 14px;
}
.current-auditors {
margin-bottom: 20px;
padding: 15px;
border: 1px solid #e4e7ed;
border-radius: 6px;
background-color: #fafafa;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 10px;
font-weight: 500;
color: #303133;
gap: 8px;
}
.auditor-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.auditor-selection-section {
margin-bottom: 20px;
}
.custom-process-config {
padding: 24px;
border: 1px solid #e4e7ed;
border-radius: 8px;
background-color: #fafbfc;
}
.auditor-selector {
width: 100%;
}
.selector-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.selected-auditors {
margin-top: 20px;
}
.auditor-flow {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
margin-bottom: 16px;
}
.auditor-node {
display: flex;
align-items: center;
gap: 12px;
}
.node-content {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.node-index {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: #409eff;
color: white;
border-radius: 50%;
font-size: 12px;
font-weight: 600;
}
.auditor-info {
flex: 1;
}
.auditor-name {
font-size: 14px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 2px;
}
.auditor-dept {
font-size: 12px;
color: #909399;
}
.flow-arrow {
display: flex;
align-items: center;
color: #409eff;
font-size: 16px;
}
.flow-description {
margin-top: 16px;
}
.empty-state {
text-align: center;
padding: 40px 20px;
}
</style>
@@ -0,0 +1,396 @@
<template>
<div class="auditor-selector">
<el-dialog
v-model="visible"
title="选择审核人"
width="900px"
append-to-body
:before-close="handleClose"
>
<div class="auditor-select-content">
<!-- 搜索区域 -->
<div class="search-section" style="margin-bottom: 20px !important;">
<el-input v-model="searchKeyword" placeholder="搜索审核人姓名或部门" clearable @input="handleSearch" style="width: 300px">
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<!-- 审核人表格 -->
<el-table
ref="auditorTableRef"
:data="tableData"
@selection-change="handleSelectionChange"
max-height="400"
empty-text="暂无可用审核人"
style="margin-top: 20px !important;"
>
<el-table-column type="selection" width="55" :selectable="isSelectable" header-align="center" align="center" />
<el-table-column label="序号" type="index" width="60" header-align="center" align="center" />
<el-table-column label="姓名" prop="nickName" width="130" header-align="center" align="center" />
<el-table-column label="部门" prop="deptName" header-align="center" align="center" />
<el-table-column label="联系电话" prop="phoneNumber" width="130" header-align="center" />
<el-table-column label="邮箱" prop="email" show-overflow-tooltip header-align="center" />
<el-table-column label="状态" prop="status" width="80" header-align="center" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === '1' ? 'success' : 'danger'" size="small">
{{ scope.row.status === '1' ? '正常' : '停用' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<div
class="dialog-footer"
style="display: flex !important; justify-content: space-between !important; align-items: center !important; width: 100% !important; margin: 0 !important;"
>
<span class="selected-count-text" style="color: #909399; font-size: 12px;"> 已选择 {{ selectedUsers.length }} 个审核人 </span>
<div class="footer-buttons" style="display: flex !important; gap: 12px !important;">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleConfirm" :disabled="selectedUsers.length === 0"> 确定添加 </el-button>
</div>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted, nextTick, computed, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { ElMessage, ElMessageBox, ElTable } from 'element-plus';
import { Search } from '@element-plus/icons-vue';
import { listStglAuditor } from '@/api/stgl/auditor';
import { listDept } from '@/api/system/dept';
import { StglAuditorVO, StglAuditorQuery } from '@/api/stgl/auditor/types';
import { DeptVO } from '@/api/system/dept/types';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const { sys_normal_disable } = proxy?.useDict('sys_normal_disable') || {};
const auditorTableRef = ref();
const emit = defineEmits<{
confirm: [users: StglAuditorVO[]];
}>();
// 响应式数据
const visible = ref(false);
const loading = ref(false);
const userList = ref<StglAuditorVO[]>([]);
const filteredUserList = ref<StglAuditorVO[]>([]);
const selectedUsers = ref<StglAuditorVO[]>([]);
const total = ref(0);
const deptOptions = ref<any[]>([]);
const searchKeyword = ref('');
// 计算表格数据
const tableData = computed(() => {
// 确保返回的数组总是有效的
if (searchKeyword.value && filteredUserList.value) {
return filteredUserList.value;
}
return userList.value || [];
});
// 查询参数
const queryParams = ref<StglAuditorQuery>({
pageNum: 1,
pageSize: 500, // 增加页面大小以显示所有审核人
nickName: '',
userName: '',
deptId: undefined,
status: '1' // 只查询正常状态的审核人
});
// 初始查询参数的备份,用于重置
const defaultQuery: StglAuditorQuery = {
pageNum: 1,
pageSize: 500, // 增加页面大小以显示所有审核人
nickName: '',
userName: '',
deptId: undefined,
status: '1'
};
/** 打开选择器 */
const open = (preSelected: StglAuditorVO[] = []) => {
visible.value = true;
selectedUsers.value = [...preSelected];
// 重置搜索状态
searchKeyword.value = '';
filteredUserList.value = [];
getDeptTree();
getList();
};
/** 获取部门树 */
const getDeptTree = async () => {
try {
const response = await listDept();
deptOptions.value = handleTree(response.data, 'id', 'parentId');
} catch (error) {
console.error('获取部门树失败:', error);
}
};
/** 获取审核人列表 */
const getList = async () => {
loading.value = true;
try {
const response = await listStglAuditor(queryParams.value);
userList.value = response.rows;
filteredUserList.value = response.rows;
total.value = response.total;
// 如果有搜索关键词,执行搜索
if (searchKeyword.value) {
handleSearch();
}
// 设置表格选中状态
nextTick(() => {
setTableSelection();
});
} catch (error) {
console.error('获取审核人列表失败:', error);
userList.value = [];
filteredUserList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
/** 设置表格选中状态 */
const setTableSelection = () => {
if (!auditorTableRef.value) return;
tableData.value.forEach(user => {
const isSelected = selectedUsers.value.some(selected =>
selected.userId === user.userId
);
auditorTableRef.value.toggleRowSelection(user, isSelected);
});
};
/** 判断行是否可选 */
const isSelectable = (row: StglAuditorVO) => {
return row.status === '1'; // 只有正常状态的审核人可选
};
/** 表格选择变化 */
const handleSelectionChange = (selection: StglAuditorVO[]) => {
// 处理当前显示数据的选择
const currentPageUserIds = tableData.value.map(user => user.userId);
// 移除当前页原有的选择
selectedUsers.value = selectedUsers.value.filter(user =>
!currentPageUserIds.includes(user.userId)
);
// 添加当前页新的选择
selectedUsers.value.push(...selection);
};
/** 移除选中审核人 */
const removeUser = (user: StglAuditorVO) => {
selectedUsers.value = selectedUsers.value.filter(selected =>
selected.userId !== user.userId
);
// 如果当前页包含该用户,需要取消表格选中
if (auditorTableRef.value) {
const currentUser = tableData.value.find(u => u.userId === user.userId);
if (currentUser) {
auditorTableRef.value.toggleRowSelection(currentUser, false);
}
}
};
/** 清空所有选择 */
const clearSelected = () => {
selectedUsers.value = [];
if (auditorTableRef.value) {
auditorTableRef.value.clearSelection();
}
};
/** 搜索处理 */
const handleSearch = () => {
if (!searchKeyword.value) {
filteredUserList.value = userList.value;
return;
}
const keyword = searchKeyword.value.toLowerCase();
filteredUserList.value = userList.value.filter(
(user) =>
(user.nickName && user.nickName.toLowerCase().includes(keyword)) ||
(user.userName && user.userName.toLowerCase().includes(keyword)) ||
(user.deptName && user.deptName.toLowerCase().includes(keyword))
);
};
/** 搜索 */
const handleQuery = () => {
queryParams.value.pageNum = 1;
getList();
};
/** 重置搜索 */
const resetQuery = () => {
Object.assign(queryParams.value, defaultQuery);
handleQuery();
};
/** 关闭弹窗 */
const handleClose = () => {
visible.value = false;
selectedUsers.value = [];
userList.value = [];
filteredUserList.value = [];
searchKeyword.value = '';
Object.assign(queryParams.value, defaultQuery);
};
/** 确认选择 */
const handleConfirm = () => {
emit('confirm', selectedUsers.value);
visible.value = false;
};
/** 处理树形数据 */
const handleTree = (data: any[], id: string, parentId: string, children = 'children') => {
const config = { id, parentId, children };
const childrenListMap: any = {};
const nodeIds: any = {};
const tree = [];
for (const d of data) {
const parentIdValue = d[config.parentId];
if (childrenListMap[parentIdValue] == null) {
childrenListMap[parentIdValue] = [];
}
nodeIds[d[config.id]] = d;
childrenListMap[parentIdValue].push(d);
}
for (const d of data) {
const parentIdValue = d[config.parentId];
if (nodeIds[parentIdValue] == null) {
tree.push(d);
}
}
for (const t of tree) {
adaptToChildrenArray(t);
}
function adaptToChildrenArray(o: any) {
if (childrenListMap[o[config.id]] !== null) {
o[config.children] = childrenListMap[o[config.id]];
}
if (o[config.children]) {
for (const c of o[config.children]) {
adaptToChildrenArray(c);
}
}
}
return tree.map(item => ({
id: item.id,
label: item.deptName,
children: item.children ? item.children.map((child: any) => convertToTreeNode(child)) : []
}));
};
const convertToTreeNode = (item: any): any => ({
id: item.id,
label: item.deptName,
children: item.children ? item.children.map((child: any) => convertToTreeNode(child)) : []
});
defineExpose({
open
});
</script>
<style scoped>
.auditor-selector {
.auditor-select-content {
max-height: 500px;
}
.search-section {
margin-top: 20px !important;
margin-bottom: 20px !important;
}
:deep(.el-table) {
margin-top: 20px !important;
}
.dialog-footer {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
width: 100%;
}
.footer-buttons {
display: flex;
gap: 12px;
}
}
/* Element Plus Dialog Footer 样式覆盖 - 使用最高优先级的选择器 */
.auditor-selector :deep(.el-dialog__footer) {
padding: 15px 20px !important;
text-align: left !important;
display: block !important;
}
.auditor-selector :deep(.el-dialog__footer .dialog-footer) {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
width: 100% !important;
margin: 0 !important;
padding: 0 !important;
box-sizing: border-box !important;
}
/* 确保按钮容器样式正确 */
.auditor-selector .dialog-footer .footer-buttons {
display: flex !important;
gap: 12px !important;
margin-left: auto !important;
flex-shrink: 0 !important;
}
/* 确保文本不会撑开容器 */
.auditor-selector .dialog-footer .selected-count-text {
flex-shrink: 0 !important;
white-space: nowrap !important;
}
/* 确保搜索框和表格间距的全局样式 */
.auditor-selector .auditor-select-content .search-section {
margin-bottom: 20px !important;
}
.auditor-selector .auditor-select-content .el-table {
margin-top: 20px !important;
}
</style>
@@ -124,8 +124,27 @@
<!-- 指导老师意见 -->
<div class="opinion-section">
<div class="opinion-title">指导老师意见</div>
<div class="opinion-content">
{{ annualReview.advisorOpinion || '暂无意见' }}
<div v-if="advisorAudits.length > 0" class="audit-records">
<div v-for="audit in advisorAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -133,8 +152,27 @@
<!-- 二级学院意见 -->
<div class="opinion-section">
<div class="opinion-title">二级学院意见</div>
<div class="opinion-content">
{{ annualReview.collegeOpinion || '暂无意见' }}
<div v-if="collegeAudits.length > 0" class="audit-records">
<div v-for="audit in collegeAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -142,8 +180,27 @@
<!-- 校团委意见 -->
<div class="opinion-section">
<div class="opinion-title">校团委意见</div>
<div class="opinion-content">
{{ annualReview.youthLeagueOpinion || '暂无意见' }}
<div v-if="youthLeagueAudits.length > 0" class="audit-records">
<div v-for="audit in youthLeagueAudits" :key="audit.id" class="audit-record">
<div class="audit-header">
<span class="approver-name">{{ audit.approverName }}</span>
<span class="audit-status" :class="getAuditStatusClass(audit.nodeStatus)">
{{ getAuditStatusText(audit.nodeStatus) }}
</span>
<span v-if="audit.approveTime" class="audit-time">
{{ parseTime(audit.approveTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
</span>
</div>
<div v-if="audit.approveOpinion" class="audit-opinion">
{{ audit.approveOpinion }}
</div>
<div v-else class="audit-opinion pending">
待审批
</div>
</div>
</div>
<div v-else class="opinion-content">
暂无审批记录
</div>
<div class="opinion-signature">签名(公章)</div>
</div>
@@ -158,7 +215,9 @@
<script setup lang="ts">
import { getStglClubAnnualReview, getAnnualReviewActivities } from '@/api/stgl/club/annualReview';
import { getAnnualReviewAuditsByReviewId } from '@/api/stgl/club/annualReviewAudit';
import { StglClubAnnualReviewVO } from '@/api/stgl/club/annualReview/type';
import { StglAnnualReviewAuditVO } from '@/api/stgl/club/annualReviewAudit/type';
// 扩展的年审VO接口(包含JSON字段)
interface ExtendedAnnualReviewVO extends StglClubAnnualReviewVO {
@@ -180,6 +239,8 @@ const annualReview = ref<ExtendedAnnualReviewVO>();
const memberList = ref<any[]>([]);
// 活动列表
const activityList = ref<any[]>([]);
// 审批记录列表
const auditList = ref<StglAnnualReviewAuditVO[]>([]);
// 年份范围计算
const yearRange = computed(() => {
@@ -208,6 +269,41 @@ const memberCountText = computed(() => {
return `${annualReview.value?.memberCount || 0}人`;
});
// 指导老师审批记录
const advisorAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'advisor_approve');
});
// 二级学院审批记录
const collegeAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'college_approve');
});
// 校团委审批记录
const youthLeagueAudits = computed(() => {
return auditList.value.filter(audit => audit.nodeCode === 'youth_league_approve');
});
/** 获取审批状态样式类 */
const getAuditStatusClass = (status: string) => {
const statusClasses: { [key: string]: string } = {
'pending': 'status-pending',
'waiting': 'status-waiting',
'approved': 'status-approved'
};
return statusClasses[status] || '';
};
/** 获取审批状态文本 */
const getAuditStatusText = (status: string) => {
const statusTexts: { [key: string]: string } = {
'pending': '待审批',
'waiting': '等待中',
'approved': '已审批'
};
return statusTexts[status] || status;
};
/** 格式化活动时间显示 */
const formatActivityTime = (startTime: string, endTime?: string): string => {
if (!startTime) return '';
@@ -302,6 +398,19 @@ const getDetail = async () => {
memberList.value = [];
}
// 获取审批记录
try {
const auditRes = await getAnnualReviewAuditsByReviewId(props.annualReviewId);
if (auditRes && auditRes.data && Array.isArray(auditRes.data)) {
auditList.value = auditRes.data;
} else {
auditList.value = [];
}
} catch (error) {
console.warn('获取审批记录失败:', error);
auditList.value = [];
}
// 从stgl_club_annual_review_activity表中获取年审记录对应的活动数据
try {
const activitiesRes = await getAnnualReviewActivities(props.annualReviewId);
@@ -524,4 +633,79 @@ watch(
line-height: 1.4;
font-size: 13px;
}
/* 审批记录样式 */
.audit-records {
margin-bottom: 10px;
}
.audit-record {
margin-bottom: 15px;
padding: 12px;
border: 1px solid #e4e7ed;
border-radius: 4px;
background: #fafafa;
}
.audit-record:last-child {
margin-bottom: 0;
}
.audit-header {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
}
.approver-name {
font-weight: bold;
color: #303133;
margin-right: 15px;
}
.audit-status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-right: 15px;
}
.status-pending {
background: #fdf6ec;
color: #e6a23c;
border: 1px solid #f5dab1;
}
.status-waiting {
background: #f4f4f5;
color: #909399;
border: 1px solid #d3d4d6;
}
.status-approved {
background: #f0f9ff;
color: #67c23a;
border: 1px solid #b3d8ff;
}
.audit-time {
color: #909399;
font-size: 12px;
margin-left: auto;
}
.audit-opinion {
line-height: 1.6;
color: #606266;
background: #fff;
padding: 8px;
border-radius: 4px;
border: 1px solid #e4e7ed;
}
.audit-opinion.pending {
color: #909399;
font-style: italic;
}
</style>
@@ -459,9 +459,24 @@ const loadClubInfo = async (clubId: string | number) => {
try {
const advisors = JSON.parse(clubData.advisorsJson);
if (Array.isArray(advisors) && advisors.length > 0) {
const firstAdvisor = advisors[0];
advisorName = firstAdvisor.name || firstAdvisor.teacher?.nickName || '';
advisorDept = firstAdvisor.dept || firstAdvisor.teacher?.deptName || '';
// 处理多个指导老师的情况
const advisorNames = [];
const advisorDepts = [];
advisors.forEach(advisor => {
const name = advisor.name || advisor.teacher?.nickName || '';
const dept = advisor.dept || advisor.teacher?.deptName || '';
if (name) {
advisorNames.push(name);
}
if (dept && !advisorDepts.includes(dept)) {
advisorDepts.push(dept);
}
});
advisorName = advisorNames.join('、');
advisorDept = advisorDepts.join('、');
}
} catch (error) {
console.warn('解析指导老师JSON数据失败:', error);
@@ -57,7 +57,7 @@
</el-row>
</template>
<el-table v-loading="loading" border :data="annualReviewList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table v-loading="loading" border :data="annualReviewList" :key="tableKey" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="社团名称" align="center" prop="clubName" min-width="150" />
<el-table-column label="成立日期" align="center" prop="establishDate" width="120">
@@ -80,11 +80,11 @@
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="100">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status) as any">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="状态" align="center" prop="status" width="120">
<template #default="scope">
<el-tag :type="getStatusTagType(scope.row.status) as any">{{ getStatusText(scope.row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="年审时间" align="center" prop="createTime" width="180">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
@@ -104,7 +104,10 @@
<el-tooltip content="撤回" placement="top" v-if="scope.row.status === '1'">
<el-button link type="warning" icon="RefreshLeft" @click="handleWithdraw(scope.row)" v-hasPermi="['stgl:clubAnnualReview:withdraw']"></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 === '1' || scope.row.status === '2') && canApprove(scope.row)">
<el-button link type="success" icon="Check" @click="handleApprove(scope.row)" v-hasPermi="['stgl:annualReviewAudit:approve']"></el-button>
</el-tooltip>
<el-tooltip content="删除" placement="top" v-if="scope.row.status === '0' || scope.row.status === '4'">
<el-button link type="danger" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['stgl:clubAnnualReview:remove']"></el-button>
</el-tooltip>
</template>
@@ -129,10 +132,15 @@
<el-dialog title="年审记录详情" v-model="detailDialog.visible" width="1000px" append-to-body>
<annual-review-detail :annual-review-id="detailDialog.annualReviewId" />
</el-dialog>
<!-- 审批对话框 -->
<approve-dialog ref="approveDialogRef" @success="handleApproveSuccess" />
</div>
</template>
<script setup name="StglClubAnnualReview" lang="ts">
import { ref, reactive, toRefs, nextTick, onMounted, getCurrentInstance } from 'vue';
import type { ComponentInternalInstance } from 'vue';
import { useRoute } from 'vue-router';
import {
listStglClubAnnualReview,
@@ -143,11 +151,16 @@ import {
exportStglClubAnnualReview
} from '@/api/stgl/club/annualReview';
import { StglClubAnnualReviewVO, StglClubAnnualReviewQuery, ANNUAL_REVIEW_STATUS_OPTIONS } from '@/api/stgl/club/annualReview/type';
import { getPendingAuditsByApproverId } from '@/api/stgl/club/annualReviewAudit';
import { StglAnnualReviewAuditVO } from '@/api/stgl/club/annualReviewAudit/type';
import { useUserStore } from '@/store/modules/user';
import AnnualReviewForm from './form.vue';
import AnnualReviewDetail from './detail.vue';
import ApproveDialog from './approveDialog.vue';
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
const route = useRoute();
const userStore = useUserStore();
const { stgl_club_type } = toRefs(proxy?.useDict('stgl_club_type') as any);
const annualReviewList = ref<StglClubAnnualReviewVO[]>([]);
@@ -157,9 +170,16 @@ const ids = ref<Array<string | number>>([]);
const single = ref(true);
const multiple = ref(true);
const total = ref(0);
const tableKey = ref(0); // 用于强制重新渲染表格
// 审批权限相关状态
const pendingAudits = ref<StglAnnualReviewAuditVO[]>([]);
const pendingAuditsLoading = ref(false);
const pendingAuditsCache = ref<Map<number, boolean>>(new Map());
const queryFormRef = ref<ElFormInstance>();
const annualReviewFormRef = ref<InstanceType<typeof AnnualReviewForm>>();
const approveDialogRef = ref<InstanceType<typeof ApproveDialog>>();
// 状态选项
const statusOptions = ref(ANNUAL_REVIEW_STATUS_OPTIONS);
@@ -214,10 +234,36 @@ const { queryParams, dialog, detailDialog } = toRefs(data);
/** 查询社团年审记录列表 */
const getList = async () => {
loading.value = true;
const res = await listStglClubAnnualReview(queryParams.value);
annualReviewList.value = res.rows;
total.value = res.total;
loading.value = false;
try {
// 并行加载年审记录和待审批记录
const [annualReviewRes] = await Promise.all([
listStglClubAnnualReview(queryParams.value),
loadPendingAudits()
]);
// 确保数据更新
if (annualReviewRes && annualReviewRes.rows) {
annualReviewList.value = annualReviewRes.rows;
total.value = annualReviewRes.total;
} else {
console.warn('获取的年审记录数据格式异常:', annualReviewRes);
annualReviewList.value = [];
total.value = 0;
}
// 强制触发 Vue 的响应式更新
nextTick(() => {
console.log('年审记录列表已更新,记录数:', annualReviewList.value.length);
});
} catch (error) {
console.error('加载数据失败:', error);
proxy?.$modal.msgError('加载数据失败');
// 重置数据
annualReviewList.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
@@ -279,20 +325,51 @@ const handleView = (row: StglClubAnnualReviewVO) => {
const handleSubmit = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认提交年审记录"${row.clubName}"?`);
// 调用提交接口
await submitStglClubAnnualReview(row.id);
await getList();
// API调用成功后,立即更新本地状态,避免被getList覆盖
await updateRowStatus(row.id, '1');
proxy?.$modal.msgSuccess('提交成功');
} catch {}
// 延迟重新获取数据,确保状态已经在界面上显示
setTimeout(async () => {
await getList();
}, 100);
} catch (error) {
console.error('提交失败:', error);
// 提交失败时才需要恢复状态,因为我们现在是先调用API再更新状态
}
};
/** 撤回按钮操作 */
const handleWithdraw = async (row: StglClubAnnualReviewVO) => {
try {
await proxy?.$modal.confirm(`是否确认撤回年审记录"${row.clubName}"?`);
// 调用撤回接口
await withdrawStglClubAnnualReview(row.id);
await getList();
// API调用成功后,立即更新本地状态,避免被getList覆盖
await updateRowStatus(row.id, '0');
proxy?.$modal.msgSuccess('撤回成功');
} catch {}
// 延迟重新获取数据,确保状态已经在界面上显示
setTimeout(async () => {
await Promise.all([
getList(),
loadPendingAudits()
]);
}, 100);
} catch (error) {
console.error('撤回失败:', error);
// 撤回失败时才需要恢复状态,因为我们现在是先调用API再更新状态
}
};
/** 删除按钮操作 */
@@ -322,6 +399,30 @@ const handleFormSuccess = () => {
getList();
};
/** 更新行状态的工具函数 */
const updateRowStatus = async (rowId: string | number, newStatus: string) => {
const rowIndex = annualReviewList.value.findIndex(item => item.id === rowId);
if (rowIndex !== -1) {
// 创建新对象确保Vue能检测到变化
const updatedRow = {
...annualReviewList.value[rowIndex],
status: newStatus
};
// 使用splice替换整个对象,触发响应式更新
annualReviewList.value.splice(rowIndex, 1, updatedRow);
// 强制触发DOM更新和表格重新渲染
tableKey.value++;
await nextTick();
console.log(`状态已同步更新: ${updatedRow.status}`);
console.log('当前数组状态:', annualReviewList.value.map(item => ({ id: item.id, status: item.status, clubName: item.clubName })));
return updatedRow;
}
return null;
};
/** 获取状态标签类型 */
const getStatusTagType = (status: string) => {
const tagTypes: { [key: string]: string } = {
@@ -336,8 +437,114 @@ const getStatusTagType = (status: string) => {
/** 获取状态文本 */
const getStatusText = (status: string) => {
// 本地状态映射,确保显示正确
const statusMap: { [key: string]: string } = {
'0': '草稿',
'1': '已提交',
'2': '审核中',
'3': '审核通过',
'4': '审核不通过'
};
// 优先使用本地映射
if (statusMap[status]) {
const result = statusMap[status];
console.log(`getStatusText: 状态=${status}, 返回文本="${result}"`);
return result;
}
// fallback到原来的逻辑
const option = statusOptions.value.find(item => item.value === status);
return option?.label || status;
const result = option?.label || status;
console.log(`getStatusText: 状态=${status}, fallback返回="${result}"`);
return result;
};
/** 获取当前用户的待审批记录 */
const loadPendingAudits = async () => {
if (!userStore.userId || pendingAuditsLoading.value) {
return;
}
try {
pendingAuditsLoading.value = true;
const response = await getPendingAuditsByApproverId(userStore.userId);
// 处理响应数据,确保 data 是数组
// 后端返回的是 R<List<StglAnnualReviewAudit>>,R.data 可能为 null 或 []
let data = [];
if (response && response.data !== undefined) {
// 如果response.data存在但为null,使用空数组
data = response.data || [];
} else if (response && response.rows !== undefined) {
// 有些API返回格式可能是分页格式
data = response.rows || [];
}
pendingAudits.value = data;
// 更新缓存
pendingAuditsCache.value.clear();
if (Array.isArray(data)) {
data.forEach((audit: StglAnnualReviewAuditVO) => {
if (audit.annualReviewId) {
pendingAuditsCache.value.set(Number(audit.annualReviewId), true);
}
});
}
} catch (error) {
console.error('获取待审批记录失败:', error);
// 重置数据
pendingAudits.value = [];
pendingAuditsCache.value.clear();
// 不显示错误提示,避免影响页面正常使用
} finally {
pendingAuditsLoading.value = false;
}
};
/** 判断当前用户是否可以审批该年审记录 */
const canApprove = (row: StglClubAnnualReviewVO) => {
// 检查当前用户是否登录
if (!userStore.userId) {
console.log('canApprove: 用户未登录');
return false;
}
// 检查年审记录状态,支持"已提交"和"审核中"状态
if (row.status !== '1' && row.status !== '2') {
console.log(`canApprove: 状态不符合 ${row.status}`);
return false;
}
// 检查缓存中是否有该年审记录的审批权限
const hasPermission = pendingAuditsCache.value.has(Number(row.id));
console.log(`canApprove: 年审ID=${row.id}, 当前用户=${userStore.userId}, 缓存权限=${hasPermission}, 缓存内容=`, Array.from(pendingAuditsCache.value.keys()));
return hasPermission;
};
/** 审批按钮操作 */
const handleApprove = async (row: StglClubAnnualReviewVO) => {
// 从待审批记录中找到对应的审批ID
const audit = pendingAudits.value.find(audit =>
Number(audit.annualReviewId) === Number(row.id)
);
if (!audit) {
proxy?.$modal.msgError('未找到对应的审批记录');
return;
}
approveDialogRef.value?.open(audit.id, row.id);
};
/** 审批成功回调 */
const handleApproveSuccess = async () => {
// 审批完成后,需要同时刷新年审记录和待审批记录
await Promise.all([
getList(),
loadPendingAudits()
]);
};
onMounted(() => {
+3 -3
View File
@@ -1278,9 +1278,9 @@ watch(
() => form.value.advisors,
(advisors: AdvisorVO[]) => {
// 防止递归更新
if (syncingAdvisors.value) return;
if (syncLock.value) return;
syncingAdvisors.value = true;
syncLock.value = true;
nextTick(() => {
try {
// 为了兼容旧版本,同步第一个指导老师的信息到旧字段
@@ -1297,7 +1297,7 @@ watch(
form.value.advisorEmail = undefined;
}
} finally {
syncingAdvisors.value = false;
syncLock.value = false;
}
});
},
@@ -33,21 +33,21 @@
<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-option v-for="college in collegeOptions" :key="college.value" :label="college.label" :value="college.value" />
</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-option v-for="major in majorOptions" :key="major.value" :label="major.label" :value="major.value" />
</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-option v-for="classItem in classOptions" :key="classItem.value" :label="classItem.label" :value="classItem.value" />
</el-select>
</el-form-item>
</el-col>
@@ -106,10 +106,28 @@
<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-if="scope.row.status === 4" type="info">待审核</el-tag>
<el-tag v-else-if="scope.row.status === 5" type="danger">已驳回</el-tag>
<el-tag v-else type="info">未知状态</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="是否需要转介" align="center" prop="needReferral" width="120">
<template #default="scope">
<div v-if="scope.row.status === 1">
<el-tag v-if="scope.row.needReferral === 2" type="warning">是</el-tag>
<el-tag v-else-if="scope.row.needReferral === 1" type="success">否</el-tag>
<span v-else>-</span>
</div>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="本学期到访次数" align="center" prop="semesterVisitCount" width="120">
<template #default="scope">
<el-tag v-if="scope.row.semesterVisitCount > 0" type="info">{{ scope.row.semesterVisitCount }}次</el-tag>
<span v-else>0次</span>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
@@ -129,6 +147,31 @@
></el-button>
</el-tooltip>
<!-- 待审核状态可审核通过或拒绝 -->
<el-tooltip content="审核通过" placement="top" v-if="scope.row.status === 4">
<el-button
link
type="success"
icon="Check"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleApprove(scope.row)"
v-hasPermi="['xljk:consultationappointment:edit']"
></el-button>
</el-tooltip>
<el-tooltip content="审核拒绝" placement="top" v-if="scope.row.status === 4">
<el-button
link
type="danger"
icon="Close"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleReject(scope.row)"
v-hasPermi="['xljk:consultationappointment:edit']"
></el-button>
</el-tooltip>
<!-- 待咨询状态可记录咨询结果 -->
<el-tooltip content="记录结果" placement="top" v-if="scope.row.status === 0">
<el-button
@@ -161,29 +204,45 @@
<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="咨询主题">{{ form.consultTheme || '未选择' }}</el-descriptions-item>
<el-descriptions-item label="咨询原因">{{ 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-if="form.status === 4" type="info">待审核</el-tag>
<el-tag v-else-if="form.status === 5" type="danger">已驳回</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">
<el-descriptions-item label="心理病史">
<el-tag v-if="form.psychologicalHistory === 2" type="warning">是</el-tag>
<el-tag v-else-if="form.psychologicalHistory === 1" type="success">否</el-tag>
<span v-else>未记录</span>
</el-descriptions-item>
<el-descriptions-item label="心理病史详情" :span="2" v-if="form.psychologicalHistory === 2">
{{ form.psychologicalHistoryDetail || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="基本情况" :span="2">
{{ form.basicSituation || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="重大生活事件" :span="2">
{{ form.majorLifeEvents || '未记录' }}
</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 label="是否需要转介">
<el-tag v-if="form.needReferral === 2" type="warning">是</el-tag>
<el-tag v-else-if="form.needReferral === 1" type="success">否</el-tag>
<span v-else>未记录</span>
</el-descriptions-item>
</template>
</el-descriptions>
@@ -196,8 +255,8 @@
</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-dialog title="记录咨询结果" v-model="recordDialogVisible" width="800px" append-to-body>
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="120px">
<el-form-item label="学生姓名">
<el-input v-model="recordForm.studentName" disabled />
</el-form-item>
@@ -215,27 +274,35 @@
</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 label="心理病史" prop="psychologicalHistory">
<el-radio-group v-model="recordForm.psychologicalHistory" @change="handlePsychologicalHistoryChange">
<el-radio :value="2">是</el-radio>
<el-radio :value="1">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="咨询师建议" prop="counselorSuggestion">
<el-input v-model="recordForm.counselorSuggestion" type="textarea" placeholder="请输入咨询师建议" :rows="4" />
<el-form-item label="心理病史详情" prop="psychologicalHistoryDetail" v-if="recordForm.psychologicalHistory === 2">
<el-input v-model="recordForm.psychologicalHistoryDetail" type="textarea" placeholder="请详细描述心理病史" :rows="3" />
</el-form-item>
<el-form-item label="基本情况" prop="basicSituation">
<el-input v-model="recordForm.basicSituation" type="textarea" placeholder="请输入基本情况" :rows="4" />
</el-form-item>
<el-form-item label="重大生活事件" prop="majorLifeEvents">
<el-input v-model="recordForm.majorLifeEvents" type="textarea" placeholder="请输入重大生活事件(可选)" :rows="3" />
</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="needReferral">
<el-radio-group v-model="recordForm.needReferral" @change="handleReferralChange">
<el-radio :value="2">是</el-radio>
<el-radio :value="1">否</el-radio>
</el-radio-group>
</el-form-item>
</template>
</el-form>
@@ -268,7 +335,8 @@ import {
CounselorOption,
CollegeOption,
MajorOption,
ClassOption
ClassOption,
XljkConsultationAppointmentQuery
} from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
@@ -281,7 +349,9 @@ const appointment_status = ref([
{ label: '待咨询', value: 0 },
{ label: '已到访', value: 1 },
{ label: '已取消', value: 2 },
{ label: '未到访', value: 3 }
{ label: '未到访', value: 3 },
{ label: '待审核', value: 4 },
{ label: '已驳回', value: 5 }
]);
// 遮罩层
@@ -304,19 +374,17 @@ 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 collegeOptions = ref<Array<{ value: string; label: string }>>([]);
const majorOptions = ref<Array<{ value: string; label: string }>>([]);
const classOptions = ref<Array<{ value: string; label: string }>>([]);
// 是否显示记录对话框
const recordDialogVisible = ref(false);
// 提交按钮加载状态
const submitLoading = ref(false);
// 查询参数
const queryParams = reactive({
// 查询参数的类型定义和字段修复
const queryParams = reactive<XljkConsultationAppointmentQuery>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
@@ -339,20 +407,28 @@ const recordForm = reactive<XljkConsultationAppointmentForm>({
scheduleId: undefined,
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
reason: '',
status: 1, // 默认已到访,使用数值类型
actualVisitTime: undefined,
communicationDuration: 30, // 默认30分钟
consultationContent: '',
counselorSuggestion: ''
counselorSuggestion: '',
studentName: '',
psychologicalHistory: 1, // 默认否
psychologicalHistoryDetail: '',
basicSituation: '',
majorLifeEvents: '',
needReferral: 1 // 默认不需要转介
});
// 记录表单校验
const recordRules = ref({
// 修复表单验证规则的类型定义
const recordRules = ref<any>({
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
psychologicalHistory: [{ required: true, message: '请选择心理病史', trigger: 'change' }],
basicSituation: [{ required: true, message: '请输入基本情况', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入本次咨询过程', trigger: 'blur' }],
needReferral: [{ required: true, message: '请选择是否需要转介', trigger: 'change' }]
});
// 监听状态变化,动态调整表单验证规则
@@ -368,9 +444,11 @@ watch(
// 已到访
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' }]
psychologicalHistory: [{ required: true, message: '请选择心理病史', trigger: 'change' }],
basicSituation: [{ required: true, message: '请输入基本情况', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入本次咨询过程', trigger: 'blur' }],
needReferral: [{ required: true, message: '请选择是否需要转介', trigger: 'change' }]
};
}
}
@@ -384,7 +462,9 @@ const getList = async () => {
loading.value = true;
try {
// 处理日期范围
queryParams.params = {};
if (!queryParams.params) {
queryParams.params = {};
}
if (dateRange.value && dateRange.value.length === 2) {
queryParams.params.beginAppointmentDate = dateRange.value[0];
queryParams.params.endAppointmentDate = dateRange.value[1];
@@ -503,24 +583,13 @@ const handleView = async (row: XljkConsultationAppointmentVO) => {
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
@@ -558,6 +627,58 @@ const handleView = async (row: XljkConsultationAppointmentVO) => {
}
};
/** 审核通过操作 */
const handleApprove = async (row: XljkConsultationAppointmentVO) => {
try {
await ElMessageBox.confirm('确认审核通过该预约申请?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 构建更新数据
const updateData = {
id: row.id,
status: "0" // 审核通过,状态改为待咨询
};
await updateXljkAppointment(updateData);
ElMessage.success('审核通过成功');
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('审核通过失败', error);
ElMessage.error('审核通过失败');
}
}
};
/** 审核拒绝操作 */
const handleReject = async (row: XljkConsultationAppointmentVO) => {
try {
await ElMessageBox.confirm('确认拒绝该预约申请?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 构建更新数据
const updateData = {
id: row.id,
status: "5" // 拒绝,状态改为已驳回
};
await updateXljkAppointment(updateData);
ElMessage.success('审核拒绝成功');
getList();
} catch (error) {
if (error !== 'cancel') {
console.error('审核拒绝失败', error);
ElMessage.error('审核拒绝失败');
}
}
};
/** 记录咨询结果操作 */
const handleRecord = async (row: XljkConsultationAppointmentVO) => {
try {
@@ -575,10 +696,15 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
appointmentDetailTime: '',
reason: '',
status: 1, // 默认已完成
actualVisitTime: new Date(),
communicationDuration: 30,
consultationContent: '',
counselorSuggestion: ''
counselorSuggestion: '',
studentName: '',
psychologicalHistory: 1, // 默认否(1=否,2=是)
psychologicalHistoryDetail: '',
basicSituation: '',
majorLifeEvents: '',
needReferral: 1 // 默认不需要转介(1=不需要,2=需要)
};
Object.assign(recordForm, defaultValues);
@@ -594,6 +720,16 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
recordForm.appointmentDetailTime = res.data.appointmentDetailTime;
recordForm.reason = res.data.reason;
recordForm.studentName = res.data.studentName;
// 如果API返回了已有的咨询记录数据,保持原有值
if (res.data.needReferral !== undefined && res.data.needReferral !== null) {
recordForm.needReferral = res.data.needReferral;
console.log('从API获取的needReferral值:', res.data.needReferral, res.data.needReferral === 2 ? '是(需要转介)' : res.data.needReferral === 1 ? '否(不需要转介)' : '未知状态');
}
if (res.data.psychologicalHistory !== undefined && res.data.psychologicalHistory !== null) {
recordForm.psychologicalHistory = res.data.psychologicalHistory;
console.log('从API获取的psychologicalHistory值:', res.data.psychologicalHistory, res.data.psychologicalHistory === 2 ? '是(有病史)' : res.data.psychologicalHistory === 1 ? '否(无病史)' : '未知状态');
}
} else {
console.warn('API响应格式不正确或无数据:', res);
// 使用行数据填充
@@ -608,6 +744,7 @@ const handleRecord = async (row: XljkConsultationAppointmentVO) => {
}
console.log('填充后的记录表单:', recordForm);
console.log('最终needReferral值:', recordForm.needReferral, recordForm.needReferral === 2 ? '是(需要转介)' : recordForm.needReferral === 1 ? '否(不需要转介)' : '未知状态');
recordDialogVisible.value = true;
} catch (error) {
console.error('获取预约详情失败', error);
@@ -625,6 +762,8 @@ const submitRecordForm = async () => {
try {
submitLoading.value = true;
console.log('提交前的recordForm数据:', recordForm);
// 构建提交数据
const submitData: any = {
id: recordForm.id,
@@ -633,39 +772,59 @@ const submitRecordForm = async () => {
// 如果是已到访状态,添加其他字段
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;
submitData.counselorSuggestion = recordForm.counselorSuggestion || '';
submitData.psychologicalHistory = recordForm.psychologicalHistory;
submitData.psychologicalHistoryDetail = recordForm.psychologicalHistoryDetail || '';
submitData.basicSituation = recordForm.basicSituation;
submitData.majorLifeEvents = recordForm.majorLifeEvents || '';
submitData.needReferral = recordForm.needReferral;
console.log('已到访状态,提交的数据字段:');
console.log('- psychologicalHistoryDetail:', submitData.psychologicalHistoryDetail, '类型:', typeof submitData.psychologicalHistoryDetail, '长度:', submitData.psychologicalHistoryDetail?.length);
console.log('- basicSituation:', submitData.basicSituation, '类型:', typeof submitData.basicSituation, '长度:', submitData.basicSituation?.length);
console.log('- majorLifeEvents:', submitData.majorLifeEvents, '类型:', typeof submitData.majorLifeEvents, '长度:', submitData.majorLifeEvents?.length);
console.log('- needReferral:', submitData.needReferral, '类型:', typeof submitData.needReferral);
console.log('- psychologicalHistory:', submitData.psychologicalHistory, '类型:', typeof submitData.psychologicalHistory);
} else {
// 未到访状态,设置默认值
submitData.actualVisitTime = null;
submitData.communicationDuration = 0;
submitData.consultationContent = '';
submitData.counselorSuggestion = '';
submitData.psychologicalHistory = null;
submitData.psychologicalHistoryDetail = '';
submitData.basicSituation = '';
submitData.majorLifeEvents = '';
submitData.needReferral = null;
}
await recordXljkConsultation(recordForm.id as number, submitData);
console.log('最终提交的数据:', submitData);
const response = await recordXljkConsultation(recordForm.id as number, submitData);
console.log('API响应:', response);
ElMessage.success('记录成功');
recordDialogVisible.value = false;
getList();
// 重新查询列表,并验证数据是否保存成功
await getList();
// 验证数据保存情况
try {
const verifyRes = await getXljkAppointment(recordForm.id as number);
console.log('验证保存结果,获取的数据:', verifyRes.data);
if (verifyRes.code === 200 && verifyRes.data) {
console.log('验证字段保存情况:');
console.log('- psychologicalHistoryDetail 保存值:', verifyRes.data.psychologicalHistoryDetail);
console.log('- basicSituation 保存值:', verifyRes.data.basicSituation);
console.log('- majorLifeEvents 保存值:', verifyRes.data.majorLifeEvents);
console.log('- needReferral 保存值:', verifyRes.data.needReferral);
console.log('- psychologicalHistory 保存值:', verifyRes.data.psychologicalHistory);
}
} catch (error) {
console.error('验证保存结果失败:', error);
}
} catch (error) {
console.error('提交记录失败', error);
ElMessage.error('提交记录失败');
@@ -710,8 +869,8 @@ const handleDelete = async (row?: XljkConsultationAppointmentVO) => {
/** 导出按钮操作 */
const handleExport = async () => {
// 构建查询参数,处理日期范围和params对象
const exportParams = { ...queryParams };
// 构建查询参数,处理日期范围
const exportParams: any = { ...queryParams };
delete exportParams.params; // 移除params对象
// 将日期范围添加到参数中
@@ -771,7 +930,7 @@ const handleExport = async () => {
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
} catch (error: any) {
console.error('导出失败:', error);
ElMessage.error(`导出失败: ${error.message}`);
}
@@ -788,7 +947,7 @@ const handleCollegeChange = async (collegeName: string) => {
if (collegeName) {
// 获取该院系下的专业
try {
const res = await getMajorOptions(collegeName);
const res = await getXljkConsultationMajorOptions(collegeName);
majorOptions.value = res.data || [];
} catch (error) {
console.error('获取专业选项失败', error);
@@ -808,7 +967,7 @@ const handleMajorChange = async (majorName: string) => {
if (majorName && queryParams.collegeName) {
// 获取专业下的班级
try {
const res = await getClassOptions(queryParams.collegeName, majorName);
const res = await getXljkConsultationClassOptions(queryParams.collegeName, majorName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
@@ -816,7 +975,7 @@ const handleMajorChange = async (majorName: string) => {
} else if (queryParams.collegeName) {
// 如果清空专业但有院系,则获取院系下所有班级
try {
const res = await getClassOptions(queryParams.collegeName);
const res = await getXljkConsultationClassOptions(queryParams.collegeName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
@@ -827,6 +986,18 @@ const handleMajorChange = async (majorName: string) => {
}
};
/** 转介选择变化时的调试日志 */
const handleReferralChange = (value: number) => {
console.log('转介字段值变化:', value, value === 2 ? '是(需要转介)' : value === 1 ? '否(不需要转介)' : '未知状态');
console.log('当前recordForm.needReferral:', recordForm.needReferral);
};
/** 心理病史选择变化时的调试日志 */
const handlePsychologicalHistoryChange = (value: number) => {
console.log('心理病史字段值变化:', value, value === 2 ? '是(有心理病史)' : value === 1 ? '否(无心理病史)' : '未知状态');
console.log('当前recordForm.psychologicalHistory:', recordForm.psychologicalHistory);
};
// 初始化
onMounted(() => {
console.log('初始化appointment_status字典数据:', appointment_status.value);
@@ -33,56 +33,75 @@
</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 }}
<!-- 动态显示所有排班时段 -->
<template v-for="timeSlotType in getUniqueTimeSlots()" :key="timeSlotType">
<div class="time-row">
<div class="time-label-cell">
<span>{{ getTimeSlotLabel(timeSlotType) }}</span>
</div>
<div v-for="day in weekDays" :key="day.date" class="day-cell">
<div
v-for="schedule in getSchedulesForDateAndTimeSlot(day.date, timeSlotType)"
:key="`${schedule.scheduleId}-${schedule.startTime}`"
class="time-slot-card"
:class="{
'morning': schedule.timeSlot === '0',
'noon': schedule.timeSlot === '1',
'afternoon': schedule.timeSlot === '2',
'fully-booked': schedule.isFullyBooked || schedule.isExpired
}"
@click="handleScheduleClickEvent(schedule)"
>
{{ schedule.startTime }}-{{ schedule.endTime }}
</div>
</div>
</div>
</div>
</template>
<!-- 下午时段 -->
<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 }}
<!-- 兼容性:如果没有新排班数据,继续显示原有的中午/下午时段 -->
<template v-if="getUniqueTimeSlots().length === 0">
<!-- 中午时段 -->
<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>
<!-- 下午时段 -->
<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>
</template>
</div>
</div>
</div>
@@ -111,25 +130,44 @@
>
<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))"
v-for="schedule in getSchedulesForDate(day.date)"
:key="`${schedule.scheduleId}-${schedule.startTime}`"
class="month-slot"
:class="{
'morning': schedule.timeSlot === '0',
'noon': schedule.timeSlot === '1',
'afternoon': schedule.timeSlot === '2',
'fully-booked': schedule.isFullyBooked || schedule.isExpired
}"
@click.stop="handleScheduleClickEvent(schedule)"
>
上午 {{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
{{ schedule.timeDisplay }}
</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>
<!-- 兼容性:如果没有新排班数据,继续显示原有的中午/下午时段 -->
<template v-if="getSchedulesForDate(day.date).length === 0">
<!-- 中午时段 -->
<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>
</template>
</div>
</div>
</div>
@@ -142,7 +180,7 @@ 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';
import { AvailableTimeSlot, ScheduleWithStatus } from '@/api/xljk/consultation/types';
// 定义组件的Props
const props = defineProps({
@@ -150,6 +188,10 @@ const props = defineProps({
type: Array as () => AvailableTimeSlot[],
default: () => []
},
calendarSchedules: {
type: Array as () => ScheduleWithStatus[],
default: () => []
},
defaultView: {
type: String,
default: 'week',
@@ -158,7 +200,7 @@ const props = defineProps({
});
// 定义组件的事件
const emit = defineEmits(['dateRangeChange', 'slotClick', 'viewChange']);
const emit = defineEmits(['dateRangeChange', 'slotClick', 'scheduleClick', 'viewChange']);
// 当前日期和周期的开始日期
const today = ref(new Date());
@@ -382,7 +424,7 @@ const monthDays = computed(() => {
// 如果是最后一行,添加额外日志
if (week === 5) {
console.log(`日期 ${formattedDate} (${date.toLocaleDateString()}) 数据状态:`, {
上午时段: morningSlot ? '有数据' : '无数据',
中午时段: morningSlot ? '有数据' : '无数据',
下午时段: afternoonSlot ? '有数据' : '无数据',
isOtherMonth
});
@@ -412,7 +454,7 @@ 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');
}
@@ -422,6 +464,76 @@ function getAfternoonSlot(dateStr: string) {
return props.availableSlots.find((slot) => slot.date === dateStr && slot.timeSlot === '1');
}
// 获取指定日期的所有排班
function getSchedulesForDate(dateStr: string) {
return props.calendarSchedules.filter(schedule => schedule.date === dateStr)
.sort((a, b) => {
// 先按时段类型排序
const timeSlotCompare = a.timeSlot.localeCompare(b.timeSlot);
if (timeSlotCompare !== 0) return timeSlotCompare;
// 再按开始时间排序
return a.startTime.localeCompare(b.startTime);
});
}
// 处理排班点击
function handleScheduleClickEvent(schedule: ScheduleWithStatus) {
if (!schedule) return;
// 如果该时段已过期,则不允许预约
if (schedule.isExpired) {
ElMessage.warning('该时段已过期,请选择其他时段');
return;
}
// 如果该时段全部约满,则不允许预约
if (schedule.isFullyBooked) {
ElMessage.warning('该时段已全部约满,请选择其他时段');
return;
}
emit('scheduleClick', schedule);
}
// 获取所有唯一的时段类型
function getUniqueTimeSlots() {
const timeSlots = new Set<string>();
props.calendarSchedules.forEach(schedule => {
timeSlots.add(schedule.timeSlot);
});
return Array.from(timeSlots).sort();
}
// 获取时段类型标签
function getTimeSlotLabel(timeSlot: string) {
switch (timeSlot) {
case '0': return '中午';
case '1': return '下午';
case '2': return '下午';
default: return '时段' + timeSlot;
}
}
// 获取指定日期和时段类型的排班
function getSchedulesForDateAndTimeSlot(dateStr: string, timeSlot: string) {
const result = props.calendarSchedules
.filter(schedule => schedule.date === dateStr && schedule.timeSlot === timeSlot)
.sort((a, b) => a.startTime.localeCompare(b.startTime));
// 特别调试9月18日time_slot=0的情况
if (dateStr === '2025-09-18' && timeSlot === '0') {
console.log('🔍 WeekView调试 - getSchedulesForDateAndTimeSlot:', {
dateStr,
timeSlot,
totalSchedules: props.calendarSchedules.length,
filteredResult: result,
resultCount: result.length
});
}
return result;
}
// 监听视图变化
watch(
() => props.defaultView,
@@ -551,12 +663,18 @@ watch(
background-color: #67c23a;
}
.time-slot-card.noon {
background-color: #e6a23c;
}
.time-slot-card.fully-booked {
background-color: #c0c0c0;
cursor: not-allowed;
opacity: 0.7;
}
/* 月视图样式 */
.month-grid {
flex: 1;
@@ -645,9 +763,15 @@ watch(
background-color: #67c23a;
}
.month-slot.noon {
background-color: #e6a23c;
}
.month-slot.fully-booked {
background-color: #c0c0c0;
cursor: not-allowed;
opacity: 0.7;
}
</style>
@@ -19,9 +19,11 @@
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
<WeekView
:available-slots="availableSlots"
:calendar-schedules="calendarSchedules"
:default-view="calendarView"
@date-range-change="handleDateRangeChange"
@slot-click="handleSlotClick"
@schedule-click="handleScheduleClick"
@view-change="handleViewChange"
/>
</div>
@@ -35,27 +37,31 @@
<el-form-item label="咨询师" prop="counselorId">
<el-input v-model="counselorDisplayName" disabled />
</el-form-item>
<el-form-item label="办公室" prop="office">
<el-input v-model="selectedOffice" disabled />
</el-form-item>
<el-form-item label="预约日期" prop="appointmentDate">
<el-input v-model="formattedDate" disabled />
</el-form-item>
<el-form-item label="姓名" prop="studentName">
<el-input v-model="studentName" disabled />
</el-form-item>
<el-form-item label="预约时段" prop="appointmentDetailTime">
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
<el-input v-model="appointmentForm.appointmentDetailTime" disabled />
</el-form-item>
<el-form-item label="咨询主题" prop="consultTheme">
<el-select v-model="appointmentForm.consultTheme" 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)"
v-for="theme in xljk_consult_theme"
:key="theme.value"
:label="theme.label"
:value="theme.value"
/>
</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>
@@ -69,13 +75,14 @@
</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">
<el-dialog v-model="myAppointmentsVisible" title="我的预约记录" width="1200px" append-to-body destroy-on-close>
<el-table :data="myAppointmentsList" border style="width: 100%" header-align="center">
<el-table-column label="预约日期" prop="appointmentDate" width="120" align="center" />
<el-table-column label="预约时段" prop="appointmentDetailTime" width="140" align="center" />
<el-table-column label="咨询师" prop="counselorName" width="100" align="center" />
<el-table-column label="办公室" prop="office" width="200" show-overflow-tooltip header-align="center" />
<el-table-column label="咨询原因" prop="reason" min-width="200" show-overflow-tooltip header-align="center" />
<el-table-column label="状态" prop="status" width="100" align="center">
<template #default="scope">
<!-- 添加调试信息 -->
<div style="display: none">状态值: {{ scope.row.status }} 类型: {{ typeof scope.row.status }} 字典数据: {{ appointment_status }}</div>
@@ -85,6 +92,8 @@
<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-if="scope.row.status === 4 || scope.row.status === '4'" type="info">待审核</el-tag>
<el-tag v-else-if="scope.row.status === 5 || scope.row.status === '5'" type="danger">已驳回</el-tag>
<el-tag v-else type="info">未知({{ scope.row.status }})</el-tag>
</template>
</el-table-column>
@@ -96,7 +105,7 @@
</div>
<!-- 使用更直接的方式显示取消按钮 -->
<template v-if="scope.row.status === 0 || scope.row.status === '0'">
<template v-if="scope.row.status === 0 || scope.row.status === '0' || scope.row.status === 4 || scope.row.status === '4'">
<el-button link type="danger" size="small" @click="cancelAppointment(scope.row)"> 取消预约 </el-button>
</template>
<span v-else>--</span>
@@ -117,6 +126,7 @@
import { ref, reactive, onMounted, computed, watch } from 'vue';
import {
getXljkAvailableTimeSlots,
getXljkCalendarSchedules,
submitXljkAppointment,
getXljkMyAppointments,
updateXljkAppointment,
@@ -124,7 +134,7 @@ import {
getXljkCounselorOptions,
getXljkAvailableDetailSlots
} from '@/api/xljk/consultation';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail, ScheduleWithStatus } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
import { useUserStore } from '@/store/modules/user';
@@ -134,12 +144,17 @@ const { proxy } = getCurrentInstance() as any;
// 不再使用后端字典数据
// const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
// 获取咨询主题字典数据
const { xljk_consult_theme } = toRefs<any>(proxy?.useDict('xljk_consult_theme'));
// 本地定义状态字典(与预约记录管理页面保持一致)
const appointment_status = ref([
{ label: '待咨询', value: 0, listClass: 'primary' },
{ label: '已到访', value: 1, listClass: 'success' },
{ label: '已取消', value: 2, listClass: 'danger' },
{ label: '未到访', value: 3, listClass: 'warning' }
{ label: '未到访', value: 3, listClass: 'warning' },
{ label: '待审核', value: 4, listClass: 'info' },
{ label: '已驳回', value: 5, listClass: 'danger' }
]);
// 用户信息
@@ -150,6 +165,7 @@ const hasStudentInfo = computed(() => Boolean(studentId.value));
// 日历相关
const availableSlots = ref<AvailableTimeSlot[]>([]);
const calendarSchedules = ref<ScheduleWithStatus[]>([]);
const calendarView = ref('month'); // 默认使用月视图
// 预约表单相关
@@ -161,6 +177,7 @@ const studentNo = ref('');
const studentName = ref('');
const studentId = ref<string | number | null>(null);
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
const selectedOffice = ref(''); // 添加办公室字段
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
counselorId: undefined,
@@ -168,6 +185,7 @@ const appointmentForm = reactive<XljkConsultationAppointmentForm>({
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
consultTheme: '', // 咨询主题
reason: '',
status: 0 // 默认待咨询状态,使用数值类型
});
@@ -175,7 +193,7 @@ const appointmentForm = reactive<XljkConsultationAppointmentForm>({
const appointmentRules = {
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
consultTheme: [{ required: true, message: '请选择咨询主题', trigger: 'change' }]
};
// 我的预约相关
@@ -196,7 +214,13 @@ const formattedTimeSlot = computed(() => {
// 计算属性:咨询师名称
const counselorDisplayName = computed(() => {
return selectedSlot.value?.counselorName || '正在加载...';
if (selectedSlot.value?.counselorName) {
return selectedSlot.value.counselorName;
}
if (selectedSlot.value?.counselorId) {
return '正在加载...';
}
return '请选择咨询师';
});
// 处理日期范围变化
@@ -230,27 +254,52 @@ function handleViewChange(view: string) {
// 加载可用预约时段
async function loadAvailableSlots(startDate: string, endDate: string) {
try {
// 加载新的排班数据
const scheduleRes = await getXljkCalendarSchedules(startDate, endDate);
if (scheduleRes.code === 200) {
calendarSchedules.value = scheduleRes.data || [];
}
// 为了兼容现有逻辑,同时保留原有的时段数据加载
const res = await getXljkAvailableTimeSlots(startDate, endDate);
if (res.code === 200) {
availableSlots.value = res.data || [];
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
try {
const counselorRes = await getXljkCounselorOptions();
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;
});
// 如果有可用时段,检查并补充咨询师姓名
if (availableSlots.value.length > 0) {
// 检查是否有时段缺少咨询师姓名或办公室信息
const hasMissingCounselorInfo = availableSlots.value.some(slot =>
(!slot.counselorName || !slot.office) && slot.counselorId
);
if (hasMissingCounselorInfo) {
try {
console.log('检测到缺少咨询师信息,开始获取咨询师详细信息...');
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
const counselors = counselorRes.data;
console.log('获取到咨询师列表:', counselors);
// 为每个时段添加咨询师姓名和办公室信息
availableSlots.value = availableSlots.value.map((slot) => {
if ((!slot.counselorName || !slot.office) && slot.counselorId) {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
const updatedSlot = {
...slot,
counselorName: slot.counselorName || counselor.name,
office: slot.office || counselor.office
};
console.log('更新时段信息:', updatedSlot);
return updatedSlot;
}
}
return slot;
});
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
}
@@ -260,19 +309,103 @@ async function loadAvailableSlots(startDate: string, endDate: string) {
}
}
// 处理新的排班点击
async function handleScheduleClick(schedule: ScheduleWithStatus) {
console.log('点击排班:', schedule);
// 如果该时段已过期,则不允许预约
if (schedule.isExpired) {
ElMessage.warning('该时段已过期,请选择其他时段');
return;
}
// 如果该时段全部约满,则不允许预约
if (schedule.isFullyBooked) {
ElMessage.warning('该时段已全部约满,请选择其他时段');
return;
}
// 获取咨询师名称和办公室信息
let counselorName = schedule.counselorName || '';
let office = schedule.office || '';
// 如果缺少咨询师姓名或办公室信息,则去获取完整的咨询师信息
if ((!counselorName || !office) && schedule.counselorId) {
try {
console.log('获取咨询师详细信息,counselorId:', schedule.counselorId);
const res = await getXljkCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === schedule.counselorId);
if (counselor) {
counselorName = counselorName || counselor.name;
office = office || counselor.office || '';
console.log('获取到咨询师信息:', { name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', schedule.counselorId);
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
console.log('最终的办公室信息:', office);
// 填充预约表单 - 直接使用排班数据,不需要再选择详细时段
appointmentForm.counselorId = schedule.counselorId;
appointmentForm.scheduleId = schedule.scheduleId;
appointmentForm.appointmentDate = schedule.date;
appointmentForm.appointmentTime = `${schedule.startTime}-${schedule.endTime}`;
appointmentForm.appointmentDetailTime = `${schedule.startTime}-${schedule.endTime}`; // 直接使用完整时段
appointmentForm.consultTheme = ''; // 清空咨询主题,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 设置选中的时段信息以便显示
selectedSlot.value = {
scheduleId: schedule.scheduleId,
counselorId: schedule.counselorId,
counselorName: counselorName,
office: office,
date: schedule.date,
timeSlot: schedule.timeSlot,
startTime: schedule.startTime,
endTime: schedule.endTime,
allBooked: schedule.isFullyBooked,
isExpired: schedule.isExpired
} as AvailableTimeSlot;
// 设置办公室信息
selectedOffice.value = office;
// 如果已登录且有用户ID,初始化学生信息
if (userId.value) {
// 初始设置为当前用户ID
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
}
appointmentFormVisible.value = true;
}
// 处理时段点击
async function handleSlotClick(slot: AvailableTimeSlot) {
selectedSlot.value = slot;
// 如果没有咨询师姓名,尝试获取咨询师信息
if (!slot.counselorName && slot.counselorId) {
// 如果没有咨询师姓名或办公室信息,尝试获取咨询师信息
if ((!slot.counselorName || !slot.office) && slot.counselorId) {
try {
console.log('获取咨询师详细信息,counselorId:', slot.counselorId);
const res = await getXljkCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
if (counselor) {
slot.counselorName = counselor.name;
slot.counselorName = slot.counselorName || counselor.name;
slot.office = slot.office || counselor.office || '';
selectedSlot.value = { ...slot };
console.log('获取到咨询师信息:', { name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', slot.counselorId);
}
}
} catch (error) {
@@ -280,12 +413,16 @@ async function handleSlotClick(slot: AvailableTimeSlot) {
}
}
// 设置办公室信息
selectedOffice.value = slot.office || '';
// 填充预约表单
appointmentForm.counselorId = slot.counselorId;
appointmentForm.scheduleId = slot.scheduleId;
appointmentForm.appointmentDate = slot.date;
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
appointmentForm.consultTheme = ''; // 清空咨询主题,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 加载详细时段
@@ -387,7 +524,12 @@ function submitAppointmentForm() {
}
if (!appointmentForm.appointmentDetailTime) {
ElMessage.warning('请选择具体预约时段');
ElMessage.warning('预约时段信息缺失,请重新点击时段');
return;
}
if (!appointmentForm.consultTheme) {
ElMessage.warning('请选择咨询主题');
return;
}
@@ -452,12 +594,36 @@ async function viewMyAppointments() {
let res = await getXljkMyAppointments(id, true); // 强制刷新数据
console.log('查询结果:', res);
// 获取咨询师信息用于补充办公室字段
let counselors: CounselorOption[] = [];
try {
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data;
console.log('获取到咨询师列表:', counselors);
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
if (res.code === 200 && res.data && res.data.length > 0) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = res.data.map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
@@ -473,7 +639,7 @@ async function viewMyAppointments() {
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office, '类型:', typeof item.status);
return item;
});
@@ -491,11 +657,23 @@ async function viewMyAppointments() {
console.log('数字类型查询结果:', res);
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = (res.data || []).map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
@@ -511,7 +689,7 @@ async function viewMyAppointments() {
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office, '类型:', typeof item.status);
return item;
});
@@ -594,7 +772,7 @@ async function initCounselorInfo() {
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, counselorName: counselor.name, office: counselor.office };
}
return slot;
});
@@ -616,9 +794,34 @@ async function refreshMyAppointments() {
try {
// 强制刷新数据
const res = await getXljkMyAppointments(studentId.value, true);
// 获取咨询师信息用于补充办公室字段
let counselors: CounselorOption[] = [];
try {
const counselorRes = await getXljkCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data;
console.log('刷新时获取到咨询师列表:', counselors);
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
// 处理数据,确保状态字段是数值类型,并补充办公室信息
myAppointmentsList.value = (res.data || []).map((item: any) => {
// 补充办公室信息
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find((c: CounselorOption) => c.id === item.counselorId);
if (counselor) {
item.office = counselor.office || '';
item.counselorName = item.counselorName || counselor.name || '';
console.log('刷新时补充咨询师信息:', { counselorId: item.counselorId, name: counselor.name, office: counselor.office });
} else {
console.log('刷新时未找到对应的咨询师信息, counselorId:', item.counselorId);
}
}
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
@@ -631,6 +834,7 @@ async function refreshMyAppointments() {
}
}
console.log('刷新后的预约记录项:', item.id, '状态:', item.status, '办公室:', item.office);
return item;
});
ElMessage.success('刷新成功');
+460 -131
View File
@@ -12,6 +12,11 @@
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="办公室" prop="office">
<el-input v-model="queryParams.office" placeholder="请输入办公室" clearable style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排班日期" prop="dateRange">
<el-date-picker
@@ -28,7 +33,7 @@
<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="0" />
<el-option label="下午" value="1" />
</el-select>
</el-form-item>
@@ -75,8 +80,9 @@
<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="office" min-width="120" show-overflow-tooltip />
<el-table-column label="日期范围" align="center" prop="dateRange" min-width="180" />
<el-table-column label="上午时段" align="center" min-width="120">
<el-table-column label="中午时段" align="center" min-width="120">
<template #default="scope">
<span v-if="scope.row.morningEnabled">{{ scope.row.morningTime }}</span>
<span v-else>-</span>
@@ -129,13 +135,16 @@
</el-card>
<!-- 添加或修改预约时段对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-dialog :title="title" v-model="open" width="800px" 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-select v-model="form.counselorId" placeholder="请选择咨询师" style="width: 100%" @change="handleCounselorChange">
<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="office">
<el-input v-model="form.office" placeholder="请输入办公室" style="width: 100%" />
</el-form-item>
<el-form-item label="排班日期" prop="dateRange" v-if="!form.id">
<el-date-picker
v-model="form.dateRange"
@@ -151,60 +160,92 @@
<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>
<!-- 中午时段设置 -->
<el-divider content-position="left">中午时段</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="启用中午" prop="noonEnabled">
<el-switch v-model="form.noonEnabled" />
</el-form-item>
</el-col>
<el-col :span="12" class="text-right">
<el-button v-if="form.noonEnabled" type="primary" link icon="Plus" class="add-time-slot-btn" @click="addNoonTimeSlot">增加时段</el-button>
</el-col>
</el-row>
<template v-if="form.noonEnabled">
<div v-for="(slot, index) in form.noonTimeSlots" :key="`noon-${index}`" class="time-slot-row">
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="index === 0 ? '开始时间' : ''" :prop="`noonTimeSlots.${index}.startTime`">
<el-time-picker
v-model="slot.startTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
@change="(time) => handleNoonStartTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item :label="index === 0 ? '结束时间' : ''" :prop="`noonTimeSlots.${index}.endTime`">
<el-time-picker
v-model="slot.endTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
@change="(time) => handleNoonEndTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="4" class="time-slot-actions">
<el-button v-if="index > 0" type="danger" link icon="Delete" class="delete-time-slot-btn" @click="removeNoonTimeSlot(index)"></el-button>
</el-col>
</el-row>
</div>
</template>
<!-- 下午时段设置 -->
<el-divider content-position="left">下午时段</el-divider>
<el-form-item label="启用下午" prop="afternoonEnabled">
<el-switch v-model="form.afternoonEnabled" />
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="启用下午" prop="afternoonEnabled">
<el-switch v-model="form.afternoonEnabled" />
</el-form-item>
</el-col>
<el-col :span="12" class="text-right">
<el-button v-if="form.afternoonEnabled" type="primary" link icon="Plus" class="add-time-slot-btn" @click="addAfternoonTimeSlot">增加时段</el-button>
</el-col>
</el-row>
<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>
<div v-for="(slot, index) in form.afternoonTimeSlots" :key="`afternoon-${index}`" class="time-slot-row">
<el-row :gutter="20">
<el-col :span="10">
<el-form-item :label="index === 0 ? '开始时间' : ''" :prop="`afternoonTimeSlots.${index}.startTime`">
<el-time-picker
v-model="slot.startTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
@change="(time) => handleAfternoonStartTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="10">
<el-form-item :label="index === 0 ? '结束时间' : ''" :prop="`afternoonTimeSlots.${index}.endTime`">
<el-time-picker
v-model="slot.endTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
@change="(time) => handleAfternoonEndTimeChange(time, index)"
/>
</el-form-item>
</el-col>
<el-col :span="4" class="time-slot-actions">
<el-button v-if="index > 0" type="danger" link icon="Delete" class="delete-time-slot-btn" @click="removeAfternoonTimeSlot(index)"></el-button>
</el-col>
</el-row>
</div>
</template>
<el-form-item label="状态" prop="status">
@@ -268,34 +309,13 @@ 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,
office: undefined,
timeSlot: undefined,
status: undefined,
params: {} as any
@@ -305,23 +325,50 @@ const queryParams = reactive({
const form = reactive<any>({
id: undefined,
counselorId: undefined,
office: '',
scheduleDate: '',
dateRange: [],
morningEnabled: true,
morningStartTime: '08:30',
morningEndTime: '12:00',
noonEnabled: true,
noonTimeSlots: [
{
startTime: '12:00',
endTime: '12:40',
startTimeTemp: (() => {
const time = new Date();
time.setHours(12, 0, 0, 0);
return time;
})(),
endTimeTemp: (() => {
const time = new Date();
time.setHours(12, 40, 0, 0);
return time;
})()
}
],
afternoonEnabled: true,
afternoonStartTime: '14:30',
afternoonEndTime: '17:00',
timeSlot: '0',
startTime: '',
endTime: '',
afternoonTimeSlots: [
{
startTime: '17:00',
endTime: '17:40',
startTimeTemp: (() => {
const time = new Date();
time.setHours(17, 0, 0, 0);
return time;
})(),
endTimeTemp: (() => {
const time = new Date();
time.setHours(17, 40, 0, 0);
return time;
})()
}
],
status: '0'
});
// 表单校验
const rules = {
counselorId: [{ required: true, message: '咨询师不能为空', trigger: 'change' }],
office: [{ required: true, message: '办公室不能为空', trigger: 'blur' }],
scheduleDate: [{ required: false, message: '排班日期不能为空', trigger: 'change' }],
dateRange: [{ required: true, message: '排班日期范围不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
@@ -377,38 +424,42 @@ const cancel = () => {
const reset = () => {
form.id = undefined;
form.counselorId = undefined;
form.office = '';
form.scheduleDate = '';
form.dateRange = [];
form.morningEnabled = true;
form.noonEnabled = 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 noonStartTime = new Date();
noonStartTime.setHours(12, 0, 0, 0);
const noonEndTime = new Date();
noonEndTime.setHours(12, 40, 0, 0);
// 设置默认时间
const morningStart = new Date();
morningStart.setHours(8, 30, 0, 0);
morningStartTimeTemp.value = morningStart;
form.noonTimeSlots = [
{
startTime: '12:00',
endTime: '12:40',
startTimeTemp: noonStartTime,
endTimeTemp: noonEndTime
}
];
const morningEnd = new Date();
morningEnd.setHours(12, 0, 0, 0);
morningEndTimeTemp.value = morningEnd;
// 设置下午时段默认值
const afternoonStartTime = new Date();
afternoonStartTime.setHours(17, 0, 0, 0);
const afternoonEndTime = new Date();
afternoonEndTime.setHours(17, 40, 0, 0);
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;
form.afternoonTimeSlots = [
{
startTime: '17:00',
endTime: '17:40',
startTimeTemp: afternoonStartTime,
endTimeTemp: afternoonEndTime
}
];
scheduleFormRef.value?.resetFields();
};
@@ -441,7 +492,7 @@ const handleAdd = () => {
};
/** 查看按钮操作 */
const handleView = async (row: XljkConsultationScheduleVO) => {
const handleView = async (row: XljkConsultationScheduleSummaryVO) => {
try {
const res = await getXljkSchedule(row.id);
if (res.code === 200 && res.data) {
@@ -455,7 +506,7 @@ const handleView = async (row: XljkConsultationScheduleVO) => {
};
/** 修改按钮操作 */
const handleUpdate = async (row?: XljkConsultationScheduleVO) => {
const handleUpdate = async (row?: XljkConsultationScheduleSummaryVO) => {
if (row && row.id) {
try {
// 修复:将字符串状态正确转换为数字进行比较
@@ -491,6 +542,7 @@ const submitForm = async () => {
await updateXljkSchedule({
id: form.id,
counselorId: form.counselorId,
office: form.office,
scheduleDate: form.scheduleDate,
timeSlot: form.timeSlot,
startTime: form.startTime,
@@ -514,26 +566,67 @@ const submitForm = async () => {
}
// 检查是否至少启用了一个时段
if (!form.morningEnabled && !form.afternoonEnabled) {
ElMessage.warning('请至少启用一个时段(上午或下午)');
if (!form.noonEnabled && !form.afternoonEnabled) {
ElMessage.warning('请至少启用一个时段(中午或下午)');
return;
}
// 确保时间数据同步 - 从时间选择器同步到字符串字段
if (form.noonEnabled) {
form.noonTimeSlots.forEach((slot, index) => {
if (slot.startTimeTemp && !slot.startTime) {
const hours = slot.startTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.startTimeTemp.getMinutes().toString().padStart(2, '0');
slot.startTime = `${hours}:${minutes}`;
}
if (slot.endTimeTemp && !slot.endTime) {
const hours = slot.endTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.endTimeTemp.getMinutes().toString().padStart(2, '0');
slot.endTime = `${hours}:${minutes}`;
}
});
}
if (form.afternoonEnabled) {
form.afternoonTimeSlots.forEach((slot, index) => {
if (slot.startTimeTemp && !slot.startTime) {
const hours = slot.startTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.startTimeTemp.getMinutes().toString().padStart(2, '0');
slot.startTime = `${hours}:${minutes}`;
}
if (slot.endTimeTemp && !slot.endTime) {
const hours = slot.endTimeTemp.getHours().toString().padStart(2, '0');
const minutes = slot.endTimeTemp.getMinutes().toString().padStart(2, '0');
slot.endTime = `${hours}:${minutes}`;
}
});
}
// 批量创建预约时段的请求数据
const batchCreateData = {
counselorId: form.counselorId,
office: form.office,
startDate: form.dateRange[0],
endDate: form.dateRange[1],
morningEnabled: form.morningEnabled,
morningStartTime: form.morningStartTime,
morningEndTime: form.morningEndTime,
noonEnabled: form.noonEnabled,
noonTimeSlots: form.noonTimeSlots.map(slot => ({
startTime: slot.startTime,
endTime: slot.endTime
})),
afternoonEnabled: form.afternoonEnabled,
afternoonStartTime: form.afternoonStartTime,
afternoonEndTime: form.afternoonEndTime,
afternoonTimeSlots: form.afternoonTimeSlots.map(slot => ({
startTime: slot.startTime,
endTime: slot.endTime
})),
status: form.status,
skipWeekends: true // 自动跳过周末
};
// 调试日志 - 确认时间数据
console.log('提交的批量创建数据:', batchCreateData);
console.log('中午时段数据:', form.noonTimeSlots);
console.log('下午时段数据:', form.afternoonTimeSlots);
// 调用批量创建API
const res = await addXljkSchedule(batchCreateData);
if (res.code === 200) {
@@ -551,7 +644,7 @@ const submitForm = async () => {
};
/** 删除按钮操作 */
const handleDelete = async (row?: XljkConsultationScheduleVO) => {
const handleDelete = async (row?: XljkConsultationScheduleSummaryVO) => {
const _ids = row?.id || ids.value;
const confirmMessage = row
? `是否确认删除该预约时段?${row.scheduleCount > 1 ? `(包含${row.scheduleCount}条记录)` : ''}`
@@ -594,47 +687,147 @@ const handleEndTimeChange = (time: Date) => {
}
};
/** 处理上午开始时间变化 */
const handleMorningStartTimeChange = (time: Date) => {
/** 处理中午开始时间变化 */
const handleNoonStartTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningStartTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.noonTimeSlots[index].startTime = timeString;
form.noonTimeSlots[index].startTimeTemp = time;
} else {
form.morningStartTime = '';
form.noonTimeSlots[index].startTime = '';
form.noonTimeSlots[index].startTimeTemp = null;
}
};
/** 处理上午结束时间变化 */
const handleMorningEndTimeChange = (time: Date) => {
/** 处理中午结束时间变化 */
const handleNoonEndTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningEndTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.noonTimeSlots[index].endTime = timeString;
form.noonTimeSlots[index].endTimeTemp = time;
} else {
form.morningEndTime = '';
form.noonTimeSlots[index].endTime = '';
form.noonTimeSlots[index].endTimeTemp = null;
}
};
/** 新增中午时段 */
const addNoonTimeSlot = () => {
const lastSlot = form.noonTimeSlots[form.noonTimeSlots.length - 1];
let startTime = '12:00';
let endTime = '12:40';
if (lastSlot && lastSlot.endTime) {
startTime = lastSlot.endTime;
// 计算结束时间(开始时间+40分钟)
const [hours, minutes] = startTime.split(':').map(Number);
const startDate = new Date();
startDate.setHours(hours, minutes, 0, 0);
const endDate = new Date(startDate.getTime() + 40 * 60 * 1000); // 加40分钟
endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
}
// 创建对应的Date对象用于时间选择器
const startTimeDate = new Date();
const [startHours, startMinutes] = startTime.split(':').map(Number);
startTimeDate.setHours(startHours, startMinutes, 0, 0);
const endTimeDate = new Date();
const [endHours, endMinutes] = endTime.split(':').map(Number);
endTimeDate.setHours(endHours, endMinutes, 0, 0);
form.noonTimeSlots.push({
startTime,
endTime,
startTimeTemp: startTimeDate,
endTimeTemp: endTimeDate
});
};
/** 移除中午时段 */
const removeNoonTimeSlot = (index: number) => {
form.noonTimeSlots.splice(index, 1);
};
/** 处理下午开始时间变化 */
const handleAfternoonStartTimeChange = (time: Date) => {
const handleAfternoonStartTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonStartTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.afternoonTimeSlots[index].startTime = timeString;
form.afternoonTimeSlots[index].startTimeTemp = time;
} else {
form.afternoonStartTime = '';
form.afternoonTimeSlots[index].startTime = '';
form.afternoonTimeSlots[index].startTimeTemp = null;
}
};
/** 处理下午结束时间变化 */
const handleAfternoonEndTimeChange = (time: Date) => {
const handleAfternoonEndTimeChange = (time: Date, index: number) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonEndTime = `${hours}:${minutes}`;
const timeString = `${hours}:${minutes}`;
form.afternoonTimeSlots[index].endTime = timeString;
form.afternoonTimeSlots[index].endTimeTemp = time;
} else {
form.afternoonEndTime = '';
form.afternoonTimeSlots[index].endTime = '';
form.afternoonTimeSlots[index].endTimeTemp = null;
}
};
/** 新增下午时段 */
const addAfternoonTimeSlot = () => {
const lastSlot = form.afternoonTimeSlots[form.afternoonTimeSlots.length - 1];
let startTime = '17:00';
let endTime = '17:40';
if (lastSlot && lastSlot.endTime) {
startTime = lastSlot.endTime;
// 计算结束时间(开始时间+40分钟)
const [hours, minutes] = startTime.split(':').map(Number);
const startDate = new Date();
startDate.setHours(hours, minutes, 0, 0);
const endDate = new Date(startDate.getTime() + 40 * 60 * 1000); // 加40分钟
endTime = `${endDate.getHours().toString().padStart(2, '0')}:${endDate.getMinutes().toString().padStart(2, '0')}`;
}
// 创建对应的Date对象用于时间选择器
const startTimeDate = new Date();
const [startHours, startMinutes] = startTime.split(':').map(Number);
startTimeDate.setHours(startHours, startMinutes, 0, 0);
const endTimeDate = new Date();
const [endHours, endMinutes] = endTime.split(':').map(Number);
endTimeDate.setHours(endHours, endMinutes, 0, 0);
form.afternoonTimeSlots.push({
startTime,
endTime,
startTimeTemp: startTimeDate,
endTimeTemp: endTimeDate
});
};
/** 移除下午时段 */
const removeAfternoonTimeSlot = (index: number) => {
form.afternoonTimeSlots.splice(index, 1);
};
/** 处理咨询师变化 */
const handleCounselorChange = (counselorId: number) => {
if (counselorId) {
const selectedCounselor = counselorOptions.value.find(counselor => counselor.id === counselorId);
if (selectedCounselor && selectedCounselor.office) {
form.office = selectedCounselor.office;
}
} else {
form.office = '';
}
};
@@ -684,4 +877,140 @@ onMounted(() => {
margin-bottom: 0 !important;
}
}
/* 时间槽行样式 */
.time-slot-row {
margin-bottom: 10px;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
/* 时间槽操作按钮样式 */
.time-slot-actions {
display: flex;
justify-content: flex-end;
align-items: center;
padding-top: 8px;
}
/* 右对齐样式 */
.text-right {
text-align: right;
}
/* 新增时段按钮样式 */
.add-time-slot-btn {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #409eff !important;
padding: 8px 12px;
font-size: 14px;
font-weight: normal;
transition: color 0.3s ease;
}
.add-time-slot-btn:hover {
color: #66b1ff !important;
background-color: transparent !important;
}
.add-time-slot-btn:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.add-time-slot-btn:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 更强的选择器覆盖 Element Plus 默认样式 */
.el-button.add-time-slot-btn.el-button--primary.is-link {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #409eff !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:hover {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #66b1ff !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.el-button.add-time-slot-btn.el-button--primary.is-link:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 删除时段按钮样式 */
.delete-time-slot-btn {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f56c6c !important;
padding: 8px;
font-size: 16px;
transition: color 0.3s ease;
}
.delete-time-slot-btn:hover {
color: #f78989 !important;
background-color: transparent !important;
}
.delete-time-slot-btn:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.delete-time-slot-btn:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
/* 更强的选择器覆盖 Element Plus 默认样式 */
.el-button.delete-time-slot-btn.el-button--danger.is-link {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f56c6c !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:hover {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
color: #f78989 !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:focus {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
outline: none !important;
}
.el-button.delete-time-slot-btn.el-button--danger.is-link:active {
border: none !important;
box-shadow: none !important;
background-color: transparent !important;
}
</style>
File diff suppressed because it is too large. Load diff