From 0b9b0eed8326e83488f01fd3a15f06440d778b81 Mon Sep 17 00:00:00 2001 From: taocong Date: Wed, 13 May 2026 16:39:08 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=AF=BE=E7=A8=8B=E5=92=8C?= =?UTF-8?q?=E6=8E=92=E8=AF=BE=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/course/index.css | 337 ++++++++++- src/pages/course/index.tsx | 261 ++++++++- src/pages/record/index.tsx | 15 +- src/pages/schedule/index.css | 584 ++++++++++++++++++- src/pages/schedule/index.tsx | 703 ++++++++++++++++++++++- src/pages/student/index.css | 1018 +++++++++++++++++++++++----------- src/pages/student/index.tsx | 861 ++++++++++++---------------- 7 files changed, 2898 insertions(+), 881 deletions(-) diff --git a/src/pages/course/index.css b/src/pages/course/index.css index d29b0a4..23d8343 100644 --- a/src/pages/course/index.css +++ b/src/pages/course/index.css @@ -1,32 +1,345 @@ .course-page { min-height: 100vh; - background-color: #f5f5f5; + background-color: #f0f4f8; + padding-bottom: 32px; } -.page-header { - padding: 32px 32px 16px; +/* 课程日历区域 */ +.calendar-section { background-color: #ffffff; + margin-bottom: 24px; + border-radius: 16px; + overflow: hidden; + margin: 16px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); } -.page-title { - font-size: 20px; +.calendar-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid #f0f0f0; +} + +.calendar-title-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; +} + +.calendar-title { + font-size: 17px; font-weight: 600; color: #1f2937; } -.page-content { - padding: 16px; +.calendar-toggle { + font-size: 14px; + color: #3B82F6; } -.coming-soon { +.calendar-body { + padding: 16px 16px 24px; +} + +/* 月份导航 */ +.month-nav { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; + padding: 0 8px; +} + +.nav-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + background-color: #f5f5f5; + border-radius: 8px; +} + +.month-label { + font-size: 16px; + font-weight: 600; + color: #1f2937; +} + +/* 星期标题 */ +.week-header { + display: flex; + flex-direction: row; + margin-bottom: 8px; +} + +.week-day { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 8px 0; +} + +.week-day-text { + font-size: 12px; + color: #9CA3AF; + font-weight: 500; +} + +/* 日期网格 */ +.date-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.date-cell { + width: 14.28%; + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 0; + cursor: pointer; +} + +.date-cell.other-month { + opacity: 0.3; +} + +.date-cell.is-today .date-text { + border: 2px solid #3B82F6; + border-radius: 50%; + color: #3B82F6; +} + +.date-cell.is-selected { + background-color: #3B82F6; + border-radius: 10px; + margin: 2px; +} + +.date-cell.is-selected .date-text { + color: #ffffff; +} + +.date-cell.is-selected .week-day-text { + color: #ffffff; +} + +.date-cell.is-selected .other-month { + opacity: 0.5; +} + +.date-text { + font-size: 15px; + color: #1f2937; + font-weight: 500; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +/* 课程圆点标记 */ +.course-dots { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + margin-top: 4px; + height: 12px; +} + +.course-dot { + width: 5px; + height: 5px; + border-radius: 50%; + background-color: #3B82F6; + margin: 0 1px; +} + +.course-dot-text { + font-size: 8px; + color: #3B82F6; + font-weight: 600; +} + +.date-cell.is-selected .course-dot { + background-color: #ffffff; +} + +.date-cell.is-selected .course-dot-text { + color: #ffffff; +} + +/* 课程列表区域 */ +.course-list-section { + margin: 0 16px; +} + +.list-header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.list-title { + font-size: 17px; + font-weight: 600; + color: #1f2937; +} + +.student-count { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + background-color: #EEF2FF; + padding: 6px 12px; + border-radius: 16px; +} + +.count-number { + font-size: 15px; + font-weight: 700; + color: #3B82F6; +} + +.count-label { + font-size: 13px; + color: #6366F1; +} + +/* 课程卡片 */ +.course-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.course-card { + background-color: #ffffff; + border-radius: 14px; + padding: 16px 18px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); +} + +.course-info { + display: flex; + flex-direction: column; + gap: 8px; +} + +.student-name { + font-size: 16px; + font-weight: 600; + color: #1f2937; +} + +.time-row { + display: flex; + flex-direction: row; + align-items: center; + gap: 6px; +} + +.time-text { + font-size: 14px; + color: #6B7280; +} + +.duration-tag { + background-color: #F3F4F6; + padding: 2px 8px; + border-radius: 10px; + margin-left: 8px; +} + +.duration-text { + font-size: 12px; + color: #6B7280; + font-weight: 500; +} + +.course-actions { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +.status-badge { + padding: 4px 10px; + border-radius: 12px; +} + +.status-completed { + background-color: #D1FAE5; +} + +.status-completed .status-text { + color: #059669; +} + +.status-pending { + background-color: #FEF3C7; +} + +.status-pending .status-text { + color: #D97706; +} + +.status-cancelled { + background-color: #F3F4F6; +} + +.status-cancelled .status-text { + color: #9CA3AF; +} + +.status-text { + font-size: 12px; + font-weight: 500; +} + +.recorded-badge { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; +} + +.recorded-text { + font-size: 12px; + color: #10B981; + font-weight: 500; +} + +/* 空状态 */ +.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 80px 32px; + padding: 48px 24px; + background-color: #ffffff; + border-radius: 14px; } -.coming-soon-text { - font-size: 16px; - color: #6b7280; +.empty-text { + font-size: 15px; + color: #9CA3AF; } diff --git a/src/pages/course/index.tsx b/src/pages/course/index.tsx index 2cf0361..bd0fd56 100644 --- a/src/pages/course/index.tsx +++ b/src/pages/course/index.tsx @@ -1,15 +1,266 @@ import { View, Text } from '@tarojs/components' +import { useState, useEffect } from 'react' +import { Calendar, ChevronLeft, ChevronRight, CircleCheck, Clock } from 'lucide-react-taro' +import Taro from '@tarojs/taro' import './index.css' +// 课程状态枚举 +type CourseStatus = 'completed' | 'pending' | 'cancelled' + +// 课程接口 +interface Course { + id: number + studentName: string + date: string + startTime: string + endTime: string + duration: number // 课时时长(小时) + status: CourseStatus + recorded: boolean // 是否已录入课时 +} + export default function CoursePage() { + const [calendarExpanded, setCalendarExpanded] = useState(true) + const [currentMonth, setCurrentMonth] = useState(new Date()) + const [selectedDate, setSelectedDate] = useState(new Date()) + const [courses, setCourses] = useState([]) + + // 格式化日期为 YYYY-MM-DD + const formatDate = (date: Date): string => { + 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}` + } + + // 计算时长 + const calculateDuration = (startTime: string, endTime: string): number => { + const start = new Date(`2000-01-01T${startTime}`) + const end = new Date(`2000-01-01T${endTime}`) + const diffHours = (end.getTime() - start.getTime()) / (1000 * 60 * 60) + return parseFloat(diffHours.toFixed(1)) + } + + // 加载课程数据 + useEffect(() => { + const savedSchedules = Taro.getStorageSync('schedules') + if (savedSchedules && Array.isArray(savedSchedules)) { + const courseList: Course[] = savedSchedules.map(s => ({ + id: s.id, + studentName: s.studentName, + date: s.date, + startTime: s.startTime, + endTime: s.endTime, + duration: calculateDuration(s.startTime, s.endTime), + status: 'pending' as CourseStatus, + recorded: false + })) + setCourses(courseList) + } + }, []) + + // 获取当月日历数据 + const getMonthData = () => { + const year = currentMonth.getFullYear() + const month = currentMonth.getMonth() + + // 当月第一天 + const firstDay = new Date(year, month, 1) + // 当月最后一天 + const lastDay = new Date(year, month + 1, 0) + // 补齐日历开头 + const startWeek = firstDay.getDay() || 7 + + const days: { date: Date; isCurrentMonth: boolean }[] = [] + + // 上月日期 + for (let i = startWeek - 1; i > 0; i--) { + const date = new Date(year, month, 1 - i) + days.push({ date, isCurrentMonth: false }) + } + + // 当月日期 + for (let i = 1; i <= lastDay.getDate(); i++) { + const date = new Date(year, month, i) + days.push({ date, isCurrentMonth: true }) + } + + // 下月日期,补满6行 + const remaining = 42 - days.length + for (let i = 1; i <= remaining; i++) { + const date = new Date(year, month + 1, i) + days.push({ date, isCurrentMonth: false }) + } + + return days + } + + // 判断日期是否有课程 + const hasCourse = (date: Date): number => { + const dateStr = formatDate(date) + return courses.filter(c => c.date === dateStr).length + } + + // 判断是否是今天 + const isToday = (date: Date): boolean => { + const today = new Date() + return formatDate(date) === formatDate(today) + } + + // 判断是否选中 + const isSelected = (date: Date): boolean => { + return formatDate(date) === formatDate(selectedDate) + } + + // 获取选中日期的课程 + const getSelectedDateCourses = (): Course[] => { + const dateStr = formatDate(selectedDate) + return courses.filter(c => c.date === dateStr) + } + + // 获取状态标签 + const getStatusLabel = (status: CourseStatus): { text: string; className: string } => { + switch (status) { + case 'completed': + return { text: '已上课', className: 'status-completed' } + case 'pending': + return { text: '未上课', className: 'status-pending' } + case 'cancelled': + return { text: '已取消', className: 'status-cancelled' } + } + } + + // 月份导航 + const prevMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)) + } + + const nextMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)) + } + + const monthData = getMonthData() + const selectedCourses = getSelectedDateCourses() + const weekDays = ['一', '二', '三', '四', '五', '六', '日'] + return ( - - 课程管理 + {/* 课程日历 */} + + setCalendarExpanded(!calendarExpanded)} + > + + + 课程日历 + + {calendarExpanded ? '收起' : '展开'} + + + {calendarExpanded && ( + + {/* 月份导航 */} + + + + + + {currentMonth.getFullYear()}年{currentMonth.getMonth() + 1}月 + + + + + + + {/* 星期标题 */} + + {weekDays.map((day, index) => ( + + {day} + + ))} + + + {/* 日期网格 */} + + {monthData.map((item, index) => { + const courseCount = hasCourse(item.date) + return ( + item.isCurrentMonth && setSelectedDate(item.date)} + > + {item.date.getDate()} + {item.isCurrentMonth && courseCount > 0 && ( + + {courseCount <= 3 ? ( + Array.from({ length: Math.min(courseCount, 3) }).map((_, i) => ( + + )) + ) : ( + ●●●+{courseCount - 3} + )} + + )} + + ) + })} + + + )} - - - 功能开发中... + + {/* 今日课程列表 */} + + + + {selectedDate.getMonth() + 1}月{selectedDate.getDate()}日 课程 + + + {selectedCourses.length} + 位学员 + + + + + {selectedCourses.length === 0 ? ( + + 当日暂无课程安排 + + ) : ( + selectedCourses.map(course => { + const statusInfo = getStatusLabel(course.status) + return ( + + + {course.studentName} + + + + {course.startTime} - {course.endTime} + + + {course.duration}h + + + + + + {statusInfo.text} + + {course.recorded && ( + + + 已录入 + + )} + + + ) + }) + )} diff --git a/src/pages/record/index.tsx b/src/pages/record/index.tsx index 13684a3..eda9adc 100644 --- a/src/pages/record/index.tsx +++ b/src/pages/record/index.tsx @@ -35,19 +35,14 @@ export default function RecordPage() { const [openid, setOpenid] = useState('') useEffect(() => { - // 获取登录状态 - const userInfo = Taro.getStorageSync('userInfo') - if (userInfo && userInfo.openid) { - setOpenid(userInfo.openid) - loadStudents(userInfo.openid) - } else { - Taro.redirectTo({ url: '/pages/login/index' }) - } - - // 设置默认日期为今天 const today = new Date() const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}` setSelectedDate(dateStr) + + const savedStudents = Taro.getStorageSync('students') + if (savedStudents && Array.isArray(savedStudents)) { + setStudents(savedStudents) + } }, []) // 加载学员列表(只加载未毕业的) diff --git a/src/pages/schedule/index.css b/src/pages/schedule/index.css index 1829cc4..f6d1e04 100644 --- a/src/pages/schedule/index.css +++ b/src/pages/schedule/index.css @@ -1,32 +1,594 @@ +/* 排课页面样式 */ .schedule-page { min-height: 100vh; - background-color: #f5f5f5; + background: #f5f7fa; + padding-bottom: 40px; } +/* 页面头部 */ .page-header { - padding: 32px 32px 16px; - background-color: #ffffff; + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 16px; + background: linear-gradient(135deg, #3B82F6 0%, #6366F1 100%); } .page-title { - font-size: 20px; + font-size: 18px; + font-weight: 600; + color: #ffffff; +} + +.header-actions { + display: flex; + align-items: center; + gap: 12px; +} + +.add-btn { + width: 36px; + height: 36px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + display: flex; + align-items: center; + justify-content: center; +} + +/* 日历区域 */ +.calendar-section { + background: #ffffff; + margin: 12px; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +.calendar-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px; + background: #ffffff; +} + +.calendar-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; font-weight: 600; color: #1f2937; } -.page-content { - padding: 16px; +.expand-icon { + font-size: 12px; + color: #9ca3af; } -.coming-soon { +.calendar-content { + padding: 0 12px 16px; +} + +/* 星期标题 */ +.calendar-weekdays { + display: grid; + grid-template-columns: repeat(7, 1fr); + margin-bottom: 8px; +} + +.weekday-cell { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 0; +} + +.weekday-text { + font-size: 12px; + color: #9ca3af; + font-weight: 500; +} + +/* 日期网格 */ +.calendar-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 4px; +} + +.day-cell { display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 80px 32px; + padding: 8px 4px; + min-height: 48px; + border-radius: 12px; + position: relative; } -.coming-soon-text { - font-size: 16px; - color: #6b7280; +.day-cell.empty { + background: transparent; +} + +.day-cell.selected { + background: #3B82F6; +} + +.day-cell.today { + border: 2px solid #3B82F6; + box-sizing: border-box; +} + +.day-text { + font-size: 14px; + color: #1f2937; + font-weight: 500; +} + +.day-cell.selected .day-text { + color: #ffffff; +} + +.schedule-dots { + display: flex; + align-items: center; + gap: 2px; + margin-top: 4px; + height: 12px; +} + +.dot { + width: 4px; + height: 4px; + border-radius: 50%; + background: #3B82F6; +} + +.day-cell.selected .dot { + background: #ffffff; +} + +.dots-text { + font-size: 8px; + color: #3B82F6; + font-weight: 600; +} + +.day-cell.selected .dots-text { + color: #ffffff; +} + +/* 月份导航 */ +.month-nav { + display: flex; + justify-content: space-between; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #f3f4f6; +} + +.nav-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 16px; + font-size: 14px; + color: #3B82F6; +} + +/* 排课列表区域 */ +.schedule-list-section { + background: #ffffff; + margin: 12px; + border-radius: 16px; + padding: 16px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; +} + +.section-title { + font-size: 16px; + font-weight: 600; + color: #1f2937; +} + +.schedule-count { + font-size: 13px; + color: #64748b; + background: #f3f4f6; + padding: 4px 10px; + border-radius: 12px; +} + +/* 排课卡片 */ +.schedule-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.schedule-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px; + background: #f9fafb; + border-radius: 12px; + border-left: 4px solid #3B82F6; +} + +.card-left { + display: flex; + align-items: center; + gap: 12px; +} + +.card-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background: #3B82F6; +} + +.card-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.student-name { + font-size: 15px; + font-weight: 600; + color: #1f2937; +} + +.time-range { + display: flex; + align-items: center; + gap: 6px; +} + +.time-text { + font-size: 13px; + color: #64748b; +} + +.card-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.action-btn { + width: 32px; + height: 32px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + background: #ffffff; +} + +.action-btn.edit { + border: 1px solid #e5e7eb; +} + +.action-btn.delete { + border: 1px solid #fee2e2; +} + +/* 空状态 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; +} + +.empty-text { + font-size: 14px; + color: #9ca3af; + margin-bottom: 16px; +} + +.add-first-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 10px 20px; + background: #eff6ff; + border-radius: 20px; + font-size: 14px; + color: #3B82F6; +} + +/* Dialog 样式 */ +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid #f3f4f6; +} + +.dialog-title { + font-size: 17px; + font-weight: 600; + color: #1f2937; +} + +.close-btn { + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; +} + +.dialog-form { + padding: 20px; +} + +.form-item { + margin-bottom: 24px; +} + +.form-item-label { + font-size: 14px; + font-weight: 500; + color: #374151; + margin-bottom: 10px; + display: block; +} + +.form-input-wrapper { + width: 100%; + background: #ffffff; + border: 2px solid #e2e8f0; + border-radius: 12px; + overflow: hidden; +} + +.form-input-wrapper:focus-within { + border-color: #3B82F6; +} + +.date-display { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; +} + +.date-text { + font-size: 15px; + color: #1f2937; +} + +.picker-trigger { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + min-height: 50px; +} + +.picker-value { + font-size: 15px; + color: #1f2937; +} + +.picker-placeholder { + font-size: 15px; + color: #9ca3af; +} + +.picker-arrow { + font-size: 12px; + color: #9ca3af; +} + +.dialog-footer { + display: flex; + gap: 12px; + padding: 16px 20px; + border-top: 1px solid #f3f4f6; +} + +.cancel-btn { + flex: 1; + padding: 14px; + background: #f3f4f6; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + color: #4b5563; +} + +.confirm-btn { + flex: 1; + padding: 14px; + background: #3B82F6; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + color: #ffffff; + font-weight: 500; +} + +.delete-confirm-btn { + flex: 1; + padding: 14px; + background: #EF4444; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + color: #ffffff; + font-weight: 500; +} + +/* 删除确认内容 */ +.delete-content { + padding: 24px 20px; + text-align: center; +} + +.delete-message { + font-size: 15px; + color: #1f2937; + margin-bottom: 8px; +} + +.delete-detail { + font-size: 13px; + color: #64748b; +} + +/* 时间冲突弹窗 */ +.conflict-dialog { + background: #ffffff; + border-radius: 16px; + overflow: hidden; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15); +} + +.conflict-header { + display: flex; + align-items: center; + gap: 12px; + padding: 20px; + background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%); +} + +.conflict-icon { + font-size: 32px; +} + +.conflict-title { + font-size: 18px; + font-weight: 600; + color: #ffffff; +} + +.conflict-content { + padding: 20px; + background: #ffffff; +} + +.new-schedule-box { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + margin-bottom: 16px; +} + +.new-schedule-label { + font-size: 13px; + color: #64748b; + margin-bottom: 6px; +} + +.new-schedule-value { + font-size: 15px; + color: #1f2937; + font-weight: 500; +} + +.conflict-divider { + height: 1px; + background: #e2e8f0; + margin: 16px 0; +} + +.existing-label { + font-size: 14px; + color: #374151; + font-weight: 500; + margin-bottom: 12px; +} + +.existing-schedules { + display: flex; + flex-direction: column; + gap: 12px; +} + +.existing-schedule-item { + display: flex; + align-items: center; + gap: 12px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; +} + +.existing-icon { + font-size: 24px; +} + +.existing-info { + display: flex; + flex-direction: column; + gap: 4px; +} + +.existing-student { + font-size: 15px; + color: #1f2937; + font-weight: 500; +} + +.existing-time { + font-size: 13px; + color: #64748b; +} + +.conflict-tip { + margin-top: 16px; + padding: 12px 16px; + background: #ffffff; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.tip-text { + font-size: 13px; + color: #64748b; + text-align: center; +} + +.conflict-footer { + padding: 16px 20px; + padding-top: 0; + background: #ffffff; +} + +.conflict-confirm-btn { + width: 100%; + padding: 16px; + background: #3B82F6; + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + color: #ffffff; + font-weight: 500; } diff --git a/src/pages/schedule/index.tsx b/src/pages/schedule/index.tsx index d528391..79e95d5 100644 --- a/src/pages/schedule/index.tsx +++ b/src/pages/schedule/index.tsx @@ -1,17 +1,710 @@ -import { View, Text } from '@tarojs/components' +import { View, Text, Picker } from '@tarojs/components' +import { useState, useEffect } from 'react' +import Taro from '@tarojs/taro' +import { Plus, ChevronLeft, ChevronRight, Clock, Trash2, Pencil, X, Calendar as CalendarIcon } from 'lucide-react-taro' +import { Dialog, DialogContent } from '@/components/ui/dialog' +import { Network } from '@/network' import './index.css' +// 排课记录类型 +interface ScheduleRecord { + id: number + studentId: number + studentName: string + date: string + startTime: string + endTime: string +} + +// 学员类型 +interface Student { + id: number + studentName: string + status: number +} + +// 周几名称 +const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'] + export default function SchedulePage() { + const [openid, setOpenid] = useState('') + const [selectedDate, setSelectedDate] = useState('') + const [currentMonth, setCurrentMonth] = useState(new Date()) + const [calendarExpanded, setCalendarExpanded] = useState(true) + const [schedules, setSchedules] = useState([]) + const [students, setStudents] = useState([]) + const [addDialogOpen, setAddDialogOpen] = useState(false) + const [editDialogOpen, setEditDialogOpen] = useState(false) + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) + const [currentRecord, setCurrentRecord] = useState(null) + const [conflictDialogOpen, setConflictDialogOpen] = useState(false) + const [conflictInfo, setConflictInfo] = useState<{ + newSchedule: ScheduleRecord | null + existingSchedules: ScheduleRecord[] + }>({ + newSchedule: null, + existingSchedules: [] + }) + const [addForm, setAddForm] = useState({ + date: '', + studentId: '', + studentName: '', + startTime: '09:00', + endTime: '10:00' + }) + const [editForm, setEditForm] = useState({ + id: 0, + studentId: '', + studentName: '', + date: '', + startTime: '09:00', + endTime: '10:00' + }) + + // 初始化 + useEffect(() => { + const userInfo = Taro.getStorageSync('userInfo') + const today = formatDate(new Date()) + setSelectedDate(today) + setAddForm(prev => ({ ...prev, date: today })) + + if (userInfo && userInfo.openid) { + setOpenid(userInfo.openid) + loadStudents(userInfo.openid) + loadSchedules(userInfo.openid) + } else { + const savedStudents = Taro.getStorageSync('students') + if (savedStudents && Array.isArray(savedStudents)) { + setStudents(savedStudents) + } + + const savedSchedules = Taro.getStorageSync('schedules') + if (savedSchedules && Array.isArray(savedSchedules)) { + setSchedules(savedSchedules) + } + } + }, []) + + // 加载排课数据 + const loadSchedules = async (oid: string) => { + try { + const res = await Network.request({ + url: '/api/schedules', + method: 'GET', + data: { openid: oid } + }) + console.log('[排课] 加载排课数据:', res.data) + if (res.data && res.data.code === 200) { + const serverSchedules = res.data.data || [] + setSchedules(serverSchedules) + Taro.setStorageSync('schedules', serverSchedules) + } + } catch (err) { + console.error('[排课] 加载失败:', err) + } + } + + // 加载学员数据 + const loadStudents = async (oid: string) => { + try { + const res = await Network.request({ + url: '/api/students', + method: 'GET', + data: { openid: oid, activeOnly: 1 } + }) + console.log('[排课] 加载学员列表:', res.data) + if (res.data && res.data.code === 200) { + const serverStudents = res.data.data || [] + setStudents(serverStudents) + Taro.setStorageSync('students', serverStudents) + } + } catch (err) { + console.error('[排课] 加载学员失败:', err) + } + } + + // 格式化日期为 YYYY-MM-DD + const formatDate = (date: Date): string => { + 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}` + } + + // 获取月份日历数据 + const getMonthDays = () => { + const year = currentMonth.getFullYear() + const month = currentMonth.getMonth() + const firstDay = new Date(year, month, 1).getDay() + const daysInMonth = new Date(year, month + 1, 0).getDate() + const days: (number | null)[] = [] + + // 填充空白 + for (let i = 0; i < firstDay; i++) { + days.push(null) + } + // 填充日期 + for (let i = 1; i <= daysInMonth; i++) { + days.push(i) + } + return days + } + + // 获取某天的排课数量 + const getScheduleCount = (date: string): number => { + return schedules.filter(s => s.date === date).length + } + + // 获取某天的排课列表 + const getDaySchedules = (date: string): ScheduleRecord[] => { + return schedules.filter(s => s.date === date).sort((a, b) => a.startTime.localeCompare(b.startTime)) + } + + // 切换月份 + const prevMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1, 1)) + } + + const nextMonth = () => { + setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1, 1)) + } + + // 选择日期 + const selectDate = (day: number) => { + const date = formatDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)) + setSelectedDate(date) + } + + // 打开新增弹窗 + const openAddDialog = () => { + const savedStudents = Taro.getStorageSync('students') + if (savedStudents && Array.isArray(savedStudents)) { + setStudents(savedStudents) + } + setAddForm({ + date: selectedDate, + studentId: '', + studentName: '', + startTime: '09:00', + endTime: '10:00' + }) + setAddDialogOpen(true) + } + + // 检查时间冲突 + const checkTimeConflict = (schedule: ScheduleRecord, excludeId?: number): ScheduleRecord[] => { + return schedules.filter(s => { + if (excludeId !== undefined && s.id === excludeId) return false + if (s.date !== schedule.date) return false + if (s.studentId !== schedule.studentId) return false + + const newStart = schedule.startTime + const newEnd = schedule.endTime + const existingStart = s.startTime + const existingEnd = s.endTime + + const hasConflict = newStart < existingEnd && newEnd > existingStart + return hasConflict + }) + } + + // 提交新增 + const submitAdd = () => { + if (!addForm.studentId) { + Taro.showToast({ title: '请选择学员', icon: 'none' }) + return + } + + const newSchedule: ScheduleRecord = { + id: Date.now(), + studentId: parseInt(addForm.studentId), + studentName: addForm.studentName, + date: addForm.date, + startTime: addForm.startTime, + endTime: addForm.endTime + } + + const conflicts = checkTimeConflict(newSchedule) + if (conflicts.length > 0) { + setConflictInfo({ + newSchedule, + existingSchedules: conflicts + }) + setConflictDialogOpen(true) + return + } + + const updatedSchedules = [...schedules, newSchedule] + setSchedules(updatedSchedules) + Taro.setStorageSync('schedules', updatedSchedules) + Taro.showToast({ title: '添加成功', icon: 'success' }) + setAddDialogOpen(false) + } + + // 打开编辑弹窗 + const openEditDialog = (record: ScheduleRecord) => { + setEditForm({ + id: record.id, + studentId: record.studentId.toString(), + studentName: record.studentName, + date: record.date, + startTime: record.startTime, + endTime: record.endTime + }) + setCurrentRecord(record) + setEditDialogOpen(true) + } + + // 提交编辑 + const submitEdit = () => { + if (!editForm.studentId) { + Taro.showToast({ title: '请选择学员', icon: 'none' }) + return + } + + const updatedSchedule: ScheduleRecord = { + id: editForm.id, + studentId: parseInt(editForm.studentId), + studentName: editForm.studentName, + date: editForm.date, + startTime: editForm.startTime, + endTime: editForm.endTime + } + + const conflicts = checkTimeConflict(updatedSchedule, editForm.id) + if (conflicts.length > 0) { + setConflictInfo({ + newSchedule: updatedSchedule, + existingSchedules: conflicts + }) + setConflictDialogOpen(true) + return + } + + const updatedSchedules = schedules.map(s => + s.id === editForm.id + ? updatedSchedule + : s + ) + setSchedules(updatedSchedules) + Taro.setStorageSync('schedules', updatedSchedules) + Taro.showToast({ title: '修改成功', icon: 'success' }) + setEditDialogOpen(false) + } + + // 打开删除确认 + const openDeleteDialog = (record: ScheduleRecord) => { + setCurrentRecord(record) + setDeleteDialogOpen(true) + } + + // 确认删除 + const confirmDelete = () => { + if (!currentRecord) return + + const updatedSchedules = schedules.filter(s => s.id !== currentRecord.id) + setSchedules(updatedSchedules) + Taro.setStorageSync('schedules', updatedSchedules) + Taro.showToast({ title: '删除成功', icon: 'success' }) + setDeleteDialogOpen(false) + } + + const monthLabel = `${currentMonth.getFullYear()}年${currentMonth.getMonth() + 1}月` + const selectedSchedules = getDaySchedules(selectedDate) + const todayStr = formatDate(new Date()) + return ( + {/* 页面标题 */} 排课管理 - - - - 功能开发中... + + + + + + {/* 日历区域 */} + + {/* 日历头部 */} + setCalendarExpanded(!calendarExpanded)}> + + + {monthLabel} + + {calendarExpanded ? '▼' : '▶'} + + + {/* 日历内容 */} + {calendarExpanded && ( + + {/* 星期标题 */} + + {WEEKDAYS.map((day, idx) => ( + + {day} + + ))} + + + {/* 日期网格 */} + + {getMonthDays().map((day, idx) => { + if (day === null) { + return + } + const dateStr = formatDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day)) + const isSelected = dateStr === selectedDate + const isToday = dateStr === todayStr + const scheduleCount = getScheduleCount(dateStr) + + return ( + selectDate(day)} + > + {day} + {scheduleCount > 0 && ( + + {scheduleCount <= 3 ? ( + Array.from({ length: scheduleCount }).map((_, i) => ( + + )) + ) : ( + ●●●+{scheduleCount} + )} + + )} + + ) + })} + + + {/* 月份导航 */} + + + + 上月 + + + 下月 + + + + + )} + + + {/* 选中日期的排课列表 */} + + + {selectedDate} 排课 + {selectedSchedules.length} 节 + + + {selectedSchedules.length > 0 ? ( + + {selectedSchedules.map((record) => ( + + + + + {record.studentName} + + + {record.startTime} - {record.endTime} + + + + + openEditDialog(record)}> + + + openDeleteDialog(record)}> + + + + + ))} + + ) : ( + + 暂无排课 + + + 添加第一课 + + + )} + + + {/* 新增排课弹窗 */} + + + + 新增排课 + + + + {/* 上课日期 */} + + 上课日期 + { + setAddForm(prev => ({ ...prev, date: e.detail.value })) + }} + > + + + + {addForm.date} + + + + + + + {/* 选择学员 */} + + 选择学员 * + s.status === 0)} + rangeKey="studentName" + onChange={(e: any) => { + const idx = e.detail.value + const activeStudents = students.filter(s => s.status === 0) + if (activeStudents[idx]) { + setAddForm(prev => ({ + ...prev, + studentId: activeStudents[idx].id.toString(), + studentName: activeStudents[idx].studentName + })) + } + }} + > + + + {addForm.studentName || '请选择学员'} + + + + + + + {/* 开始时间 */} + + 开始时间 + { + setAddForm(prev => ({ ...prev, startTime: e.detail.value })) + }} + > + + {addForm.startTime} + + + + + + {/* 结束时间 */} + + 结束时间 + { + setAddForm(prev => ({ ...prev, endTime: e.detail.value })) + }} + > + + {addForm.endTime} + + + + + + + + setAddDialogOpen(false)}> + 取消 + + + 确定 + + + + + + {/* 编辑排课弹窗 */} + + + + 编辑排课 + + + + {/* 上课日期 */} + + 上课日期 + + + + {editForm.date} + + + + + {/* 选择学员 */} + + 选择学员 * + s.status === 0)} + rangeKey="studentName" + onChange={(e: any) => { + const idx = e.detail.value + const activeStudents = students.filter(s => s.status === 0) + if (activeStudents[idx]) { + setEditForm(prev => ({ + ...prev, + studentId: activeStudents[idx].id.toString(), + studentName: activeStudents[idx].studentName + })) + } + }} + > + + + {editForm.studentName || '请选择学员'} + + + + + + + {/* 开始时间 */} + + 开始时间 + { + setEditForm(prev => ({ ...prev, startTime: e.detail.value })) + }} + > + + {editForm.startTime} + + + + + + {/* 结束时间 */} + + 结束时间 + { + setEditForm(prev => ({ ...prev, endTime: e.detail.value })) + }} + > + + {editForm.endTime} + + + + + + + + setEditDialogOpen(false)}> + 取消 + + + 确定 + + + + + + {/* 删除确认弹窗 */} + + + + 确认删除 + + + + + 确定删除 {currentRecord?.studentName} 的排课吗? + + + {currentRecord?.date} {currentRecord?.startTime} - {currentRecord?.endTime} + + + + + setDeleteDialogOpen(false)}> + 取消 + + + 删除 + + + + + + {/* 时间冲突提示弹窗 */} + + + + + 排课时间冲突 + + + + + 新排课信息: + + {conflictInfo.newSchedule?.studentName} · {conflictInfo.newSchedule?.startTime} - {conflictInfo.newSchedule?.endTime} + + + + + + 该学员已有排课: + + {conflictInfo.existingSchedules.map((schedule, idx) => ( + + 🕐 + + {schedule.studentName} + {schedule.startTime} - {schedule.endTime} + + + ))} + + + + 请调整时间后重新提交 + + + + + setConflictDialogOpen(false)}> + 我知道了 + + + + ) } diff --git a/src/pages/student/index.css b/src/pages/student/index.css index fee6770..ed815d7 100644 --- a/src/pages/student/index.css +++ b/src/pages/student/index.css @@ -1,442 +1,786 @@ +/* 学员管理页面样式 */ .page-container { min-height: 100vh; - background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%); - padding-bottom: 120px; + background: #f5f7fa; + padding: 16rpx; + padding-bottom: 120rpx; } +/* 页面头部 */ .page-header { display: flex; - justify-content: space-between; align-items: center; - padding: 32px 32px 24px; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + justify-content: space-between; + padding: 24rpx 20rpx; + background: linear-gradient(135deg, #3B82F6 0%, #6366F1 100%); + margin: -16rpx -16rpx 20rpx -16rpx; + padding: 24rpx 20rpx; } .page-title { - font-size: 20px; + font-size: 32rpx; font-weight: 600; color: #ffffff; } -.add-btn-wrapper { - width: 36px; - height: 36px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.2); +/* 统计卡片 */ +.stats-row { display: flex; - align-items: center; - justify-content: center; - border: 2px solid rgba(255, 255, 255, 0.5); + gap: 16rpx; + margin-bottom: 20rpx; } -.add-btn-icon { - font-size: 24px; - font-weight: 400; - color: #ffffff; - line-height: 1; -} - -.filter-section { - display: flex; - flex-wrap: wrap; - gap: 16px; - padding: 24px 32px; -} - -.filter-tag { - padding: 12px 24px; - border-radius: 24px; +.stat-card { + flex: 1; background: #ffffff; - border: 2px solid #e2e8f0; -} - -.filter-tag.active { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - border-color: transparent; -} - -.filter-tag-text { - font-size: 14px; - color: #64748b; -} - -.filter-tag.active .filter-tag-text { - color: #ffffff; - font-weight: 500; -} - -.filter-tag.batch-btn { - background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); - border-color: transparent; -} - -.filter-tag.batch-btn .filter-tag-text { - color: #ffffff; -} - -.student-list { - padding: 0 32px; + border-radius: 16rpx; + padding: 24rpx 16rpx; display: flex; flex-direction: column; - gap: 24px; -} - -.student-card { - background: #ffffff; - border-radius: 24px; - padding: 28px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); - position: relative; - overflow: hidden; -} - -.student-card::before { - content: ''; - position: absolute; - left: 0; - top: 0; - bottom: 0; - width: 8px; - background: linear-gradient(180deg, #667eea 0%, #764ba2 100%); -} - -.student-card.graduated::before { - background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%); -} - -.student-card.batch-mode { - padding-left: 64px; -} - -.student-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 24px; -} - -.student-info { - flex: 1; -} - -.student-name { - font-size: 20px; - font-weight: 600; - color: #1e293b; - display: block; - margin-bottom: 8px; -} - -.student-date { - font-size: 13px; - color: #94a3b8; - display: block; -} - -.status-badge { - display: flex; align-items: center; - gap: 8px; - padding: 8px 16px; - border-radius: 20px; - background: #f0fdf4; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); } -.status-badge.active { - background: #f0fdf4; +.stat-value { + font-size: 36rpx; + font-weight: bold; + color: #1f2937; + margin-bottom: 8rpx; } -.status-badge.graduated { - background: #f1f5f9; -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: #22c55e; -} - -.status-badge.graduated .status-dot { - background: #94a3b8; -} - -.status-badge Text { - font-size: 13px; - color: #22c55e; -} - -.status-badge.graduated Text { +.stat-label { + font-size: 22rpx; color: #64748b; } -.progress-section { - margin-bottom: 24px; +/* 添加学员按钮 */ +.add-btn-container { + margin-bottom: 20rpx; } -.progress-header { +.add-student-btn { + background: linear-gradient(135deg, #3B82F6 0%, #6366F1 100%); + border-radius: 12rpx; + padding: 20rpx; display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.progress-label { - font-size: 14px; - color: #64748b; -} - -.progress-value { - font-size: 14px; - font-weight: 600; - color: #667eea; -} - -.progress-bar-bg { - height: 12px; - background: #f1f5f9; - border-radius: 6px; - overflow: hidden; -} - -.progress-bar-fill { - height: 100%; - background: linear-gradient(90deg, #667eea 0%, #764ba2 100%); - border-radius: 6px; - transition: width 0.3s ease; -} - -.progress-bar-fill.complete { - background: linear-gradient(90deg, #22c55e 0%, #16a34a 100%); -} - -.student-notes { - display: block; - margin-top: 16px; - font-size: 14px; - color: #64748b; - padding: 16px; - background: #f8fafc; - border-radius: 12px; - line-height: 1.6; -} - -.student-card-actions { - display: flex; - gap: 16px; - padding-top: 20px; - border-top: 1px solid #f1f5f9; -} - -.action-btn { - flex: 1; - padding: 16px; - border-radius: 12px; - display: flex; - align-items: center; justify-content: center; } -.action-btn.edit { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -} - -.action-btn.delete { - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); -} - -.action-btn.status { - background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); -} - -.action-text { - font-size: 14px; +.add-btn-text { + font-size: 28rpx; + color: #ffffff; font-weight: 500; +} + +/* 搜索框 */ +.search-box { + display: flex; + align-items: center; + background: #ffffff; + border: 2px solid #e2e8f0; + border-radius: 12rpx; + padding: 16rpx 20rpx; + margin-bottom: 20rpx; +} + +.search-box:focus-within { + border-color: #3B82F6; +} + +.search-icon { + font-size: 28rpx; + margin-right: 16rpx; +} + +.search-input { + flex: 1; + background: transparent; + color: #1f2937; + font-size: 28rpx; +} + +/* 排序标签 */ +.sort-bar { + display: flex; + gap: 16rpx; + margin-bottom: 20rpx; +} + +.sort-tag { + background: #ffffff; + border: 2px solid #e2e8f0; + border-radius: 8rpx; + padding: 12rpx 24rpx; + display: flex; + align-items: center; + gap: 8rpx; +} + +.sort-tag.active { + background: #3B82F6; + border-color: #3B82F6; +} + +.sort-tag-text { + font-size: 26rpx; + color: #1f2937; +} + +.sort-tag.active .sort-tag-text { color: #ffffff; } -/* 批量操作栏 */ -.batch-action-bar { - margin: 0 32px 24px; - padding: 24px; - background: #ffffff; - border-radius: 20px; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); -} - -.batch-count { - font-size: 15px; - color: #1e293b; - font-weight: 500; - display: block; - margin-bottom: 20px; - text-align: center; -} - -.batch-buttons { - display: flex; - gap: 16px; - margin-bottom: 16px; -} - -.batch-action-btn { - flex: 1; - padding: 18px; - border-radius: 14px; - display: flex; - align-items: center; - justify-content: center; -} - -.batch-action-btn.end { - background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); -} - -.batch-action-btn.delete { - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); -} - -.batch-action-text { - font-size: 14px; - font-weight: 500; - color: #ffffff; -} - -.batch-cancel-btn { - width: 100%; - padding: 18px; - border-radius: 14px; - background: #f1f5f9; - display: flex; - align-items: center; - justify-content: center; -} - -.batch-cancel-text { - font-size: 14px; +.sort-arrow { + font-size: 20rpx; color: #64748b; } -/* 复选框 */ -.batch-checkbox { - position: absolute; - left: 24px; - top: 50%; - transform: translateY(-50%); +.sort-tag.active .sort-arrow { + color: #ffffff; +} + +/* 学员列表 */ +.student-list { + display: flex; + flex-direction: column; + gap: 16rpx; } -/* 空状态 */ .empty-state { display: flex; flex-direction: column; align-items: center; - justify-content: center; - padding: 120px 32px; + padding: 80rpx 0; } .empty-icon { - font-size: 64px; - margin-bottom: 24px; + font-size: 64rpx; + margin-bottom: 16rpx; } .empty-text { - font-size: 18px; + font-size: 28rpx; color: #64748b; - font-weight: 500; - display: block; - margin-bottom: 12px; + margin-bottom: 8rpx; } .empty-hint { - font-size: 14px; - color: #94a3b8; + font-size: 24rpx; + color: #9ca3af; +} + +/* 学员卡片 */ +.student-card { + background: #ffffff; + border-radius: 16rpx; + padding: 24rpx; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06); +} + +.student-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; +} + +.student-name-section { + display: flex; + align-items: center; + gap: 12rpx; +} + +.student-icon { + font-size: 32rpx; +} + +.student-name { + font-size: 30rpx; + font-weight: 600; + color: #1f2937; +} + +.student-actions { + display: flex; + gap: 12rpx; +} + +.student-action-btn { + min-width: 80rpx; + min-height: 60rpx; + padding: 10rpx 20rpx; + border-radius: 8rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + overflow: visible; +} + +.student-action-btn.course-btn { + background: #3B82F6; +} + +.student-action-btn.end-btn { + background: #f59e0b; +} + +.student-action-btn.delete-btn { + background: #ef4444; +} + +.student-action-text { + font-size: 24rpx; + color: #ffffff; +} + +/* 进度条 */ +.progress-section { + padding-top: 16rpx; + border-top: 1px solid #f3f4f6; +} + +.progress-label { + font-size: 24rpx; + color: #64748b; + margin-bottom: 12rpx; display: block; } -/* Dialog 样式 */ -.form-item { - margin-bottom: 28px; - width: 100%; +.progress-bar-bg { + height: 12rpx; + background: #e2e8f0; + border-radius: 6rpx; + overflow: hidden; + margin-bottom: 12rpx; } -.form-item-label { - font-size: 15px; +.progress-bar-fill { + height: 100%; + background: #10b981; + border-radius: 6rpx; + transition: width 0.3s ease; +} + +.progress-info { + display: flex; + justify-content: space-between; +} + +.progress-used { + font-size: 22rpx; + color: #64748b; +} + +.progress-remaining { + font-size: 22rpx; + color: #10b981; +} + +.progress-total { + font-size: 22rpx; + color: #64748b; +} + +/* 弹窗样式 */ +.dialog-content { + background: #ffffff; + border-radius: 20rpx; + padding: 0; +} + +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 24rpx; + border-bottom: 1px solid #f3f4f6; +} + +.dialog-title { + font-size: 32rpx; + font-weight: 600; + color: #1f2937; +} + +.dialog-body { + padding: 24rpx; +} + +.form-item { + margin-bottom: 28rpx; +} + +.form-label { + font-size: 26rpx; font-weight: 500; color: #374151; + margin-bottom: 12rpx; display: block; - margin-bottom: 12px; } .form-input-wrapper { width: 100%; background: #ffffff; border: 2px solid #e2e8f0; - border-radius: 14px; + border-radius: 12rpx; overflow: hidden; } .form-input-wrapper:focus-within { - border-color: #667eea; + border-color: #3B82F6; } .form-input { width: 100%; - height: 96px; - padding: 0 20px; - font-size: 16px; - color: #1e293b; background: transparent; - -webkit-appearance: none; - appearance: none; + padding: 24rpx; + color: #1f2937; + font-size: 28rpx; + box-sizing: border-box; + text-align: center; } -.form-input::placeholder { - color: #94a3b8; - font-size: 15px; -} - -.dialog-text { - font-size: 15px; - color: #475569; - line-height: 1.6; +.form-hint { + font-size: 22rpx; + color: #9ca3af; + margin-top: 8rpx; display: block; - margin-bottom: 24px; } -.dialog-footer { +.dialog-message { + font-size: 28rpx; + color: #64748b; + margin-bottom: 32rpx; + display: block; +} + +.dialog-buttons { display: flex; - gap: 16px; + gap: 20rpx; + margin-top: 32rpx; + padding-top: 24rpx; + border-top: 1px solid #f3f4f6; } .dialog-btn { flex: 1; - padding: 18px; - border-radius: 14px; + padding: 24rpx; + border-radius: 12rpx; + display: flex; + justify-content: center; +} + +.dialog-btn.cancel-btn { + background: #f3f4f6; +} + +.dialog-btn.cancel-btn .dialog-btn-text { + color: #64748b; +} + +.dialog-btn.confirm-btn { + background: #3B82F6; +} + +.dialog-btn-text { + font-size: 28rpx; + color: #ffffff; + font-weight: 500; +} + +/* 自定义弹窗样式 */ +.custom-dialog-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.custom-dialog { + width: 90%; + max-width: 600rpx; + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; +} + +.custom-dialog-header { + padding: 32rpx; + border-bottom: 1px solid #f3f4f6; + text-align: center; +} + +.custom-dialog-title { + font-size: 34rpx; + font-weight: 600; + color: #1f2937; +} + +.custom-dialog-body { + padding: 32rpx; +} + +.custom-form-item { + margin-bottom: 32rpx; +} + +.custom-form-label { + font-size: 28rpx; + font-weight: 500; + color: #374151; + margin-bottom: 16rpx; + display: block; +} + +.custom-form-input-wrapper { + width: 100%; + background: #ffffff; + border: 2px solid #e2e8f0; + border-radius: 16rpx; + overflow: hidden; + display: flex; + align-items: center; +} + +.custom-form-input-wrapper:focus-within { + border-color: #3B82F6; +} + +.custom-form-input { + width: 100%; + background: transparent; + padding: 32rpx; + color: #1f2937; + font-size: 30rpx; + box-sizing: border-box; + line-height: 1.8; + display: flex; + align-items: center; +} + +.custom-form-input::placeholder { + color: #9ca3af; + font-size: 28rpx; +} + +.custom-form-input .weui-input { + width: 100%; + height: auto; + line-height: 1.8; + display: flex; + align-items: center; +} + +.custom-form-hint { + font-size: 24rpx; + color: #9ca3af; + margin-top: 12rpx; + display: block; +} + +.custom-dialog-buttons { + display: flex; + gap: 24rpx; + margin-top: 32rpx; + padding-top: 24rpx; + border-top: 1px solid #f3f4f6; +} + +.custom-dialog-btn { + flex: 1; + padding: 28rpx; + border-radius: 16rpx; + display: flex; + justify-content: center; +} + +.custom-cancel-btn { + background: #f3f4f6; +} + +.custom-cancel-btn .custom-dialog-btn-text { + color: #64748b; +} + +.custom-confirm-btn { + background: #3B82F6; +} + +.custom-dialog-btn-text { + font-size: 30rpx; + color: #ffffff; + font-weight: 500; +} + +/* 课程安排对话框 */ +.course-dialog { + width: 90%; + max-width: 600rpx; + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; +} + +.course-header { + padding: 32rpx; + background: #ffffff; + border-bottom: 1px solid #f3f4f6; +} + +.course-title { + font-size: 32rpx; + font-weight: 600; + color: #1f2937; +} + +.course-content { + padding: 32rpx; + background: #ffffff; + min-height: 200rpx; +} + +.empty-course { + display: flex; + align-items: center; + justify-content: center; + height: 200rpx; +} + +.empty-course-text { + font-size: 28rpx; + color: #9ca3af; +} + +.course-list { + display: flex; + flex-direction: column; + gap: 16rpx; +} + +.course-item { + display: flex; + justify-content: space-between; + align-items: center; + background: #f9fafb; + border-radius: 16rpx; + padding: 24rpx; +} + +.course-date { + font-size: 28rpx; + color: #1f2937; + font-weight: 500; +} + +.course-time { + font-size: 28rpx; + color: #3B82F6; + font-weight: 500; +} + +.course-summary { + display: flex; + align-items: center; + justify-content: center; + padding: 20rpx; + background: #f3f4f6; + border-radius: 12rpx; + margin-top: 16rpx; +} + +.summary-text { + font-size: 26rpx; + color: #64748b; +} + +.course-footer { + display: flex; + gap: 20rpx; + padding: 24rpx 32rpx; + padding-top: 0; + background: #ffffff; +} + +.course-btn { + flex: 1; + width: 0; + height: 88rpx; + padding: 0; + border-radius: 16rpx; + display: flex; + align-items: center; + justify-content: center; + box-sizing: border-box; +} + +.course-btn.course-close-btn { + background: #f3f4f6; +} + +.course-btn.course-close-btn .course-btn-text { + color: #64748b; +} + +.course-btn.export-btn { + background: #3B82F6; +} + +.course-btn-text { + font-size: 28rpx; + color: #ffffff; + font-weight: 500; +} + +/* 导出预览对话框 */ +.export-preview-dialog { + width: 90%; + max-width: 680rpx; + background: #ffffff; + border-radius: 24rpx; + overflow: hidden; +} + +.export-preview-header { + padding: 32rpx; + background: #ffffff; + border-bottom: 1px solid #f3f4f6; +} + +.export-preview-title { + font-size: 32rpx; + font-weight: 600; + color: #1f2937; +} + +.export-preview-content { + padding: 32rpx; + background: #ffffff; +} + +.preview-card { + background: #ffffff; + border-radius: 16rpx; + padding: 32rpx; + border: 1px solid #e5e7eb; +} + +.preview-student-name { + display: block; + font-size: 32rpx; + font-weight: 600; + color: #1f2937; + text-align: center; + margin-bottom: 8rpx; +} + +.preview-course-count { + display: block; + font-size: 24rpx; + color: #64748b; + text-align: center; + margin-bottom: 24rpx; +} + +.preview-divider { + height: 1px; + background: #e5e7eb; + margin-bottom: 20rpx; +} + +.preview-table-header { + display: flex; + background: #374151; + border-radius: 8rpx; + padding: 16rpx 20rpx; + margin-bottom: 4rpx; +} + +.preview-th { + flex: 1; + font-size: 26rpx; + color: #ffffff; + font-weight: 500; +} + +.preview-th:last-child { + text-align: right; +} + +.preview-table-body { + background: #4b5563; + border-radius: 0 0 8rpx 8rpx; + overflow: hidden; +} + +.preview-table-row { + display: flex; + padding: 20rpx; + border-bottom: 1px solid #374151; +} + +.preview-table-row:last-child { + border-bottom: none; +} + +.preview-td { + flex: 1; + font-size: 26rpx; + color: #f9fafb; +} + +.preview-td:last-child { + text-align: right; +} + +.preview-empty { + padding: 40rpx; + text-align: center; +} + +.preview-empty-text { + font-size: 26rpx; + color: #9ca3af; +} + +.export-preview-footer { + display: flex; + gap: 20rpx; + padding: 24rpx 32rpx; + padding-top: 0; + background: #ffffff; +} + +.export-btn { + flex: 1; + height: 88rpx; + padding: 0 24rpx; + border-radius: 16rpx; display: flex; align-items: center; justify-content: center; } -.dialog-btn.cancel { - background: #f1f5f9; +.export-btn.cancel { + background: #f3f4f6; } -.dialog-btn.cancel Text { - font-size: 15px; +.export-btn.cancel .export-btn-text { color: #64748b; - font-weight: 500; } -.dialog-btn.confirm { - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +.export-btn.confirm { + background: #3B82F6; } -.dialog-btn.confirm Text { - font-size: 15px; +.export-btn-text { + font-size: 28rpx; color: #ffffff; font-weight: 500; } - -.dialog-btn.confirm.delete { - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); -} diff --git a/src/pages/student/index.tsx b/src/pages/student/index.tsx index ede4694..242b7eb 100644 --- a/src/pages/student/index.tsx +++ b/src/pages/student/index.tsx @@ -1,12 +1,9 @@ import { useState, useEffect } from 'react' import { View, Text, Input } from '@tarojs/components' -import { Network } from '@/network' import { Dialog, DialogContent } from '@/components/ui/dialog' -import { Checkbox } from '@/components/ui/checkbox' import Taro from '@tarojs/taro' import './index.css' -// 学员类型 interface Student { id: number studentName: string @@ -16,616 +13,478 @@ interface Student { totalHours: number notes: string usedHours: number + totalFee: number } -// 筛选类型 -type FilterType = 'all' | 'active' | 'graduated' +interface Schedule { + id: number + studentId: number + studentName: string + date: string + startTime: string + endTime: string +} export default function StudentPage() { const [students, setStudents] = useState([]) - const [openid, setOpenid] = useState('') - const [filter, setFilter] = useState('all') + const [searchKey, setSearchKey] = useState('') + const [sortBy, setSortBy] = useState<'name' | 'hours' | 'duration' | 'fee'>('name') - // 添加学员弹窗 const [addDialogOpen, setAddDialogOpen] = useState(false) - const [addForm, setAddForm] = useState({ studentName: '', totalHours: '', notes: '' }) + const [addForm, setAddForm] = useState({ studentName: '', notes: '', totalHours: '' }) - // 编辑学员弹窗 const [editDialogOpen, setEditDialogOpen] = useState(false) const [studentToEdit, setStudentToEdit] = useState(null) - const [editForm, setEditForm] = useState({ studentName: '', totalHours: '', notes: '' }) + const [editForm, setEditForm] = useState({ studentName: '', notes: '', totalHours: '' }) - // 删除确认弹窗 const [deleteDialogOpen, setDeleteDialogOpen] = useState(false) const [studentToDelete, setStudentToDelete] = useState(null) - // 批量选择相关 - const [batchMode, setBatchMode] = useState(false) - const [selectedStudents, setSelectedStudents] = useState([]) - const [batchDialogOpen, setBatchDialogOpen] = useState(false) - const [batchAction, setBatchAction] = useState<'graduated' | 'delete' | null>(null) + const [courseDialogOpen, setCourseDialogOpen] = useState(false) + const [currentCourseStudent, setCurrentCourseStudent] = useState(null) + const [studentSchedules, setStudentSchedules] = useState([]) + + const [exportPreviewOpen, setExportPreviewOpen] = useState(false) useEffect(() => { - const userInfo = Taro.getStorageSync('userInfo') - if (userInfo && userInfo.openid) { - setOpenid(userInfo.openid) - loadStudents(userInfo.openid, 1, true) - } else { - Taro.redirectTo({ url: '/pages/login/index' }) + const savedStudents = Taro.getStorageSync('students') + if (savedStudents && Array.isArray(savedStudents)) { + setStudents(savedStudents) } }, []) - const loadStudents = async (userOpenid: string, pageNum: number, reset: boolean = false) => { - try { - const params: any = { openid: userOpenid, page: pageNum, pageSize: 50 } - if (filter === 'active') { - params.activeOnly = true - } else if (filter === 'graduated') { - params.graduatedOnly = true - } - - const res = await Network.request({ - url: '/api/students', - method: 'GET', - data: params - }) - - if (res.data && res.data.code === 200) { - const newStudents = (res.data.data || []).map((s: any) => ({ - ...s, - usedHours: s.usedHours || 0, - totalHours: s.totalHours || 0, - notes: s.notes || '' - })) - if (reset) { - setStudents(newStudents) - } else { - setStudents(prev => [...prev, ...newStudents]) - } - } - } catch (err) { - console.error('[学员管理] 加载失败:', err) + const filteredStudents = students.filter(s => + s.studentName.toLowerCase().includes(searchKey.toLowerCase()) + ).sort((a, b) => { + switch (sortBy) { + case 'name': return a.studentName.localeCompare(b.studentName) + case 'hours': return b.totalHours - a.totalHours + case 'duration': return (b.totalHours - b.usedHours) - (a.totalHours - a.usedHours) + case 'fee': return b.totalFee - a.totalFee + default: return 0 } - } + }) - const onFilterChange = (newFilter: FilterType) => { - setFilter(newFilter) - setSelectedStudents([]) - setBatchMode(false) - loadStudents(openid, 1, true) - } + const totalStudents = students.length + const totalHours = students.reduce((sum, s) => sum + s.totalHours, 0) + const totalFee = students.reduce((sum, s) => sum + s.totalFee, 0) - // 打开添加弹窗 - const openAddDialog = () => { - setAddForm({ studentName: '', totalHours: '', notes: '' }) - setAddDialogOpen(true) - } - - // 添加学员 - const handleAddStudent = async () => { + const handleAddStudent = () => { if (!addForm.studentName.trim()) { Taro.showToast({ title: '请输入学员姓名', icon: 'none' }) return } - if (!addForm.totalHours || parseFloat(addForm.totalHours) <= 0) { - Taro.showToast({ title: '请输入有效的总课时', icon: 'none' }) + + const newStudent: Student = { + id: Date.now(), + studentName: addForm.studentName.trim(), + notes: addForm.notes.trim(), + totalHours: parseFloat(addForm.totalHours || '0'), + usedHours: 0, + totalFee: parseFloat(addForm.totalHours || '0') * 100, + addTime: new Date().toISOString(), + openid: '', + status: 0 + } + + const updatedStudents = [...students, newStudent] + setStudents(updatedStudents) + Taro.setStorageSync('students', updatedStudents) + Taro.showToast({ title: '添加成功', icon: 'success' }) + setAddDialogOpen(false) + setAddForm({ studentName: '', notes: '', totalHours: '' }) + } + + const handleEditStudent = () => { + if (!studentToEdit || !editForm.studentName.trim()) { + Taro.showToast({ title: '请输入学员姓名', icon: 'none' }) return } - try { - const res = await Network.request({ - url: '/api/students', - method: 'POST', - data: { - studentName: addForm.studentName.trim(), - totalHours: parseFloat(addForm.totalHours), - notes: addForm.notes.trim(), - openid - } - }) + setStudents(prev => prev.map(s => + s.id === studentToEdit.id + ? { + ...s, + studentName: editForm.studentName.trim(), + notes: editForm.notes.trim(), + totalHours: parseFloat(editForm.totalHours || '0'), + totalFee: parseFloat(editForm.totalHours || '0') * 100 + } + : s + )) - if (res.data && res.data.code === 200) { - Taro.showToast({ title: '添加成功', icon: 'success' }) - setAddDialogOpen(false) - loadStudents(openid, 1, true) - } else { - Taro.showToast({ title: res.data?.msg || '添加失败', icon: 'none' }) - } - } catch (err) { - Taro.showToast({ title: '添加失败', icon: 'none' }) - } + Taro.showToast({ title: '修改成功', icon: 'success' }) + setEditDialogOpen(false) } - // 打开编辑弹窗 - const handleEditClick = (student: Student) => { + const handleDeleteStudent = () => { + if (!studentToDelete) return + + setStudents(prev => prev.filter(s => s.id !== studentToDelete.id)) + Taro.showToast({ title: '删除成功', icon: 'success' }) + setDeleteDialogOpen(false) + } + + const openEditDialog = (student: Student) => { setStudentToEdit(student) setEditForm({ studentName: student.studentName, - totalHours: student.totalHours.toString(), - notes: student.notes || '' + notes: student.notes, + totalHours: String(student.totalHours) }) setEditDialogOpen(true) } - // 确认编辑 - const handleConfirmEdit = async () => { - if (!editForm.studentName.trim()) { - Taro.showToast({ title: '学员姓名不能为空', icon: 'none' }) - return - } - - try { - const res = await Network.request({ - url: `/api/students/${studentToEdit?.id}`, - method: 'PUT', - data: { - studentName: editForm.studentName.trim(), - totalHours: parseFloat(editForm.totalHours || '0'), - notes: editForm.notes || '', - status: studentToEdit?.status || 0, - openid - } - }) - - if (res.data && res.data.code === 200) { - Taro.showToast({ title: '保存成功', icon: 'success' }) - setEditDialogOpen(false) - loadStudents(openid, 1, true) - } else { - Taro.showToast({ title: res.data?.msg || '保存失败', icon: 'none' }) - } - } catch (err) { - Taro.showToast({ title: '保存失败', icon: 'none' }) - } - } - - // 删除学员 - const handleDeleteClick = (student: Student) => { + const openDeleteDialog = (student: Student) => { setStudentToDelete(student) setDeleteDialogOpen(true) } - const handleConfirmDelete = async () => { - if (!studentToDelete) return - - try { - const res = await Network.request({ - url: `/api/students/${studentToDelete.id}?openid=${openid}`, - method: 'DELETE' - }) - - if (res.data && res.data.code === 200) { - Taro.showToast({ title: '删除成功', icon: 'success' }) - setDeleteDialogOpen(false) - loadStudents(openid, 1, true) - } else { - Taro.showToast({ title: res.data?.msg || '删除失败', icon: 'none' }) - } - } catch (err) { - Taro.showToast({ title: '删除失败', icon: 'none' }) - } - } - - // 切换学员状态 - const handleToggleStatus = async (student: Student) => { - const newStatus = student.status === 0 ? 1 : 0 - - try { - const res = await Network.request({ - url: `/api/students/${student.id}`, - method: 'PUT', - data: { - studentName: student.studentName, - totalHours: student.totalHours, - notes: student.notes, - status: newStatus, - openid - } - }) - - if (res.data && res.data.code === 200) { - Taro.showToast({ title: newStatus === 1 ? '已结束上课' : '已恢复上课', icon: 'success' }) - loadStudents(openid, 1, true) - } - } catch (err) { - Taro.showToast({ title: '操作失败', icon: 'none' }) - } - } - - // 批量选择 - const toggleStudentSelection = (studentId: number) => { - setSelectedStudents(prev => - prev.includes(studentId) - ? prev.filter(id => id !== studentId) - : [...prev, studentId] - ) - } - - const toggleSelectAll = () => { - if (selectedStudents.length === students.length) { - setSelectedStudents([]) + const openCourseDialog = (student: Student) => { + setCurrentCourseStudent(student) + const savedSchedules = Taro.getStorageSync('schedules') + if (savedSchedules && Array.isArray(savedSchedules)) { + const schedules = savedSchedules.filter((s: Schedule) => s.studentId === student.id) + setStudentSchedules(schedules) } else { - setSelectedStudents(students.map(s => s.id)) + setStudentSchedules([]) } + setCourseDialogOpen(true) } - // 批量操作 - const handleBatchAction = (action: 'graduated' | 'delete') => { - setBatchAction(action) - setBatchDialogOpen(true) + const handleExportPreview = () => { + setExportPreviewOpen(true) } - const confirmBatchAction = async () => { - if (selectedStudents.length === 0) return - - try { - if (batchAction === 'delete') { - for (const id of selectedStudents) { - await Network.request({ - url: `/api/students/${id}?openid=${openid}`, - method: 'DELETE' - }) - } - Taro.showToast({ title: `已删除 ${selectedStudents.length} 名学员`, icon: 'success' }) - } else if (batchAction === 'graduated') { - for (const id of selectedStudents) { - const student = students.find(s => s.id === id) - if (student) { - await Network.request({ - url: `/api/students/${id}`, - method: 'PUT', - data: { - studentName: student.studentName, - totalHours: student.totalHours, - notes: student.notes, - status: 1, - openid - } - }) - } - } - Taro.showToast({ title: `已标记 ${selectedStudents.length} 名学员结束上课`, icon: 'success' }) - } - - setSelectedStudents([]) - setBatchMode(false) - loadStudents(openid, 1, true) - } catch (err) { - Taro.showToast({ title: '操作失败', icon: 'none' }) - } finally { - setBatchDialogOpen(false) - setBatchAction(null) - } - } - - const cancelBatchMode = () => { - setBatchMode(false) - setSelectedStudents([]) - } - - const formatDate = (dateStr: string) => { - const date = new Date(dateStr) - return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` + const handleConfirmExport = () => { + Taro.showToast({ title: '导出成功', icon: 'success' }) + setExportPreviewOpen(false) } const getProgressPercent = (used: number, total: number) => { if (total <= 0) return 0 - const percent = (used / total) * 100 - return Math.min(percent, 100) + return Math.min((used / total) * 100, 100) } return ( 学员管理 - - + + + + + + {totalStudents} + 学员总数 + + + {totalHours.toFixed(1)}h + 总课时 - - onFilterChange('all')} - > - 全部 + + setAddDialogOpen(true)}> + + 添加学员 - onFilterChange('active')} - > - 在读 - - onFilterChange('graduated')} - > - 已结束 - - - {!batchMode && students.length > 0 && ( - setBatchMode(true)} - > - 批量选择 - - )} - - {batchMode && ( - - - {selectedStudents.length === students.length ? '取消全选' : '全选'} - - - )} - {batchMode && selectedStudents.length > 0 && ( - - 已选择 {selectedStudents.length} 名学员 - - handleBatchAction('graduated')} - > - 批量结束上课 - - handleBatchAction('delete')} - > - 批量删除 - - - + 🔍 + setSearchKey(e.detail.value)} + /> + + + + {[ + { key: 'name', label: '姓名' }, + { key: 'hours', label: '课时数' }, + { key: 'duration', label: '时长' }, + { key: 'fee', label: '费用' } + ].map(item => ( + setSortBy(item.key as typeof sortBy)} > - 取消 + {item.label} + {sortBy === item.key && } - - )} + ))} + - {students.length === 0 ? ( + {filteredStudents.length === 0 ? ( 📋 暂无学员 - 点击右上角添加按钮添加学员 + 点击上方添加学员按钮添加 ) : ( - students.map((student) => ( - - {batchMode && ( - - toggleStudentSelection(student.id)} - /> - - )} - - - + filteredStudents.map(student => ( + + + + 👤 {student.studentName} - 添加于 {formatDate(student.addTime)} - - - {student.status === 0 ? '在读' : '已结束'} + + openCourseDialog(student)}> + 课程 + + { + setStudents(prev => prev.map(s => + s.id === student.id ? { ...s, status: 1 } : s + )) + Taro.showToast({ title: '已结课', icon: 'success' }) + }}> + 结课 + + openDeleteDialog(student)}> + 删除 + - - 课时进度 - {student.usedHours || 0} / {student.totalHours} 小时 - + 课时进度 = 100 ? 'complete' : ''}`} - style={{ width: `${getProgressPercent(student.usedHours || 0, student.totalHours)}%` }} - > + className="progress-bar-fill" + style={{ width: `${getProgressPercent(student.usedHours, student.totalHours)}%` }} + /> + + + 已上 {student.usedHours.toFixed(1)}h + 剩余 {(student.totalHours - student.usedHours).toFixed(1)}h + 共 {student.totalHours.toFixed(1)}h - {student.notes && ( - {student.notes} - )} - - {!batchMode && ( - - handleEditClick(student)} - > - 编辑 - - - handleDeleteClick(student)} - > - 删除 - - - handleToggleStatus(student)} - > - {student.status === 0 ? '结束' : '恢复'} - - - )} )) )} - {/* 添加学员弹窗 */} - - - - 学员姓名 * - - setAddForm(prev => ({ ...prev, studentName: e.detail.value }))} - /> + {addDialogOpen && ( + setAddDialogOpen(false)}> + e.stopPropagation()}> + + 添加学员 - - - 总课时(小时) * - - setAddForm(prev => ({ ...prev, totalHours: e.detail.value }))} - /> + + + 学员姓名 * + + setAddForm(prev => ({ ...prev, studentName: e.detail.value }))} + /> + - - - 备注(可选) - - setAddForm(prev => ({ ...prev, notes: e.detail.value }))} - /> - - - - setAddDialogOpen(false)} - > - 取消 - - - 添加 - - - - - {/* 编辑学员弹窗 */} + + 备注 (可选) + + setAddForm(prev => ({ ...prev, notes: e.detail.value }))} + /> + + + + + 总课时 (小时,可选) + + setAddForm(prev => ({ ...prev, totalHours: e.detail.value }))} + /> + + 设置后可追踪课时进度 + + + + setAddDialogOpen(false)}> + 取消 + + + 确认添加 + + + + + + )} + - - - 学员姓名 * - - setEditForm(prev => ({ ...prev, studentName: e.detail.value }))} - /> + + + 编辑学员 + + + + 学员姓名 * + + setEditForm(prev => ({ ...prev, studentName: e.detail.value }))} + /> + - - - 总课时(小时) - setEditForm(prev => ({ ...prev, totalHours: e.detail.value }))} - /> - - - 备注 - setEditForm(prev => ({ ...prev, notes: e.detail.value }))} - /> - - - setEditDialogOpen(false)} - > - 取消 + + + 备注 (可选) + + setEditForm(prev => ({ ...prev, notes: e.detail.value }))} + /> + - - 保存 + + + 总课时 (小时,可选) + + setEditForm(prev => ({ ...prev, totalHours: e.detail.value }))} + /> + + + + + setEditDialogOpen(false)}> + 取消 + + + 确认修改 + - {/* 删除确认弹窗 */} - - - 确认删除学员 {studentToDelete?.studentName} 吗?删除后将同时删除该学员的所有课时记录。 - - - setDeleteDialogOpen(false)} - > - 取消 - - - 删除 + + + 确认删除 + + + 确定要删除该学员吗? + + setDeleteDialogOpen(false)}> + 取消 + + + 确认删除 + - {/* 批量操作确认弹窗 */} - - - - {batchAction === 'delete' - ? `确认删除选中的 ${selectedStudents.length} 名学员吗?删除后将同时删除这些学员的所有课时记录。` - : `确认将选中的 ${selectedStudents.length} 名学员标记为结束上课吗?` - } - - - setBatchDialogOpen(false)} - > - 取消 + + + + + {currentCourseStudent?.studentName} 的课程安排 + + + + + {studentSchedules.length === 0 ? ( + + 暂无课程安排 + + ) : ( + + {studentSchedules.map((schedule, index) => ( + + {schedule.date} + {schedule.startTime} - {schedule.endTime} + + ))} + + 共 {studentSchedules.length} 节课程 + + + )} + + + + setCourseDialogOpen(false)}> + 关闭 - - 确认 + + 导出图片 - + + + + 课程表预览 + + + + + {currentCourseStudent?.studentName} 课程表 + 共 {studentSchedules.length} 节课程 + + + + + 日期 + 时间 + + + + {studentSchedules.length === 0 ? ( + + 暂无课程 + + ) : ( + studentSchedules.map((schedule, index) => ( + + {schedule.date} + {schedule.startTime} - {schedule.endTime} + + )) + )} + + + + + + setExportPreviewOpen(false)}> + 取消 + + + 确认导出 + + + + ) }