心理咨询预约移动端

This commit is contained in:
李旭明 committed 2025-08-29 23:02:52 +08:00
1 parent f256e580dc
commit 69b73a9373
7 files changed
+2752 -3

No files matched your search

@@ -431,7 +431,23 @@ public class XljkConsultationAppointmentServiceImpl implements IXljkConsultation
slot.put("availableCount", totalDetailSlots - bookedCount); // 可用数量
slot.put("isExpired", isExpired); // 是否已过期
// TODO: 添加咨询师姓名等信息
// 查询咨询师姓名
if (schedule.getCounselorId() != null) {
try {
String sql = "SELECT nick_name FROM sys_user WHERE user_id = ?";
List<Map<String, Object>> result = jdbcTemplate.queryForList(sql, schedule.getCounselorId());
if (!result.isEmpty()) {
slot.put("counselorName", result.get(0).get("nick_name"));
} else {
slot.put("counselorName", null);
}
} catch (Exception e) {
log.warn("查询咨询师姓名失败, counselorId: {}, error: {}", schedule.getCounselorId(), e.getMessage());
slot.put("counselorName", null);
}
} else {
slot.put("counselorName", null);
}
availableSlots.add(slot);
}
+236
View File
@@ -0,0 +1,236 @@
import { request } from '@/common/request';
/**
* 心理健康咨询预约 API
* 移动端API接口 - 基于web端功能移植
*/
// ===== 预约时段管理 API =====
/**
* 获取可预约时段列表(用于日历视图)
* @param {string} startDate 开始日期 YYYY-MM-DD
* @param {string} endDate 结束日期 YYYY-MM-DD
*/
export const getAvailableTimeSlots = (startDate, endDate) => {
return request({
url: '/xljk/consultation/appointment/available-slots',
method: 'GET',
params: { startDate, endDate }
});
};
/**
* 获取可用的详细时段列表(30分钟一个时段)
* @param {string} date 日期 YYYY-MM-DD
* @param {string} timeSlot 时段 如 "09:00-12:00"
*/
export const getAvailableDetailSlots = (date, timeSlot) => {
return request({
url: '/xljk/consultation/appointment/available-detail-slots',
method: 'GET',
params: { date, timeSlot }
});
};
// ===== 预约记录管理 API =====
/**
* 学生提交预约
* @param {object} data 预约数据
*/
export const submitAppointment = (data) => {
return request({
url: '/xljk/consultation/appointment/submit',
method: 'POST',
data
});
};
/**
* 获取我的预约记录(学生用)
* @param {string|number} studentId 学生ID
* @param {boolean} forceRefresh 是否强制刷新
*/
export const getMyAppointments = (studentId, forceRefresh = false) => {
let url = `/xljk/consultation/appointment/my-appointments/${studentId}`;
// 强制添加时间戳参数,确保绕过浏览器缓存
url += `?_t=${new Date().getTime()}`;
// 如果需要额外强制刷新
if (forceRefresh) {
url += `&force=true`;
}
return request({
url,
method: 'GET',
header: {
// 禁止所有缓存
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
});
};
/**
* 查询预约记录详细信息
* @param {string|number} id 预约记录ID
*/
export const getAppointmentDetail = (id) => {
return request({
url: `/xljk/consultation/appointment/${id}`,
method: 'GET'
});
};
/**
* 取消预约(更新状态为已取消,不删除记录)
* @param {string|number} id 预约记录ID
*/
export const cancelAppointment = (id) => {
return request({
url: `/xljk/consultation/appointment`,
method: 'PUT',
data: {
id: id,
status: 2 // 已取消状态
}
});
};
/**
* 更新预约信息(通用方法)
* @param {object} data 更新数据,必须包含id字段
*/
export const updateAppointment = (data) => {
return request({
url: `/xljk/consultation/appointment`,
method: 'PUT',
data
});
};
// ===== 选项数据 API =====
/**
* 获取咨询师选项
*/
export const getCounselorOptions = () => {
return request({
url: '/xljk/consultation/schedule/options/counselors',
method: 'GET'
});
};
/**
* 获取学生信息(通过学号)
* @param {string} studentNo 学号
*/
export const getStudentByNo = (studentNo) => {
return request({
url: `/xljk/consultation/student/${studentNo}`,
method: 'GET'
});
};
/**
* 获取院系选项
*/
export const getCollegeOptions = () => {
return request({
url: '/xljk/consultation/appointment/options/colleges',
method: 'GET'
});
};
/**
* 获取专业选项
* @param {string} collegeName 院系名称(可选)
*/
export const getMajorOptions = (collegeName) => {
return request({
url: '/xljk/consultation/appointment/options/majors',
method: 'GET',
params: { collegeName }
});
};
/**
* 获取班级选项
* @param {string} collegeName 院系名称(可选)
* @param {string} majorName 专业名称(可选)
*/
export const getClassOptions = (collegeName, majorName) => {
return request({
url: '/xljk/consultation/appointment/options/classes',
method: 'GET',
params: { collegeName, majorName }
});
};
// ===== 移动端专用辅助方法 =====
/**
* 格式化日期为 YYYY-MM-DD
* @param {Date} date 日期对象
*/
export const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
/**
* 获取指定日期的周日期范围
* @param {Date} date 指定日期
* @returns {object} { startDate, endDate, weekDates }
*/
export const getWeekDateRange = (date = new Date()) => {
const currentDate = new Date(date);
const currentDay = currentDate.getDay(); // 0=Sunday, 1=Monday, ...
// 计算本周一的日期
const monday = new Date(currentDate);
monday.setDate(currentDate.getDate() - currentDay + (currentDay === 0 ? -6 : 1));
// 生成一周的日期数组
const weekDates = [];
for (let i = 0; i < 7; i++) {
const weekDate = new Date(monday);
weekDate.setDate(monday.getDate() + i);
weekDates.push({
date: formatDate(weekDate),
dayName: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'][i],
dayNumber: weekDate.getDate(),
isToday: formatDate(weekDate) === formatDate(new Date())
});
}
return {
startDate: formatDate(monday),
endDate: formatDate(new Date(monday.getTime() + 6 * 24 * 60 * 60 * 1000)),
weekDates
};
};
/**
* 检查预约限制
* @param {string|number} studentId 学生ID
*/
export const checkAppointmentLimitation = async (studentId) => {
try {
// 这里可能需要导入noshow相关的API,暂时使用简单的实现
return request({
url: `/xljk/consultation/appointment/check-limitation/${studentId}`,
method: 'GET'
});
} catch (error) {
console.error('检查预约限制失败', error);
// 检查失败不阻止预约
return { code: 200, data: { limited: false } };
}
};
+25 -1
View File
@@ -302,7 +302,31 @@
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
}
},
{
"path": "pages/xljk/reservation/index",
"style": {
"navigationBarTitleText": "心理咨询预约",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xljk/my-appointments/index",
"style": {
"navigationBarTitleText": "我的预约",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/xljk/reservation/appointment-form",
"style": {
"navigationBarTitleText": "预约咨询",
"navigationBarBackgroundColor": "#0051d9",
"navigationBarTextStyle": "white"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
+23 -1
View File
@@ -262,6 +262,20 @@
/>
</template>
</ServiceItem>
<ServiceItem
v-if="hasPermi(['student:counsel:reservation'])"
title="心理咨询预约"
@click="gotoPsychologicalReservation"
>
<template #icon>
<image
src="/static/00113.png"
mode="aspectFit"
style="width: 24px; height: 24px"
/>
</template>
</ServiceItem>
</GridLayout>
</Card>
@@ -710,7 +724,8 @@ export default {
this.hasPermi(['student:dormhygiene:listapp']) ||
this.hasPermi(['student:workstudy:list']) ||
this.hasPermi(['student:violation:listapp']) ||
this.hasPermi(['student:assessment:applyapp'])
this.hasPermi(['student:assessment:applyapp']) ||
this.hasPermi(['student:counsel:reservation'])
);
},
@@ -966,6 +981,13 @@ export default {
});
},
// 跳转到心理咨询预约页面
gotoPsychologicalReservation() {
uni.navigateTo({
url: '/pages/xljk/reservation/index',
});
},
// 跳转到修改密码页面
gotoResetPassword() {
uni.navigateTo({
@@ -0,0 +1,737 @@
<template>
<view class="page-container">
<!-- 导航栏 -->
<u-navbar
title="我的预约"
:border="false"
bg-color="#ffffff"
title-color="#333333"
@leftClick="goBack"
>
<template #left>
<u-icon name="arrow-left" size="22" color="#333333"></u-icon>
</template>
</u-navbar>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced>
<!-- 筛选器 -->
<view class="filter-section">
<view class="filter-tabs">
<view
v-for="(tab, index) in filterTabs"
:key="index"
class="filter-tab"
:class="{ active: activeFilterIndex === index }"
@click="switchFilter(index)"
>
<text class="tab-text">{{ tab.label }}</text>
<!-- 只有待咨询标签显示圆形标记 -->
<view v-if="tab.value === '0' && tab.count > 0" class="tab-badge-circle"></view>
</view>
</view>
</view>
<!-- 加载状态 -->
<view v-if="loading" class="loading-container">
<u-loading-icon mode="circle"></u-loading-icon>
<text class="loading-text">加载中...</text>
</view>
<!-- 预约列表 -->
<view v-else-if="appointmentList.length > 0" class="appointment-list">
<view
v-for="appointment in appointmentList"
:key="appointment.id"
class="appointment-card"
@click="viewAppointmentDetail(appointment)"
>
<!-- 预约状态 -->
<view class="appointment-header">
<view class="appointment-date">
<text class="date-text">{{ appointment.appointmentDate }}</text>
<text class="time-text">{{ appointment.appointmentTime }}</text>
</view>
<view class="appointment-status">
<u-tag
:text="getStatusText(appointment.status)"
:type="getStatusType(appointment.status)"
size="mini"
></u-tag>
</view>
</view>
<!-- 咨询师信息 -->
<view class="counselor-section">
<view class="counselor-avatar">
<u-avatar
:src="appointment.counselorAvatar"
size="40"
:text="appointment.counselorName ? appointment.counselorName.charAt(0) : '咨'"
></u-avatar>
</view>
<view class="counselor-info">
<text class="counselor-name">{{ appointment.counselorName || '未知咨询师' }}</text>
<text class="appointment-reason">{{ appointment.reason || '心理咨询' }}</text>
</view>
<!-- 取消按钮 -->
<view v-if="appointment.status === '0'" class="inline-action">
<button
class="cancel-button-inline"
@click.stop="handleCancelClick(appointment)"
:disabled="isCancelButtonDisabled(appointment.id)"
>
{{ getCancelButtonText(appointment.id) }}
</button>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view v-else class="empty-container">
<u-empty
mode="data"
:text="getEmptyText()"
textColor="#909399"
iconColor="#c8c9cc"
></u-empty>
<u-button
type="primary"
size="normal"
@click="goToReservation"
style="margin-top: 40rpx;"
>
立即预约
</u-button>
</view>
</scroll-view>
</view>
</template>
<script>
import { getMyAppointments, cancelAppointment } from '@/api/xljk'
export default {
name: 'MyAppointments',
data() {
return {
// 筛选相关
filterTabs: [
{ 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 }
],
activeFilterIndex: 0,
// 列表相关
appointmentList: [],
allAppointments: [], // 存储所有预约数据用于筛选
loading: false,
// 取消预约相关
selectedAppointment: null,
cancelLoading: false,
cancelingAppointmentId: null // 正在取消的预约ID
}
},
onLoad() {
// 初始化时重置取消相关状态
this.resetCancelState()
this.loadAppointments()
},
onShow() {
// 页面显示时刷新数据,因为可能从预约页面返回
// 同时重置取消相关状态,防止从其他页面返回时状态异常
this.resetCancelState()
this.loadAppointments()
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack()
},
// 加载预约数据
async loadAppointments() {
this.loading = true
try {
// 正确获取用户信息 - 使用专门的获取方法
const { userStorage } = await import('@/common/storage')
const userInfo = userStorage.getUserInfo()
console.log('🔍 [我的预约] 获取到的完整用户信息:', userInfo)
// 从正确的路径获取用户ID
let studentId = null
if (userInfo && userInfo.user) {
studentId = userInfo.user.userId
}
console.log('🔍 [我的预约] 解析到的学生ID:', studentId)
if (!studentId) {
uni.showToast({
title: '请先登录',
icon: 'error'
})
// 跳转到登录页面
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index'
})
}, 1500)
return
}
// 调用API获取我的预约数据
const response = await getMyAppointments(studentId)
console.log('🔍 [我的预约] API响应数据:', response)
if (response.code === 200 && response.data) {
console.log('🔍 [我的预约] 原始数据列表:', response.data)
// 打印每条数据的status字段类型和值
response.data.forEach((item, index) => {
console.log(`🔍 [我的预约] 数据${index}: ID=${item.appointmentId}, status=${item.status} (${typeof item.status})`)
})
// 转换API数据格式
const appointments = response.data.map(item => {
return {
id: item.id, // 修复:使用 id 而不是 appointmentId
appointmentDate: item.appointmentDate,
appointmentTime: item.appointmentDetailTime || item.appointmentTime,
counselorName: item.counselorName,
counselorAvatar: '',
reason: item.reason,
status: String(item.status), // 转换为字符串,便于筛选比较
originalData: item
}
})
this.allAppointments = appointments
console.log('🔍 [我的预约] 转换后的数据:', appointments)
this.updateFilterCounts()
this.filterAppointments()
} else {
this.allAppointments = []
this.appointmentList = []
this.updateFilterCounts()
}
} catch (error) {
console.error('加载预约数据失败:', error)
uni.showToast({
title: '加载失败,请检查网络连接',
icon: 'error'
})
this.allAppointments = []
this.appointmentList = []
this.updateFilterCounts()
} finally {
this.loading = false
}
},
// 更新筛选标签数量
updateFilterCounts() {
console.log('🔍 [我的预约] 更新筛选标签数量,总数据:', this.allAppointments.length)
// 统计各状态数量
const status0Count = this.allAppointments.filter(item => item.status === '0').length
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
console.log('🔍 [我的预约] 状态统计: 待咨询(0)=', status0Count, ', 已完成(1)=', status1Count, ', 已取消(2)=', status2Count, ', 未到访(3)=', status3Count)
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 // 未到访
},
// 切换筛选器
switchFilter(index) {
this.activeFilterIndex = index
this.filterAppointments()
},
// 筛选预约数据
filterAppointments() {
const currentFilter = this.filterTabs[this.activeFilterIndex]
console.log('🔍 [我的预约] 开始筛选,当前筛选条件:', currentFilter)
if (currentFilter.value === 'all') {
this.appointmentList = [...this.allAppointments]
} else {
this.appointmentList = this.allAppointments.filter(item => item.status === currentFilter.value)
}
console.log('🔍 [我的预约] 筛选结果数量:', this.appointmentList.length)
console.log('🔍 [我的预约] 筛选结果详情:', this.appointmentList)
},
// 获取状态文本
getStatusText(status) {
const statusMap = {
'0': '待咨询',
'1': '已完成',
'2': '已取消',
'3': '未到访',
// 兼容英文状态值
'pending': '待咨询',
'completed': '已完成',
'cancelled': '已取消',
'expired': '已过期'
}
return statusMap[status] || '未知状态'
},
// 获取状态类型(用于标签颜色)
getStatusType(status) {
const typeMap = {
'0': 'warning',
'1': 'success',
'2': 'primary', // 已取消状态使用蓝色
'3': 'info', // 未到访状态使用灰色
// 兼容英文状态值
'pending': 'warning',
'completed': 'success',
'cancelled': 'primary', // 已取消状态使用蓝色
'expired': 'info'
}
return typeMap[status] || 'info'
},
// 获取空状态文本
getEmptyText() {
const currentFilter = this.filterTabs[this.activeFilterIndex]
const textMap = {
'all': '暂无预约记录',
'0': '暂无待咨询预约',
'1': '暂无已完成预约',
'2': '暂无已取消预约',
'3': '暂无未到访预约'
}
return textMap[currentFilter.value] || '暂无数据'
},
// 查看预约详情
viewAppointmentDetail(appointment) {
// 这里可以跳转到预约详情页面,或者显示详情弹窗
console.log('查看预约详情:', appointment)
// uni.navigateTo({
// url: `/pages/xljk/appointment-detail/index?id=${appointment.id}`
// })
},
// 处理取消按钮点击
handleCancelClick(appointment) {
console.log('🔄 [取消预约] 按钮点击处理:', {
appointmentId: appointment.id,
appointmentStatus: appointment.status,
当前状态: {
cancelLoading: this.cancelLoading,
cancelingAppointmentId: this.cancelingAppointmentId,
isDisabled: this.isCancelButtonDisabled(appointment.id)
}
})
// 调用取消预约方法
this.cancelAppointment(appointment)
},
// 取消预约
cancelAppointment(appointment) {
console.log('点击取消预约按钮,预约信息:', appointment)
if (appointment.status !== '0') {
uni.showToast({
title: '只能取消待咨询的预约',
icon: 'none'
})
return
}
// 检查是否已经在取消中
if (this.cancelingAppointmentId === appointment.id) {
return
}
console.log('使用原生弹窗显示取消确认')
this.selectedAppointment = appointment
// 使用原生确认弹窗
uni.showModal({
title: '确认取消',
content: '确定要取消这次预约吗?取消后将无法恢复。',
confirmText: '确认取消',
cancelText: '再想想',
confirmColor: '#ff4757',
success: (res) => {
if (res.confirm) {
console.log('用户确认取消')
this.confirmCancel()
} else if (res.cancel) {
console.log('用户取消了取消操作')
this.closeCancelModal()
}
}
})
},
// 确认取消预约
async confirmCancel() {
if (!this.selectedAppointment || this.cancelLoading) return
const appointmentId = this.selectedAppointment.id
this.cancelLoading = true
this.cancelingAppointmentId = appointmentId // 标记正在取消的预约
console.log('🔄 [取消预约] 开始处理:', {
appointmentId,
当前状态: {
cancelLoading: this.cancelLoading,
cancelingAppointmentId: this.cancelingAppointmentId
}
})
// 强制超时重置机制,防止状态一直不重置
const forceResetTimeout = setTimeout(() => {
console.log('⚠️ [取消预约] 强制超时重置状态')
this.cancelLoading = false
this.cancelingAppointmentId = null
uni.showToast({
title: '操作超时,已重置',
icon: 'none'
})
}, 45000) // 45秒强制重置
try {
if (!appointmentId) {
throw new Error('预约ID为空')
}
console.log('📡 [取消预约] 发送API请求...')
// 添加超时控制,手机端网络可能较慢
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new Error('请求超时,请重试'))
}, 30000) // 30秒超时
})
const apiPromise = cancelAppointment(appointmentId)
const response = await Promise.race([apiPromise, timeoutPromise])
console.log('📡 [取消预约] API响应:', response)
if (response && response.code === 200) {
console.log('✅ [取消预约] 取消成功')
uni.showToast({
title: '取消成功',
icon: 'success'
})
// 立即更新本地数据,避免重新请求
const appointmentIndex = this.allAppointments.findIndex(item => item.id === appointmentId)
if (appointmentIndex !== -1) {
// 更新状态为已取消
this.allAppointments[appointmentIndex].status = '2'
console.log('📊 [取消预约] 本地数据已更新:', this.allAppointments[appointmentIndex])
// 立即刷新筛选和计数
this.updateFilterCounts()
this.filterAppointments()
}
this.closeCancelModal()
// 注释掉重新加载,使用本地更新提升用户体验
// this.loadAppointments()
} else {
const errorMsg = response?.msg || '取消失败'
console.log('❌ [取消预约] 业务错误:', errorMsg)
uni.showToast({
title: errorMsg,
icon: 'error'
})
}
} catch (error) {
console.error('❌ [取消预约] 请求失败:', error)
let errorMessage = '取消失败,请检查网络连接'
if (error.message === '请求超时,请重试') {
errorMessage = '请求超时,请重试'
} else if (error.errMsg && error.errMsg.includes('timeout')) {
errorMessage = '网络超时,请检查网络连接'
} else if (error.errMsg && error.errMsg.includes('fail')) {
errorMessage = '网络连接失败,请检查网络'
}
uni.showToast({
title: errorMessage,
icon: 'error'
})
} finally {
console.log('🔄 [取消预约] 清理状态')
clearTimeout(forceResetTimeout) // 清除强制重置定时器
this.cancelLoading = false
this.cancelingAppointmentId = null // 重置正在取消的预约ID
}
},
// 重置取消预约状态
closeCancelModal() {
console.log('🔄 [取消预约] 重置所有状态:', {
之前状态: {
selectedAppointment: this.selectedAppointment?.id,
cancelLoading: this.cancelLoading,
cancelingAppointmentId: this.cancelingAppointmentId
}
})
this.selectedAppointment = null
this.cancelLoading = false // 重置加载状态
this.cancelingAppointmentId = null // 重置正在取消的预约ID
console.log('🔄 [取消预约] 状态重置完成:', {
当前状态: {
selectedAppointment: this.selectedAppointment,
cancelLoading: this.cancelLoading,
cancelingAppointmentId: this.cancelingAppointmentId
}
})
},
// 跳转到预约页面
goToReservation() {
uni.navigateBack()
},
// 检查特定预约是否正在取消
isAppointmentCanceling(appointmentId) {
return this.cancelingAppointmentId === appointmentId
},
// 获取取消按钮文本
getCancelButtonText(appointmentId) {
return this.cancelingAppointmentId === appointmentId ? '取消中...' : '取消预约'
},
// 检查取消按钮是否禁用
isCancelButtonDisabled(appointmentId) {
return this.cancelingAppointmentId === appointmentId
},
// 重置取消相关状态
resetCancelState() {
console.log('🔄 [取消预约] 重置取消相关状态')
this.selectedAppointment = null
this.cancelLoading = false
this.cancelingAppointmentId = null
}
}
}
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.main-content {
height: calc(100vh - 88rpx); // 减去导航栏高度
padding-top: 20rpx;
}
.filter-section {
background: #ffffff;
margin-bottom: 20rpx;
.filter-tabs {
display: flex;
padding: 20rpx;
.filter-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 20rpx 10rpx;
border-radius: 12rpx;
transition: all 0.3s ease;
&.active {
background: rgba(24, 144, 255, 0.1);
.tab-text {
color: #1890ff;
font-weight: 600;
}
}
.tab-text {
font-size: 28rpx;
color: #666;
margin-right: 8rpx;
}
.tab-badge-circle {
width: 16rpx;
height: 16rpx;
background: #ff4d4f;
border-radius: 50%;
margin-left: 8rpx;
}
}
}
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 20rpx;
.loading-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
.appointment-list {
padding: 0 20rpx;
.appointment-card {
background: #ffffff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
&:last-child {
margin-bottom: 0;
}
&:active {
transform: scale(0.98);
background: rgba(24, 144, 255, 0.02);
}
.appointment-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 24rpx;
.appointment-date {
.date-text {
font-size: 32rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.time-text {
font-size: 26rpx;
color: #666;
display: block;
}
}
}
.counselor-section {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.counselor-avatar {
margin-right: 20rpx;
}
.counselor-info {
flex: 1;
.counselor-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.appointment-reason {
font-size: 26rpx;
color: #666;
display: block;
}
}
.inline-action {
.cancel-button-inline {
background-color: #ff4757;
color: white;
border: none;
border-radius: 12rpx;
padding: 12rpx 20rpx;
font-size: 24rpx;
font-weight: 500;
cursor: pointer;
outline: none;
min-width: 120rpx;
&:disabled {
background-color: #cccccc;
color: #999999;
cursor: not-allowed;
}
&:active:not(:disabled) {
background-color: #ff3742;
transform: scale(0.98);
}
}
}
}
}
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 20rpx;
}
.cancel-modal-content {
padding: 20rpx 0;
.cancel-text {
font-size: 28rpx;
color: #666;
line-height: 1.5;
text-align: center;
}
}
</style>
@@ -0,0 +1,708 @@
<template>
<view class="page-container">
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-left" @click="goBack">
<u-icon name="arrow-left" size="20" color="#ffffff"></u-icon>
</view>
<text class="navbar-title">预约咨询</text>
<view class="navbar-right"></view>
</view>
<!-- 主要内容区域 -->
<scroll-view class="main-content" scroll-y enhanced :show-scrollbar="false">
<!-- 预约信息确认 -->
<view class="confirm-section">
<view class="section-title">
<u-icon name="calendar" size="16" color="#3c9cff"></u-icon>
<text class="title-text">预约信息确认</text>
</view>
<view class="confirm-card">
<view class="confirm-item">
<text class="confirm-label">预约日期:</text>
<text class="confirm-value">{{ formattedDate }}</text>
</view>
<view class="confirm-item">
<text class="confirm-label">预约时段:</text>
<text class="confirm-value">{{ appointmentData.timeRange }}</text>
</view>
<view class="confirm-item">
<text class="confirm-label">咨询师:</text>
<text class="confirm-value">{{ appointmentData.counselorName }}</text>
</view>
</view>
</view>
<!-- 个人信息 -->
<view class="form-section">
<view class="section-title">
<u-icon name="account" size="16" color="#3c9cff"></u-icon>
<text class="title-text">个人信息</text>
</view>
<view class="form-card">
<view class="form-item">
<text class="form-label">姓名:</text>
<text class="form-value">{{ userInfo.nickName || userInfo.userName || '未设置' }}</text>
</view>
<view class="form-item">
<text class="form-label">学号:</text>
<text class="form-value">{{ userInfo.userName || '未设置' }}</text>
</view>
<!-- 院系、专业、班级信息 -->
<view class="form-item">
<text class="form-label">院系:</text>
<u-picker
:columns="[collegeOptions]"
keyName="label"
v-model="collegeIndex"
:show="showCollegePicker"
@confirm="onCollegeConfirm"
@close="showCollegePicker = false"
></u-picker>
<view class="picker-input" @click="showCollegePicker = true">
<text class="picker-text" :class="{ placeholder: !form.collegeName }">
{{ form.collegeName || '请选择院系' }}
</text>
<u-icon name="arrow-right" size="14" color="#c8c9cc"></u-icon>
</view>
</view>
<view class="form-item">
<text class="form-label">专业:</text>
<u-picker
:columns="[majorOptions]"
keyName="label"
v-model="majorIndex"
:show="showMajorPicker"
@confirm="onMajorConfirm"
@close="showMajorPicker = false"
></u-picker>
<view class="picker-input" @click="handleMajorPickerClick">
<text class="picker-text" :class="{ placeholder: !form.majorName }">
{{ form.majorName || '请选择专业' }}
</text>
<u-icon name="arrow-right" size="14" color="#c8c9cc"></u-icon>
</view>
</view>
<view class="form-item">
<text class="form-label">班级:</text>
<u-picker
:columns="[classOptions]"
keyName="label"
v-model="classIndex"
:show="showClassPicker"
@confirm="onClassConfirm"
@close="showClassPicker = false"
></u-picker>
<view class="picker-input" @click="handleClassPickerClick">
<text class="picker-text" :class="{ placeholder: !form.className }">
{{ form.className || '请选择班级' }}
</text>
<u-icon name="arrow-right" size="14" color="#c8c9cc"></u-icon>
</view>
</view>
</view>
</view>
<!-- 咨询原因 -->
<view class="form-section">
<view class="section-title">
<u-icon name="chat" size="16" color="#3c9cff"></u-icon>
<text class="title-text">咨询原因</text>
<text class="required-mark">*</text>
</view>
<view class="form-card">
<u-input
v-model="form.reason"
type="textarea"
placeholder="请详细描述您希望咨询的问题或困扰,以便咨询师更好地了解您的需求"
:maxlength="500"
:show-word-limit="true"
:auto-height="true"
:custom-style="{
minHeight: '120px',
backgroundColor: 'transparent',
border: 'none'
}"
></u-input>
</view>
</view>
<!-- 温馨提示 -->
<view class="tips-section">
<view class="section-title">
<u-icon name="info-circle" size="16" color="#ff9900"></u-icon>
<text class="title-text">温馨提示</text>
</view>
<view class="tips-card">
<view class="tip-item">
<text class="tip-number">1.</text>
<text class="tip-text">请提前5-10分钟到达咨询室,迟到可能影响咨询效果</text>
</view>
<view class="tip-item">
<text class="tip-number">2.</text>
<text class="tip-text">咨询内容严格保密,请放心倾述您的困扰</text>
</view>
<view class="tip-item">
<text class="tip-number">3.</text>
<text class="tip-text">如需取消预约,请提前24小时操作</text>
</view>
<view class="tip-item">
<text class="tip-number">4.</text>
<text class="tip-text">连续3次未到访将被限制预约功能</text>
</view>
</view>
</view>
<!-- 底部间距 -->
<view class="bottom-spacer"></view>
</scroll-view>
<!-- 底部提交按钮 -->
<view class="submit-container">
<u-button
text="提交预约"
type="primary"
:loading="submitting"
:disabled="!canSubmit"
@click="submitAppointment"
:custom-style="{
width: '100%',
height: '50px',
fontSize: '18px',
fontWeight: 'bold'
}"
></u-button>
</view>
</view>
</template>
<script>
import {
submitAppointment,
getCollegeOptions,
getMajorOptions,
getClassOptions,
checkAppointmentLimitation
} from '@/api/xljk';
import { userStorage } from '@/common/storage';
export default {
name: 'AppointmentForm',
data() {
return {
// 用户信息
userInfo: {},
// 预约数据
appointmentData: {
date: '',
scheduleId: '',
counselorId: '',
timeRange: '',
counselorName: ''
},
// 表单数据
form: {
collegeName: '',
majorName: '',
className: '',
reason: ''
},
// 选项数据
collegeOptions: [],
majorOptions: [],
classOptions: [],
// 选择器状态
showCollegePicker: false,
showMajorPicker: false,
showClassPicker: false,
// 选择器索引
collegeIndex: [0],
majorIndex: [0],
classIndex: [0],
// 提交状态
submitting: false
};
},
computed: {
formattedDate() {
if (!this.appointmentData.date) return '';
const date = new Date(this.appointmentData.date);
const month = date.getMonth() + 1;
const day = date.getDate();
const weekDay = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()];
return `${month}月${day}日 ${weekDay}`;
},
canSubmit() {
return this.form.reason.trim().length > 0 &&
this.form.collegeName &&
this.form.majorName &&
this.form.className;
}
},
onLoad(options) {
console.log('预约表单页面加载,参数:', options);
this.checkLoginStatus();
this.parseParams(options);
this.loadOptions();
},
methods: {
// 检查登录状态
checkLoginStatus() {
if (!userStorage.isLoggedIn()) {
uni.reLaunch({
url: '/pages/login/index',
});
return false;
}
const userStorage_data = userStorage.getUserInfo();
if (userStorage_data && userStorage_data.user) {
this.userInfo = userStorage_data.user;
}
console.log('用户信息:', this.userInfo);
return true;
},
// 解析页面参数
parseParams(options) {
this.appointmentData = {
date: options.date || '',
scheduleId: options.scheduleId || '',
counselorId: options.counselorId || '',
timeRange: options.timeRange || '',
counselorName: decodeURIComponent(options.counselorName || '')
};
console.log('预约数据:', this.appointmentData);
},
// 加载选项数据
async loadOptions() {
try {
// 加载院系选项
const collegeRes = await getCollegeOptions();
if (collegeRes.code === 200 && collegeRes.data) {
this.collegeOptions = collegeRes.data.map(item => ({
label: item.label || item.name,
value: item.value || item.name
}));
}
// 加载所有专业选项
const majorRes = await getMajorOptions();
if (majorRes.code === 200 && majorRes.data) {
this.majorOptions = majorRes.data.map(item => ({
label: item.label || item.name,
value: item.value || item.name
}));
}
// 加载所有班级选项
const classRes = await getClassOptions();
if (classRes.code === 200 && classRes.data) {
this.classOptions = classRes.data.map(item => ({
label: item.label || item.name,
value: item.value || item.name
}));
}
console.log('选项数据加载完成:', {
colleges: this.collegeOptions.length,
majors: this.majorOptions.length,
classes: this.classOptions.length
});
} catch (error) {
console.error('加载选项数据失败:', error);
}
},
// 院系选择确认
async onCollegeConfirm(e) {
console.log('院系选择:', e);
const index = e.value[0];
const selectedCollege = this.collegeOptions[index];
if (selectedCollege) {
this.form.collegeName = selectedCollege.value;
this.collegeIndex = [index];
// 清空专业和班级
this.form.majorName = '';
this.form.className = '';
this.majorIndex = [0];
this.classIndex = [0];
// 重新加载专业选项
try {
const majorRes = await getMajorOptions(selectedCollege.value);
if (majorRes.code === 200 && majorRes.data) {
this.majorOptions = majorRes.data.map(item => ({
label: item.label || item.name,
value: item.value || item.name
}));
}
} catch (error) {
console.error('加载专业选项失败:', error);
}
}
this.showCollegePicker = false;
},
// 专业选择确认
async onMajorConfirm(e) {
console.log('专业选择:', e);
const index = e.value[0];
const selectedMajor = this.majorOptions[index];
if (selectedMajor) {
this.form.majorName = selectedMajor.value;
this.majorIndex = [index];
// 清空班级
this.form.className = '';
this.classIndex = [0];
// 重新加载班级选项
try {
const classRes = await getClassOptions(this.form.collegeName, selectedMajor.value);
if (classRes.code === 200 && classRes.data) {
this.classOptions = classRes.data.map(item => ({
label: item.label || item.name,
value: item.value || item.name
}));
}
} catch (error) {
console.error('加载班级选项失败:', error);
}
}
this.showMajorPicker = false;
},
// 班级选择确认
onClassConfirm(e) {
console.log('班级选择:', e);
const index = e.value[0];
const selectedClass = this.classOptions[index];
if (selectedClass) {
this.form.className = selectedClass.value;
this.classIndex = [index];
}
this.showClassPicker = false;
},
// 处理专业选择器点击
handleMajorPickerClick() {
if (!this.form.collegeName) {
uni.showToast({
title: '请先选择院系',
icon: 'none'
});
return;
}
this.showMajorPicker = true;
},
// 处理班级选择器点击
handleClassPickerClick() {
if (!this.form.collegeName) {
uni.showToast({
title: '请先选择院系',
icon: 'none'
});
return;
}
if (!this.form.majorName) {
uni.showToast({
title: '请先选择专业',
icon: 'none'
});
return;
}
this.showClassPicker = true;
},
// 提交预约
async submitAppointment() {
if (!this.canSubmit) {
uni.showToast({
title: '请完善预约信息',
icon: 'none'
});
return;
}
this.submitting = true;
try {
// 检查预约限制
if (this.userInfo.userId) {
const limitationCheck = await checkAppointmentLimitation(this.userInfo.userId);
if (limitationCheck.code === 200 && limitationCheck.data && limitationCheck.data.limited) {
uni.showToast({
title: `您暂时无法预约,限制将于 ${limitationCheck.data.expireDateStr} 解除`,
icon: 'none',
duration: 3000
});
return;
}
}
// 构建提交数据
const submitData = {
studentId: this.userInfo.userId,
studentName: this.userInfo.nickName || this.userInfo.userName,
studentNo: this.userInfo.userName,
collegeName: this.form.collegeName,
majorName: this.form.majorName,
className: this.form.className,
scheduleId: this.appointmentData.scheduleId,
counselorId: this.appointmentData.counselorId,
counselorName: this.appointmentData.counselorName,
appointmentDate: this.appointmentData.date,
appointmentTime: this.appointmentData.timeRange,
appointmentDetailTime: this.appointmentData.timeRange,
reason: this.form.reason.trim()
};
console.log('提交预约数据:', submitData);
const response = await submitAppointment(submitData);
if (response.code === 200) {
uni.showToast({
title: '预约成功',
icon: 'success'
});
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
uni.navigateBack({
delta: 2 // 返回到主页面
});
}, 1500);
} else {
uni.showToast({
title: response.msg || '预约失败',
icon: 'none',
duration: 3000
});
}
} catch (error) {
console.error('提交预约失败:', error);
uni.showToast({
title: '网络错误,请重试',
icon: 'none'
});
} finally {
this.submitting = false;
}
},
// 返回上一页
goBack() {
uni.navigateBack();
}
}
};
</script>
<style lang="scss" scoped>
.page-container {
min-height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
}
.custom-navbar {
background: #1890ff;
padding: calc(var(--status-bar-height) + 12px) 16px 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.navbar-left, .navbar-right {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.navbar-title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
}
.main-content {
flex: 1;
padding-bottom: 80px;
}
/* 通用样式 */
.confirm-section, .form-section, .tips-section {
margin: 16px;
}
.section-title {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.title-text {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-left: 8px;
}
.required-mark {
color: #f56c6c;
margin-left: 4px;
}
.confirm-card, .form-card, .tips-card {
background-color: #ffffff;
border-radius: 16px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 确认信息 */
.confirm-item {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.confirm-item:last-child {
margin-bottom: 0;
}
.confirm-label {
font-size: 14px;
color: #606266;
margin-right: 8px;
min-width: 80px;
}
.confirm-value {
font-size: 14px;
color: #303133;
font-weight: 500;
}
/* 表单样式 */
.form-item {
display: flex;
align-items: center;
margin-bottom: 16px;
min-height: 40px;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 14px;
color: #606266;
margin-right: 12px;
min-width: 60px;
flex-shrink: 0;
}
.form-value {
font-size: 14px;
color: #303133;
flex: 1;
}
.picker-input {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
padding: 8px 12px;
background-color: #f8f9fa;
border-radius: 12px;
min-height: 24px;
border: 1px solid #eee;
}
.picker-text {
font-size: 14px;
color: #303133;
flex: 1;
}
.picker-text.placeholder {
color: #c8c9cc;
}
/* 温馨提示 */
.tip-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
}
.tip-item:last-child {
margin-bottom: 0;
}
.tip-number {
font-size: 14px;
color: #ff9900;
margin-right: 8px;
font-weight: 500;
flex-shrink: 0;
}
.tip-text {
font-size: 14px;
color: #606266;
line-height: 1.5;
}
/* 底部提交按钮 */
.submit-container {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #ffffff;
padding: 16px;
border-top: 1px solid #eee;
padding-bottom: calc(16px + env(safe-area-inset-bottom));
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
}
.bottom-spacer {
height: 20px;
}
</style>
File diff suppressed because it is too large. Load diff