更新心理健康模块配置和前端依赖

This commit is contained in:
李旭明 committed 2025-08-04 11:04:00 +08:00
1 parent 593aa01a70
commit 7ad62a88c4
120 files changed
+19796 -82

No files matched your search

+5
View File
@@ -21,6 +21,11 @@
},
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@fullcalendar/core": "^6.1.18",
"@fullcalendar/daygrid": "^6.1.18",
"@fullcalendar/interaction": "^6.1.18",
"@fullcalendar/timegrid": "^6.1.18",
"@fullcalendar/vue3": "^6.1.18",
"@highlightjs/vue-plugin": "2.1.0",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.1.0",
@@ -0,0 +1 @@
+214
View File
@@ -0,0 +1,214 @@
import request from '@/utils/request';
import {
XljkConsultationScheduleVO,
XljkConsultationScheduleForm,
XljkConsultationScheduleQuery,
XljkConsultationAppointmentVO,
XljkConsultationAppointmentForm,
XljkConsultationAppointmentQuery,
AvailableTimeSlot,
TimeSlotDetail,
CounselorOption
} from './types';
// ===== 预约时段管理 API =====
// 查询预约时段列表
export const listSchedule = (query: XljkConsultationScheduleQuery) => {
return request({
url: '/api/xljk/consultation/schedule/list',
method: 'get',
params: query
});
};
// 查询预约时段详细信息
export const getSchedule = (id: string | number) => {
return request({
url: `/api/xljk/consultation/schedule/${id}`,
method: 'get'
});
};
// 新增预约时段
export const addSchedule = (data: any) => {
return request({
url: '/api/xljk/consultation/schedule',
method: 'post',
data: data
});
};
// 修改预约时段
export const updateSchedule = (data: XljkConsultationScheduleForm) => {
return request({
url: '/api/xljk/consultation/schedule',
method: 'put',
data: data
});
};
// 删除预约时段
export const delSchedule = (ids: string | number | Array<string | number>) => {
return request({
url: `/api/xljk/consultation/schedule/${typeof ids === 'object' ? ids.join(',') : ids}`,
method: 'delete'
});
};
// ===== 预约记录管理 API =====
// 查询预约记录列表
export const listAppointment = (query: XljkConsultationAppointmentQuery) => {
return request({
url: '/api/xljk/consultation/appointment/list',
method: 'get',
params: query
});
};
// 查询预约记录详细信息
export const getAppointment = (id: string | number) => {
return request({
url: `/api/xljk/consultation/appointment/${id}`,
method: 'get'
});
};
// 修改预约记录
export const updateAppointment = (data: XljkConsultationAppointmentForm) => {
return request({
url: '/api/xljk/consultation/appointment',
method: 'put',
data: data
});
};
// 删除预约记录
export const delAppointment = (ids: string | number | Array<string | number>) => {
return request({
url: `/api/xljk/consultation/appointment/${typeof ids === 'object' ? ids.join(',') : ids}`,
method: 'delete'
});
};
// 获取可预约时段列表(用于日历视图)
export const getAvailableTimeSlots = (startDate: string, endDate: string) => {
return request({
url: '/api/xljk/consultation/appointment/available-slots',
method: 'get',
params: { startDate, endDate }
});
};
// 获取可用的详细时段列表(30分钟一个时段)
export const getAvailableDetailSlots = (date: string, timeSlot: string) => {
return request({
url: '/api/xljk/consultation/appointment/available-detail-slots',
method: 'get',
params: { date, timeSlot }
});
};
// 学生提交预约
export const submitAppointment = async (data: XljkConsultationAppointmentForm) => {
// 检查是否被限制预约
try {
const { checkLimitation } = await import('./noshow');
if (data.studentId) {
const res = await checkLimitation(data.studentId);
if (res.code === 200 && res.data && res.data.limited) {
return {
code: 400,
msg: `由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDateStr} 自动解除。如有特殊情况,请联系心理咨询中心。`,
data: null
};
}
}
} catch (error) {
console.error('检查预约限制失败', error);
// 检查失败不阻止预约,继续提交
}
// 提交预约申请
return request({
url: '/api/xljk/consultation/appointment/submit',
method: 'post',
data: data
});
};
// 获取我的预约记录(学生用)
export const getMyAppointments = (studentId: string | number, forceRefresh: boolean = false) => {
let url = `/api/xljk/consultation/appointment/my-appointments/${studentId}`;
// 强制添加时间戳参数,确保绕过浏览器缓存
url += `?_t=${new Date().getTime()}`;
// 如果需要额外强制刷新(如用户手动点击刷新按钮)
if (forceRefresh) {
url += `&force=true`;
}
return request({
url,
method: 'get',
headers: {
// 禁止所有缓存
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
};
// 记录咨询结果
export const recordConsultation = (id: string | number, data: any) => {
return request({
url: `/api/xljk/consultation/appointment/record-consultation/${id}`,
method: 'post',
data: data
});
};
// 获取咨询师选项
export const getCounselorOptions = () => {
return request({
url: '/api/xljk/consultation/schedule/options/counselors',
method: 'get'
});
};
// 获取学生信息(通过学号)
export const getStudentByNo = (studentNo: string) => {
return request({
url: `/api/xljk/consultation/student/${studentNo}`,
method: 'get'
});
};
// 获取院系选项
export const getCollegeOptions = () => {
return request({
url: '/api/xljk/consultation/appointment/options/colleges',
method: 'get'
});
};
// 获取专业选项
export const getMajorOptions = (collegeName?: string) => {
return request({
url: '/api/xljk/consultation/appointment/options/majors',
method: 'get',
params: { collegeName }
});
};
// 获取班级选项
export const getClassOptions = (collegeName?: string, majorName?: string) => {
return request({
url: '/api/xljk/consultation/appointment/options/classes',
method: 'get',
params: { collegeName, majorName }
});
};
+60
View File
@@ -0,0 +1,60 @@
import request from '@/utils/request';
import { NoShowQuery, NoShowVO } from './types';
/**
* 查询未到访记录列表
* @param query 查询参数
* @returns 未到访记录列表
*/
export function listNoShow(query: NoShowQuery) {
// 创建一个新的查询对象,避免修改原始对象
const params = { ...query };
// 如果状态是'all',表示"全部",则从参数中移除
if (params.status === 'all') {
delete params.status;
}
return request({
url: '/api/xljk/consultation/no-show/list',
method: 'get',
params
});
}
/**
* 获取未到访记录详情
* @param id 未到访记录ID
* @returns 未到访记录详情
*/
export function getNoShow(id: string | number) {
return request({
url: `/api/xljk/consultation/no-show/${id}`,
method: 'get'
});
}
/**
* 解除预约限制
* @param id 未到访记录ID
* @returns 操作结果
*/
export function releaseNoShowLimitation(id: string | number) {
return request({
url: `/api/xljk/consultation/no-show/${id}/release`,
method: 'put'
});
}
/**
* 检查学生是否被限制预约
* @param studentId 学生ID
* @returns 限制状态
*/
export function checkLimitation(studentId: string | number) {
return request({
url: '/api/xljk/consultation/no-show/check-limitation',
method: 'get',
params: { studentId }
});
}
+604
View File
@@ -0,0 +1,604 @@
export interface XljkConsultationScheduleVO {
/**
* 主键ID
*/
id: string | number;
/**
* 咨询师ID
*/
counselorId: number;
/**
* 咨询师姓名(关联展示字段)
*/
counselorName?: string;
/**
* 排班日期
*/
scheduleDate: string;
/**
* 时段类型(0上午 1下午)
*/
timeSlot: string;
/**
* 开始时间(HH:mm)
*/
startTime: string;
/**
* 结束时间(HH:mm)
*/
endTime: string;
/**
* 状态(0正常 1停用)
*/
status: string;
/**
* 创建者
*/
createBy: number;
/**
* 创建时间
*/
createTime: string;
/**
* 更新者
*/
updateBy: number;
/**
* 更新时间
*/
updateTime: string;
}
export interface XljkConsultationScheduleForm extends BaseEntity {
/**
* 主键ID
*/
id?: string | number;
/**
* 咨询师ID
*/
counselorId?: number;
/**
* 排班日期
*/
scheduleDate?: string;
/**
* 时段类型(0上午 1下午)
*/
timeSlot?: string;
/**
* 开始时间(HH:mm)
*/
startTime?: string;
/**
* 结束时间(HH:mm)
*/
endTime?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
}
export interface XljkConsultationScheduleQuery extends PageQuery {
/**
* 咨询师ID
*/
counselorId?: number;
/**
* 排班日期开始
*/
beginScheduleDate?: string;
/**
* 排班日期结束
*/
endScheduleDate?: string;
/**
* 时段类型(0上午 1下午)
*/
timeSlot?: string;
/**
* 状态(0正常 1停用)
*/
status?: string;
/**
* 日期范围参数
*/
params?: any;
}
export interface XljkConsultationAppointmentVO extends BaseEntity {
/**
* 学生ID
*/
studentId?: number;
/**
* 学号
*/
studentNo?: string;
/**
* 咨询师ID
*/
counselorId?: number;
/**
* 排班ID
*/
scheduleId?: number;
/**
* 预约日期
*/
appointmentDate?: string;
/**
* 预约时段
*/
appointmentTime?: string;
/**
* 具体预约时间段,如"08:30-09:00"
*/
appointmentDetailTime?: string;
/**
* 咨询原因
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
*/
status?: number;
/**
* 实际到访时间
*/
actualVisitTime?: any;
/**
* 沟通时长(分钟)
*/
communicationDuration?: number;
/**
* 咨询内容
*/
consultationContent?: string;
/**
* 咨询师建议
*/
counselorSuggestion?: string;
/**
* 学生姓名
*/
studentName?: string;
/**
* 班级
*/
className?: string;
/**
* 咨询师姓名
*/
counselorName?: string;
/**
* 院系
*/
collegeName?: string;
/**
* 专业
*/
majorName?: string;
}
export interface XljkConsultationAppointmentForm {
/**
* 主键
*/
id?: string | number;
/**
* 学生ID
*/
studentId?: string | number;
/**
* 学号
*/
studentNo?: string;
/**
* 咨询师ID
*/
counselorId?: number;
/**
* 排班ID
*/
scheduleId?: number;
/**
* 预约日期
*/
appointmentDate?: string;
/**
* 预约时段
*/
appointmentTime?: string;
/**
* 具体预约时间段,如"08:30-09:00"
*/
appointmentDetailTime?: string;
/**
* 咨询原因
*/
reason?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
*/
status?: number;
/**
* 实际到访时间
*/
actualVisitTime?: any;
/**
* 沟通时长(分钟)
*/
communicationDuration?: number;
/**
* 咨询内容
*/
consultationContent?: string;
/**
* 咨询师建议
*/
counselorSuggestion?: string;
}
export interface XljkConsultationAppointmentQuery extends PageQuery {
/**
* 学生ID
*/
studentId?: number;
/**
* 学生姓名
*/
studentName?: string;
/**
* 学号
*/
studentNo?: string;
/**
* 院系
*/
collegeName?: string;
/**
* 专业
*/
majorName?: string;
/**
* 咨询师ID
*/
counselorId?: number;
/**
* 预约日期开始
*/
beginAppointmentDate?: string;
/**
* 预约日期结束
*/
endAppointmentDate?: string;
/**
* 状态(0待咨询 1已完成 2已取消 3未到访)
*/
status?: string;
/**
* 日期范围参数
*/
params?: any;
}
export interface AvailableTimeSlot {
/**
* 预约时段ID
*/
scheduleId: number;
/**
* 咨询师ID
*/
counselorId: number;
/**
* 咨询师姓名
*/
counselorName?: string;
/**
* 日期
*/
date: string;
/**
* 时段类型(0上午 1下午)
*/
timeSlot: string;
/**
* 开始时间
*/
startTime: string;
/**
* 结束时间
*/
endTime: string;
/**
* 是否全部约满
*/
allBooked?: boolean;
/**
* 可用数量
*/
availableCount?: number;
/**
* 是否已过期
*/
isExpired?: boolean;
/**
* 日历事件标题
*/
title?: string;
/**
* 日历事件开始时间
*/
start?: string;
/**
* 日历事件结束时间
*/
end?: string;
}
export interface TimeSlotDetail {
/**
* 开始时间(HH:mm)
*/
startTime: string;
/**
* 结束时间(HH:mm)
*/
endTime: string;
/**
* 是否已预约
*/
isBooked: boolean;
/**
* 是否已过期
*/
isExpired: boolean;
/**
* 完整时段表示(如"08:30-09:00")
*/
timeSlot: string;
}
export interface CounselorOption {
/**
* 咨询师ID
*/
id: number;
/**
* 咨询师姓名
*/
name: string;
}
// 预约时段汇总视图对象
export interface XljkConsultationScheduleSummaryVO {
id: number;
tenantId: string;
createDept: number;
createBy: number;
createTime: string;
updateBy: number;
updateTime: string;
deleted: number;
counselorId: number;
counselorName: string;
dateRange: string;
morningEnabled: boolean;
morningTime: string;
afternoonEnabled: boolean;
afternoonTime: string;
status: string;
scheduleCount: number;
batchId: string;
}
// 院系选项
export interface CollegeOption {
/**
* 院系ID
*/
id: number;
/**
* 院系名称
*/
name: string;
}
// 专业选项
export interface MajorOption {
/**
* 专业ID
*/
id: number;
/**
* 专业名称
*/
name: string;
}
// 班级选项
export interface ClassOption {
/**
* 班级ID
*/
id: number;
/**
* 班级名称
*/
name: string;
}
// 未到访记录查询参数
export interface NoShowQuery extends PageQuery {
/**
* 学生姓名
*/
studentName?: string;
/**
* 学号
*/
studentNo?: string;
/**
* 状态(0生效中 1已过期 2已解除 'all'全部)
*/
status?: string;
/**
* 未到访日期开始
*/
startDate?: string;
/**
* 未到访日期结束
*/
endDate?: string;
/**
* 日期范围参数
*/
params?: any;
}
// 未到访记录VO
export interface NoShowVO {
/**
* 主键ID
*/
id: string | number;
/**
* 学生ID
*/
studentId: string | number;
/**
* 学生姓名
*/
studentName?: string;
/**
* 学号
*/
studentNo?: string;
/**
* 院系
*/
collegeName?: string;
/**
* 专业
*/
majorName?: string;
/**
* 班级
*/
className?: string;
/**
* 咨询师名称
*/
counselorName?: string;
/**
* 预约日期
*/
appointmentDate?: string;
/**
* 预约时段
*/
appointmentDetailTime?: string;
/**
* 咨询原因
*/
reason?: string;
/**
* 预约记录ID
*/
appointmentId: string | number;
/**
* 未到访日期
*/
noShowDate: string;
/**
* 限制到期日
*/
expireDate?: string;
/**
* 状态(0生效中 1已过期 2已解除)
*/
status?: string;
/**
* 创建时间
*/
createTime?: string;
/**
* 更新时间
*/
updateTime?: string;
}
+19 -19
View File
@@ -15,7 +15,7 @@ import {
// 查询心理健康测评列表
export const listEvaluation = (query: XljkEvaluationQuery) => {
return request({
url: '/xljk/evaluation/list',
url: '/api/xljk/evaluation/list',
method: 'get',
params: query
});
@@ -24,7 +24,7 @@ export const listEvaluation = (query: XljkEvaluationQuery) => {
// 查询心理健康测评详细信息
export const getEvaluation = (id: string | number) => {
return request({
url: `/xljk/evaluation/${id}`,
url: `/api/xljk/evaluation/${id}`,
method: 'get'
});
};
@@ -45,7 +45,7 @@ export const addEvaluation = (data: XljkEvaluationForm) => {
}
return request({
url: '/xljk/evaluation',
url: '/api/xljk/evaluation',
method: 'post',
data: data
});
@@ -54,7 +54,7 @@ export const addEvaluation = (data: XljkEvaluationForm) => {
// 修改心理健康测评
export const updateEvaluation = (data: XljkEvaluationForm) => {
return request({
url: '/xljk/evaluation',
url: '/api/xljk/evaluation',
method: 'put',
data: data
});
@@ -63,7 +63,7 @@ export const updateEvaluation = (data: XljkEvaluationForm) => {
// 删除心理健康测评
export const delEvaluation = (ids: string | number | Array<string | number>) => {
return request({
url: `/xljk/evaluation/${typeof ids === 'object' ? ids.join(',') : ids}`,
url: `/api/xljk/evaluation/${typeof ids === 'object' ? ids.join(',') : ids}`,
method: 'delete'
});
};
@@ -71,7 +71,7 @@ export const delEvaluation = (ids: string | number | Array<string | number>) =>
// 获取院系选项
export const getDepartmentOptions = () => {
return request({
url: '/xljk/evaluation/options/department',
url: '/api/xljk/evaluation/options/department',
method: 'get'
});
};
@@ -79,7 +79,7 @@ export const getDepartmentOptions = () => {
// 获取院系选项(新接口)
export const getCollegeOptions = () => {
return request({
url: '/xljk/evaluation/options/colleges',
url: '/api/xljk/evaluation/options/colleges',
method: 'get'
});
};
@@ -87,7 +87,7 @@ export const getCollegeOptions = () => {
// 获取专业选项
export const getMajorOptions = (collegeName?: string) => {
return request({
url: '/xljk/evaluation/options/majors',
url: '/api/xljk/evaluation/options/majors',
method: 'get',
params: { collegeName }
});
@@ -96,7 +96,7 @@ export const getMajorOptions = (collegeName?: string) => {
// 获取班级选项
export const getClassOptions = (collegeName?: string, majorName?: string) => {
return request({
url: '/xljk/evaluation/options/classes',
url: '/api/xljk/evaluation/options/classes',
method: 'get',
params: { collegeName, majorName }
});
@@ -105,7 +105,7 @@ export const getClassOptions = (collegeName?: string, majorName?: string) => {
// 获取班级学生列表
export const getStudentsByClass = (collegeName?: string, majorName?: string, className?: string) => {
return request({
url: '/xljk/evaluation/students',
url: '/api/xljk/evaluation/students',
method: 'get',
params: { collegeName, majorName, className }
});
@@ -114,7 +114,7 @@ export const getStudentsByClass = (collegeName?: string, majorName?: string, cla
// 获取学期选项
export const getSemesterOptions = () => {
return request({
url: '/xljk/evaluation/options/semester',
url: '/api/xljk/evaluation/options/semester',
method: 'get'
});
};
@@ -122,7 +122,7 @@ export const getSemesterOptions = () => {
// 发布测评(已废弃,改为提交功能)
export const publishEvaluation = (id: string | number) => {
return request({
url: `/xljk/evaluation/${id}/publish`,
url: `/api/xljk/evaluation/${id}/publish`,
method: 'put'
});
};
@@ -130,7 +130,7 @@ export const publishEvaluation = (id: string | number) => {
// 提交测评
export const completeEvaluation = (id: string | number) => {
return request({
url: `/xljk/evaluation/${id}/complete`,
url: `/api/xljk/evaluation/${id}/complete`,
method: 'put'
});
};
@@ -138,7 +138,7 @@ export const completeEvaluation = (id: string | number) => {
// 获取测评统计数据
export const getEvaluationStatistics = (id: string | number) => {
return request({
url: `/xljk/evaluation/${id}/statistics`,
url: `/api/xljk/evaluation/${id}/statistics`,
method: 'get'
});
};
@@ -146,7 +146,7 @@ export const getEvaluationStatistics = (id: string | number) => {
// 获取重点关注学生统计
export const getFocusStudentsStatistics = (query: XljkEvaluationQuery) => {
return request({
url: '/xljk/evaluation/statistics/focus',
url: '/api/xljk/evaluation/statistics/focus',
method: 'get',
params: query
});
@@ -155,7 +155,7 @@ export const getFocusStudentsStatistics = (query: XljkEvaluationQuery) => {
// 查询测评明细列表
export const listEvaluationDetail = (query: XljkEvaluationDetailQuery) => {
return request({
url: '/xljk/evaluation/detail/list',
url: '/api/xljk/evaluation/detail/list',
method: 'get',
params: query
});
@@ -164,7 +164,7 @@ export const listEvaluationDetail = (query: XljkEvaluationDetailQuery) => {
// 获取测评明细列表
export const getEvaluationDetailsByEvaluationId = (evaluationId: string | number) => {
return request({
url: `/xljk/evaluation/detail/evaluation/${evaluationId}`,
url: `/api/xljk/evaluation/detail/evaluation/${evaluationId}`,
method: 'get'
});
};
@@ -172,7 +172,7 @@ export const getEvaluationDetailsByEvaluationId = (evaluationId: string | number
// 批量新增测评明细
export const batchAddEvaluationDetails = (data: XljkEvaluationDetailForm[]) => {
return request({
url: '/xljk/evaluation/detail/batch',
url: '/api/xljk/evaluation/detail/batch',
method: 'post',
data: data
});
@@ -184,7 +184,7 @@ export const batchUpdateMentalStatus = (data: XljkEvaluationDetailForm[]) => {
console.log('批量更新数据 - 第一条记录evaluationId:', data[0]?.evaluationId);
return request({
url: '/xljk/evaluation/detail/batch/mentalStatus',
url: '/api/xljk/evaluation/detail/batch/mentalStatus',
method: 'put',
data: data
});
+10
View File
@@ -87,6 +87,16 @@ declare global {
declare interface PageQuery {
pageNum: number;
pageSize: number;
/**
* 排序字段
*/
orderByColumn?: string;
/**
* 排序方式(asc/desc)
*/
isAsc?: string;
}
declare interface LayoutSetting {
/**
@@ -0,0 +1,903 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="预约日期" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="院系" prop="collegeName">
<el-select v-model="queryParams.collegeName" placeholder="请选择院系" clearable style="width: 100%" @change="handleCollegeChange">
<el-option
v-for="college in collegeOptions"
:key="college.name"
:label="college.name"
:value="college.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="专业" prop="majorName">
<el-select v-model="queryParams.majorName" placeholder="请选择专业" clearable style="width: 100%" @change="handleMajorChange">
<el-option
v-for="major in majorOptions"
:key="major.name"
:label="major.name"
:value="major.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="班级" prop="className">
<el-select v-model="queryParams.className" placeholder="请选择班级" clearable style="width: 100%">
<el-option
v-for="classItem in classOptions"
:key="classItem.name"
:label="classItem.name"
:value="classItem.name"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option v-for="dict in appointment_status" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button icon="Download" @click="handleExport" v-hasPermi="['xljk:consultationappointment:export']">导出</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="appointmentList" @selection-change="handleSelectionChange" @row-dblclick="handleView">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="学生姓名" align="center" prop="studentName" min-width="100" show-overflow-tooltip />
<el-table-column label="学号" align="center" prop="studentNo" min-width="100" show-overflow-tooltip />
<el-table-column label="院系" align="center" prop="collegeName" min-width="120" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" min-width="120" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" min-width="120" show-overflow-tooltip />
<el-table-column label="咨询原因" align="center" prop="reason" min-width="150" show-overflow-tooltip />
<el-table-column label="咨询师" align="center" prop="counselorName" min-width="100" show-overflow-tooltip />
<el-table-column label="预约日期" align="center" prop="appointmentDate" min-width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.appointmentDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="预约时段" align="center" prop="appointmentDetailTime" width="120" />
<el-table-column label="状态" align="center" prop="status" width="120">
<template #default="scope">
<div>
<el-tag v-if="scope.row.status === 0" type="primary">待咨询</el-tag>
<el-tag v-else-if="scope.row.status === 1" type="success">已到访</el-tag>
<el-tag v-else-if="scope.row.status === 2" type="danger">已取消</el-tag>
<el-tag v-else-if="scope.row.status === 3" type="warning">未到访</el-tag>
<el-tag v-else type="info">未知状态</el-tag>
</div>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="info"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleView(scope.row)"
v-hasPermi="['xljk:consultationappointment:query']"
></el-button>
</el-tooltip>
<!-- 待咨询状态可记录咨询结果 -->
<el-tooltip content="记录结果" placement="top" v-if="scope.row.status === 0">
<el-button
link
type="primary"
icon="Edit"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleRecord(scope.row)"
v-hasPermi="['xljk:consultationappointment:edit']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 查看预约详情对话框 -->
<el-dialog :title="title" v-model="open" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="学生姓名">{{ form.studentName }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ form.studentNo }}</el-descriptions-item>
<el-descriptions-item label="院系">{{ form.collegeName }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ form.majorName }}</el-descriptions-item>
<el-descriptions-item label="班级">{{ form.className }}</el-descriptions-item>
<el-descriptions-item label="咨询师">{{ form.counselorName }}</el-descriptions-item>
<el-descriptions-item label="预约日期">{{ parseTime(form.appointmentDate, '{y}-{m}-{d}') }}</el-descriptions-item>
<el-descriptions-item label="预约时段">{{ form.appointmentDetailTime }}</el-descriptions-item>
<el-descriptions-item label="咨询原因" :span="2">{{ form.reason }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="form.status === 0" type="primary">待咨询</el-tag>
<el-tag v-else-if="form.status === 1" type="success">已到访</el-tag>
<el-tag v-else-if="form.status === 2" type="danger">已取消</el-tag>
<el-tag v-else-if="form.status === 3" type="warning">未到访</el-tag>
<el-tag v-else type="info">未知状态({{ form.status }})</el-tag>
</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ parseTime(form.createTime) }}</el-descriptions-item>
<!-- 只有在已到访状态时才显示咨询结果相关字段 -->
<template v-if="form.status == 1">
<el-descriptions-item label="实际到访时间">
{{ form.actualVisitTime ? parseTime(form.actualVisitTime, '{y}-{m}-{d} {h}:{i}:{s}') : '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="沟通时长">
{{ form.communicationDuration ? form.communicationDuration + ' 分钟' : '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="咨询内容" :span="2">
{{ form.consultationContent || '未记录' }}
</el-descriptions-item>
<el-descriptions-item label="咨询师建议" :span="2">
{{ form.counselorSuggestion || '未记录' }}
</el-descriptions-item>
</template>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="open = false">关 闭</el-button>
</div>
</template>
</el-dialog>
<!-- 记录咨询结果对话框 -->
<el-dialog title="记录咨询结果" v-model="recordDialogVisible" width="600px" append-to-body>
<el-form ref="recordFormRef" :model="recordForm" :rules="recordRules" label-width="100px">
<el-form-item label="学生姓名">
<el-input v-model="recordForm.studentName" disabled />
</el-form-item>
<el-form-item label="预约日期">
<el-input :value="parseTime(recordForm.appointmentDate, '{y}-{m}-{d}')" disabled />
</el-form-item>
<el-form-item label="预约时段">
<el-input v-model="recordForm.appointmentDetailTime" disabled />
</el-form-item>
<el-form-item label="咨询结果" prop="status">
<el-radio-group v-model="recordForm.status">
<el-radio :label="1">已到访</el-radio>
<el-radio :label="3">未到访</el-radio>
</el-radio-group>
</el-form-item>
<!-- 只有在已到访状态时才显示以下字段 -->
<template v-if="recordForm.status === 1">
<el-form-item label="实际到访时间" prop="actualVisitTime" class="no-wrap-label">
<el-date-picker
v-model="recordForm.actualVisitTime"
type="datetime"
placeholder="选择日期时间"
style="width: 100%"
value-format="YYYY-MM-DD HH:mm:ss"
format="YYYY-MM-DD HH:mm:ss"
/>
</el-form-item>
<el-form-item label="沟通时长" prop="communicationDuration">
<div class="flex-row align-center">
<el-input-number v-model="recordForm.communicationDuration" :min="1" :max="180" />
<span class="ml-2">分钟</span>
</div>
</el-form-item>
<el-form-item label="咨询内容" prop="consultationContent">
<el-input
v-model="recordForm.consultationContent"
type="textarea"
placeholder="请输入咨询内容"
:rows="4"
/>
</el-form-item>
<el-form-item label="咨询师建议" prop="counselorSuggestion">
<el-input
v-model="recordForm.counselorSuggestion"
type="textarea"
placeholder="请输入咨询师建议"
:rows="4"
/>
</el-form-item>
</template>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="recordDialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitRecordForm" :loading="submitLoading">确 定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { listAppointment, getAppointment, delAppointment, updateAppointment, recordConsultation, getCounselorOptions, getCollegeOptions, getMajorOptions, getClassOptions } from '@/api/xljk/consultation';
import { XljkConsultationAppointmentVO, XljkConsultationAppointmentForm, CounselorOption, CollegeOption, MajorOption, ClassOption } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
import { download } from '@/utils/request';
import { getToken } from '@/utils/auth';
const { proxy } = getCurrentInstance() as any;
// 使用本地字典数据,确保状态过滤条件有选项
const appointment_status = ref([
{ label: '待咨询', value: 0 },
{ label: '已到访', value: 1 },
{ label: '已取消', value: 2 },
{ label: '未到访', value: 3 }
]);
// 遮罩层
const loading = ref(true);
// 选中数组
const ids = ref<Array<string | number>>([]);
// 非多个禁用
const multiple = ref(true);
// 显示搜索条件
const showSearch = ref(true);
// 总条数
const total = ref(0);
// 预约记录表格数据
const appointmentList = ref<XljkConsultationAppointmentVO[]>([]);
// 弹出层标题
const title = ref('');
// 是否显示弹出层
const open = ref(false);
// 日期范围
const dateRange = ref<string[]>([]);
// 咨询师选项
const counselorOptions = ref<CounselorOption[]>([]);
// 院系选项
const collegeOptions = ref<CollegeOption[]>([]);
// 专业选项
const majorOptions = ref<MajorOption[]>([]);
// 班级选项
const classOptions = ref<ClassOption[]>([]);
// 是否显示记录对话框
const recordDialogVisible = ref(false);
// 提交按钮加载状态
const submitLoading = ref(false);
// 查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
collegeName: undefined,
majorName: undefined,
className: undefined,
status: undefined,
params: {}
});
// 表单参数
const form = reactive<XljkConsultationAppointmentVO>({} as XljkConsultationAppointmentVO);
// 记录表单参数
const recordForm = reactive<XljkConsultationAppointmentForm>({
id: undefined,
studentId: undefined,
counselorId: undefined,
scheduleId: undefined,
appointmentDate: '',
appointmentTime: '',
reason: '',
status: 1, // 默认已到访,使用数值类型
actualVisitTime: undefined,
communicationDuration: 30, // 默认30分钟
consultationContent: '',
counselorSuggestion: ''
});
// 记录表单校验
const recordRules = ref({
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
});
// 监听状态变化,动态调整表单验证规则
watch(() => recordForm.status, (newStatus) => {
if (newStatus === 3) { // 未到访
recordRules.value = {
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }]
};
} else { // 已到访
recordRules.value = {
status: [{ required: true, message: '请选择咨询结果', trigger: 'change' }],
actualVisitTime: [{ required: true, message: '请选择实际到访时间', trigger: 'change' }],
communicationDuration: [{ required: true, message: '请输入沟通时长', trigger: 'blur' }],
consultationContent: [{ required: true, message: '请输入咨询内容', trigger: 'blur' }]
};
}
});
const queryFormRef = ref();
const recordFormRef = ref();
/** 查询预约记录列表 */
const getList = async () => {
loading.value = true;
try {
// 处理日期范围
queryParams.params = {};
if (dateRange.value && dateRange.value.length === 2) {
queryParams.params.beginAppointmentDate = dateRange.value[0];
queryParams.params.endAppointmentDate = dateRange.value[1];
}
const res = await listAppointment(queryParams);
appointmentList.value = res.rows || [];
total.value = res.total || 0;
// 添加日志输出
console.log('appointment_status字典数据:', appointment_status.value);
console.log('appointmentList数据:', appointmentList.value);
if (appointmentList.value.length > 0) {
console.log('第一条记录status值:', appointmentList.value[0].status);
console.log('第一条记录status类型:', typeof appointmentList.value[0].status);
}
} catch (error) {
console.error('获取预约记录列表失败', error);
} finally {
loading.value = false;
}
};
/** 获取咨询师选项 */
const getCounselors = async () => {
try {
const res = await getCounselorOptions();
counselorOptions.value = res.data || [];
} catch (error) {
console.error('获取咨询师选项失败', error);
}
};
/** 获取院系选项 */
const getColleges = async () => {
try {
const res = await getCollegeOptions();
collegeOptions.value = res.data || [];
} catch (error) {
console.error('获取院系选项失败', error);
}
};
/** 获取专业选项 */
const getMajors = async () => {
try {
const res = await getMajorOptions();
majorOptions.value = res.data || [];
} catch (error) {
console.error('获取专业选项失败', error);
}
};
/** 获取班级选项 */
const getClasses = async () => {
try {
const res = await getClassOptions();
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
dateRange.value = [];
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: XljkConsultationAppointmentVO[]) => {
ids.value = selection.map((item) => item.id);
// 判断是否有非待咨询状态
const hasNonPending = selection.some(item => item.status !== 0);
multiple.value = !selection.length || hasNonPending;
};
/** 查看按钮操作 */
const handleView = async (row: XljkConsultationAppointmentVO) => {
try {
console.log('查看预约详情,行数据:', row);
console.log('行数据status类型:', typeof row.status, '值:', row.status);
const res = await getAppointment(row.id);
console.log('获取预约详情API响应:', res);
// 清空表单,确保没有残留数据
Object.keys(form).forEach(key => {
form[key] = undefined;
});
// 检查API响应格式
if (res.code === 200 && res.data) {
// 将API响应数据复制到表单
Object.assign(form, res.data);
// 确保状态是数值类型
console.log('API返回的status类型:', typeof form.status, '值:', form.status);
if (form.status === null || form.status === undefined) {
// 如果API返回的status为空,使用行数据的status
form.status = row.status;
console.log('使用行数据的status:', form.status);
}
if (typeof form.status === 'string') {
form.status = parseInt(form.status);
console.log('转换后的status:', form.status);
}
// 检查实际到访时间
console.log('实际到访时间:', form.actualVisitTime);
if (form.status === 1 && !form.actualVisitTime) {
console.warn('已到访状态但实际到访时间为空,尝试使用行数据');
if (row.actualVisitTime) {
form.actualVisitTime = row.actualVisitTime;
console.log('使用行数据的实际到访时间:', form.actualVisitTime);
} else {
console.warn('行数据也没有实际到访时间');
}
}
console.log('复制后的表单数据:', form);
// 如果是已到访状态,确保咨询结果相关字段有值
if (form.status === 1) {
console.log('已到访状态,咨询结果相关字段:', {
actualVisitTime: form.actualVisitTime,
communicationDuration: form.communicationDuration,
consultationContent: form.consultationContent,
counselorSuggestion: form.counselorSuggestion
});
}
} else {
console.warn('API响应格式不正确或无数据:', res);
// 使用行数据填充表单
Object.assign(form, row);
// 确保状态是数值类型
console.log('使用行数据,status类型:', typeof form.status, '值:', form.status);
if (typeof form.status === 'string') {
form.status = parseInt(form.status);
console.log('转换后的status:', form.status);
}
console.log('使用行数据填充表单:', form);
}
open.value = true;
title.value = '查看预约详情';
} catch (error) {
console.error('获取预约详情失败', error);
ElMessage.error('获取预约详情失败');
// 出错时也使用行数据填充
Object.assign(form, row);
// 确保状态是数值类型
if (typeof form.status === 'string') {
form.status = parseInt(form.status);
}
}
};
/** 记录咨询结果操作 */
const handleRecord = async (row: XljkConsultationAppointmentVO) => {
try {
console.log('记录咨询结果,行数据:', row);
const res = await getAppointment(row.id);
console.log('获取预约详情API响应:', res);
// 重置记录表单
const defaultValues = {
id: undefined,
studentId: undefined,
counselorId: undefined,
scheduleId: undefined,
appointmentDate: '',
appointmentDetailTime: '',
reason: '',
status: 1, // 默认已完成
actualVisitTime: new Date(),
communicationDuration: 30,
consultationContent: '',
counselorSuggestion: ''
};
Object.assign(recordForm, defaultValues);
// 检查API响应格式
if (res.code === 200 && res.data) {
// 填充API返回的数据
recordForm.id = res.data.id;
recordForm.studentId = res.data.studentId;
recordForm.counselorId = res.data.counselorId;
recordForm.scheduleId = res.data.scheduleId;
recordForm.appointmentDate = res.data.appointmentDate;
recordForm.appointmentDetailTime = res.data.appointmentDetailTime;
recordForm.reason = res.data.reason;
recordForm.studentName = res.data.studentName;
} else {
console.warn('API响应格式不正确或无数据:', res);
// 使用行数据填充
recordForm.id = row.id;
recordForm.studentId = row.studentId;
recordForm.counselorId = row.counselorId;
recordForm.scheduleId = row.scheduleId;
recordForm.appointmentDate = row.appointmentDate;
recordForm.appointmentDetailTime = row.appointmentDetailTime;
recordForm.reason = row.reason;
recordForm.studentName = row.studentName;
}
console.log('填充后的记录表单:', recordForm);
recordDialogVisible.value = true;
} catch (error) {
console.error('获取预约详情失败', error);
ElMessage.error('获取预约详情失败');
}
};
/** 提交记录表单 */
const submitRecordForm = async () => {
if (!recordFormRef.value) return;
await recordFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
try {
submitLoading.value = true;
// 构建提交数据
const submitData: any = {
id: recordForm.id,
status: recordForm.status
};
// 如果是已到访状态,添加其他字段
if (recordForm.status === 1) {
// 确保日期格式正确
if (recordForm.actualVisitTime) {
// 如果是Date对象,转换为ISO字符串格式
if (recordForm.actualVisitTime instanceof Date) {
const year = recordForm.actualVisitTime.getFullYear();
const month = String(recordForm.actualVisitTime.getMonth() + 1).padStart(2, '0');
const day = String(recordForm.actualVisitTime.getDate()).padStart(2, '0');
const hours = String(recordForm.actualVisitTime.getHours()).padStart(2, '0');
const minutes = String(recordForm.actualVisitTime.getMinutes()).padStart(2, '0');
const seconds = String(recordForm.actualVisitTime.getSeconds()).padStart(2, '0');
submitData.actualVisitTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
} else {
submitData.actualVisitTime = recordForm.actualVisitTime;
}
console.log('提交的实际到访时间:', submitData.actualVisitTime);
}
submitData.communicationDuration = recordForm.communicationDuration;
submitData.consultationContent = recordForm.consultationContent;
submitData.counselorSuggestion = recordForm.counselorSuggestion;
} else {
// 未到访状态,设置默认值
submitData.actualVisitTime = null;
submitData.communicationDuration = 0;
submitData.consultationContent = '';
submitData.counselorSuggestion = '';
}
await recordConsultation(recordForm.id as number, submitData);
ElMessage.success('记录成功');
recordDialogVisible.value = false;
getList();
} catch (error) {
console.error('提交记录失败', error);
ElMessage.error('提交记录失败');
} finally {
submitLoading.value = false;
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: XljkConsultationAppointmentVO) => {
const _ids = row?.id || ids.value;
// 检查是否有非待咨询状态的记录
if (!row) {
const selectedRecords = appointmentList.value.filter(item => ids.value.includes(item.id));
if (selectedRecords.some(item => item.status !== 0)) {
ElMessage.error('只能删除待咨询状态的预约记录');
return;
}
} else if (row.status !== 0) {
ElMessage.error('只能删除待咨询状态的预约记录');
return;
}
const confirmMessage = row
? `是否确认删除该预约记录?`
: `是否确认删除选中的${ids.value.length}条数据?`;
try {
await ElMessageBox.confirm(confirmMessage, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
await delAppointment(_ids);
ElMessage.success('删除成功');
getList();
} catch (error) {
console.error('删除失败', error);
}
};
/** 导出按钮操作 */
const handleExport = async () => {
// 构建查询参数,处理日期范围和params对象
const exportParams = { ...queryParams };
delete exportParams.params; // 移除params对象
// 将日期范围添加到参数中
if (dateRange.value && dateRange.value.length === 2) {
exportParams.beginAppointmentDate = dateRange.value[0];
exportParams.endAppointmentDate = dateRange.value[1];
}
// 移除undefined值
Object.keys(exportParams).forEach(key => {
if (exportParams[key] === undefined) {
delete exportParams[key];
}
});
// 构建URL参数
const params = new URLSearchParams();
Object.keys(exportParams).forEach(key => {
params.append(key, exportParams[key]);
});
// 获取token
const token = getToken();
try {
// 使用fetch API发送请求,正确设置Authorization头
const response = await fetch(`http://localhost:8080/api/xljk/consultation/appointment/list/export?${params.toString()}`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'clientid': import.meta.env.VITE_APP_CLIENT_ID || 'e5cd7e4891bf95d1d19206ce24a7b32e'
}
});
if (!response.ok) {
throw new Error(`导出失败: ${response.status} ${response.statusText}`);
}
// 获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let filename = '预约记录.xlsx';
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1].replace(/['"]/g, '');
}
}
// 下载文件
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
} catch (error) {
console.error('导出失败:', error);
ElMessage.error(`导出失败: ${error.message}`);
}
};
/** 院系选择变化时更新专业选项 */
const handleCollegeChange = async (collegeName: string) => {
// 清空专业和班级选择
queryParams.majorName = undefined;
queryParams.className = undefined;
majorOptions.value = [];
classOptions.value = [];
if (collegeName) {
// 获取该院系下的专业
try {
const res = await getMajorOptions(collegeName);
majorOptions.value = res.data || [];
} catch (error) {
console.error('获取专业选项失败', error);
}
} else {
// 如果清空院系,则获取所有专业
await getMajors();
}
};
/** 专业选择变化时更新班级选项 */
const handleMajorChange = async (majorName: string) => {
// 清空班级选择
queryParams.className = undefined;
classOptions.value = [];
if (majorName && queryParams.collegeName) {
// 获取专业下的班级
try {
const res = await getClassOptions(queryParams.collegeName, majorName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
}
} else if (queryParams.collegeName) {
// 如果清空专业但有院系,则获取院系下所有班级
try {
const res = await getClassOptions(queryParams.collegeName);
classOptions.value = res.data || [];
} catch (error) {
console.error('获取班级选项失败', error);
}
} else {
// 如果都为空,则获取所有班级
await getClasses();
}
};
// 初始化
onMounted(() => {
console.log('初始化appointment_status字典数据:', appointment_status.value);
getCounselors();
getColleges();
getMajors();
getClasses();
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
.flex-row {
display: flex;
flex-direction: row;
}
.align-center {
align-items: center;
}
.ml-2 {
margin-left: 8px;
}
.no-wrap-label :deep(.el-form-item__label) {
white-space: nowrap;
}
</style>
@@ -0,0 +1,673 @@
<template>
<div class="week-view-container">
<!-- 日期导航和视图切换 -->
<div class="calendar-header">
<div class="nav-buttons">
<el-button type="primary" plain @click="prevPeriod">
<el-icon><ArrowLeft /></el-icon>
</el-button>
<span class="period-title">{{ periodTitle }}</span>
<el-button type="primary" plain @click="nextPeriod">
<el-icon><ArrowRight /></el-icon>
</el-button>
<el-button type="primary" plain @click="goToday">今天</el-button>
</div>
<div class="view-switch">
<el-radio-group v-model="currentView" size="small" @change="handleViewChange">
<el-radio-button label="month">月视图</el-radio-button>
<el-radio-button label="week">周视图</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 周视图 -->
<div v-if="currentView === 'week'" class="week-grid">
<!-- 表头:显示日期和星期 -->
<div class="week-header">
<div class="time-label-cell">
<span>时段</span>
</div>
<div
v-for="day in weekDays"
:key="day.date"
class="day-cell"
:class="{ 'today': day.isToday }"
>
<div class="day-number">{{ day.dayNumber }}</div>
<div class="day-name">{{ day.dayName }}</div>
</div>
</div>
<!-- 表格内容:显示全天和时间段 -->
<div class="week-body">
<div class="time-row">
<div class="time-label-cell">
<span>全天</span>
</div>
<div
v-for="day in weekDays"
:key="day.date"
class="day-cell"
>
<!-- 这里可以放全天事件,目前为空 -->
</div>
</div>
<!-- 时间段行 -->
<div class="time-slots-container">
<!-- 上午时段 -->
<div class="time-row">
<div class="time-label-cell">
<span>上午</span>
</div>
<div
v-for="day in weekDays"
:key="day.date"
class="day-cell"
>
<div
v-if="getMorningSlot(day.date)"
class="time-slot-card morning"
:class="{
'fully-booked': getMorningSlot(day.date).allBooked || getMorningSlot(day.date).isExpired,
}"
@click="handleSlotClick(getMorningSlot(day.date))"
>
{{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
</div>
</div>
</div>
<!-- 下午时段 -->
<div class="time-row">
<div class="time-label-cell">
<span>下午</span>
</div>
<div
v-for="day in weekDays"
:key="day.date"
class="day-cell"
>
<div
v-if="getAfternoonSlot(day.date)"
class="time-slot-card afternoon"
:class="{
'fully-booked': getAfternoonSlot(day.date).allBooked || getAfternoonSlot(day.date).isExpired,
}"
@click="handleSlotClick(getAfternoonSlot(day.date))"
>
{{ getAfternoonSlot(day.date).startTime }}-{{ getAfternoonSlot(day.date).endTime }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 月视图 -->
<div v-else class="month-grid">
<!-- 表头:星期几 -->
<div class="month-header">
<div v-for="day in ['周一', '周二', '周三', '周四', '周五', '周六', '周日']" :key="day" class="month-header-cell">
{{ day }}
</div>
</div>
<!-- 日期单元格 -->
<div class="month-body">
<div v-for="week in monthDays" :key="week[0].date" class="month-row">
<div
v-for="day in week"
:key="day.date"
class="month-cell"
:class="{
'today': day.isToday,
'other-month': day.isOtherMonth
}"
@click="day.isOtherMonth ? null : handleDateClick(day.date)"
>
<div class="month-day-number">{{ day.dayNumber }}</div>
<!-- 上午时段 -->
<div
v-if="getMorningSlot(day.date)"
class="month-slot morning"
:class="{ 'fully-booked': getMorningSlot(day.date).allBooked || getMorningSlot(day.date).isExpired }"
@click.stop="handleSlotClick(getMorningSlot(day.date))"
>
上午 {{ getMorningSlot(day.date).startTime }}-{{ getMorningSlot(day.date).endTime }}
</div>
<!-- 下午时段 -->
<div
v-if="getAfternoonSlot(day.date)"
class="month-slot afternoon"
:class="{ 'fully-booked': getAfternoonSlot(day.date).allBooked || getAfternoonSlot(day.date).isExpired }"
@click.stop="handleSlotClick(getAfternoonSlot(day.date))"
>
下午 {{ getAfternoonSlot(day.date).startTime }}-{{ getAfternoonSlot(day.date).endTime }}
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { parseTime } from '@/utils/ruoyi';
import { ElMessage } from 'element-plus';
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue';
import { AvailableTimeSlot } from '@/api/xljk/consultation/types';
// 定义组件的Props
const props = defineProps({
availableSlots: {
type: Array as () => AvailableTimeSlot[],
default: () => []
},
defaultView: {
type: String,
default: 'week',
validator: (value: string) => ['week', 'month'].includes(value)
}
});
// 定义组件的事件
const emit = defineEmits(['dateRangeChange', 'slotClick', 'viewChange']);
// 当前日期和周期的开始日期
const today = ref(new Date());
const periodStart = ref(new Date());
const currentView = ref(props.defaultView);
// 初始化周期的开始日期
onMounted(() => {
goToday();
});
// 设置当前日期为今天
function goToday() {
const currentDate = new Date();
today.value = currentDate;
if (currentView.value === 'week') {
// 计算本周的周一
const day = currentDate.getDay() || 7; // 周日是0,转换为7
const diff = day - 1; // 计算与周一的天数差
periodStart.value = new Date(currentDate.getTime() - diff * 24 * 60 * 60 * 1000);
} else {
// 计算本月的1号
periodStart.value = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
}
// 触发日期范围变化事件
emitDateRangeChange();
}
// 上一个周期(周或月)
function prevPeriod() {
if (currentView.value === 'week') {
// 上一周
periodStart.value = new Date(periodStart.value.getTime() - 7 * 24 * 60 * 60 * 1000);
} else {
// 上一月
periodStart.value = new Date(periodStart.value.getFullYear(), periodStart.value.getMonth() - 1, 1);
}
emitDateRangeChange();
}
// 下一个周期(周或月)
function nextPeriod() {
if (currentView.value === 'week') {
// 下一周
periodStart.value = new Date(periodStart.value.getTime() + 7 * 24 * 60 * 60 * 1000);
} else {
// 下一月
periodStart.value = new Date(periodStart.value.getFullYear(), periodStart.value.getMonth() + 1, 1);
}
emitDateRangeChange();
}
// 处理视图切换
function handleViewChange(view: string) {
// 重新计算周期开始日期
if (view === 'week') {
// 计算当前日期所在周的周一
const currentDate = new Date();
const day = currentDate.getDay() || 7;
const diff = day - 1;
periodStart.value = new Date(currentDate.getTime() - diff * 24 * 60 * 60 * 1000);
} else {
// 计算当前月的1号
periodStart.value = new Date(today.value.getFullYear(), today.value.getMonth(), 1);
}
// 触发视图变化事件
emit('viewChange', view);
// 触发日期范围变化事件
emitDateRangeChange();
}
// 处理月视图中的日期点击
function handleDateClick(date: string) {
// 可以添加日期点击的逻辑,比如跳转到该日期的详细视图
console.log('点击日期:', date);
}
// 触发日期范围变化事件
function emitDateRangeChange() {
let startDate, endDate;
if (currentView.value === 'week') {
// 周视图:周一到周日
startDate = formatDate(periodStart.value);
endDate = formatDate(new Date(periodStart.value.getTime() + 6 * 24 * 60 * 60 * 1000));
} else {
// 月视图:需要包含日历中显示的所有日期,包括上月和下月的日期
// 计算本月第一天
const year = periodStart.value.getFullYear();
const month = periodStart.value.getMonth();
// 本月第一天
const firstDay = new Date(year, month, 1);
// 本月第一天是星期几(0-6,0是星期日)
let firstDayOfWeek = firstDay.getDay() || 7; // 转换为1-7,1是星期一
// 计算本月第一周的第一天(可能是上个月的日期)
const firstDayOfCalendar = new Date(firstDay);
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
// 计算本月最后一天
const lastDay = new Date(year, month + 1, 0);
// 本月最后一天是星期几(0-6,0是星期日)
let lastDayOfWeek = lastDay.getDay() || 7; // 转换为1-7,1是星期一
// 计算本月最后一周的最后一天(可能是下个月的日期)
const lastDayOfCalendar = new Date(lastDay);
lastDayOfCalendar.setDate(lastDayOfCalendar.getDate() + (7 - lastDayOfWeek));
// 确保我们覆盖整个日历视图中显示的所有日期,包括6行的完整数据
// 这意味着我们至少需要包含42天的数据(6行 x 7天)
const minEndDate = new Date(firstDayOfCalendar);
minEndDate.setDate(minEndDate.getDate() + 41); // 41 = 6*7-1
// 使用日历的第一天和至少覆盖6周的结束日期(或最后一周的最后一天,取较大者)
startDate = formatDate(firstDayOfCalendar);
// 确保结束日期至少覆盖6行
if (lastDayOfCalendar < minEndDate) {
endDate = formatDate(minEndDate);
} else {
endDate = formatDate(lastDayOfCalendar);
}
console.log(`日历日期范围:${startDate} 到 ${endDate},确保覆盖所有6行`);
}
emit('dateRangeChange', { startDate, endDate });
}
// 处理时段点击
function handleSlotClick(slot: AvailableTimeSlot | undefined) {
if (!slot) return;
// 如果该时段已过期,则不允许预约
if (slot.isExpired) {
ElMessage.warning('该时段已过期,请选择其他时段');
return;
}
// 如果该时段全部约满,则不允许预约
if (slot.allBooked) {
ElMessage.warning('该时段已全部约满,请选择其他时段');
return;
}
emit('slotClick', slot);
}
// 计算周期标题
const periodTitle = computed(() => {
if (currentView.value === 'week') {
// 周视图:显示 "2025-07-29 至 2025-08-04"
const start = formatDate(periodStart.value);
const end = formatDate(new Date(periodStart.value.getTime() + 6 * 24 * 60 * 60 * 1000));
return `${start} 至 ${end}`;
} else {
// 月视图:显示 "2025年7月"
const year = periodStart.value.getFullYear();
const month = periodStart.value.getMonth() + 1;
return `${year}年${month}月`;
}
});
// 计算周内的日期
const weekDays = computed(() => {
const days = [];
const dayNames = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
for (let i = 0; i < 7; i++) {
const date = new Date(periodStart.value.getTime() + i * 24 * 60 * 60 * 1000);
const formattedDate = formatDate(date);
const isToday = isSameDay(date, today.value);
days.push({
date: formattedDate,
dayNumber: date.getDate(), // 日期数字
dayName: dayNames[i], // 星期几
isToday
});
}
return days;
});
// 计算月视图的日期
const monthDays = computed(() => {
const weeks = [];
const year = periodStart.value.getFullYear();
const month = periodStart.value.getMonth();
// 本月第一天
const firstDay = new Date(year, month, 1);
// 本月第一天是星期几(0-6,0是星期日)
let firstDayOfWeek = firstDay.getDay() || 7; // 转换为1-7,1是星期一
// 计算本月第一周的第一天(可能是上个月的日期)
const firstDayOfCalendar = new Date(firstDay);
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
// 生成6周的日期(确保能显示完整月份)
for (let week = 0; week < 6; week++) {
const weekDays = [];
for (let day = 0; day < 7; day++) {
const date = new Date(firstDayOfCalendar);
date.setDate(date.getDate() + week * 7 + day);
const formattedDate = formatDate(date);
const isToday = isSameDay(date, today.value);
const isOtherMonth = date.getMonth() !== month;
// 调试信息,查看是否有对应的时段数据
const morningSlot = getMorningSlot(formattedDate);
const afternoonSlot = getAfternoonSlot(formattedDate);
// 如果是最后一行,添加额外日志
if (week === 5) {
console.log(`日期 ${formattedDate} (${date.toLocaleDateString()}) 数据状态:`, {
上午时段: morningSlot ? '有数据' : '无数据',
下午时段: afternoonSlot ? '有数据' : '无数据',
isOtherMonth
});
}
weekDays.push({
date: formattedDate,
dayNumber: date.getDate(),
isToday,
isOtherMonth
});
}
weeks.push(weekDays);
}
return weeks;
});
// 判断两个日期是否是同一天
function isSameDay(date1: Date, date2: Date) {
return date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate();
}
// 格式化日期为 yyyy-MM-dd 格式
function formatDate(date: Date) {
return parseTime(date, '{y}-{m}-{d}');
}
// 获取指定日期的上午时段
function getMorningSlot(dateStr: string) {
return props.availableSlots.find(slot =>
slot.date === dateStr && slot.timeSlot === '0'
);
}
// 获取指定日期的下午时段
function getAfternoonSlot(dateStr: string) {
return props.availableSlots.find(slot =>
slot.date === dateStr && slot.timeSlot === '1'
);
}
// 监听视图变化
watch(() => props.defaultView, (newView) => {
currentView.value = newView;
});
</script>
<style scoped>
.week-view-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.calendar-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.nav-buttons {
display: flex;
align-items: center;
gap: 10px;
}
.period-title {
font-size: 16px;
font-weight: 500;
margin: 0 16px;
min-width: 120px;
text-align: center;
}
/* 周视图样式 */
.week-grid {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 4px;
}
.week-header {
display: flex;
border-bottom: 1px solid #ebeef5;
background-color: #f5f7fa;
}
.time-label-cell {
width: 80px;
padding: 8px;
text-align: center;
font-weight: 500;
border-right: 1px solid #ebeef5;
display: flex;
align-items: center;
justify-content: center;
}
.day-cell {
flex: 1;
padding: 8px;
text-align: center;
border-right: 1px solid #ebeef5;
min-height: 60px;
}
.day-cell:last-child {
border-right: none;
}
.day-number {
font-size: 18px;
font-weight: 500;
}
.day-name {
font-size: 14px;
color: #606266;
}
.today {
background-color: #ecf5ff;
}
.week-body {
flex: 1;
display: flex;
flex-direction: column;
}
.time-row {
display: flex;
border-bottom: 1px solid #ebeef5;
}
.time-row:last-child {
border-bottom: none;
}
.time-slots-container {
flex: 1;
display: flex;
flex-direction: column;
}
.time-slot-card {
padding: 8px;
margin: 4px;
border-radius: 4px;
cursor: pointer;
color: white;
font-size: 14px;
text-align: center;
}
.time-slot-card.morning {
background-color: #409EFF;
}
.time-slot-card.afternoon {
background-color: #67C23A;
}
.time-slot-card.fully-booked {
background-color: #C0C0C0;
cursor: not-allowed;
opacity: 0.7;
}
/* 月视图样式 */
.month-grid {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow-y: auto; /* 添加垂直滚动条 */
max-height: calc(100vh - 180px); /* 设置最大高度,确保可以滚动 */
}
.month-header {
display: flex;
background-color: #f5f7fa;
border-bottom: 1px solid #ebeef5;
position: sticky; /* 表头固定 */
top: 0;
z-index: 1;
}
.month-header-cell {
flex: 1;
padding: 10px;
text-align: center;
font-weight: 500;
}
.month-body {
flex: 1;
display: flex;
flex-direction: column;
}
.month-row {
display: flex;
min-height: 120px; /* 增加行高,确保有足够空间显示预约时段 */
border-bottom: 1px solid #ebeef5;
}
.month-row:last-child {
border-bottom: none;
}
.month-cell {
flex: 1;
min-height: 120px; /* 增加单元格高度 */
border-right: 1px solid #ebeef5;
padding: 5px;
position: relative;
}
.month-cell:last-child {
border-right: none;
}
.month-day-number {
font-size: 16px;
font-weight: 500;
margin-bottom: 5px;
text-align: center;
}
.other-month {
background-color: #f9f9f9;
color: #c0c4cc;
}
.month-slot {
margin: 2px 0;
padding: 4px;
border-radius: 3px;
font-size: 12px;
cursor: pointer;
color: white;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.month-slot.morning {
background-color: #409EFF;
}
.month-slot.afternoon {
background-color: #67C23A;
}
.month-slot.fully-booked {
background-color: #C0C0C0;
cursor: not-allowed;
opacity: 0.7;
}
</style>
@@ -0,0 +1,48 @@
// 读取原始文件
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'index.vue');
let content = fs.readFileSync(filePath, 'utf8');
// 添加导入语句
content = content.replace(
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';",
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';\nimport { checkLimitation } from '@/api/xljk/consultation/noShow';"
);
// 修改submitAppointmentForm函数
content = content.replace(
"function submitAppointmentForm() {",
"async function submitAppointmentForm() {"
);
// 添加预约限制检查
content = content.replace(
"async function submitAppointmentForm() {",
`async function submitAppointmentForm() {
// 表单验证
if (!form.value.studentId) {
ElMessage.warning('请选择学生');
return;
}
// 检查学生是否被限制预约
try {
const res = await checkLimitation(form.value.studentId);
if (res.data && res.data.limited) {
ElMessageBox.alert(
\`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 \${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。\`,
'预约受限',
{ type: 'warning' }
);
return;
}
} catch (error) {
console.error('检查预约限制失败', error);
}
`);
// 写回文件
fs.writeFileSync(filePath, content);
console.log('文件修改成功');
@@ -0,0 +1,48 @@
// 读取原始文件
const fs = require('fs');
const path = require('path');
const filePath = path.join(__dirname, 'index.vue');
let content = fs.readFileSync(filePath, 'utf8');
// 添加导入语句
content = content.replace(
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';",
"import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';\nimport { checkLimitation } from '@/api/xljk/consultation/noShow';"
);
// 修改submitAppointmentForm函数
content = content.replace(
"function submitAppointmentForm() {",
"async function submitAppointmentForm() {"
);
// 添加预约限制检查
content = content.replace(
"function submitAppointmentForm() {",
`async function submitAppointmentForm() {
// 表单验证
if (!form.value.studentId) {
ElMessage.warning('请选择学生');
return;
}
// 检查学生是否被限制预约
try {
const res = await checkLimitation(form.value.studentId);
if (res.data && res.data.limited) {
ElMessageBox.alert(
\`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 \${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。\`,
'预约受限',
{ type: 'warning' }
);
return;
}
} catch (error) {
console.error('检查预约限制失败', error);
}
`);
// 写回文件
fs.writeFileSync(filePath, content);
console.log('文件修改成功');
@@ -0,0 +1,187 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never" class="debug-card">
<template #header>
<h3>心理咨询预约系统调试工具</h3>
</template>
<el-row :gutter="20">
<el-col :span="24">
<el-form :model="form" label-width="120px">
<el-form-item label="学号">
<el-input v-model="form.studentNo" placeholder="请输入学号,例如:2025WRJ010"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getStudentInfo">查询学生信息</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
<el-divider content-position="left">学生信息结果</el-divider>
<el-row>
<el-col :span="24">
<pre>{{ studentInfo }}</pre>
</el-col>
</el-row>
<el-divider content-position="left">预约记录查询</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form :model="appointmentForm" label-width="120px">
<el-form-item label="学生ID">
<el-input v-model="appointmentForm.studentId" placeholder="请输入学生ID"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getAppointments">查询预约记录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
<el-divider content-position="left">预约记录结果</el-divider>
<el-row>
<el-col :span="24">
<pre>{{ appointmentList }}</pre>
</el-col>
</el-row>
<el-divider content-position="left">直接查询数据库</el-divider>
<el-row :gutter="20">
<el-col :span="24">
<el-form :model="sqlForm" label-width="120px">
<el-form-item label="SQL语句">
<el-input
v-model="sqlForm.sql"
type="textarea"
:rows="3"
placeholder="SELECT * FROM xljk_consultation_appointment WHERE student_id = ?"
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="executeSql">执行SQL</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
<el-divider content-position="left">SQL执行结果</el-divider>
<el-row>
<el-col :span="24">
<pre>{{ sqlResult }}</pre>
</el-col>
</el-row>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue';
import { getStudentByNo, getMyAppointments } from '@/api/xljk/consultation';
import { ElMessage } from 'element-plus';
import request from '@/utils/request';
const form = reactive({
studentNo: '2025WRJ010'
});
const appointmentForm = reactive({
studentId: ''
});
const sqlForm = reactive({
sql: 'SELECT * FROM xljk_consultation_appointment WHERE student_id = ?'
});
const studentInfo = ref<any>(null);
const appointmentList = ref<any[]>([]);
const sqlResult = ref<any>(null);
// 查询学生信息
async function getStudentInfo() {
try {
const res = await getStudentByNo(form.studentNo);
studentInfo.value = res.data;
if (res.data && res.data.id) {
appointmentForm.studentId = res.data.id;
ElMessage.success('查询成功,已自动填充学生ID');
} else {
ElMessage.warning('未找到该学号对应的学生信息');
}
} catch (error) {
console.error('获取学生信息失败', error);
ElMessage.error('获取学生信息失败');
}
}
// 查询预约记录
async function getAppointments() {
if (!appointmentForm.studentId) {
ElMessage.warning('请先输入学生ID');
return;
}
try {
const res = await getMyAppointments(appointmentForm.studentId);
appointmentList.value = res.data || [];
if (appointmentList.value.length === 0) {
ElMessage.info('未找到该学生的预约记录');
} else {
ElMessage.success(`查询到${appointmentList.value.length}条预约记录`);
}
} catch (error) {
console.error('获取预约记录失败', error);
ElMessage.error('获取预约记录失败');
}
}
// 执行SQL
async function executeSql() {
if (!sqlForm.sql) {
ElMessage.warning('请输入SQL语句');
return;
}
try {
const res = await request({
url: '/api/xljk/consultation/debug/execute-sql',
method: 'post',
data: { sql: sqlForm.sql }
});
sqlResult.value = res.data;
ElMessage.success('SQL执行成功');
} catch (error) {
console.error('SQL执行失败', error);
ElMessage.error('SQL执行失败,可能是权限问题或SQL语法错误');
}
}
</script>
<style scoped>
.page-container {
padding: 8px;
}
.debug-card {
margin-top: 0;
}
.debug-card :deep(.el-card__body) {
padding: 20px !important;
}
pre {
background-color: #f5f7fa;
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 10px;
font-family: monospace;
white-space: pre-wrap;
word-wrap: break-word;
max-height: 300px;
overflow-y: auto;
}
</style>
@@ -0,0 +1,864 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never" class="calendar-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="12">
<h3 class="calendar-title">心理咨询预约日历</h3>
</el-col>
<el-col :span="12" class="text-right">
<el-button type="primary" plain @click="refreshCalendar">刷新日历</el-button>
<el-button type="success" plain @click="viewMyAppointments">我的预约</el-button>
</el-col>
</el-row>
</template>
<el-row :gutter="20">
<el-col :span="24">
<div class="calendar-container">
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
<WeekView
:available-slots="availableSlots"
:default-view="calendarView"
@date-range-change="handleDateRangeChange"
@slot-click="handleSlotClick"
@view-change="handleViewChange"
/>
</div>
</el-col>
</el-row>
</el-card>
<!-- 预约表单弹窗 -->
<el-dialog
v-model="appointmentFormVisible"
title="预约咨询"
width="500px"
append-to-body
destroy-on-close
>
<el-form
ref="appointmentFormRef"
:model="appointmentForm"
:rules="appointmentRules"
label-width="100px"
>
<el-form-item label="咨询师" prop="counselorId">
<el-input v-model="counselorDisplayName" disabled />
</el-form-item>
<el-form-item label="预约日期" prop="appointmentDate">
<el-input v-model="formattedDate" disabled />
</el-form-item>
<el-form-item label="预约时段" prop="appointmentDetailTime">
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
<el-option
v-for="slot in availableDetailSlots"
:key="slot.timeSlot"
:label="slot.timeSlot"
:value="slot.timeSlot"
:disabled="slot.isBooked || slot.isExpired"
:style="getTimeSlotStyle(slot)"
/>
</el-select>
</el-form-item>
<el-form-item label="学号" prop="studentNo" v-if="!hasStudentInfo">
<el-input v-model="studentNo" placeholder="请输入学号" @blur="handleStudentNoBlur" />
</el-form-item>
<el-form-item label="姓名" prop="studentName">
<el-input v-model="studentName" disabled />
</el-form-item>
<el-form-item label="咨询原因" prop="reason">
<el-input
v-model="appointmentForm.reason"
type="textarea"
placeholder="请简要描述咨询原因"
:rows="4"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitAppointmentForm" :loading="submitLoading">确 定</el-button>
<el-button @click="appointmentFormVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 我的预约列表弹窗 -->
<el-dialog
v-model="myAppointmentsVisible"
title="我的预约记录"
width="800px"
append-to-body
destroy-on-close
>
<el-table :data="myAppointmentsList" border style="width: 100%">
<el-table-column label="预约日期" prop="appointmentDate" width="120" />
<el-table-column label="预约时段" prop="appointmentDetailTime" width="120" />
<el-table-column label="咨询师" prop="counselorName" width="100" />
<el-table-column label="咨询原因" prop="reason" show-overflow-tooltip />
<el-table-column label="状态" prop="status" width="100">
<template #default="scope">
<!-- 添加调试信息 -->
<div style="display: none;">
状态值: {{ scope.row.status }}
类型: {{ typeof scope.row.status }}
字典数据: {{ appointment_status }}
</div>
<!-- 根据预约记录管理页面统一样式 -->
<el-tag v-if="scope.row.status === 0 || scope.row.status === '0'" type="primary">待咨询</el-tag>
<el-tag v-else-if="scope.row.status === 1 || scope.row.status === '1'" type="success">已到访</el-tag>
<el-tag v-else-if="scope.row.status === 2 || scope.row.status === '2'" type="danger">已取消</el-tag>
<el-tag v-else-if="scope.row.status === 3 || scope.row.status === '3'" type="warning">未到访</el-tag>
<el-tag v-else type="info">未知({{ scope.row.status }})</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<!-- 添加调试信息 -->
<div style="display: none;">
状态值: {{ scope.row.status }}
类型: {{ typeof scope.row.status }}
条件结果: {{ scope.row.status === 0 || scope.row.status === '0' }}
</div>
<!-- 使用更直接的方式显示取消按钮 -->
<template v-if="scope.row.status === 0 || scope.row.status === '0'">
<el-button
link
type="danger"
size="small"
@click="cancelAppointment(scope.row)"
>
取消预约
</el-button>
</template>
<span v-else>--</span>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="refreshMyAppointments" :loading="refreshLoading">刷 新</el-button>
<el-button @click="myAppointmentsVisible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { getAvailableTimeSlots, submitAppointment, getMyAppointments, updateAppointment, getStudentByNo, getCounselorOptions, getAvailableDetailSlots } from '@/api/xljk/consultation';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
import { useUserStore } from '@/store/modules/user';
import WeekView from './WeekView.vue';
const { proxy } = getCurrentInstance() as any;
// 不再使用后端字典数据
// const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
// 本地定义状态字典(与预约记录管理页面保持一致)
const appointment_status = ref([
{ label: '待咨询', value: 0, listClass: 'primary' },
{ label: '已到访', value: 1, listClass: 'success' },
{ label: '已取消', value: 2, listClass: 'danger' },
{ label: '未到访', value: 3, listClass: 'warning' }
]);
// 用户信息
const userStore = useUserStore();
const userId = computed(() => userStore.userId);
const nickname = computed(() => userStore.nickname);
const hasStudentInfo = computed(() => Boolean(studentId.value));
// 日历相关
const availableSlots = ref<AvailableTimeSlot[]>([]);
const calendarView = ref('month'); // 默认使用月视图
// 预约表单相关
const appointmentFormVisible = ref(false);
const appointmentFormRef = ref();
const submitLoading = ref(false);
const selectedSlot = ref<AvailableTimeSlot>({} as AvailableTimeSlot);
const studentNo = ref('');
const studentName = ref('');
const studentId = ref<string | number | null>(null);
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
counselorId: undefined,
scheduleId: undefined,
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
reason: '',
status: 0 // 默认待咨询状态,使用数值类型
});
const appointmentRules = {
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
};
// 我的预约相关
const myAppointmentsVisible = ref(false);
const myAppointmentsList = ref<any[]>([]);
const refreshLoading = ref(false);
// 计算属性
const formattedDate = computed(() => {
if (!selectedSlot.value.date) return '';
return selectedSlot.value.date;
});
const formattedTimeSlot = computed(() => {
if (!selectedSlot.value.startTime || !selectedSlot.value.endTime) return '';
return `${selectedSlot.value.startTime} - ${selectedSlot.value.endTime}`;
});
// 计算属性:咨询师名称
const counselorDisplayName = computed(() => {
return selectedSlot.value?.counselorName || '正在加载...';
});
// 处理日期范围变化
function handleDateRangeChange({ startDate, endDate }: { startDate: string, endDate: string }) {
console.log(`日期范围变化:${startDate} 到 ${endDate},立即加载数据`);
// 确保日期范围足够大,覆盖整个日历视图
const startDateObj = new Date(startDate.replace(/-/g, '/'));
const endDateObj = new Date(endDate.replace(/-/g, '/'));
// 计算日期差(天数)
const diffTime = endDateObj.getTime() - startDateObj.getTime();
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
// 如果日期范围小于42天(6周),则扩展结束日期
if (diffDays < 42) {
const expandedEndDate = new Date(startDateObj);
expandedEndDate.setDate(expandedEndDate.getDate() + 42);
endDate = parseTime(expandedEndDate, '{y}-{m}-{d}');
console.log(`日期范围不足42天,扩展至:${startDate} 到 ${endDate}`);
}
loadAvailableSlots(startDate, endDate);
}
// 处理视图切换
function handleViewChange(view: string) {
calendarView.value = view;
}
// 加载可用预约时段
async function loadAvailableSlots(startDate: string, endDate: string) {
try {
const res = await getAvailableTimeSlots(startDate, endDate);
if (res.code === 200) {
availableSlots.value = res.data || [];
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
try {
const counselorRes = await getCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
const counselors = counselorRes.data;
// 为每个时段添加咨询师姓名
availableSlots.value = availableSlots.value.map(slot => {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
}
return slot;
});
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
}
} catch (error) {
console.error('获取可预约时段失败', error);
ElMessage.error('获取可预约时段失败');
}
}
// 处理时段点击
async function handleSlotClick(slot: AvailableTimeSlot) {
selectedSlot.value = slot;
// 如果没有咨询师姓名,尝试获取咨询师信息
if (!slot.counselorName && slot.counselorId) {
try {
const res = await getCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
if (counselor) {
slot.counselorName = counselor.name;
selectedSlot.value = { ...slot };
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
// 填充预约表单
appointmentForm.counselorId = slot.counselorId;
appointmentForm.scheduleId = slot.scheduleId;
appointmentForm.appointmentDate = slot.date;
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 加载详细时段
await loadDetailTimeSlots(slot.date, slot.timeSlot);
// 如果已登录且有用户ID,初始化学生信息
if (userId.value) {
// 初始设置为当前用户ID
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
}
appointmentFormVisible.value = true;
}
// 加载详细时段
async function loadDetailTimeSlots(date: string, timeSlot: string) {
try {
const res = await getAvailableDetailSlots(date, timeSlot);
if (res.code === 200) {
availableDetailSlots.value = res.data || [];
} else {
availableDetailSlots.value = [];
ElMessage.warning('获取可用时段失败');
}
} catch (error) {
console.error('获取详细时段失败', error);
ElMessage.error('获取详细时段失败');
availableDetailSlots.value = [];
}
}
// 获取时间段样式
function getTimeSlotStyle(slot: TimeSlotDetail) {
if (slot.isBooked) {
// 已预约:灰色+删除线
return {
color: '#999',
textDecoration: 'line-through',
backgroundColor: '#f5f7fa'
};
} else if (slot.isExpired) {
// 已过期:灰色,无删除线
return {
color: '#999',
backgroundColor: '#f5f7fa'
};
} else {
// 可预约:正常样式
return {};
}
}
// 获取状态文本
function getStatusText(status: string | number) {
console.log('getStatusText 被调用,status:', status, '类型:', typeof status);
const statusMap: Record<string, string> = {
'0': '待咨询',
'1': '已到访',
'2': '已取消',
'3': '未到访'
};
// 将数值类型转为字符串
if (typeof status === 'number') {
status = status.toString();
}
return statusMap[status] || `未知(${status})`;
}
// 获取状态样式类
function getStatusClass(status: string | number) {
console.log('getStatusClass 被调用,status:', status, '类型:', typeof status);
const classMap: Record<string, string> = {
'0': 'text-warning',
'1': 'text-success',
'2': 'text-info',
'3': 'text-danger'
};
// 将数值类型转为字符串
if (typeof status === 'number') {
status = status.toString();
}
return classMap[status] || '';
}
// 提交预约表单
function submitAppointmentForm() {
appointmentFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
if (!studentId.value) {
ElMessage.warning('请先输入有效的学号');
return;
}
if (!appointmentForm.appointmentDetailTime) {
ElMessage.warning('请选择具体预约时段');
return;
}
try {
submitLoading.value = true;
// 构建提交数据
const submitData: XljkConsultationAppointmentForm = {
...appointmentForm,
studentId: studentId.value, // 不进行Number转换,保持原始值
studentNo: studentNo.value // 添加学号
};
console.log('提交预约的studentId:', submitData.studentId);
const res = await submitAppointment(submitData);
if (res.code === 200) {
ElMessage.success('预约成功');
appointmentFormVisible.value = false;
refreshCalendar(); // 刷新日历
// 预约成功后,自动打开"我的预约"弹窗
setTimeout(() => {
viewMyAppointments();
}, 500);
} else {
ElMessage.error(res.msg || '预约失败,该时段可能已被预约');
}
} catch (error) {
console.error('提交预约失败', error);
ElMessage.error('提交预约失败');
} finally {
submitLoading.value = false;
}
});
}
// 查看我的预约
async function viewMyAppointments() {
if (!userId.value) {
ElMessage.warning('请先登录');
return;
}
if (!studentId.value) {
ElMessage.warning('请先在学号输入框中输入学号并查询学生信息,或刷新页面让系统自动获取');
return;
}
try {
console.log('查询预约记录的studentId:', studentId.value, '类型:', typeof studentId.value);
// 添加一个时间戳参数,强制绕过浏览器缓存
const timestamp = new Date().getTime();
console.log(`获取预约记录,添加时间戳${timestamp}强制刷新数据`);
// 尝试多种类型转换,确保能正确查询到记录
let id = studentId.value;
// 尝试使用原始studentId查询,强制刷新数据
console.log('尝试使用原始studentId查询:', id);
let res = await getMyAppointments(id, true); // 强制刷新数据
console.log('查询结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
// 处理数据,确保状态字段是数值类型
myAppointmentsList.value = res.data.map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
console.log('状态为空,设置默认值0');
}
// 如果状态是字符串类型,尝试转换为数值
else if (typeof item.status === 'string') {
const statusNum = parseInt(item.status);
if (!isNaN(statusNum)) {
item.status = statusNum;
console.log('状态从字符串转换为数值:', statusNum);
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
return item;
});
console.log('预约记录数据:', myAppointmentsList.value);
console.log('字典数据:', appointment_status.value);
myAppointmentsVisible.value = true;
} else {
console.log('原始ID查询无结果,尝试转换为数字类型');
// 尝试转换为数字类型
const numericId = Number(id);
if (!isNaN(numericId)) {
console.log('尝试使用数字类型studentId查询:', numericId);
res = await getMyAppointments(numericId, true); // 强制刷新数据
console.log('数字类型查询结果:', res);
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
myAppointmentsList.value = (res.data || []).map((item: any) => {
console.log('原始记录项:', item);
// 注意:移除了本地状态优先的逻辑,始终使用后端最新状态
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
console.log('状态为空,设置默认值0');
}
// 如果状态是字符串类型,尝试转换为数值
else if (typeof item.status === 'string') {
const statusNum = parseInt(item.status);
if (!isNaN(statusNum)) {
item.status = statusNum;
console.log('状态从字符串转换为数值:', statusNum);
}
}
console.log('处理后的预约记录项:', item.id, '状态:', item.status, '类型:', typeof item.status);
return item;
});
console.log('预约记录数据:', myAppointmentsList.value);
console.log('字典数据:', appointment_status.value);
myAppointmentsVisible.value = true;
}
}
// 无论是否找到记录,都显示弹窗
myAppointmentsVisible.value = true;
// 如果没有预约记录,显示提示
if (myAppointmentsList.value.length === 0) {
ElMessage.info('暂无预约记录');
}
}
} catch (error) {
console.error('获取预约记录失败', error);
ElMessage.error('获取预约记录失败');
}
}
// 取消预约
async function cancelAppointment(row: any) {
try {
await ElMessageBox.confirm('确定要取消该预约吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
// 记录原始行数据,用于调试
console.log('取消预约的原始行数据:', row);
const updateData = {
id: row.id,
status: 2 // 已取消,使用数值类型
};
console.log('发送取消预约请求:', updateData);
const res = await updateAppointment(updateData);
console.log('取消预约响应:', res);
if (res.code === 200) {
ElMessage.success('取消预约成功');
// 直接更新本地数据,避免重新请求
if (myAppointmentsList.value && myAppointmentsList.value.length > 0) {
const index = myAppointmentsList.value.findIndex(item => item.id === row.id);
if (index !== -1) {
myAppointmentsList.value[index].status = 2;
console.log('本地数据已更新,新状态:', myAppointmentsList.value[index].status);
}
}
// 不再调用viewMyAppointments(),避免重新请求导致状态被覆盖
// 只刷新日历,不刷新预约列表
refreshCalendar();
} else {
ElMessage.error(res.msg || '取消预约失败');
}
} catch (error) {
console.error('取消预约失败', error);
if (error !== 'cancel') {
ElMessage.error('取消预约失败,请稍后重试');
}
}
}
// 初始化咨询师信息
async function initCounselorInfo() {
try {
const res = await getCounselorOptions();
if (res.code === 200 && res.data) {
const counselors = res.data;
// 存储咨询师信息,以备后用
if (availableSlots.value.length > 0) {
availableSlots.value = availableSlots.value.map(slot => {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
}
return slot;
});
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
// 刷新"我的预约"数据
async function refreshMyAppointments() {
if (!studentId.value) {
ElMessage.warning('无法获取学生ID');
return;
}
refreshLoading.value = true;
try {
// 强制刷新数据
const res = await getMyAppointments(studentId.value, true);
if (res.code === 200) {
// 处理数据,确保状态字段是数值类型
myAppointmentsList.value = (res.data || []).map((item: any) => {
// 如果status是undefined或null,设置默认值为0
if (item.status === undefined || item.status === null) {
item.status = 0; // 默认为待咨询
}
// 如果状态是字符串类型,尝试转换为数值
else if (typeof item.status === 'string') {
const statusNum = parseInt(item.status);
if (!isNaN(statusNum)) {
item.status = statusNum;
}
}
return item;
});
ElMessage.success('刷新成功');
} else {
ElMessage.error(res.msg || '刷新失败');
}
} catch (error) {
console.error('刷新预约记录失败', error);
ElMessage.error('刷新预约记录失败');
} finally {
refreshLoading.value = false;
}
}
// 刷新日历
function refreshCalendar() {
// 获取当前日期
const now = new Date();
// 获取当前月的第一天
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// 获取当前月第一天所在周的周一(以确保完整覆盖当前月份)
const firstDayOfWeek = firstDayOfMonth.getDay() || 7; // 转换为1-7,1是周一
const firstDayOfCalendar = new Date(firstDayOfMonth);
firstDayOfCalendar.setDate(firstDayOfCalendar.getDate() - (firstDayOfWeek - 1));
// 计算显示6周需要的结束日期(从日历第一天开始算的42天后)
const sixWeeksLater = new Date(firstDayOfCalendar);
sixWeeksLater.setDate(sixWeeksLater.getDate() + 42); // 显示6周
// 获取30天前的日期,确保能看到历史记录
const thirtyDaysBefore = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
// 使用较早的日期作为开始日期
const startDate = parseTime(thirtyDaysBefore < firstDayOfCalendar ? thirtyDaysBefore : firstDayOfCalendar, '{y}-{m}-{d}');
// 结束日期取下月底和sixWeeksLater的较大者,确保完整覆盖日历视图
const nextMonthEnd = new Date(now.getFullYear(), now.getMonth() + 2, 0); // 下个月的最后一天
const endDateValue = new Date(Math.max(sixWeeksLater.getTime(), nextMonthEnd.getTime(), now.getTime() + 30 * 24 * 60 * 60 * 1000));
const endDate = parseTime(endDateValue, '{y}-{m}-{d}');
console.log(`刷新日历,日期范围:${startDate} 到 ${endDate},确保包含完整的6周显示`);
loadAvailableSlots(startDate, endDate);
}
// 学号输入框失去焦点时查询学生信息
async function handleStudentNoBlur() {
if (!studentNo.value) return;
try {
console.log('查询学生信息,学号:', studentNo.value);
const res = await getStudentByNo(studentNo.value);
console.log('查询学生信息结果:', res);
if (res.code === 200 && res.data) {
studentId.value = res.data.id;
studentName.value = res.data.name || '';
console.log('获取到学生ID:', studentId.value, '类型:', typeof studentId.value);
// 确保studentId是数字类型
if (typeof studentId.value === 'string') {
studentId.value = Number(studentId.value);
console.log('转换为数字类型后的studentId:', studentId.value);
}
} else {
ElMessage.warning('未找到该学号对应的学生信息');
studentId.value = null;
studentName.value = '';
}
} catch (error) {
console.error('获取学生信息失败', error);
ElMessage.error('获取学生信息失败');
}
}
// 页面初始化
onMounted(async () => {
// 初始化咨询师信息
initCounselorInfo();
// 刷新日历,确保初次加载就包含完整的数据
refreshCalendar();
// 额外请求一次下个月的数据
const now = new Date();
const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
const nextMonthEnd = new Date(now.getFullYear(), now.getMonth() + 2, 0);
const startDate = parseTime(nextMonth, '{y}-{m}-{d}');
const endDate = parseTime(nextMonthEnd, '{y}-{m}-{d}');
console.log(`额外请求下个月数据:${startDate} 到 ${endDate}`);
loadAvailableSlots(startDate, endDate);
// 自动获取学生信息(如果用户已登录且用户名为学号格式)
if (userId.value && nickname.value) {
console.log('当前用户信息:', { userId: userId.value, nickname: nickname.value });
// 尝试直接使用nickname作为学号查询
try {
console.log('尝试使用nickname查询学生信息:', nickname.value);
const res = await getStudentByNo(nickname.value);
console.log('查询学生信息结果:', res);
if (res.code === 200 && res.data) {
studentId.value = res.data.id;
studentName.value = res.data.name || '';
studentNo.value = res.data.studentNo || nickname.value;
console.log('自动获取学生信息成功:', res.data);
// 查询是否有预约记录
try {
const appointmentRes = await getMyAppointments(studentId.value);
console.log('学生预约记录:', appointmentRes.data);
} catch (e) {
console.error('查询预约记录失败:', e);
}
} else {
console.log('用户可能不是学生或学号格式不正确,尝试其他方式');
// 尝试特定格式处理,例如"2025WRJ010林伟"格式可能需要提取前面的学号部分
const studentNoMatch = nickname.value.match(/^(\d{4}[A-Za-z]{3}\d{3})/);
if (studentNoMatch && studentNoMatch[1]) {
const extractedStudentNo = studentNoMatch[1];
console.log('尝试提取学号:', extractedStudentNo);
try {
const res2 = await getStudentByNo(extractedStudentNo);
if (res2.code === 200 && res2.data) {
studentId.value = res2.data.id;
studentName.value = res2.data.name || '';
studentNo.value = res2.data.studentNo || extractedStudentNo;
console.log('使用提取的学号获取学生信息成功:', res2.data);
return;
}
} catch (e) {
console.error('使用提取的学号查询失败:', e);
}
}
// 如果上述方法都失败,直接使用userId作为studentId(临时解决方案)
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
console.log('使用userId作为studentId:', userId.value);
}
} catch (error) {
console.error('自动获取学生信息失败,使用userId作为studentId', error);
// 如果自动获取失败,直接使用userId作为studentId(临时解决方案)
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
console.log('使用userId作为studentId:', userId.value);
}
} else {
console.log('用户未登录或没有nickname');
}
});
</script>
<style scoped>
.page-container {
padding: 8px;
}
.calendar-card {
margin-top: 0;
}
.calendar-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
.calendar-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #303133;
}
.text-right {
text-align: right;
}
.calendar-container {
height: calc(100vh - 240px);
min-height: 600px;
}
/* 状态文本样式 */
.text-warning {
color: #E6A23C;
}
.text-success {
color: #67C23A;
}
.text-info {
color: #909399;
}
.text-danger {
color: #F56C6C;
}
</style>
@@ -0,0 +1,683 @@
<template>
<div class="p-2 page-container">
<el-card shadow="never" class="calendar-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="12">
<h3 class="calendar-title">心理咨询预约日历</h3>
</el-col>
<el-col :span="12" class="text-right">
<el-button type="primary" plain @click="refreshCalendar">刷新日历</el-button>
<el-button type="success" plain @click="viewMyAppointments">我的预约</el-button>
</el-col>
</el-row>
</template>
<el-row :gutter="20">
<el-col :span="24">
<div class="calendar-container">
<!-- 使用WeekView组件,支持月视图和周视图切换 -->
<WeekView
:available-slots="availableSlots"
:default-view="calendarView"
@date-range-change="handleDateRangeChange"
@slot-click="handleSlotClick"
@view-change="handleViewChange"
/>
</div>
</el-col>
</el-row>
</el-card>
<!-- 预约表单弹窗 -->
<el-dialog
v-model="appointmentFormVisible"
title="预约咨询"
width="500px"
append-to-body
destroy-on-close
>
<el-form
ref="appointmentFormRef"
:model="appointmentForm"
:rules="appointmentRules"
label-width="100px"
>
<el-form-item label="咨询师" prop="counselorId">
<el-input v-model="counselorDisplayName" disabled />
</el-form-item>
<el-form-item label="预约日期" prop="appointmentDate">
<el-input v-model="formattedDate" disabled />
</el-form-item>
<el-form-item label="预约时段" prop="appointmentDetailTime">
<el-select v-model="appointmentForm.appointmentDetailTime" placeholder="请选择具体预约时段" style="width: 100%">
<el-option
v-for="slot in availableDetailSlots"
:key="slot.timeSlot"
:label="slot.timeSlot"
:value="slot.timeSlot"
:disabled="slot.isBooked || slot.isExpired"
:style="getTimeSlotStyle(slot)"
/>
</el-select>
</el-form-item>
<el-form-item label="学号" prop="studentNo" v-if="!hasStudentInfo">
<el-input v-model="studentNo" placeholder="请输入学号" @blur="handleStudentNoBlur" />
</el-form-item>
<el-form-item label="姓名" prop="studentName">
<el-input v-model="studentName" disabled />
</el-form-item>
<el-form-item label="咨询原因" prop="reason">
<el-input
v-model="appointmentForm.reason"
type="textarea"
placeholder="请简要描述咨询原因"
:rows="4"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="submitAppointmentForm" :loading="submitLoading">确 定</el-button>
<el-button @click="appointmentFormVisible = false">取 消</el-button>
</div>
</template>
</el-dialog>
<!-- 我的预约列表弹窗 -->
<el-dialog
v-model="myAppointmentsVisible"
title="我的预约记录"
width="800px"
append-to-body
destroy-on-close
>
<el-table :data="myAppointmentsList" border style="width: 100%">
<el-table-column label="预约日期" prop="appointmentDate" width="120" />
<el-table-column label="预约时段" prop="appointmentDetailTime" width="120" />
<el-table-column label="咨询师" prop="counselorName" width="100" />
<el-table-column label="咨询原因" prop="reason" show-overflow-tooltip />
<el-table-column label="状态" prop="status" width="100">
<template #default="scope">
<dict-tag :options="appointment_status" :value="scope.row.status" />
<!-- 备用显示方案,如果字典数据没有加载,显示原始值 -->
<span v-if="!appointment_status || appointment_status.length === 0" :class="getStatusClass(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</span>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="scope">
<el-button
link
type="danger"
size="small"
@click="cancelAppointment(scope.row)"
v-if="scope.row.status === '0'"
>
取消预约
</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<div class="dialog-footer">
<el-button @click="myAppointmentsVisible = false">关 闭</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { getAvailableTimeSlots, submitAppointment, getMyAppointments, updateAppointment, getStudentByNo, getCounselorOptions, getAvailableDetailSlots } from '@/api/xljk/consultation';
import { AvailableTimeSlot, XljkConsultationAppointmentForm, CounselorOption, TimeSlotDetail } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
import { useUserStore } from '@/store/modules/user';
import WeekView from './WeekView.vue';
import { checkLimitation } from '@/api/xljk/consultation/noShow';
const { proxy } = getCurrentInstance() as any;
const { appointment_status } = toRefs(proxy?.useDict('appointment_status'));
// 用户信息
const userStore = useUserStore();
const userId = computed(() => userStore.userId);
const nickname = computed(() => userStore.nickname);
const hasStudentInfo = computed(() => Boolean(studentId.value));
// 日历相关
const availableSlots = ref<AvailableTimeSlot[]>([]);
const calendarView = ref('month'); // 默认使用月视图
// 预约表单相关
const appointmentFormVisible = ref(false);
const appointmentFormRef = ref();
const submitLoading = ref(false);
const selectedSlot = ref<AvailableTimeSlot>({} as AvailableTimeSlot);
const studentNo = ref('');
const studentName = ref('');
const studentId = ref<string | number | null>(null);
const availableDetailSlots = ref<TimeSlotDetail[]>([]);
const appointmentForm = reactive<XljkConsultationAppointmentForm>({
counselorId: undefined,
scheduleId: undefined,
appointmentDate: '',
appointmentTime: '',
appointmentDetailTime: '',
reason: '',
status: '0' // 默认待咨询状态
});
const appointmentRules = {
reason: [{ required: true, message: '请输入咨询原因', trigger: 'blur' }],
studentNo: [{ required: true, message: '请输入学号', trigger: 'blur' }],
appointmentDetailTime: [{ required: true, message: '请选择具体预约时段', trigger: 'change' }]
};
// 我的预约相关
const myAppointmentsVisible = ref(false);
const myAppointmentsList = ref<any[]>([]);
// 计算属性
const formattedDate = computed(() => {
if (!selectedSlot.value.date) return '';
return selectedSlot.value.date;
});
const formattedTimeSlot = computed(() => {
if (!selectedSlot.value.startTime || !selectedSlot.value.endTime) return '';
return `${selectedSlot.value.startTime} - ${selectedSlot.value.endTime}`;
});
// 计算属性:咨询师名称
const counselorDisplayName = computed(() => {
return selectedSlot.value?.counselorName || '正在加载...';
});
// 处理日期范围变化
function handleDateRangeChange({ startDate, endDate }: { startDate: string, endDate: string }) {
loadAvailableSlots(startDate, endDate);
}
// 处理视图切换
function handleViewChange(view: string) {
calendarView.value = view;
}
// 加载可用预约时段
async function loadAvailableSlots(startDate: string, endDate: string) {
try {
const res = await getAvailableTimeSlots(startDate, endDate);
if (res.code === 200) {
availableSlots.value = res.data || [];
// 如果有可用时段但没有咨询师姓名,尝试加载咨询师信息
if (availableSlots.value.length > 0 && !availableSlots.value[0].counselorName) {
try {
const counselorRes = await getCounselorOptions();
if (counselorRes.code === 200 && counselorRes.data) {
const counselors = counselorRes.data;
// 为每个时段添加咨询师姓名
availableSlots.value = availableSlots.value.map(slot => {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
}
return slot;
});
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
}
} catch (error) {
console.error('获取可预约时段失败', error);
ElMessage.error('获取可预约时段失败');
}
}
// 处理时段点击
async function handleSlotClick(slot: AvailableTimeSlot) {
selectedSlot.value = slot;
// 如果没有咨询师姓名,尝试获取咨询师信息
if (!slot.counselorName && slot.counselorId) {
try {
const res = await getCounselorOptions();
if (res.code === 200 && res.data) {
const counselor = res.data.find((item: CounselorOption) => item.id === slot.counselorId);
if (counselor) {
slot.counselorName = counselor.name;
selectedSlot.value = { ...slot };
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
// 填充预约表单
appointmentForm.counselorId = slot.counselorId;
appointmentForm.scheduleId = slot.scheduleId;
appointmentForm.appointmentDate = slot.date;
appointmentForm.appointmentTime = `${slot.startTime}-${slot.endTime}`;
appointmentForm.appointmentDetailTime = ''; // 清空详细时段,等待用户选择
appointmentForm.reason = ''; // 清空咨询原因,避免带出上次的内容
// 加载详细时段
await loadDetailTimeSlots(slot.date, slot.timeSlot);
// 如果已登录且有用户ID,初始化学生信息
if (userId.value) {
// 初始设置为当前用户ID
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
}
appointmentFormVisible.value = true;
}
// 加载详细时段
async function loadDetailTimeSlots(date: string, timeSlot: string) {
try {
const res = await getAvailableDetailSlots(date, timeSlot);
if (res.code === 200) {
availableDetailSlots.value = res.data || [];
} else {
availableDetailSlots.value = [];
ElMessage.warning('获取可用时段失败');
}
} catch (error) {
console.error('获取详细时段失败', error);
ElMessage.error('获取详细时段失败');
availableDetailSlots.value = [];
}
}
// 获取时间段样式
function getTimeSlotStyle(slot: TimeSlotDetail) {
if (slot.isBooked) {
// 已预约:灰色+删除线
return {
color: '#999',
textDecoration: 'line-through',
backgroundColor: '#f5f7fa'
};
} else if (slot.isExpired) {
// 已过期:灰色,无删除线
return {
color: '#999',
backgroundColor: '#f5f7fa'
};
} else {
// 可预约:正常样式
return {};
}
}
// 获取状态文本
function getStatusText(status: string) {
const statusMap: Record<string, string> = {
'0': '待咨询',
'1': '已完成',
'2': '已取消',
'3': '未到访'
};
return statusMap[status] || status;
}
// 获取状态样式类
function getStatusClass(status: string) {
const classMap: Record<string, string> = {
'0': 'text-warning',
'1': 'text-success',
'2': 'text-info',
'3': 'text-danger'
};
return classMap[status] || '';
}
// 提交预约表单
function submitAppointmentForm() {
appointmentFormRef.value?.validate(async (valid: boolean) => {
if (!valid) return;
if (!studentId.value) {
ElMessage.warning('请先输入有效的学号');
return;
}
if (!appointmentForm.appointmentDetailTime) {
ElMessage.warning('请选择具体预约时段');
return;
}
// 检查学生是否被限制预约
try {
const res = await checkLimitation(studentId.value);
if (res.data.limited) {
ElMessageBox.alert(
`由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。`,
'预约受限',
{ type: 'warning' }
);
return;
}
} catch (error) {
console.error('检查预约限制失败', error);
}
try {
submitLoading.value = true;
// 构建提交数据
const submitData: XljkConsultationAppointmentForm = {
...appointmentForm,
studentId: studentId.value, // 不进行Number转换,保持原始值
studentNo: studentNo.value // 添加学号
};
console.log('提交预约的studentId:', submitData.studentId);
const res = await submitAppointment(submitData);
if (res.code === 200) {
ElMessage.success('预约成功');
appointmentFormVisible.value = false;
refreshCalendar(); // 刷新日历
// 预约成功后,自动打开"我的预约"弹窗
setTimeout(() => {
viewMyAppointments();
}, 500);
} else {
ElMessage.error(res.msg || '预约失败,该时段可能已被预约');
}
} catch (error) {
console.error('提交预约失败', error);
ElMessage.error('提交预约失败');
} finally {
submitLoading.value = false;
}
});
}
// 查看我的预约
async function viewMyAppointments() {
if (!userId.value) {
ElMessage.warning('请先登录');
return;
}
if (!studentId.value) {
ElMessage.warning('请先在学号输入框中输入学号并查询学生信息,或刷新页面让系统自动获取');
return;
}
try {
console.log('查询预约记录的studentId:', studentId.value, '类型:', typeof studentId.value);
// 尝试多种类型转换,确保能正确查询到记录
let id = studentId.value;
// 尝试使用原始studentId查询
console.log('尝试使用原始studentId查询:', id);
let res = await getMyAppointments(id);
console.log('查询结果:', res);
if (res.code === 200 && res.data && res.data.length > 0) {
myAppointmentsList.value = res.data;
console.log('预约记录数据:', myAppointmentsList.value);
console.log('字典数据:', appointment_status.value);
myAppointmentsVisible.value = true;
} else {
console.log('原始ID查询无结果,尝试转换为数字类型');
// 尝试转换为数字类型
const numericId = Number(id);
if (!isNaN(numericId)) {
console.log('尝试使用数字类型studentId查询:', numericId);
res = await getMyAppointments(numericId);
console.log('数字类型查询结果:', res);
if (res.code === 200) {
myAppointmentsList.value = res.data || [];
console.log('预约记录数据:', myAppointmentsList.value);
console.log('字典数据:', appointment_status.value);
myAppointmentsVisible.value = true;
}
}
// 无论是否找到记录,都显示弹窗
myAppointmentsVisible.value = true;
// 如果没有预约记录,显示提示
if (myAppointmentsList.value.length === 0) {
ElMessage.info('暂无预约记录');
}
}
} catch (error) {
console.error('获取预约记录失败', error);
ElMessage.error('获取预约记录失败');
}
}
// 取消预约
async function cancelAppointment(row: any) {
try {
await ElMessageBox.confirm('确定要取消该预约吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
const updateData = {
id: row.id,
status: '2' // 已取消
};
const res = await updateAppointment(updateData);
if (res.code === 200) {
ElMessage.success('取消预约成功');
viewMyAppointments(); // 刷新预约列表
refreshCalendar(); // 刷新日历
} else {
ElMessage.error(res.msg || '取消预约失败');
}
} catch (error) {
console.error('取消预约失败', error);
}
}
// 初始化咨询师信息
async function initCounselorInfo() {
try {
const res = await getCounselorOptions();
if (res.code === 200 && res.data) {
const counselors = res.data;
// 存储咨询师信息,以备后用
if (availableSlots.value.length > 0) {
availableSlots.value = availableSlots.value.map(slot => {
const counselor = counselors.find((c: CounselorOption) => c.id === slot.counselorId);
if (counselor) {
return { ...slot, counselorName: counselor.name };
}
return slot;
});
}
}
} catch (error) {
console.error('获取咨询师信息失败', error);
}
}
// 刷新日历
function refreshCalendar() {
// 获取当前日期
const now = new Date();
// 获取当前月的第一天
const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// 获取30天前的日期,确保能看到历史记录
const thirtyDaysBefore = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
// 使用较早的日期作为开始日期
const startDate = parseTime(thirtyDaysBefore < firstDayOfMonth ? thirtyDaysBefore : firstDayOfMonth, '{y}-{m}-{d}');
// 结束日期保持为30天后
const endDate = parseTime(new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000), '{y}-{m}-{d}');
loadAvailableSlots(startDate, endDate);
}
// 学号输入框失去焦点时查询学生信息
async function handleStudentNoBlur() {
if (!studentNo.value) return;
try {
console.log('查询学生信息,学号:', studentNo.value);
const res = await getStudentByNo(studentNo.value);
console.log('查询学生信息结果:', res);
if (res.code === 200 && res.data) {
studentId.value = res.data.id;
studentName.value = res.data.name || '';
console.log('获取到学生ID:', studentId.value, '类型:', typeof studentId.value);
// 确保studentId是数字类型
if (typeof studentId.value === 'string') {
studentId.value = Number(studentId.value);
console.log('转换为数字类型后的studentId:', studentId.value);
}
} else {
ElMessage.warning('未找到该学号对应的学生信息');
studentId.value = null;
studentName.value = '';
}
} catch (error) {
console.error('获取学生信息失败', error);
ElMessage.error('获取学生信息失败');
}
}
// 页面初始化
onMounted(async () => {
// 初始化咨询师信息
initCounselorInfo();
// 刷新日历
refreshCalendar();
// 自动获取学生信息(如果用户已登录且用户名为学号格式)
if (userId.value && nickname.value) {
console.log('当前用户信息:', { userId: userId.value, nickname: nickname.value });
// 尝试直接使用nickname作为学号查询
try {
console.log('尝试使用nickname查询学生信息:', nickname.value);
const res = await getStudentByNo(nickname.value);
console.log('查询学生信息结果:', res);
if (res.code === 200 && res.data) {
studentId.value = res.data.id;
studentName.value = res.data.name || '';
studentNo.value = res.data.studentNo || nickname.value;
console.log('自动获取学生信息成功:', res.data);
// 查询是否有预约记录
try {
const appointmentRes = await getMyAppointments(studentId.value);
console.log('学生预约记录:', appointmentRes.data);
} catch (e) {
console.error('查询预约记录失败:', e);
}
} else {
console.log('用户可能不是学生或学号格式不正确,尝试其他方式');
// 尝试特定格式处理,例如"2025WRJ010林伟"格式可能需要提取前面的学号部分
const studentNoMatch = nickname.value.match(/^(\d{4}[A-Za-z]{3}\d{3})/);
if (studentNoMatch && studentNoMatch[1]) {
const extractedStudentNo = studentNoMatch[1];
console.log('尝试提取学号:', extractedStudentNo);
try {
const res2 = await getStudentByNo(extractedStudentNo);
if (res2.code === 200 && res2.data) {
studentId.value = res2.data.id;
studentName.value = res2.data.name || '';
studentNo.value = res2.data.studentNo || extractedStudentNo;
console.log('使用提取的学号获取学生信息成功:', res2.data);
return;
}
} catch (e) {
console.error('使用提取的学号查询失败:', e);
}
}
// 如果上述方法都失败,直接使用userId作为studentId(临时解决方案)
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
console.log('使用userId作为studentId:', userId.value);
}
} catch (error) {
console.error('自动获取学生信息失败,使用userId作为studentId', error);
// 如果自动获取失败,直接使用userId作为studentId(临时解决方案)
studentId.value = userId.value;
studentName.value = nickname.value || '';
studentNo.value = nickname.value || '';
console.log('使用userId作为studentId:', userId.value);
}
} else {
console.log('用户未登录或没有nickname');
}
});
</script>
<style scoped>
.page-container {
padding: 8px;
}
.calendar-card {
margin-top: 0;
}
.calendar-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
.calendar-title {
margin: 0;
font-size: 18px;
font-weight: 500;
color: #303133;
}
.text-right {
text-align: right;
}
.calendar-container {
height: calc(100vh - 240px);
min-height: 600px;
}
/* 状态文本样式 */
.text-warning {
color: #E6A23C;
}
.text-success {
color: #67C23A;
}
.text-info {
color: #909399;
}
.text-danger {
color: #F56C6C;
}
</style>
@@ -0,0 +1,40 @@
--- index.vue.orig 2025-07-30 11:30:00
+++ index.vue 2025-07-30 11:30:00
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch, getCurrentInstance } from 'vue';
import { getMyAppointments, submitAppointment as submitAppointmentApi, getAvailableTimeSlots, getTimeSlotDetail } from '@/api/xljk/consultation/appointment';
+import { checkLimitation } from '@/api/xljk/consultation/noShow';
import { getStudentOptions, getCounselorOptions } from '@/api/xljk/consultation/options';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
@@ -344,7 +345,24 @@
}
// 提交预约
-function submitAppointmentForm() {
+async function submitAppointmentForm() {
+ // 表单验证
+ if (!form.value.studentId) {
+ ElMessage.warning('请选择学生');
+ return;
+ }
+
+ // 检查学生是否被限制预约
+ try {
+ const res = await checkLimitation(form.value.studentId);
+ if (res.data && res.data.limited) {
+ ElMessageBox.alert(
+ `由于您近期多次未按时到访,暂时无法预约咨询服务。限制将于 ${res.data.expireDate} 自动解除。如有特殊情况,请联系心理咨询中心。`,
+ '预约受限',
+ { type: 'warning' }
+ );
+ return;
+ }
+ } catch (error) {
+ console.error('检查预约限制失败', error);
+ }
+
// 表单验证
if (!form.value.studentId) {
ElMessage.warning('请选择学生');
+11
View File
@@ -0,0 +1,11 @@
<template>
<router-view v-slot="{ Component }">
<transition name="fade-transform" mode="out-in">
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts">
// 主入口文件,用于路由跳转
</script>
@@ -0,0 +1,446 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="学生姓名" prop="studentName">
<el-input v-model="queryParams.studentName" placeholder="请输入学生姓名" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="学号" prop="studentNo">
<el-input v-model="queryParams.studentNo" placeholder="请输入学号" clearable @keyup.enter="handleQuery" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" style="width: 100%" popper-class="noshow-status-popper">
<el-option
v-for="dict in limitationStatusOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="未到访日期" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="noShowList" @row-dblclick="handleView">
<el-table-column label="序号" type="index" width="55" align="center" />
<el-table-column label="学生姓名" align="center" prop="studentName" min-width="100" show-overflow-tooltip />
<el-table-column label="学号" align="center" prop="studentNo" min-width="100" show-overflow-tooltip />
<el-table-column label="院系" align="center" prop="collegeName" min-width="120" show-overflow-tooltip />
<el-table-column label="专业" align="center" prop="majorName" min-width="120" show-overflow-tooltip />
<el-table-column label="班级" align="center" prop="className" min-width="120" show-overflow-tooltip />
<el-table-column label="未到访日期" align="center" prop="noShowDate" min-width="100">
<template #default="scope">
<span>{{ parseTime(scope.row.noShowDate, '{y}-{m}-{d}') }}</span>
</template>
</el-table-column>
<el-table-column label="限制到期日" align="center" prop="expireDate" min-width="100">
<template #default="scope">
<span>{{ scope.row.expireDate ? parseTime(scope.row.expireDate, '{y}-{m}-{d}') : '-' }}</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center" width="120">
<template #default="scope">
<div>
<el-tag v-if="scope.row.status === '0'" type="danger">生效中</el-tag>
<el-tag v-else-if="scope.row.status === '1'" type="info">已过期</el-tag>
<el-tag v-else-if="scope.row.status === '2'" type="success">已解除</el-tag>
<span v-else>-</span>
</div>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="200" fixed="right">
<template #default="scope">
<el-tooltip content="查看" placement="top">
<el-button
link
type="info"
icon="View"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleView(scope.row)"
v-hasPermi="['xljk:consultationnoshow:query']"
></el-button>
</el-tooltip>
<!-- 生效中状态可解除限制 -->
<el-tooltip content="解除限制" placement="top" v-if="scope.row.status === '0'">
<el-button
link
type="primary"
icon="Key"
size="default"
style="font-size: 16px"
@click="handleRelease(scope.row)"
v-hasPermi="['xljk:consultationnoshow:release']"
></el-button>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 查看详情对话框 -->
<el-dialog title="查看未到访记录" v-model="viewOpen" width="700px" append-to-body>
<el-descriptions :column="2" border>
<!-- 第一行:学生姓名、学号 -->
<el-descriptions-item label="学生姓名">{{ form.studentName || '-' }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ form.studentNo || '-' }}</el-descriptions-item>
<!-- 第二行:院系、专业 -->
<el-descriptions-item label="院系">{{ form.collegeName || '-' }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ form.majorName || '-' }}</el-descriptions-item>
<!-- 第三行:班级、咨询师 -->
<el-descriptions-item label="班级">{{ form.className || '-' }}</el-descriptions-item>
<el-descriptions-item label="咨询师">{{ form.counselorName || '-' }}</el-descriptions-item>
<!-- 第四行:预约日期、预约时段 -->
<el-descriptions-item label="预约日期">{{ form.appointmentDate ? parseTime(form.appointmentDate, '{y}-{m}-{d}') : '-' }}</el-descriptions-item>
<el-descriptions-item label="预约时段">{{ form.appointmentDetailTime || '-' }}</el-descriptions-item>
<!-- 第五行:咨询原因(占两列) -->
<el-descriptions-item label="咨询原因" :span="2">{{ form.reason || '-' }}</el-descriptions-item>
<!-- 第六行:未到访日期(占两列) -->
<el-descriptions-item label="未到访日期" :span="2">{{ parseTime(form.noShowDate, '{y}-{m}-{d}') || '-' }}</el-descriptions-item>
<!-- 最后一行:限制到期日、状态 -->
<el-descriptions-item label="限制到期日">{{ form.expireDate ? parseTime(form.expireDate, '{y}-{m}-{d}') : '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag v-if="form.status === '0'" type="danger">生效中</el-tag>
<el-tag v-else-if="form.status === '1'" type="info">已过期</el-tag>
<el-tag v-else-if="form.status === '2'" type="success">已解除</el-tag>
<span v-else>-</span>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<div class="dialog-footer">
<el-button @click="viewOpen = false">关 闭</el-button>
<el-button
v-if="form.status === '0'"
type="primary"
@click="handleRelease(form)"
v-hasPermi="['xljk:consultationnoshow:release']"
>解除限制</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, getCurrentInstance } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Search, Refresh, View, Key } from '@element-plus/icons-vue';
import { listNoShow, getNoShow, releaseNoShowLimitation } from '@/api/xljk/consultation/noshow';
import { NoShowVO, NoShowQuery } from '@/api/xljk/consultation/types';
import { parseTime } from '@/utils/ruoyi';
const { proxy } = getCurrentInstance() as any;
// 遮罩层
const loading = ref(false);
// 显示搜索条件
const showSearch = ref(true);
// 总条数
const total = ref(0);
// 未到访记录表格数据
const noShowList = ref<NoShowVO[]>([]);
// 查看对话框
const viewOpen = ref(false);
// 日期范围
const dateRange = ref<[string, string]>();
// 查询参数
const queryParams = reactive<NoShowQuery>({
pageNum: 1,
pageSize: 10,
studentName: undefined,
studentNo: undefined,
status: '0', // 默认查询"生效中"状态
params: {
startDate: undefined,
endDate: undefined
}
});
// 表单参数
const form = reactive<NoShowVO>({
id: '',
studentId: '',
studentName: '',
studentNo: '',
collegeName: '',
majorName: '',
className: '',
counselorName: '',
appointmentDate: '',
appointmentDetailTime: '',
reason: '',
appointmentId: '',
noShowDate: '',
expireDate: '',
status: '',
createTime: '',
updateTime: ''
});
// 限制状态选项
const limitationStatusOptions = ref([
{ value: 'all', label: '全部' }, // 使用'all'代替空字符串
{ value: '0', label: '生效中' },
{ value: '1', label: '已过期' },
{ value: '2', label: '已解除' }
]);
const queryFormRef = ref();
/** 查询未到访记录列表 */
function getList() {
loading.value = true;
// 确保params对象存在
if (!queryParams.params) {
queryParams.params = {};
}
// 处理日期范围
if (dateRange.value) {
queryParams.params.startDate = dateRange.value[0];
queryParams.params.endDate = dateRange.value[1];
} else {
queryParams.params.startDate = undefined;
queryParams.params.endDate = undefined;
}
// 添加排序参数,实现倒序排列,按未到访日期排序
queryParams.orderByColumn = 'noShowDate';
queryParams.isAsc = 'desc';
// 创建一个新的查询参数对象,避免修改原始对象
const query = { ...queryParams };
listNoShow(query).then(response => {
if (response.code === 200) {
// 记录响应数据格式以便调试
console.log('API响应数据:', response);
// 处理不同的数据结构可能性
if (response.rows) {
// 直接包含rows的情况
noShowList.value = response.rows || [];
total.value = response.total || 0;
} else if (response.data) {
if (Array.isArray(response.data)) {
// 数据是数组的情况
noShowList.value = response.data;
total.value = response.data.length;
} else if (response.data.rows) {
// 标准分页格式
noShowList.value = response.data.rows || [];
total.value = response.data.total || 0;
} else {
// 其他情况,可能data本身就是数据
noShowList.value = [].concat(response.data || []);
total.value = [].concat(response.data || []).length;
}
} else {
// 没有找到数据
noShowList.value = [];
total.value = 0;
}
} else {
ElMessage.error(response.msg || '获取未到访记录列表失败');
noShowList.value = [];
total.value = 0;
}
loading.value = false;
}).catch(error => {
console.error('获取未到访记录列表失败', error);
ElMessage.error('获取未到访记录列表失败,请稍后重试');
noShowList.value = [];
total.value = 0;
loading.value = false;
});
}
/** 搜索按钮操作 */
function handleQuery() {
queryParams.pageNum = 1;
getList();
}
/** 重置按钮操作 */
function resetQuery() {
dateRange.value = undefined;
queryFormRef.value?.resetFields();
// 确保params对象存在并重置日期范围
if (!queryParams.params) {
queryParams.params = {};
}
queryParams.params.startDate = undefined;
queryParams.params.endDate = undefined;
// 确保重置后状态仍为"生效中"
queryParams.status = '0';
handleQuery();
}
/** 查看按钮操作 */
function handleView(row: NoShowVO) {
const id = row.id;
loading.value = true;
getNoShow(id).then(response => {
if (response.code === 200) {
console.log('获取详情响应:', response);
// 处理不同的数据结构可能性
if (response.data) {
Object.assign(form, response.data);
} else {
// 可能直接返回了数据对象
const data = {...response};
delete data.code;
delete data.msg;
Object.assign(form, data);
}
viewOpen.value = true;
} else {
ElMessage.error(response.msg || '获取未到访记录详情失败');
}
loading.value = false;
}).catch(error => {
console.error('获取未到访记录详情失败', error);
ElMessage.error('获取未到访记录详情失败,请稍后重试');
loading.value = false;
});
}
/** 解除限制按钮操作 */
function handleRelease(row: NoShowVO) {
const id = row.id;
ElMessageBox.confirm('确认解除该学生的预约限制吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
loading.value = true;
releaseNoShowLimitation(id).then(response => {
if (response.code === 200) {
ElMessage.success('解除限制成功');
getList();
if (viewOpen.value) {
viewOpen.value = false;
}
} else {
ElMessage.error(response.msg || '解除限制失败');
}
loading.value = false;
}).catch(error => {
console.error('解除限制失败', error);
ElMessage.error('解除限制失败,请稍后重试');
loading.value = false;
});
}).catch(() => {});
}
onMounted(() => {
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -0,0 +1,677 @@
<template>
<div class="p-2 page-container">
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
<div v-show="showSearch" class="mb-[10px]">
<el-card shadow="hover">
<el-form ref="queryFormRef" :model="queryParams" label-width="100px">
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="咨询师" prop="counselorId">
<el-select v-model="queryParams.counselorId" placeholder="请选择咨询师" clearable style="width: 100%">
<el-option
v-for="item in counselorOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="排班日期" prop="dateRange">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="时段类型" prop="timeSlot">
<el-select v-model="queryParams.timeSlot" placeholder="请选择时段类型" clearable style="width: 100%">
<el-option label="上午" value="0" />
<el-option label="下午" value="1" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 100%">
<el-option label="正常" value="0" />
<el-option label="停用" value="1" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="24" class="search-btn-container">
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
</div>
</transition>
<el-card shadow="never" class="button-card table-card">
<template #header>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['xljk:consultationschedule:add']">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['xljk:consultationschedule:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['xljk:consultationschedule:remove']">删除</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
</template>
<el-table v-loading="loading" border :data="scheduleList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="咨询师" align="center" prop="counselorName" min-width="100" show-overflow-tooltip />
<el-table-column label="日期范围" align="center" prop="dateRange" min-width="180" />
<el-table-column label="上午时段" align="center" min-width="150">
<template #default="scope">
<span v-if="scope.row.morningEnabled">{{ scope.row.morningTime }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="下午时段" align="center" min-width="150">
<template #default="scope">
<span v-if="scope.row.afternoonEnabled">{{ scope.row.afternoonTime }}</span>
<span v-else>-</span>
</template>
</el-table-column>
<el-table-column label="预约时段数" align="center" prop="scheduleCount" width="100" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<dict-tag :options="schedule_status" :value="scope.row.status" />
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="150">
<template #default="scope">
<span>{{ parseTime(scope.row.createTime) }}</span>
</template>
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150">
<template #default="scope">
<el-button
link
type="primary"
icon="Edit"
size="default"
style="font-size: 16px; margin-right: 8px"
@click="handleUpdate(scope.row)"
v-hasPermi="['xljk:consultationschedule:edit']"
></el-button>
<el-button
link
type="danger"
icon="Delete"
size="default"
style="font-size: 16px"
@click="handleDelete(scope.row)"
v-hasPermi="['xljk:consultationschedule:remove']"
></el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
</div>
</el-card>
<!-- 添加或修改预约时段对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="scheduleFormRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="咨询师" prop="counselorId">
<el-select v-model="form.counselorId" placeholder="请选择咨询师" style="width: 100%">
<el-option
v-for="item in counselorOptions"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="排班日期" prop="dateRange" v-if="!form.id">
<el-date-picker
v-model="form.dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="排班日期" prop="scheduleDate" v-else>
<el-date-picker
v-model="form.scheduleDate"
type="date"
placeholder="选择日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
<!-- 上午时段设置 -->
<el-divider content-position="left">上午时段</el-divider>
<el-form-item label="启用上午" prop="morningEnabled">
<el-switch v-model="form.morningEnabled" />
</el-form-item>
<template v-if="form.morningEnabled">
<el-form-item label="开始时间" prop="morningStartTime">
<el-time-picker
v-model="morningStartTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="morningTimePickerOptions"
@change="handleMorningStartTimeChange"
/>
</el-form-item>
<el-form-item label="结束时间" prop="morningEndTime">
<el-time-picker
v-model="morningEndTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="morningTimePickerOptions"
@change="handleMorningEndTimeChange"
/>
</el-form-item>
</template>
<!-- 下午时段设置 -->
<el-divider content-position="left">下午时段</el-divider>
<el-form-item label="启用下午" prop="afternoonEnabled">
<el-switch v-model="form.afternoonEnabled" />
</el-form-item>
<template v-if="form.afternoonEnabled">
<el-form-item label="开始时间" prop="afternoonStartTime">
<el-time-picker
v-model="afternoonStartTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="afternoonTimePickerOptions"
@change="handleAfternoonStartTimeChange"
/>
</el-form-item>
<el-form-item label="结束时间" prop="afternoonEndTime">
<el-time-picker
v-model="afternoonEndTimeTemp"
format="HH:mm"
placeholder="选择时间"
style="width: 100%"
:picker-options="afternoonTimePickerOptions"
@change="handleAfternoonEndTimeChange"
/>
</el-form-item>
</template>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio label="0">正常</el-radio>
<el-radio label="1">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="submitForm">确 定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed, watch } from 'vue';
import { listSchedule, getSchedule, delSchedule, addSchedule, updateSchedule, getCounselorOptions } from '@/api/xljk/consultation';
import { XljkConsultationScheduleVO, XljkConsultationScheduleForm, CounselorOption, XljkConsultationScheduleSummaryVO } from '@/api/xljk/consultation/types';
import { ElMessage, ElMessageBox } from 'element-plus';
import { parseTime } from '@/utils/ruoyi';
const { proxy } = getCurrentInstance() as any;
const { schedule_status } = toRefs(proxy?.useDict('schedule_status'));
// 遮罩层
const loading = ref(true);
// 选中数组
const ids = ref<Array<string | number>>([]);
// 非单个禁用
const single = ref(true);
// 非多个禁用
const multiple = ref(true);
// 显示搜索条件
const showSearch = ref(true);
// 总条数
const total = ref(0);
// 预约时段表格数据
const scheduleList = ref<XljkConsultationScheduleSummaryVO[]>([]);
// 弹出层标题
const title = ref('');
// 是否显示弹出层
const open = ref(false);
// 日期范围
const dateRange = ref<string[]>([]);
// 咨询师选项
const counselorOptions = ref<CounselorOption[]>([]);
// 时间选择器临时变量
const startTimeTemp = ref<Date | null>(null);
const endTimeTemp = ref<Date | null>(null);
const morningStartTimeTemp = ref<Date | null>(null);
const morningEndTimeTemp = ref<Date | null>(null);
const afternoonStartTimeTemp = ref<Date | null>(null);
const afternoonEndTimeTemp = ref<Date | null>(null);
// 时间选择器配置
const morningTimePickerOptions = {
start: '08:00',
step: '00:30',
end: '12:00',
format: 'HH:mm'
};
const afternoonTimePickerOptions = {
start: '14:00',
step: '00:30',
end: '17:00',
format: 'HH:mm'
};
// 查询参数
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
counselorId: undefined,
timeSlot: undefined,
status: undefined,
params: {} as any
});
// 表单参数
const form = reactive<any>({
id: undefined,
counselorId: undefined,
scheduleDate: '',
dateRange: [],
morningEnabled: true,
morningStartTime: '08:30',
morningEndTime: '12:00',
afternoonEnabled: true,
afternoonStartTime: '14:30',
afternoonEndTime: '17:00',
timeSlot: '0',
startTime: '',
endTime: '',
status: '0'
});
// 表单校验
const rules = {
counselorId: [{ required: true, message: '咨询师不能为空', trigger: 'change' }],
scheduleDate: [{ required: false, message: '排班日期不能为空', trigger: 'change' }],
dateRange: [{ required: true, message: '排班日期范围不能为空', trigger: 'change' }],
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
};
const queryFormRef = ref();
const scheduleFormRef = ref();
// 处理日期范围
watch(dateRange, (val) => {
if (val && val.length === 2) {
queryParams.params.beginScheduleDate = val[0];
queryParams.params.endScheduleDate = val[1];
} else {
queryParams.params.beginScheduleDate = undefined;
queryParams.params.endScheduleDate = undefined;
}
});
/** 查询预约时段列表 */
const getList = async () => {
loading.value = true;
try {
const res = await listSchedule(queryParams);
scheduleList.value = res.rows || [];
total.value = res.total || 0;
} catch (error) {
console.error('获取预约时段列表失败', error);
} finally {
loading.value = false;
}
};
/** 获取咨询师选项 */
const getCounselors = async () => {
try {
const res = await getCounselorOptions();
console.log('咨询师选项API返回数据:', res);
counselorOptions.value = res.data || [];
console.log('处理后的咨询师选项:', counselorOptions.value);
} catch (error) {
console.error('获取咨询师选项失败', error);
}
};
/** 取消按钮 */
const cancel = () => {
open.value = false;
reset();
};
/** 表单重置 */
const reset = () => {
form.id = undefined;
form.counselorId = undefined;
form.scheduleDate = '';
form.dateRange = [];
form.morningEnabled = true;
form.afternoonEnabled = true;
form.morningStartTime = '08:30';
form.morningEndTime = '12:00';
form.afternoonStartTime = '14:30';
form.afternoonEndTime = '17:00';
form.timeSlot = '0';
form.startTime = '';
form.endTime = '';
form.status = '0';
startTimeTemp.value = null;
endTimeTemp.value = null;
// 设置默认时间
const morningStart = new Date();
morningStart.setHours(8, 30, 0, 0);
morningStartTimeTemp.value = morningStart;
const morningEnd = new Date();
morningEnd.setHours(12, 0, 0, 0);
morningEndTimeTemp.value = morningEnd;
const afternoonStart = new Date();
afternoonStart.setHours(14, 30, 0, 0);
afternoonStartTimeTemp.value = afternoonStart;
const afternoonEnd = new Date();
afternoonEnd.setHours(17, 0, 0, 0);
afternoonEndTimeTemp.value = afternoonEnd;
scheduleFormRef.value?.resetFields();
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNum = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
dateRange.value = [];
queryFormRef.value?.resetFields();
handleQuery();
};
/** 多选框选中数据 */
const handleSelectionChange = (selection: XljkConsultationScheduleSummaryVO[]) => {
ids.value = selection.map((item) => item.id);
single.value = selection.length !== 1;
multiple.value = !selection.length;
};
/** 新增按钮操作 */
const handleAdd = () => {
reset();
open.value = true;
title.value = '添加预约时段';
};
/** 修改按钮操作 */
const handleUpdate = async (row?: XljkConsultationScheduleSummaryVO) => {
reset();
const _id = row?.id || ids.value[0];
try {
const res = await getSchedule(_id);
Object.assign(form, res.data);
// 设置时间选择器的值
if (form.startTime) {
const [hours, minutes] = form.startTime.split(':').map(Number);
startTimeTemp.value = new Date();
startTimeTemp.value.setHours(hours, minutes, 0, 0);
}
if (form.endTime) {
const [hours, minutes] = form.endTime.split(':').map(Number);
endTimeTemp.value = new Date();
endTimeTemp.value.setHours(hours, minutes, 0, 0);
}
open.value = true;
title.value = '修改预约时段';
} catch (error) {
console.error('获取预约时段详情失败', error);
}
};
/** 提交按钮 */
const submitForm = async () => {
if (!scheduleFormRef.value) return;
await scheduleFormRef.value.validate(async (valid: boolean) => {
if (!valid) return;
try {
if (form.id) {
// 修改单个预约时段
await updateSchedule({
id: form.id,
counselorId: form.counselorId,
scheduleDate: form.scheduleDate,
timeSlot: form.timeSlot,
startTime: form.startTime,
endTime: form.endTime,
status: form.status
});
ElMessage.success('修改成功');
} else {
// 批量创建预约时段
if (!form.dateRange || form.dateRange.length !== 2) {
ElMessage.warning('请选择排班日期范围');
return;
}
const startDate = new Date(form.dateRange[0]);
const endDate = new Date(form.dateRange[1]);
if (startDate > endDate) {
ElMessage.warning('开始日期不能大于结束日期');
return;
}
// 检查是否至少启用了一个时段
if (!form.morningEnabled && !form.afternoonEnabled) {
ElMessage.warning('请至少启用一个时段(上午或下午)');
return;
}
// 批量创建预约时段的请求数据
const batchCreateData = {
counselorId: form.counselorId,
startDate: form.dateRange[0],
endDate: form.dateRange[1],
morningEnabled: form.morningEnabled,
morningStartTime: form.morningStartTime,
morningEndTime: form.morningEndTime,
afternoonEnabled: form.afternoonEnabled,
afternoonStartTime: form.afternoonStartTime,
afternoonEndTime: form.afternoonEndTime,
status: form.status,
skipWeekends: true // 自动跳过周末
};
// 调用批量创建API
const res = await addSchedule(batchCreateData);
ElMessage.success(`批量创建成功,共创建${res.data || 0}条预约时段`);
}
open.value = false;
getList();
} catch (error) {
console.error('提交表单失败', error);
}
});
};
/** 删除按钮操作 */
const handleDelete = async (row?: XljkConsultationScheduleSummaryVO) => {
const _ids = row?.id || ids.value;
const confirmMessage = row
? `是否确认删除该预约时段?${row.scheduleCount > 1 ? `(包含${row.scheduleCount}条记录)` : ''}`
: `是否确认删除选中的${ids.value.length}条数据?`;
try {
await ElMessageBox.confirm(confirmMessage, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
});
await delSchedule(_ids);
ElMessage.success('删除成功');
getList();
} catch (error) {
console.error('删除失败', error);
}
};
/** 处理开始时间变化 */
const handleStartTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.startTime = `${hours}:${minutes}`;
} else {
form.startTime = '';
}
};
/** 处理结束时间变化 */
const handleEndTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.endTime = `${hours}:${minutes}`;
} else {
form.endTime = '';
}
};
/** 处理上午开始时间变化 */
const handleMorningStartTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningStartTime = `${hours}:${minutes}`;
} else {
form.morningStartTime = '';
}
};
/** 处理上午结束时间变化 */
const handleMorningEndTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.morningEndTime = `${hours}:${minutes}`;
} else {
form.morningEndTime = '';
}
};
/** 处理下午开始时间变化 */
const handleAfternoonStartTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonStartTime = `${hours}:${minutes}`;
} else {
form.afternoonStartTime = '';
}
};
/** 处理下午结束时间变化 */
const handleAfternoonEndTimeChange = (time: Date) => {
if (time) {
const hours = time.getHours().toString().padStart(2, '0');
const minutes = time.getMinutes().toString().padStart(2, '0');
form.afternoonEndTime = `${hours}:${minutes}`;
} else {
form.afternoonEndTime = '';
}
};
// 初始化
onMounted(() => {
getCounselors();
getList();
});
</script>
<style scoped>
/* 页面容器样式 */
.page-container {
padding: 8px;
}
/* 表格卡片布局 */
.table-card {
margin-top: 0;
}
.table-card :deep(.el-card__body) {
padding: 0px 20px 20px 20px !important;
}
/* 移除按钮栏底部边框 */
.button-card :deep(.el-card__header) {
border-bottom: none !important;
}
/* 分页容器样式 */
.pagination-container {
display: flex;
justify-content: flex-end;
padding: 16px 20px 0 0 !important;
background-color: #fff;
margin-top: 0px !important;
}
/* 搜索按钮容器样式 */
.search-btn-container {
display: flex;
justify-content: flex-end;
margin-top: 10px;
.el-form-item {
margin-bottom: 0 !important;
}
}
</style>