新增课程和排课基本功能
This commit is contained in:
parent
1bd9eecdcf
commit
0b9b0eed83
|
|
@ -1,32 +1,345 @@
|
||||||
.course-page {
|
.course-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background-color: #f0f4f8;
|
||||||
|
padding-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-header {
|
/* 课程日历区域 */
|
||||||
padding: 32px 32px 16px;
|
.calendar-section {
|
||||||
background-color: #ffffff;
|
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 {
|
.calendar-header {
|
||||||
font-size: 20px;
|
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;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-content {
|
.calendar-toggle {
|
||||||
padding: 16px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 32px;
|
padding: 48px 24px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-radius: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coming-soon-text {
|
.empty-text {
|
||||||
font-size: 16px;
|
font-size: 15px;
|
||||||
color: #6b7280;
|
color: #9CA3AF;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,266 @@
|
||||||
import { View, Text } from '@tarojs/components'
|
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'
|
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() {
|
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<Course[]>([])
|
||||||
|
|
||||||
|
// 格式化日期为 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 (
|
return (
|
||||||
<View className="course-page">
|
<View className="course-page">
|
||||||
<View className="page-header">
|
{/* 课程日历 */}
|
||||||
<Text className="page-title">课程管理</Text>
|
<View className="calendar-section">
|
||||||
|
<View
|
||||||
|
className="calendar-header"
|
||||||
|
onClick={() => setCalendarExpanded(!calendarExpanded)}
|
||||||
|
>
|
||||||
|
<View className="calendar-title-row">
|
||||||
|
<Calendar size={20} color="#3B82F6" />
|
||||||
|
<Text className="calendar-title">课程日历</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="calendar-toggle">{calendarExpanded ? '收起' : '展开'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{calendarExpanded && (
|
||||||
|
<View className="calendar-body">
|
||||||
|
{/* 月份导航 */}
|
||||||
|
<View className="month-nav">
|
||||||
|
<View className="nav-btn" onClick={prevMonth}>
|
||||||
|
<ChevronLeft size={20} color="#6B7280" />
|
||||||
|
</View>
|
||||||
|
<Text className="month-label">
|
||||||
|
{currentMonth.getFullYear()}年{currentMonth.getMonth() + 1}月
|
||||||
|
</Text>
|
||||||
|
<View className="nav-btn" onClick={nextMonth}>
|
||||||
|
<ChevronRight size={20} color="#6B7280" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 星期标题 */}
|
||||||
|
<View className="week-header">
|
||||||
|
{weekDays.map((day, index) => (
|
||||||
|
<View key={index} className="week-day">
|
||||||
|
<Text className="week-day-text">{day}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 日期网格 */}
|
||||||
|
<View className="date-grid">
|
||||||
|
{monthData.map((item, index) => {
|
||||||
|
const courseCount = hasCourse(item.date)
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={index}
|
||||||
|
className={`date-cell ${!item.isCurrentMonth ? 'other-month' : ''} ${isToday(item.date) ? 'is-today' : ''} ${isSelected(item.date) ? 'is-selected' : ''}`}
|
||||||
|
onClick={() => item.isCurrentMonth && setSelectedDate(item.date)}
|
||||||
|
>
|
||||||
|
<Text className="date-text">{item.date.getDate()}</Text>
|
||||||
|
{item.isCurrentMonth && courseCount > 0 && (
|
||||||
|
<View className="course-dots">
|
||||||
|
{courseCount <= 3 ? (
|
||||||
|
Array.from({ length: Math.min(courseCount, 3) }).map((_, i) => (
|
||||||
|
<View key={i} className="course-dot" />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className="course-dot-text">●●●+{courseCount - 3}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View className="page-content">
|
|
||||||
<View className="coming-soon">
|
{/* 今日课程列表 */}
|
||||||
<Text className="coming-soon-text">功能开发中...</Text>
|
<View className="course-list-section">
|
||||||
|
<View className="list-header">
|
||||||
|
<Text className="list-title">
|
||||||
|
{selectedDate.getMonth() + 1}月{selectedDate.getDate()}日 课程
|
||||||
|
</Text>
|
||||||
|
<View className="student-count">
|
||||||
|
<Text className="count-number">{selectedCourses.length}</Text>
|
||||||
|
<Text className="count-label">位学员</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="course-list">
|
||||||
|
{selectedCourses.length === 0 ? (
|
||||||
|
<View className="empty-state">
|
||||||
|
<Text className="empty-text">当日暂无课程安排</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
selectedCourses.map(course => {
|
||||||
|
const statusInfo = getStatusLabel(course.status)
|
||||||
|
return (
|
||||||
|
<View key={course.id} className="course-card">
|
||||||
|
<View className="course-info">
|
||||||
|
<Text className="student-name">{course.studentName}</Text>
|
||||||
|
<View className="time-row">
|
||||||
|
<Clock size={14} color="#6B7280" />
|
||||||
|
<Text className="time-text">
|
||||||
|
{course.startTime} - {course.endTime}
|
||||||
|
</Text>
|
||||||
|
<View className="duration-tag">
|
||||||
|
<Text className="duration-text">{course.duration}h</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="course-actions">
|
||||||
|
<View className={`status-badge ${statusInfo.className}`}>
|
||||||
|
<Text className="status-text">{statusInfo.text}</Text>
|
||||||
|
</View>
|
||||||
|
{course.recorded && (
|
||||||
|
<View className="recorded-badge">
|
||||||
|
<CircleCheck size={14} color="#10B981" />
|
||||||
|
<Text className="recorded-text">已录入</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
||||||
|
|
@ -35,19 +35,14 @@ export default function RecordPage() {
|
||||||
const [openid, setOpenid] = useState<string>('')
|
const [openid, setOpenid] = useState<string>('')
|
||||||
|
|
||||||
useEffect(() => {
|
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 today = new Date()
|
||||||
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`
|
||||||
setSelectedDate(dateStr)
|
setSelectedDate(dateStr)
|
||||||
|
|
||||||
|
const savedStudents = Taro.getStorageSync('students')
|
||||||
|
if (savedStudents && Array.isArray(savedStudents)) {
|
||||||
|
setStudents(savedStudents)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 加载学员列表(只加载未毕业的)
|
// 加载学员列表(只加载未毕业的)
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,594 @@
|
||||||
|
/* 排课页面样式 */
|
||||||
.schedule-page {
|
.schedule-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background-color: #f5f5f5;
|
background: #f5f7fa;
|
||||||
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 页面头部 */
|
||||||
.page-header {
|
.page-header {
|
||||||
padding: 32px 32px 16px;
|
display: flex;
|
||||||
background-color: #ffffff;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 20px 16px;
|
||||||
|
background: linear-gradient(135deg, #3B82F6 0%, #6366F1 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-title {
|
.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;
|
font-weight: 600;
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-content {
|
.expand-icon {
|
||||||
padding: 16px;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 80px 32px;
|
padding: 8px 4px;
|
||||||
|
min-height: 48px;
|
||||||
|
border-radius: 12px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coming-soon-text {
|
.day-cell.empty {
|
||||||
font-size: 16px;
|
background: transparent;
|
||||||
color: #6b7280;
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
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() {
|
export default function SchedulePage() {
|
||||||
|
const [openid, setOpenid] = useState<string>('')
|
||||||
|
const [selectedDate, setSelectedDate] = useState<string>('')
|
||||||
|
const [currentMonth, setCurrentMonth] = useState<Date>(new Date())
|
||||||
|
const [calendarExpanded, setCalendarExpanded] = useState(true)
|
||||||
|
const [schedules, setSchedules] = useState<ScheduleRecord[]>([])
|
||||||
|
const [students, setStudents] = useState<Student[]>([])
|
||||||
|
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
|
const [currentRecord, setCurrentRecord] = useState<ScheduleRecord | null>(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 (
|
return (
|
||||||
<View className="schedule-page">
|
<View className="schedule-page">
|
||||||
|
{/* 页面标题 */}
|
||||||
<View className="page-header">
|
<View className="page-header">
|
||||||
<Text className="page-title">排课管理</Text>
|
<Text className="page-title">排课管理</Text>
|
||||||
</View>
|
<View className="header-actions">
|
||||||
<View className="page-content">
|
<View className="add-btn" onClick={openAddDialog}>
|
||||||
<View className="coming-soon">
|
<Plus size={20} color="#ffffff" />
|
||||||
<Text className="coming-soon-text">功能开发中...</Text>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* 日历区域 */}
|
||||||
|
<View className="calendar-section">
|
||||||
|
{/* 日历头部 */}
|
||||||
|
<View className="calendar-header" onClick={() => setCalendarExpanded(!calendarExpanded)}>
|
||||||
|
<View className="calendar-title">
|
||||||
|
<CalendarIcon size={18} color="#3B82F6" />
|
||||||
|
<Text className="block">{monthLabel}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="block expand-icon">{calendarExpanded ? '▼' : '▶'}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 日历内容 */}
|
||||||
|
{calendarExpanded && (
|
||||||
|
<View className="calendar-content">
|
||||||
|
{/* 星期标题 */}
|
||||||
|
<View className="calendar-weekdays">
|
||||||
|
{WEEKDAYS.map((day, idx) => (
|
||||||
|
<View key={idx} className="weekday-cell">
|
||||||
|
<Text className="block weekday-text">{day}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 日期网格 */}
|
||||||
|
<View className="calendar-grid">
|
||||||
|
{getMonthDays().map((day, idx) => {
|
||||||
|
if (day === null) {
|
||||||
|
return <View key={idx} className="day-cell empty" />
|
||||||
|
}
|
||||||
|
const dateStr = formatDate(new Date(currentMonth.getFullYear(), currentMonth.getMonth(), day))
|
||||||
|
const isSelected = dateStr === selectedDate
|
||||||
|
const isToday = dateStr === todayStr
|
||||||
|
const scheduleCount = getScheduleCount(dateStr)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={idx}
|
||||||
|
className={`day-cell ${isSelected ? 'selected' : ''} ${isToday ? 'today' : ''}`}
|
||||||
|
onClick={() => selectDate(day)}
|
||||||
|
>
|
||||||
|
<Text className="block day-text">{day}</Text>
|
||||||
|
{scheduleCount > 0 && (
|
||||||
|
<View className="schedule-dots">
|
||||||
|
{scheduleCount <= 3 ? (
|
||||||
|
Array.from({ length: scheduleCount }).map((_, i) => (
|
||||||
|
<View key={i} className="dot" />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Text className="block dots-text">●●●+{scheduleCount}</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 月份导航 */}
|
||||||
|
<View className="month-nav">
|
||||||
|
<View className="nav-btn" onClick={prevMonth}>
|
||||||
|
<ChevronLeft size={20} color="#3B82F6" />
|
||||||
|
<Text className="block">上月</Text>
|
||||||
|
</View>
|
||||||
|
<View className="nav-btn" onClick={nextMonth}>
|
||||||
|
<Text className="block">下月</Text>
|
||||||
|
<ChevronRight size={20} color="#3B82F6" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 选中日期的排课列表 */}
|
||||||
|
<View className="schedule-list-section">
|
||||||
|
<View className="section-header">
|
||||||
|
<Text className="block section-title">{selectedDate} 排课</Text>
|
||||||
|
<Text className="block schedule-count">{selectedSchedules.length} 节</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{selectedSchedules.length > 0 ? (
|
||||||
|
<View className="schedule-list">
|
||||||
|
{selectedSchedules.map((record) => (
|
||||||
|
<View key={record.id} className="schedule-card">
|
||||||
|
<View className="card-left">
|
||||||
|
<View className="card-indicator" />
|
||||||
|
<View className="card-info">
|
||||||
|
<Text className="block student-name">{record.studentName}</Text>
|
||||||
|
<View className="time-range">
|
||||||
|
<Clock size={14} color="#64748b" />
|
||||||
|
<Text className="block time-text">{record.startTime} - {record.endTime}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View className="card-actions">
|
||||||
|
<View className="action-btn edit" onClick={() => openEditDialog(record)}>
|
||||||
|
<Pencil size={16} color="#3B82F6" />
|
||||||
|
</View>
|
||||||
|
<View className="action-btn delete" onClick={() => openDeleteDialog(record)}>
|
||||||
|
<Trash2 size={16} color="#EF4444" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View className="empty-state">
|
||||||
|
<Text className="block empty-text">暂无排课</Text>
|
||||||
|
<View className="add-first-btn" onClick={openAddDialog}>
|
||||||
|
<Plus size={16} color="#3B82F6" />
|
||||||
|
<Text className="block">添加第一课</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 新增排课弹窗 */}
|
||||||
|
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen} closeOnOverlayClick={false}>
|
||||||
|
<DialogContent>
|
||||||
|
<View className="dialog-header">
|
||||||
|
<Text className="block dialog-title">新增排课</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-form">
|
||||||
|
{/* 上课日期 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">上课日期</Text>
|
||||||
|
<Picker
|
||||||
|
mode="date"
|
||||||
|
value={addForm.date}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setAddForm(prev => ({ ...prev, date: e.detail.value }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<View className="date-display">
|
||||||
|
<CalendarIcon size={16} color="#64748b" />
|
||||||
|
<Text className="block date-text">{addForm.date}</Text>
|
||||||
|
</View>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 选择学员 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">选择学员 *</Text>
|
||||||
|
<Picker
|
||||||
|
mode="selector"
|
||||||
|
range={students.filter(s => 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className={addForm.studentName ? 'picker-value' : 'picker-placeholder'}>
|
||||||
|
{addForm.studentName || '请选择学员'}
|
||||||
|
</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 开始时间 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">开始时间</Text>
|
||||||
|
<Picker
|
||||||
|
mode="time"
|
||||||
|
value={addForm.startTime}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setAddForm(prev => ({ ...prev, startTime: e.detail.value }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className="picker-value">{addForm.startTime}</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 结束时间 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">结束时间</Text>
|
||||||
|
<Picker
|
||||||
|
mode="time"
|
||||||
|
value={addForm.endTime}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setAddForm(prev => ({ ...prev, endTime: e.detail.value }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className="picker-value">{addForm.endTime}</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-footer">
|
||||||
|
<View className="cancel-btn" onClick={() => setAddDialogOpen(false)}>
|
||||||
|
<Text className="block">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="confirm-btn" onClick={submitAdd}>
|
||||||
|
<Text className="block">确定</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 编辑排课弹窗 */}
|
||||||
|
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen} closeOnOverlayClick={false}>
|
||||||
|
<DialogContent>
|
||||||
|
<View className="dialog-header">
|
||||||
|
<Text className="block dialog-title">编辑排课</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-form">
|
||||||
|
{/* 上课日期 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">上课日期</Text>
|
||||||
|
<View className="form-input-wrapper">
|
||||||
|
<View className="date-display">
|
||||||
|
<CalendarIcon size={16} color="#64748b" />
|
||||||
|
<Text className="block date-text">{editForm.date}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 选择学员 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">选择学员 *</Text>
|
||||||
|
<Picker
|
||||||
|
mode="selector"
|
||||||
|
range={students.filter(s => 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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className={editForm.studentName ? 'picker-value' : 'picker-placeholder'}>
|
||||||
|
{editForm.studentName || '请选择学员'}
|
||||||
|
</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 开始时间 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">开始时间</Text>
|
||||||
|
<Picker
|
||||||
|
mode="time"
|
||||||
|
value={editForm.startTime}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setEditForm(prev => ({ ...prev, startTime: e.detail.value }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className="picker-value">{editForm.startTime}</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 结束时间 */}
|
||||||
|
<View className="form-item">
|
||||||
|
<Text className="form-item-label">结束时间</Text>
|
||||||
|
<Picker
|
||||||
|
mode="time"
|
||||||
|
value={editForm.endTime}
|
||||||
|
onChange={(e: any) => {
|
||||||
|
setEditForm(prev => ({ ...prev, endTime: e.detail.value }))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View className="form-input-wrapper picker-trigger">
|
||||||
|
<Text className="picker-value">{editForm.endTime}</Text>
|
||||||
|
<Text className="block picker-arrow">▼</Text>
|
||||||
|
</View>
|
||||||
|
</Picker>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-footer">
|
||||||
|
<View className="cancel-btn" onClick={() => setEditDialogOpen(false)}>
|
||||||
|
<Text className="block">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="confirm-btn" onClick={submitEdit}>
|
||||||
|
<Text className="block">确定</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 删除确认弹窗 */}
|
||||||
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} closeOnOverlayClick={false}>
|
||||||
|
<DialogContent>
|
||||||
|
<View className="dialog-header">
|
||||||
|
<Text className="block dialog-title">确认删除</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="delete-content">
|
||||||
|
<Text className="block delete-message">
|
||||||
|
确定删除 {currentRecord?.studentName} 的排课吗?
|
||||||
|
</Text>
|
||||||
|
<Text className="block delete-detail">
|
||||||
|
{currentRecord?.date} {currentRecord?.startTime} - {currentRecord?.endTime}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-footer">
|
||||||
|
<View className="cancel-btn" onClick={() => setDeleteDialogOpen(false)}>
|
||||||
|
<Text className="block">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="delete-confirm-btn" onClick={confirmDelete}>
|
||||||
|
<Text className="block">删除</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
{/* 时间冲突提示弹窗 */}
|
||||||
|
<Dialog open={conflictDialogOpen} onOpenChange={setConflictDialogOpen} closeOnOverlayClick={false}>
|
||||||
|
<DialogContent className="conflict-dialog">
|
||||||
|
<View className="conflict-header">
|
||||||
|
<View className="conflict-icon">⚠</View>
|
||||||
|
<Text className="block conflict-title">排课时间冲突</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="conflict-content">
|
||||||
|
<View className="new-schedule-box">
|
||||||
|
<Text className="block new-schedule-label">新排课信息:</Text>
|
||||||
|
<Text className="block new-schedule-value">
|
||||||
|
{conflictInfo.newSchedule?.studentName} · {conflictInfo.newSchedule?.startTime} - {conflictInfo.newSchedule?.endTime}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="conflict-divider" />
|
||||||
|
|
||||||
|
<Text className="block existing-label">该学员已有排课:</Text>
|
||||||
|
<View className="existing-schedules">
|
||||||
|
{conflictInfo.existingSchedules.map((schedule, idx) => (
|
||||||
|
<View key={idx} className="existing-schedule-item">
|
||||||
|
<View className="existing-icon">🕐</View>
|
||||||
|
<View className="existing-info">
|
||||||
|
<Text className="block existing-student">{schedule.studentName}</Text>
|
||||||
|
<Text className="block existing-time">{schedule.startTime} - {schedule.endTime}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="conflict-tip">
|
||||||
|
<Text className="block tip-text">请调整时间后重新提交</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="conflict-footer">
|
||||||
|
<View className="conflict-confirm-btn" onClick={() => setConflictDialogOpen(false)}>
|
||||||
|
<Text className="block">我知道了</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,12 +1,9 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { View, Text, Input } from '@tarojs/components'
|
import { View, Text, Input } from '@tarojs/components'
|
||||||
import { Network } from '@/network'
|
|
||||||
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
|
||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
// 学员类型
|
|
||||||
interface Student {
|
interface Student {
|
||||||
id: number
|
id: number
|
||||||
studentName: string
|
studentName: string
|
||||||
|
|
@ -16,616 +13,478 @@ interface Student {
|
||||||
totalHours: number
|
totalHours: number
|
||||||
notes: string
|
notes: string
|
||||||
usedHours: number
|
usedHours: number
|
||||||
|
totalFee: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 筛选类型
|
interface Schedule {
|
||||||
type FilterType = 'all' | 'active' | 'graduated'
|
id: number
|
||||||
|
studentId: number
|
||||||
|
studentName: string
|
||||||
|
date: string
|
||||||
|
startTime: string
|
||||||
|
endTime: string
|
||||||
|
}
|
||||||
|
|
||||||
export default function StudentPage() {
|
export default function StudentPage() {
|
||||||
const [students, setStudents] = useState<Student[]>([])
|
const [students, setStudents] = useState<Student[]>([])
|
||||||
const [openid, setOpenid] = useState<string>('')
|
const [searchKey, setSearchKey] = useState('')
|
||||||
const [filter, setFilter] = useState<FilterType>('all')
|
const [sortBy, setSortBy] = useState<'name' | 'hours' | 'duration' | 'fee'>('name')
|
||||||
|
|
||||||
// 添加学员弹窗
|
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
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 [editDialogOpen, setEditDialogOpen] = useState(false)
|
||||||
const [studentToEdit, setStudentToEdit] = useState<Student | null>(null)
|
const [studentToEdit, setStudentToEdit] = useState<Student | null>(null)
|
||||||
const [editForm, setEditForm] = useState({ studentName: '', totalHours: '', notes: '' })
|
const [editForm, setEditForm] = useState({ studentName: '', notes: '', totalHours: '' })
|
||||||
|
|
||||||
// 删除确认弹窗
|
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
|
||||||
const [studentToDelete, setStudentToDelete] = useState<Student | null>(null)
|
const [studentToDelete, setStudentToDelete] = useState<Student | null>(null)
|
||||||
|
|
||||||
// 批量选择相关
|
const [courseDialogOpen, setCourseDialogOpen] = useState(false)
|
||||||
const [batchMode, setBatchMode] = useState(false)
|
const [currentCourseStudent, setCurrentCourseStudent] = useState<Student | null>(null)
|
||||||
const [selectedStudents, setSelectedStudents] = useState<number[]>([])
|
const [studentSchedules, setStudentSchedules] = useState<Schedule[]>([])
|
||||||
const [batchDialogOpen, setBatchDialogOpen] = useState(false)
|
|
||||||
const [batchAction, setBatchAction] = useState<'graduated' | 'delete' | null>(null)
|
const [exportPreviewOpen, setExportPreviewOpen] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const userInfo = Taro.getStorageSync('userInfo')
|
const savedStudents = Taro.getStorageSync('students')
|
||||||
if (userInfo && userInfo.openid) {
|
if (savedStudents && Array.isArray(savedStudents)) {
|
||||||
setOpenid(userInfo.openid)
|
setStudents(savedStudents)
|
||||||
loadStudents(userInfo.openid, 1, true)
|
|
||||||
} else {
|
|
||||||
Taro.redirectTo({ url: '/pages/login/index' })
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const loadStudents = async (userOpenid: string, pageNum: number, reset: boolean = false) => {
|
const filteredStudents = students.filter(s =>
|
||||||
try {
|
s.studentName.toLowerCase().includes(searchKey.toLowerCase())
|
||||||
const params: any = { openid: userOpenid, page: pageNum, pageSize: 50 }
|
).sort((a, b) => {
|
||||||
if (filter === 'active') {
|
switch (sortBy) {
|
||||||
params.activeOnly = true
|
case 'name': return a.studentName.localeCompare(b.studentName)
|
||||||
} else if (filter === 'graduated') {
|
case 'hours': return b.totalHours - a.totalHours
|
||||||
params.graduatedOnly = true
|
case 'duration': return (b.totalHours - b.usedHours) - (a.totalHours - a.usedHours)
|
||||||
}
|
case 'fee': return b.totalFee - a.totalFee
|
||||||
|
default: return 0
|
||||||
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 onFilterChange = (newFilter: FilterType) => {
|
const totalStudents = students.length
|
||||||
setFilter(newFilter)
|
const totalHours = students.reduce((sum, s) => sum + s.totalHours, 0)
|
||||||
setSelectedStudents([])
|
const totalFee = students.reduce((sum, s) => sum + s.totalFee, 0)
|
||||||
setBatchMode(false)
|
|
||||||
loadStudents(openid, 1, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打开添加弹窗
|
const handleAddStudent = () => {
|
||||||
const openAddDialog = () => {
|
|
||||||
setAddForm({ studentName: '', totalHours: '', notes: '' })
|
|
||||||
setAddDialogOpen(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加学员
|
|
||||||
const handleAddStudent = async () => {
|
|
||||||
if (!addForm.studentName.trim()) {
|
if (!addForm.studentName.trim()) {
|
||||||
Taro.showToast({ title: '请输入学员姓名', icon: 'none' })
|
Taro.showToast({ title: '请输入学员姓名', icon: 'none' })
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
setStudents(prev => prev.map(s =>
|
||||||
const res = await Network.request({
|
s.id === studentToEdit.id
|
||||||
url: '/api/students',
|
? {
|
||||||
method: 'POST',
|
...s,
|
||||||
data: {
|
studentName: editForm.studentName.trim(),
|
||||||
studentName: addForm.studentName.trim(),
|
notes: editForm.notes.trim(),
|
||||||
totalHours: parseFloat(addForm.totalHours),
|
totalHours: parseFloat(editForm.totalHours || '0'),
|
||||||
notes: addForm.notes.trim(),
|
totalFee: parseFloat(editForm.totalHours || '0') * 100
|
||||||
openid
|
}
|
||||||
}
|
: s
|
||||||
})
|
))
|
||||||
|
|
||||||
if (res.data && res.data.code === 200) {
|
Taro.showToast({ title: '修改成功', icon: 'success' })
|
||||||
Taro.showToast({ title: '添加成功', icon: 'success' })
|
setEditDialogOpen(false)
|
||||||
setAddDialogOpen(false)
|
|
||||||
loadStudents(openid, 1, true)
|
|
||||||
} else {
|
|
||||||
Taro.showToast({ title: res.data?.msg || '添加失败', icon: 'none' })
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Taro.showToast({ title: '添加失败', icon: 'none' })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开编辑弹窗
|
const handleDeleteStudent = () => {
|
||||||
const handleEditClick = (student: Student) => {
|
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)
|
setStudentToEdit(student)
|
||||||
setEditForm({
|
setEditForm({
|
||||||
studentName: student.studentName,
|
studentName: student.studentName,
|
||||||
totalHours: student.totalHours.toString(),
|
notes: student.notes,
|
||||||
notes: student.notes || ''
|
totalHours: String(student.totalHours)
|
||||||
})
|
})
|
||||||
setEditDialogOpen(true)
|
setEditDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确认编辑
|
const openDeleteDialog = (student: Student) => {
|
||||||
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) => {
|
|
||||||
setStudentToDelete(student)
|
setStudentToDelete(student)
|
||||||
setDeleteDialogOpen(true)
|
setDeleteDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmDelete = async () => {
|
const openCourseDialog = (student: Student) => {
|
||||||
if (!studentToDelete) return
|
setCurrentCourseStudent(student)
|
||||||
|
const savedSchedules = Taro.getStorageSync('schedules')
|
||||||
try {
|
if (savedSchedules && Array.isArray(savedSchedules)) {
|
||||||
const res = await Network.request({
|
const schedules = savedSchedules.filter((s: Schedule) => s.studentId === student.id)
|
||||||
url: `/api/students/${studentToDelete.id}?openid=${openid}`,
|
setStudentSchedules(schedules)
|
||||||
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([])
|
|
||||||
} else {
|
} else {
|
||||||
setSelectedStudents(students.map(s => s.id))
|
setStudentSchedules([])
|
||||||
}
|
}
|
||||||
|
setCourseDialogOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量操作
|
const handleExportPreview = () => {
|
||||||
const handleBatchAction = (action: 'graduated' | 'delete') => {
|
setExportPreviewOpen(true)
|
||||||
setBatchAction(action)
|
|
||||||
setBatchDialogOpen(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmBatchAction = async () => {
|
const handleConfirmExport = () => {
|
||||||
if (selectedStudents.length === 0) return
|
Taro.showToast({ title: '导出成功', icon: 'success' })
|
||||||
|
setExportPreviewOpen(false)
|
||||||
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 getProgressPercent = (used: number, total: number) => {
|
const getProgressPercent = (used: number, total: number) => {
|
||||||
if (total <= 0) return 0
|
if (total <= 0) return 0
|
||||||
const percent = (used / total) * 100
|
return Math.min((used / total) * 100, 100)
|
||||||
return Math.min(percent, 100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="page-container">
|
<View className="page-container">
|
||||||
<View className="page-header">
|
<View className="page-header">
|
||||||
<Text className="page-title">学员管理</Text>
|
<Text className="page-title">学员管理</Text>
|
||||||
<View className="add-btn-wrapper" onClick={openAddDialog}>
|
</View>
|
||||||
<Text className="add-btn-icon">+</Text>
|
|
||||||
|
<View className="stats-row">
|
||||||
|
<View className="stat-card">
|
||||||
|
<Text className="stat-value">{totalStudents}</Text>
|
||||||
|
<Text className="stat-label">学员总数</Text>
|
||||||
|
</View>
|
||||||
|
<View className="stat-card">
|
||||||
|
<Text className="stat-value">{totalHours.toFixed(1)}h</Text>
|
||||||
|
<Text className="stat-label">总课时</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="filter-section">
|
<View className="add-btn-container">
|
||||||
<View
|
<View className="add-student-btn" onClick={() => setAddDialogOpen(true)}>
|
||||||
className={`filter-tag ${filter === 'all' ? 'active' : ''}`}
|
<Text className="add-btn-text">+ 添加学员</Text>
|
||||||
onClick={() => onFilterChange('all')}
|
|
||||||
>
|
|
||||||
<Text className="filter-tag-text">全部</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View
|
|
||||||
className={`filter-tag ${filter === 'active' ? 'active' : ''}`}
|
|
||||||
onClick={() => onFilterChange('active')}
|
|
||||||
>
|
|
||||||
<Text className="filter-tag-text">在读</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
className={`filter-tag ${filter === 'graduated' ? 'active' : ''}`}
|
|
||||||
onClick={() => onFilterChange('graduated')}
|
|
||||||
>
|
|
||||||
<Text className="filter-tag-text">已结束</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!batchMode && students.length > 0 && (
|
|
||||||
<View
|
|
||||||
className="filter-tag batch-btn"
|
|
||||||
onClick={() => setBatchMode(true)}
|
|
||||||
>
|
|
||||||
<Text className="filter-tag-text">批量选择</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{batchMode && (
|
|
||||||
<View
|
|
||||||
className="filter-tag active"
|
|
||||||
onClick={toggleSelectAll}
|
|
||||||
>
|
|
||||||
<Text className="filter-tag-text">
|
|
||||||
{selectedStudents.length === students.length ? '取消全选' : '全选'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{batchMode && selectedStudents.length > 0 && (
|
<View className="search-box">
|
||||||
<View className="batch-action-bar">
|
<Text className="search-icon">🔍</Text>
|
||||||
<Text className="batch-count">已选择 {selectedStudents.length} 名学员</Text>
|
<Input
|
||||||
<View className="batch-buttons">
|
className="search-input"
|
||||||
<View
|
placeholder="搜索学员"
|
||||||
className="batch-action-btn end"
|
value={searchKey}
|
||||||
onClick={() => handleBatchAction('graduated')}
|
onChange={(e) => setSearchKey(e.detail.value)}
|
||||||
>
|
/>
|
||||||
<Text className="batch-action-text">批量结束上课</Text>
|
</View>
|
||||||
</View>
|
|
||||||
<View
|
<View className="sort-bar">
|
||||||
className="batch-action-btn delete"
|
{[
|
||||||
onClick={() => handleBatchAction('delete')}
|
{ key: 'name', label: '姓名' },
|
||||||
>
|
{ key: 'hours', label: '课时数' },
|
||||||
<Text className="batch-action-text">批量删除</Text>
|
{ key: 'duration', label: '时长' },
|
||||||
</View>
|
{ key: 'fee', label: '费用' }
|
||||||
</View>
|
].map(item => (
|
||||||
<View
|
<View
|
||||||
className="batch-cancel-btn"
|
key={item.key}
|
||||||
onClick={cancelBatchMode}
|
className={`sort-tag ${sortBy === item.key ? 'active' : ''}`}
|
||||||
|
onClick={() => setSortBy(item.key as typeof sortBy)}
|
||||||
>
|
>
|
||||||
<Text className="batch-cancel-text">取消</Text>
|
<Text className="sort-tag-text">{item.label}</Text>
|
||||||
|
{sortBy === item.key && <Text className="sort-arrow">↑</Text>}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
))}
|
||||||
)}
|
</View>
|
||||||
|
|
||||||
<View className="student-list">
|
<View className="student-list">
|
||||||
{students.length === 0 ? (
|
{filteredStudents.length === 0 ? (
|
||||||
<View className="empty-state">
|
<View className="empty-state">
|
||||||
<Text className="empty-icon">📋</Text>
|
<Text className="empty-icon">📋</Text>
|
||||||
<Text className="empty-text">暂无学员</Text>
|
<Text className="empty-text">暂无学员</Text>
|
||||||
<Text className="empty-hint">点击右上角添加按钮添加学员</Text>
|
<Text className="empty-hint">点击上方添加学员按钮添加</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
students.map((student) => (
|
filteredStudents.map(student => (
|
||||||
<View
|
<View key={student.id} className="student-card">
|
||||||
key={student.id}
|
<View className="student-header">
|
||||||
className={`student-card ${student.status === 1 ? 'graduated' : ''} ${batchMode ? 'batch-mode' : ''}`}
|
<View className="student-name-section">
|
||||||
>
|
<Text className="student-icon">👤</Text>
|
||||||
{batchMode && (
|
|
||||||
<View className="batch-checkbox">
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedStudents.includes(student.id)}
|
|
||||||
onCheckedChange={() => toggleStudentSelection(student.id)}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View className="student-card-header">
|
|
||||||
<View className="student-info">
|
|
||||||
<Text className="student-name">{student.studentName}</Text>
|
<Text className="student-name">{student.studentName}</Text>
|
||||||
<Text className="student-date">添加于 {formatDate(student.addTime)}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View className={`status-badge ${student.status === 0 ? 'active' : 'graduated'}`}>
|
<View className="student-actions">
|
||||||
<Text className="status-dot"></Text>
|
<View className="student-action-btn course-btn" onClick={() => openCourseDialog(student)}>
|
||||||
<Text>{student.status === 0 ? '在读' : '已结束'}</Text>
|
<Text className="student-action-text">课程</Text>
|
||||||
|
</View>
|
||||||
|
<View className="student-action-btn end-btn" onClick={() => {
|
||||||
|
setStudents(prev => prev.map(s =>
|
||||||
|
s.id === student.id ? { ...s, status: 1 } : s
|
||||||
|
))
|
||||||
|
Taro.showToast({ title: '已结课', icon: 'success' })
|
||||||
|
}}>
|
||||||
|
<Text className="student-action-text">结课</Text>
|
||||||
|
</View>
|
||||||
|
<View className="student-action-btn delete-btn" onClick={() => openDeleteDialog(student)}>
|
||||||
|
<Text className="student-action-text">删除</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="progress-section">
|
<View className="progress-section">
|
||||||
<View className="progress-header">
|
<Text className="progress-label">课时进度</Text>
|
||||||
<Text className="progress-label">课时进度</Text>
|
|
||||||
<Text className="progress-value">{student.usedHours || 0} / {student.totalHours} 小时</Text>
|
|
||||||
</View>
|
|
||||||
<View className="progress-bar-bg">
|
<View className="progress-bar-bg">
|
||||||
<View
|
<View
|
||||||
className={`progress-bar-fill ${getProgressPercent(student.usedHours || 0, student.totalHours) >= 100 ? 'complete' : ''}`}
|
className="progress-bar-fill"
|
||||||
style={{ width: `${getProgressPercent(student.usedHours || 0, student.totalHours)}%` }}
|
style={{ width: `${getProgressPercent(student.usedHours, student.totalHours)}%` }}
|
||||||
></View>
|
/>
|
||||||
|
</View>
|
||||||
|
<View className="progress-info">
|
||||||
|
<Text className="progress-used">已上 {student.usedHours.toFixed(1)}h</Text>
|
||||||
|
<Text className="progress-remaining">剩余 {(student.totalHours - student.usedHours).toFixed(1)}h</Text>
|
||||||
|
<Text className="progress-total">共 {student.totalHours.toFixed(1)}h</Text>
|
||||||
</View>
|
</View>
|
||||||
{student.notes && (
|
|
||||||
<Text className="student-notes">{student.notes}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{!batchMode && (
|
|
||||||
<View className="student-card-actions">
|
|
||||||
<View
|
|
||||||
className="action-btn edit"
|
|
||||||
onClick={() => handleEditClick(student)}
|
|
||||||
>
|
|
||||||
<Text className="action-text">编辑</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
className="action-btn delete"
|
|
||||||
onClick={() => handleDeleteClick(student)}
|
|
||||||
>
|
|
||||||
<Text className="action-text">删除</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View
|
|
||||||
className="action-btn status"
|
|
||||||
onClick={() => handleToggleStatus(student)}
|
|
||||||
>
|
|
||||||
<Text className="action-text">{student.status === 0 ? '结束' : '恢复'}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* 添加学员弹窗 */}
|
{addDialogOpen && (
|
||||||
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen} closeOnOverlayClick={false}>
|
<View className="custom-dialog-overlay" onClick={() => setAddDialogOpen(false)}>
|
||||||
<DialogContent title="添加学员">
|
<View className="custom-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
<View className="form-item">
|
<View className="custom-dialog-header">
|
||||||
<Text className="form-item-label">学员姓名 *</Text>
|
<Text className="custom-dialog-title">添加学员</Text>
|
||||||
<View className="form-input-wrapper">
|
|
||||||
<Input
|
|
||||||
className="form-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入学员姓名"
|
|
||||||
value={addForm.studentName}
|
|
||||||
onInput={(e: any) => setAddForm(prev => ({ ...prev, studentName: e.detail.value }))}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
<View className="custom-dialog-body">
|
||||||
<View className="form-item">
|
<View className="custom-form-item">
|
||||||
<Text className="form-item-label">总课时(小时) *</Text>
|
<Text className="custom-form-label">学员姓名 *</Text>
|
||||||
<View className="form-input-wrapper">
|
<View className="custom-form-input-wrapper">
|
||||||
<Input
|
<Input
|
||||||
className="form-input"
|
className="custom-form-input"
|
||||||
type="digit"
|
placeholder="请输入学员姓名"
|
||||||
placeholder="请输入总课时"
|
value={addForm.studentName}
|
||||||
value={addForm.totalHours}
|
onInput={(e) => setAddForm(prev => ({ ...prev, studentName: e.detail.value }))}
|
||||||
onInput={(e: any) => setAddForm(prev => ({ ...prev, totalHours: e.detail.value }))}
|
/>
|
||||||
/>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
<View className="form-item">
|
|
||||||
<Text className="form-item-label">备注(可选)</Text>
|
|
||||||
<View className="form-input-wrapper">
|
|
||||||
<Input
|
|
||||||
className="form-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入备注信息"
|
|
||||||
value={addForm.notes}
|
|
||||||
onInput={(e: any) => setAddForm(prev => ({ ...prev, notes: e.detail.value }))}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View className="dialog-footer">
|
|
||||||
<View
|
|
||||||
className="dialog-btn cancel"
|
|
||||||
onClick={() => setAddDialogOpen(false)}
|
|
||||||
>
|
|
||||||
<Text>取消</Text>
|
|
||||||
</View>
|
|
||||||
<View
|
|
||||||
className="dialog-btn confirm"
|
|
||||||
onClick={handleAddStudent}
|
|
||||||
>
|
|
||||||
<Text>添加</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 编辑学员弹窗 */}
|
<View className="custom-form-item">
|
||||||
|
<Text className="custom-form-label">备注 (可选)</Text>
|
||||||
|
<View className="custom-form-input-wrapper">
|
||||||
|
<Input
|
||||||
|
className="custom-form-input"
|
||||||
|
placeholder="如联系方式、课程类型等"
|
||||||
|
value={addForm.notes}
|
||||||
|
onInput={(e) => setAddForm(prev => ({ ...prev, notes: e.detail.value }))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="custom-form-item">
|
||||||
|
<Text className="custom-form-label">总课时 (小时,可选)</Text>
|
||||||
|
<View className="custom-form-input-wrapper">
|
||||||
|
<Input
|
||||||
|
className="custom-form-input"
|
||||||
|
placeholder="如 10、20、30"
|
||||||
|
type="digit"
|
||||||
|
value={addForm.totalHours}
|
||||||
|
onInput={(e) => setAddForm(prev => ({ ...prev, totalHours: e.detail.value }))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Text className="custom-form-hint">设置后可追踪课时进度</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="custom-dialog-buttons">
|
||||||
|
<View className="custom-dialog-btn custom-cancel-btn" onClick={() => setAddDialogOpen(false)}>
|
||||||
|
<Text className="custom-dialog-btn-text">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="custom-dialog-btn custom-confirm-btn" onClick={handleAddStudent}>
|
||||||
|
<Text className="custom-dialog-btn-text">确认添加</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen} closeOnOverlayClick={false}>
|
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen} closeOnOverlayClick={false}>
|
||||||
<DialogContent title="编辑学员">
|
<DialogContent className="dialog-content">
|
||||||
<View className="form-item">
|
<View className="dialog-header">
|
||||||
<Text className="form-item-label">学员姓名 *</Text>
|
<Text className="dialog-title">编辑学员</Text>
|
||||||
<View className="form-input-wrapper">
|
</View>
|
||||||
<Input
|
<View className="dialog-body">
|
||||||
className="form-input"
|
<View className="form-item">
|
||||||
type="text"
|
<Text className="form-label">学员姓名 *</Text>
|
||||||
placeholder="请输入学员姓名"
|
<View className="form-input-wrapper">
|
||||||
value={editForm.studentName}
|
<Input
|
||||||
onInput={(e: any) => setEditForm(prev => ({ ...prev, studentName: e.detail.value }))}
|
className="form-input"
|
||||||
/>
|
placeholder="请输入学员姓名"
|
||||||
|
value={editForm.studentName}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, studentName: e.detail.value }))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
|
||||||
<View className="form-item">
|
<View className="form-item">
|
||||||
<Text className="form-item-label">总课时(小时)</Text>
|
<Text className="form-label">备注 (可选)</Text>
|
||||||
<Input
|
<View className="form-input-wrapper">
|
||||||
className="form-input"
|
<Input
|
||||||
type="digit"
|
className="form-input"
|
||||||
placeholder="请输入总课时"
|
placeholder="如联系方式、课程类型等"
|
||||||
value={editForm.totalHours}
|
value={editForm.notes}
|
||||||
onInput={(e: any) => setEditForm(prev => ({ ...prev, totalHours: e.detail.value }))}
|
onChange={(e) => setEditForm(prev => ({ ...prev, notes: e.detail.value }))}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View className="form-item">
|
|
||||||
<Text className="form-item-label">备注</Text>
|
|
||||||
<Input
|
|
||||||
className="form-input"
|
|
||||||
type="text"
|
|
||||||
placeholder="请输入备注信息"
|
|
||||||
value={editForm.notes}
|
|
||||||
onInput={(e: any) => setEditForm(prev => ({ ...prev, notes: e.detail.value }))}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View className="dialog-footer">
|
|
||||||
<View
|
|
||||||
className="dialog-btn cancel"
|
|
||||||
onClick={() => setEditDialogOpen(false)}
|
|
||||||
>
|
|
||||||
<Text>取消</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View
|
|
||||||
className="dialog-btn confirm"
|
<View className="form-item">
|
||||||
onClick={handleConfirmEdit}
|
<Text className="form-label">总课时 (小时,可选)</Text>
|
||||||
>
|
<View className="form-input-wrapper">
|
||||||
<Text>保存</Text>
|
<Input
|
||||||
|
className="form-input"
|
||||||
|
placeholder="如 10、20、30"
|
||||||
|
type="digit"
|
||||||
|
value={editForm.totalHours}
|
||||||
|
onChange={(e) => setEditForm(prev => ({ ...prev, totalHours: e.detail.value }))}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="dialog-buttons">
|
||||||
|
<View className="dialog-btn cancel-btn" onClick={() => setEditDialogOpen(false)}>
|
||||||
|
<Text className="dialog-btn-text">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="dialog-btn confirm-btn" onClick={handleEditStudent}>
|
||||||
|
<Text className="dialog-btn-text">确认修改</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 删除确认弹窗 */}
|
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} closeOnOverlayClick={false}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen} closeOnOverlayClick={false}>
|
||||||
<DialogContent title="确认删除">
|
<DialogContent className="dialog-content">
|
||||||
<Text className="dialog-text">
|
<View className="dialog-header">
|
||||||
确认删除学员 {studentToDelete?.studentName} 吗?删除后将同时删除该学员的所有课时记录。
|
<Text className="dialog-title">确认删除</Text>
|
||||||
</Text>
|
</View>
|
||||||
<View className="dialog-footer">
|
<View className="dialog-body">
|
||||||
<View
|
<Text className="dialog-message">确定要删除该学员吗?</Text>
|
||||||
className="dialog-btn cancel"
|
<View className="dialog-buttons">
|
||||||
onClick={() => setDeleteDialogOpen(false)}
|
<View className="dialog-btn cancel-btn" onClick={() => setDeleteDialogOpen(false)}>
|
||||||
>
|
<Text className="dialog-btn-text">取消</Text>
|
||||||
<Text>取消</Text>
|
</View>
|
||||||
</View>
|
<View className="dialog-btn confirm-btn" onClick={handleDeleteStudent}>
|
||||||
<View
|
<Text className="dialog-btn-text">确认删除</Text>
|
||||||
className="dialog-btn confirm delete"
|
</View>
|
||||||
onClick={handleConfirmDelete}
|
|
||||||
>
|
|
||||||
<Text>删除</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* 批量操作确认弹窗 */}
|
<Dialog open={courseDialogOpen} onOpenChange={setCourseDialogOpen} closeOnOverlayClick={false}>
|
||||||
<Dialog open={batchDialogOpen} onOpenChange={setBatchDialogOpen} closeOnOverlayClick={false}>
|
<DialogContent className="course-dialog">
|
||||||
<DialogContent title="确认操作">
|
<View className="course-header">
|
||||||
<Text className="dialog-text">
|
<Text className="course-title">
|
||||||
{batchAction === 'delete'
|
{currentCourseStudent?.studentName} 的课程安排
|
||||||
? `确认删除选中的 ${selectedStudents.length} 名学员吗?删除后将同时删除这些学员的所有课时记录。`
|
</Text>
|
||||||
: `确认将选中的 ${selectedStudents.length} 名学员标记为结束上课吗?`
|
</View>
|
||||||
}
|
|
||||||
</Text>
|
<View className="course-content">
|
||||||
<View className="dialog-footer">
|
{studentSchedules.length === 0 ? (
|
||||||
<View
|
<View className="empty-course">
|
||||||
className="dialog-btn cancel"
|
<Text className="empty-course-text">暂无课程安排</Text>
|
||||||
onClick={() => setBatchDialogOpen(false)}
|
</View>
|
||||||
>
|
) : (
|
||||||
<Text>取消</Text>
|
<View className="course-list">
|
||||||
|
{studentSchedules.map((schedule, index) => (
|
||||||
|
<View key={schedule.id || index} className="course-item">
|
||||||
|
<Text className="course-date">{schedule.date}</Text>
|
||||||
|
<Text className="course-time">{schedule.startTime} - {schedule.endTime}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
<View className="course-summary">
|
||||||
|
<Text className="summary-text">共 {studentSchedules.length} 节课程</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="course-footer">
|
||||||
|
<View className="course-btn course-close-btn" onClick={() => setCourseDialogOpen(false)}>
|
||||||
|
<Text className="course-btn-text">关闭</Text>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View className="course-btn export-btn" onClick={handleExportPreview}>
|
||||||
className="dialog-btn confirm"
|
<Text className="course-btn-text">导出图片</Text>
|
||||||
onClick={confirmBatchAction}
|
|
||||||
>
|
|
||||||
<Text>确认</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<View style={{ height: '100px' }} />
|
<Dialog open={exportPreviewOpen} onOpenChange={setExportPreviewOpen} closeOnOverlayClick={false}>
|
||||||
|
<DialogContent className="export-preview-dialog">
|
||||||
|
<View className="export-preview-header">
|
||||||
|
<Text className="export-preview-title">课程表预览</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="export-preview-content">
|
||||||
|
<View className="preview-card">
|
||||||
|
<Text className="preview-student-name">{currentCourseStudent?.studentName} 课程表</Text>
|
||||||
|
<Text className="preview-course-count">共 {studentSchedules.length} 节课程</Text>
|
||||||
|
|
||||||
|
<View className="preview-divider" />
|
||||||
|
|
||||||
|
<View className="preview-table-header">
|
||||||
|
<Text className="preview-th">日期</Text>
|
||||||
|
<Text className="preview-th">时间</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="preview-table-body">
|
||||||
|
{studentSchedules.length === 0 ? (
|
||||||
|
<View className="preview-empty">
|
||||||
|
<Text className="preview-empty-text">暂无课程</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
studentSchedules.map((schedule, index) => (
|
||||||
|
<View key={schedule.id || index} className="preview-table-row">
|
||||||
|
<Text className="preview-td">{schedule.date}</Text>
|
||||||
|
<Text className="preview-td">{schedule.startTime} - {schedule.endTime}</Text>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className="export-preview-footer">
|
||||||
|
<View className="export-btn cancel" onClick={() => setExportPreviewOpen(false)}>
|
||||||
|
<Text className="export-btn-text">取消</Text>
|
||||||
|
</View>
|
||||||
|
<View className="export-btn confirm" onClick={handleConfirmExport}>
|
||||||
|
<Text className="export-btn-text">确认导出</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue