心理健康2.0

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

No files matched your search

+23
View File
@@ -33,6 +33,19 @@ export const getAvailableDetailSlots = (date, timeSlot) => {
});
};
/**
* 获取日历视图的排班数据(带预约状态)- 直接从xljk_consultation_schedule表查询
* @param {string} startDate 开始日期 YYYY-MM-DD
* @param {string} endDate 结束日期 YYYY-MM-DD
*/
export const getCalendarSchedules = (startDate, endDate) => {
return request({
url: '/xljk/consultation/appointment/calendar-schedules',
method: 'GET',
params: { startDate, endDate }
});
};
// ===== 预约记录管理 API =====
/**
@@ -171,6 +184,16 @@ export const getClassOptions = (collegeName, majorName) => {
});
};
/**
* 获取咨询主题选项(字典数据)
*/
export const getConsultThemeOptions = () => {
return request({
url: '/system/dict/data/type/xljk_consult_theme',
method: 'GET'
});
};
// ===== 移动端专用辅助方法 =====
/**
+83 -18
View File
@@ -19,11 +19,11 @@
<view class="filter-section">
<view class="filter-tabs">
<view
v-for="(tab, index) in filterTabs"
v-for="(tab, index) in visibleFilterTabs"
:key="index"
class="filter-tab"
:class="{ active: activeFilterIndex === index }"
@click="switchFilter(index)"
:class="{ active: getOriginalTabIndex(index) === activeFilterIndex }"
@click="switchFilter(getOriginalTabIndex(index))"
>
<text class="tab-text">{{ tab.label }}</text>
<!-- 只有待咨询标签显示圆形标记 -->
@@ -71,12 +71,15 @@
></u-avatar>
</view>
<view class="counselor-info">
<text class="counselor-name">{{ appointment.counselorName || '未知咨询师' }}</text>
<view class="counselor-name-row">
<text class="counselor-name">{{ appointment.counselorName || '未知咨询师' }}</text>
<text class="appointment-office">{{ appointment.office || '暂无办公室信息' }}</text>
</view>
<text class="appointment-reason">{{ appointment.reason || '心理咨询' }}</text>
</view>
<!-- 取消按钮 -->
<view v-if="appointment.status === '0'" class="inline-action">
<view v-if="appointment.status === '0' || appointment.status === '4'" class="inline-action">
<button
class="cancel-button-inline"
@click.stop="handleCancelClick(appointment)"
@@ -113,7 +116,7 @@
</template>
<script>
import { getMyAppointments, cancelAppointment } from '@/api/xljk'
import { getMyAppointments, cancelAppointment, getCounselorOptions } from '@/api/xljk'
export default {
name: 'MyAppointments',
@@ -124,8 +127,10 @@ export default {
{ label: '全部', value: 'all', count: 0 },
{ label: '待咨询', value: '0', count: 0 },
{ label: '已完成', value: '1', count: 0 },
{ label: '已取消', value: '2', count: 0 },
{ label: '未到访', value: '3', count: 0 }
{ label: '已取消', value: '2', count: 0, hidden: true }, // 隐藏已取消标签
{ label: '未到访', value: '3', count: 0 },
{ label: '待审核', value: '4', count: 0, hidden: true }, // 隐藏待审核标签
{ label: '已驳回', value: '5', count: 0 }
],
activeFilterIndex: 0,
@@ -140,6 +145,12 @@ export default {
cancelingAppointmentId: null // 正在取消的预约ID
}
},
computed: {
// 获取可见的筛选标签(过滤掉隐藏的标签)
visibleFilterTabs() {
return this.filterTabs.filter(tab => !tab.hidden)
}
},
onLoad() {
// 初始化时重置取消相关状态
this.resetCancelState()
@@ -152,6 +163,12 @@ export default {
this.loadAppointments()
},
methods: {
// 获取可见标签在原始数组中的索引
getOriginalTabIndex(visibleIndex) {
const visibleTab = this.visibleFilterTabs[visibleIndex]
return this.filterTabs.findIndex(tab => tab.value === visibleTab.value)
},
// 返回上一页
goBack() {
uni.navigateBack()
@@ -203,14 +220,38 @@ export default {
response.data.forEach((item, index) => {
console.log(`🔍 [我的预约] 数据${index}: ID=${item.appointmentId}, status=${item.status} (${typeof item.status})`)
})
// 获取咨询师信息用于补充办公室字段
let counselors = []
try {
const counselorRes = await getCounselorOptions()
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data
console.log('🔍 [我的预约] 获取到咨询师列表:', counselors)
}
} catch (error) {
console.error('获取咨询师信息失败:', error)
}
// 转换API数据格式
const appointments = response.data.map(item => {
// 补充办公室信息
let office = item.office || '暂无办公室信息'
if (item.counselorId && counselors.length > 0) {
const counselor = counselors.find(c => c.id === item.counselorId)
if (counselor && counselor.office) {
office = counselor.office
console.log('🔍 [我的预约] 补充咨询师办公室信息:', { counselorId: item.counselorId, office: counselor.office })
}
}
return {
id: item.id, // 修复:使用 id 而不是 appointmentId
appointmentDate: item.appointmentDate,
appointmentTime: item.appointmentDetailTime || item.appointmentTime,
counselorName: item.counselorName,
counselorAvatar: '',
office: office, // 使用补充后的办公室信息
reason: item.reason,
status: String(item.status), // 转换为字符串,便于筛选比较
originalData: item
@@ -249,14 +290,18 @@ export default {
const status1Count = this.allAppointments.filter(item => item.status === '1').length
const status2Count = this.allAppointments.filter(item => item.status === '2').length
const status3Count = this.allAppointments.filter(item => item.status === '3').length
const status4Count = this.allAppointments.filter(item => item.status === '4').length
const status5Count = this.allAppointments.filter(item => item.status === '5').length
console.log('🔍 [我的预约] 状态统计: 待咨询(0)=', status0Count, ', 已完成(1)=', status1Count, ', 已取消(2)=', status2Count, ', 未到访(3)=', status3Count)
console.log('🔍 [我的预约] 状态统计: 待咨询(0)=', status0Count, ', 已完成(1)=', status1Count, ', 已取消(2)=', status2Count, ', 未到访(3)=', status3Count, ', 待审核(4)=', status4Count, ', 已驳回(5)=', status5Count)
this.filterTabs[0].count = this.allAppointments.length // 全部
this.filterTabs[1].count = status0Count // 待咨询
this.filterTabs[2].count = status1Count // 已完成
this.filterTabs[3].count = status2Count // 已取消
this.filterTabs[4].count = status3Count // 未到访
this.filterTabs[5].count = status4Count // 待审核
this.filterTabs[6].count = status5Count // 已驳回
},
// 切换筛选器
@@ -288,6 +333,8 @@ export default {
'1': '已完成',
'2': '已取消',
'3': '未到访',
'4': '待审核',
'5': '已驳回',
// 兼容英文状态值
'pending': '待咨询',
'completed': '已完成',
@@ -304,6 +351,8 @@ export default {
'1': 'success',
'2': 'primary', // 已取消状态使用蓝色
'3': 'info', // 未到访状态使用灰色
'4': 'info', // 待审核状态使用灰色
'5': 'error', // 已驳回状态使用红色
// 兼容英文状态值
'pending': 'warning',
'completed': 'success',
@@ -321,7 +370,9 @@ export default {
'0': '暂无待咨询预约',
'1': '暂无已完成预约',
'2': '暂无已取消预约',
'3': '暂无未到访预约'
'3': '暂无未到访预约',
'4': '暂无待审核预约',
'5': '暂无已驳回预约'
}
return textMap[currentFilter.value] || '暂无数据'
},
@@ -355,9 +406,9 @@ export default {
cancelAppointment(appointment) {
console.log('点击取消预约按钮,预约信息:', appointment)
if (appointment.status !== '0') {
if (appointment.status !== '0' && appointment.status !== '4') {
uni.showToast({
title: '只能取消待咨询的预约',
title: '只能取消待咨询或待审核的预约',
icon: 'none'
})
return
@@ -670,12 +721,26 @@ export default {
.counselor-info {
flex: 1;
.counselor-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
.counselor-name-row {
display: flex;
align-items: center;
margin-bottom: 6rpx;
.counselor-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-right: 12rpx;
}
.appointment-office {
font-size: 24rpx;
color: #1890ff;
background: rgba(24, 144, 255, 0.1);
padding: 4rpx 8rpx;
border-radius: 8rpx;
flex-shrink: 0;
}
}
.appointment-reason {
+205 -91
View File
@@ -43,10 +43,10 @@
<!-- 分组时间段列表 -->
<view v-else-if="timeSlots.length > 0" class="grouped-time-slots">
<!-- 上午时段组 -->
<!-- 中午时段组 -->
<view v-if="groupedTimeSlots.morning.length > 0" class="time-group">
<view class="group-header">
<text class="group-title">上午时段</text>
<text class="group-title">中午时段</text>
</view>
<view class="slots-grid">
<view
@@ -133,6 +133,30 @@
<text class="detail-label">咨询师:</text>
<text class="detail-value">{{ selectedBookingSlot && selectedBookingSlot.counselorName }}</text>
</view>
<view class="booking-detail-item">
<text class="detail-label">办公室:</text>
<text class="detail-value">{{ selectedBookingSlot && selectedBookingSlot.office || '暂无信息' }}</text>
</view>
<!-- 咨询主题选择 -->
<view class="booking-detail-item">
<text class="detail-label required">咨询主题:</text>
</view>
<view class="theme-select-container">
<u-picker
:show="showThemePicker"
:columns="themeColumns"
@confirm="onThemeConfirm"
@cancel="showThemePicker = false"
keyName="dictLabel"
></u-picker>
<view class="theme-select-input" @click="showThemePicker = true">
<text class="theme-select-text" :class="{ placeholder: !selectedTheme }">
{{ selectedTheme ? selectedTheme.dictLabel : '请选择咨询主题' }}
</text>
<u-icon name="arrow-down" size="14" color="#c0c4cc"></u-icon>
</view>
</view>
<!-- 咨询原因输入框 -->
<view class="booking-detail-item reason-item">
@@ -158,7 +182,7 @@
</template>
<script>
import { getAvailableTimeSlots, getAvailableDetailSlots, submitAppointment } from '@/api/xljk'
import { getAvailableTimeSlots, getAvailableDetailSlots, getCalendarSchedules, submitAppointment, getConsultThemeOptions, getCounselorOptions } from '@/api/xljk'
export default {
name: 'XljkReservation',
@@ -182,7 +206,13 @@ export default {
showBookingModal: false,
selectedBookingSlot: null,
bookingLoading: false,
consultationReason: ''
consultationReason: '',
// 咨询主题相关
showThemePicker: false,
themeOptions: [],
themeColumns: [[]],
selectedTheme: null
}
},
onLoad() {
@@ -190,6 +220,7 @@ export default {
console.log('🔍 [预约页面] 开始初始化')
this.initWeekDates()
this.selectTodayAsDefault()
this.loadConsultThemeOptions()
console.log('🔍 [预约页面] 初始化完成')
} catch (error) {
console.error('🔍 [预约页面] 初始化失败:', error)
@@ -286,88 +317,104 @@ export default {
return `${date.getMonth() + 1}月${date.getDate()}日`
},
// 加载时间段数据 - 优化为加载详细时段
// 加载时间段数据 - 直接从xljk_consultation_schedule表获取
async loadTimeSlots(date) {
this.loadingTimeSlots = true
this.timeSlots = []
this.groupedTimeSlots = { morning: [], afternoon: [] }
try {
// 1. 先获取大时段列表 - 增加参数验证
// 参数验证
if (!date) {
throw new Error('日期参数无效')
}
console.log('🔍 开始获取大时段数据,日期:', date)
const bigSlotsResponse = await getAvailableTimeSlots(date, date)
console.log('🔍 大时段API返回:', bigSlotsResponse)
console.log('🔍 开始获取排班数据,日期:', date)
// 直接调用calendar-schedules接口获取排班数据(跟web端一样)
const response = await getCalendarSchedules(date, date)
console.log('🔍 排班数据API返回:', response)
// 验证API响应结构
if (!bigSlotsResponse || typeof bigSlotsResponse !== 'object') {
if (!response || typeof response !== 'object') {
throw new Error('API响应格式异常')
}
if (bigSlotsResponse.code === 200 && bigSlotsResponse.data && bigSlotsResponse.data.length > 0) {
// 2. 并行获取所有详细时段 - 增加数据安全性检查
const detailPromises = bigSlotsResponse.data
.filter(bigSlot => bigSlot && bigSlot.timeSlot) // 过滤无效数据
.map(bigSlot =>
getAvailableDetailSlots(date, bigSlot.timeSlot)
.then(response => ({
bigSlot,
detailSlots: response && response.data ? response.data : []
}))
.catch(error => {
console.error(`获取时段 ${bigSlot.timeSlot} 的详细数据失败:`, error)
return { bigSlot, detailSlots: [] }
})
)
if (response.code === 200 && response.data && Array.isArray(response.data)) {
// 如果排班数据缺少办公室信息,获取咨询师信息来补充
let counselors = []
const hasMissingOfficeInfo = response.data.some(schedule =>
(!schedule.office || schedule.office.trim() === '') && schedule.counselorId
)
const allDetailResults = await Promise.all(detailPromises)
console.log('🔍 详细时段API返回:', allDetailResults)
if (hasMissingOfficeInfo) {
try {
console.log('🔍 检测到缺少办公室信息,开始获取咨询师详细信息...')
const counselorRes = await getCounselorOptions()
if (counselorRes.code === 200 && counselorRes.data) {
counselors = counselorRes.data
console.log('🔍 获取到咨询师列表:', counselors)
}
} catch (error) {
console.error('获取咨询师信息失败:', error)
}
}
// 3. 扁平化并增强数据 - 增加数据安全性检查
const allDetailSlots = []
allDetailResults.forEach(({ bigSlot, detailSlots }) => {
// 确保 bigSlot 和 detailSlots 都是有效的
if (!bigSlot || !Array.isArray(detailSlots)) {
console.warn('无效的时段数据:', { bigSlot, detailSlots })
// 处理排班数据,转换为移动端需要的格式
const scheduleSlots = []
response.data.forEach(schedule => {
// 确保必要的字段存在
if (!schedule || !schedule.startTime || !schedule.endTime) {
console.warn('无效的排班数据:', schedule)
return
}
detailSlots.forEach(detail => {
// 确保必要的字段存在
if (!detail || !detail.timeSlot) {
console.warn('无效的详细时段数据:', detail)
return
try {
// 构建时间范围显示
const timeRange = `${schedule.startTime}-${schedule.endTime}`
// 判断是否可预约
const available = !schedule.isFullyBooked && !schedule.isExpired
// 补充办公室信息
let office = schedule.office || '暂无信息'
if ((!schedule.office || schedule.office.trim() === '') && schedule.counselorId && counselors.length > 0) {
const counselor = counselors.find(c => c.id === schedule.counselorId)
if (counselor && counselor.office) {
office = counselor.office
console.log('🔍 补充排班办公室信息:', { scheduleId: schedule.scheduleId, counselorId: schedule.counselorId, office: counselor.office })
}
}
try {
allDetailSlots.push({
id: `${bigSlot.scheduleId || 'unknown'}_${detail.timeSlot}`,
timeRange: detail.timeSlot,
available: !detail.isBooked && !detail.isExpired,
isBooked: Boolean(detail.isBooked),
isExpired: Boolean(detail.isExpired),
counselorName: bigSlot.counselorName || '未知咨询师',
counselorId: bigSlot.counselorId || null,
counselorAvatar: '',
dateText: this.selectedDateText || '',
originalData: {
...detail,
parentScheduleId: bigSlot.scheduleId || null,
counselorId: bigSlot.counselorId || null,
timeSlot: bigSlot.timeSlot || null
}
})
} catch (err) {
console.error('处理时段数据时出错:', err, { bigSlot, detail })
}
})
scheduleSlots.push({
id: `schedule_${schedule.scheduleId}`,
timeRange: timeRange,
available: available,
isBooked: Boolean(schedule.isFullyBooked),
isExpired: Boolean(schedule.isExpired),
counselorName: schedule.counselorName || '未知咨询师',
counselorId: schedule.counselorId || null,
counselorAvatar: '',
office: office, // 使用补充后的办公室信息
dateText: this.selectedDateText || '',
originalData: {
scheduleId: schedule.scheduleId,
counselorId: schedule.counselorId,
timeSlot: schedule.timeSlot,
startTime: schedule.startTime,
endTime: schedule.endTime,
available: available,
office: office // 在原始数据中也保存补充后的办公室信息
}
})
} catch (err) {
console.error('处理排班数据时出错:', err, schedule)
}
})
// 4. 按时间排序 - 增加安全性检查
allDetailSlots.sort((a, b) => {
// 按时间排序
scheduleSlots.sort((a, b) => {
try {
// 安全地获取时间用于排序
const timeA = (a.timeRange && typeof a.timeRange === 'string') ? a.timeRange.split('-')[0] : '00:00'
@@ -375,27 +422,27 @@ export default {
return timeA.localeCompare(timeB)
} catch (err) {
console.error('时间排序出错:', err, { a, b })
return 0 // 如果排序出错,保持原顺序
return 0
}
})
// 5. 按上午下午分组
this.groupAndSetTimeSlots(allDetailSlots)
// 按上午下午分组
this.groupAndSetTimeSlots(scheduleSlots)
console.log('🔍 最终处理的详细时段数据:', {
total: allDetailSlots.length,
console.log('🔍 最终处理的排班时段数据:', {
total: scheduleSlots.length,
morning: this.groupedTimeSlots.morning.length,
afternoon: this.groupedTimeSlots.afternoon.length
})
} else {
// 如果没有大时段数据
// 如果没有排班数据
this.timeSlots = []
this.groupedTimeSlots = { morning: [], afternoon: [] }
// 移除Toast提示,避免在没有排班时显示不必要的消息
console.log('🔍 当天无排班数据')
}
} catch (error) {
console.error('加载详细时段失败:', error)
console.error('加载排班时段失败:', error)
// 网络错误时显示友好提示
uni.showToast({
@@ -412,7 +459,7 @@ export default {
}
},
// 按上午下午分组时段 - 增加安全性检查
// 按时段分组 - 基于timeSlot字段分组(0-中午时段,1-下午时段)
groupAndSetTimeSlots(detailSlots) {
try {
// 确保输入是有效数组
@@ -423,35 +470,29 @@ export default {
return
}
// 按上午下午分组 - 增加安全性检查
// 基于timeSlot字段分组 - 增加安全性检查
const morningSlots = detailSlots.filter(slot => {
try {
if (!slot || !slot.timeRange || typeof slot.timeRange !== 'string') {
if (!slot || !slot.originalData || slot.originalData.timeSlot === undefined) {
console.warn('缺少timeSlot数据的时段:', slot)
return false
}
const timeParts = slot.timeRange.split(':')
if (timeParts.length === 0) {
return false
}
const hour = parseInt(timeParts[0])
return !isNaN(hour) && hour < 12
// timeSlot为0的是中午时段
return slot.originalData.timeSlot === 0 || slot.originalData.timeSlot === '0'
} catch (err) {
console.warn('处理上午时段时出错:', err, slot)
console.warn('处理中午时段时出错:', err, slot)
return false
}
})
const afternoonSlots = detailSlots.filter(slot => {
try {
if (!slot || !slot.timeRange || typeof slot.timeRange !== 'string') {
if (!slot || !slot.originalData || slot.originalData.timeSlot === undefined) {
console.warn('缺少timeSlot数据的时段:', slot)
return false
}
const timeParts = slot.timeRange.split(':')
if (timeParts.length === 0) {
return false
}
const hour = parseInt(timeParts[0])
return !isNaN(hour) && hour >= 12
// timeSlot为1的是下午时段
return slot.originalData.timeSlot === 1 || slot.originalData.timeSlot === '1'
} catch (err) {
console.warn('处理下午时段时出错:', err, slot)
return false
@@ -459,13 +500,15 @@ export default {
})
this.groupedTimeSlots = {
morning: morningSlots,
morning: morningSlots, // 实际上是中午时段
afternoon: afternoonSlots
}
// 保持原有的timeSlots格式兼容现有代码
this.timeSlots = [...morningSlots, ...afternoonSlots]
} catch (error) {
console.error('分组时段时出错:', error)
// 发生错误时设置为空数组
@@ -479,6 +522,39 @@ export default {
this.loadTimeSlots(this.selectedDate)
},
// 加载咨询主题选项
async loadConsultThemeOptions() {
try {
const response = await getConsultThemeOptions()
console.log('🔍 咨询主题字典数据:', response)
if (response.code === 200 && response.data && Array.isArray(response.data)) {
this.themeOptions = response.data // 直接使用所有字典数据
this.themeColumns = [this.themeOptions]
console.log('🔍 处理后的咨询主题选项:', this.themeOptions)
} else {
console.warn('咨询主题字典数据格式异常:', response)
this.themeOptions = []
this.themeColumns = [[]]
}
} catch (error) {
console.error('加载咨询主题选项失败:', error)
this.themeOptions = []
this.themeColumns = [[]]
// 不显示错误提示,避免影响用户体验
}
},
// 确认选择咨询主题
onThemeConfirm(value) {
console.log('🔍 选择的咨询主题:', value)
if (value && value.value && value.value.length > 0) {
this.selectedTheme = value.value[0]
console.log('🔍 设置选中的咨询主题:', this.selectedTheme)
}
this.showThemePicker = false
},
// 处理时间段点击
handleTimeSlotClick(slot) {
console.log('🔍 点击时间段:', slot)
@@ -500,6 +576,7 @@ export default {
}
this.selectedBookingSlot = slot
this.selectedTheme = null // 重置咨询主题选择
this.showBookingModal = true
console.log('🔍 设置弹窗显示状态:', this.showBookingModal)
console.log('🔍 选中的预约时段数据:', this.selectedBookingSlot)
@@ -509,6 +586,15 @@ export default {
async confirmBooking() {
if (this.bookingLoading) return
// 验证咨询主题
if (!this.selectedTheme || !this.selectedTheme.dictValue) {
uni.showToast({
title: '请选择咨询主题',
icon: 'none'
})
return
}
// 验证咨询原因
if (!this.consultationReason || this.consultationReason.trim() === '') {
uni.showToast({
@@ -567,12 +653,13 @@ export default {
// 构造预约数据
const appointmentData = {
studentId: studentId,
scheduleId: this.selectedBookingSlot.originalData.parentScheduleId,
scheduleId: this.selectedBookingSlot.originalData.scheduleId,
counselorId: this.selectedBookingSlot.originalData.counselorId,
appointmentDate: this.selectedDate,
appointmentTime: this.selectedBookingSlot.originalData.timeSlot,
appointmentDetailTime: this.selectedBookingSlot.timeRange,
reason: this.consultationReason.trim() // 使用用户输入的咨询原因
reason: this.consultationReason.trim(), // 使用用户输入的咨询原因
consultTheme: this.selectedTheme.dictValue // 咨询主题字典值
}
// 调用真实API提交预约
@@ -587,6 +674,7 @@ export default {
this.showBookingModal = false
this.selectedBookingSlot = null
this.consultationReason = '' // 重置咨询原因
this.selectedTheme = null // 重置咨询主题
// 刷新时间段数据
this.refreshTimeSlots()
@@ -625,6 +713,7 @@ export default {
this.showBookingModal = false
this.selectedBookingSlot = null
this.consultationReason = '' // 重置咨询原因
this.selectedTheme = null // 重置咨询主题
},
@@ -981,11 +1070,36 @@ export default {
}
}
.theme-select-container {
margin-bottom: 24rpx;
.theme-select-input {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16rpx 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
background-color: #fafafa;
min-height: 60rpx;
.theme-select-text {
font-size: 28rpx;
color: #333;
flex: 1;
&.placeholder {
color: #999;
}
}
}
}
.reason-input-container {
margin-bottom: 24rpx;
padding: 0 4rpx;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
border-radius: 8rpx;
background-color: #fafafa;
}