2026-03-19 18:14:25 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<a-modal
|
|
|
|
|
|
v-model:open="visible"
|
|
|
|
|
|
title="预约上课"
|
|
|
|
|
|
:confirm-loading="loading"
|
|
|
|
|
|
@ok="handleSubmit"
|
|
|
|
|
|
@cancel="handleCancel"
|
|
|
|
|
|
width="700px"
|
|
|
|
|
|
destroy-on-close
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
|
|
|
|
|
<a-step title="选择课程包" />
|
|
|
|
|
|
<a-step title="选择课程类型" />
|
|
|
|
|
|
<a-step title="选择班级" />
|
|
|
|
|
|
<a-step title="设置时间" />
|
|
|
|
|
|
</a-steps>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="step-content">
|
2026-03-23 14:31:48 +08:00
|
|
|
|
<!-- 步骤1: 选择课程包(多套餐时顶部展示套餐选择器) -->
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<div v-show="currentStep === 0" class="step-panel">
|
2026-03-19 18:31:54 +08:00
|
|
|
|
<h3>选择课程包</h3>
|
2026-03-23 14:09:03 +08:00
|
|
|
|
<!-- 多套餐时:先选套餐 -->
|
|
|
|
|
|
<div v-if="collections.length > 1" class="collection-selector">
|
|
|
|
|
|
<div class="selector-label">选择课程套餐</div>
|
|
|
|
|
|
<a-radio-group v-model:value="formData.collectionId" button-style="solid" class="collection-radio-group" @change="onCollectionChange">
|
|
|
|
|
|
<a-radio-button
|
|
|
|
|
|
v-for="col in collections"
|
|
|
|
|
|
:key="col.id"
|
|
|
|
|
|
:value="col.id as number"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ col.name }}
|
|
|
|
|
|
</a-radio-button>
|
|
|
|
|
|
</a-radio-group>
|
|
|
|
|
|
</div>
|
2026-03-19 18:31:54 +08:00
|
|
|
|
<a-spin :spinning="loadingPackages">
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
2026-03-23 14:09:03 +08:00
|
|
|
|
<div v-if="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<div class="packages-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="pkg in selectedCollection.packages"
|
|
|
|
|
|
:key="pkg.id"
|
2026-03-23 14:31:48 +08:00
|
|
|
|
:class="['package-card', { active: formData.packageId == pkg.id }]"
|
|
|
|
|
|
@click="selectPackage(Number(pkg.id))"
|
2026-03-19 18:14:25 +08:00
|
|
|
|
>
|
|
|
|
|
|
<div class="package-name">{{ pkg.name }}</div>
|
|
|
|
|
|
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
|
|
|
|
|
|
<div class="package-count">{{ pkg.courseCount }} 门课程</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-23 10:20:24 +08:00
|
|
|
|
<div v-else-if="!loadingPackages && selectedCollection && (!selectedCollection.packages || selectedCollection.packages.length === 0)" class="packages-section">
|
|
|
|
|
|
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
|
2026-03-19 18:31:54 +08:00
|
|
|
|
</div>
|
2026-03-23 10:20:24 +08:00
|
|
|
|
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
|
|
|
|
|
|
<a-alert message="请先选择套餐" type="info" show-icon />
|
2026-03-19 18:31:54 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 排课计划参考(与学校端一致) -->
|
|
|
|
|
|
<div v-if="scheduleRefDisplay.length > 0" class="schedule-ref-card">
|
|
|
|
|
|
<div class="ref-header">
|
|
|
|
|
|
<CalendarOutlined class="ref-icon" />
|
|
|
|
|
|
<span class="ref-title">排课计划参考</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-table
|
|
|
|
|
|
:columns="scheduleRefColumns"
|
|
|
|
|
|
:data-source="scheduleRefDisplay"
|
|
|
|
|
|
:pagination="false"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
bordered
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #bodyCell="{ column, record }">
|
|
|
|
|
|
<template v-if="column.key === 'dayOfWeek'">
|
|
|
|
|
|
{{ formatDayOfWeek(record.dayOfWeek) }}
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else-if="column.key === 'lessonType'">
|
|
|
|
|
|
<a-tag v-if="record.lessonType" size="small" :style="getLessonTagStyle(record.lessonType)">
|
|
|
|
|
|
{{ getLessonTypeName(record.lessonType) }}
|
|
|
|
|
|
</a-tag>
|
|
|
|
|
|
<span v-else>-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-table>
|
2026-03-19 18:14:25 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-23 14:31:48 +08:00
|
|
|
|
<!-- 步骤2: 选择课程类型 -->
|
|
|
|
|
|
<div v-show="currentStep === 1" class="step-panel">
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<h3>选择课程类型</h3>
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
message="请选择一个课程类型进行排课"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom: 16px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-spin :spinning="loadingLessonTypes">
|
|
|
|
|
|
<div v-if="!loadingLessonTypes && lessonTypes.length === 0" class="lesson-type-empty">
|
|
|
|
|
|
该课程包暂无课程类型,请先选择其他课程包
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="lesson-type-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="type in lessonTypes"
|
|
|
|
|
|
:key="type.lessonType"
|
|
|
|
|
|
:class="['lesson-type-card', { active: formData.lessonType === type.lessonType }]"
|
|
|
|
|
|
:style="getLessonTagStyle(type.lessonType)"
|
|
|
|
|
|
@click="selectLessonType(type.lessonType)"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="type-icon">
|
|
|
|
|
|
<component :is="getLessonTypeIconComponent(type.lessonType)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="type-name">{{ getLessonTypeName(type.lessonType) }}</div>
|
|
|
|
|
|
<div class="type-count">{{ type.count }} 节课</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-spin>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-23 14:31:48 +08:00
|
|
|
|
<!-- 步骤3: 选择班级(教师端单选) -->
|
|
|
|
|
|
<div v-show="currentStep === 2" class="step-panel">
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<h3>选择班级</h3>
|
|
|
|
|
|
<a-alert
|
|
|
|
|
|
message="请选择要排课的班级"
|
|
|
|
|
|
type="info"
|
|
|
|
|
|
show-icon
|
|
|
|
|
|
style="margin-bottom: 16px"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div class="class-select-grid">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="cls in myClasses"
|
|
|
|
|
|
:key="cls.id"
|
|
|
|
|
|
:class="['class-card', { active: formData.classId === cls.id }]"
|
|
|
|
|
|
@click="formData.classId = cls.id"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="class-name">{{ cls.name }}</div>
|
|
|
|
|
|
<div class="class-detail">{{ cls.studentCount }} 名学生</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-23 14:31:48 +08:00
|
|
|
|
<!-- 步骤4: 设置时间 -->
|
|
|
|
|
|
<div v-show="currentStep === 3" class="step-panel">
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<h3>设置时间</h3>
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<a-form-item label="排课日期" required>
|
|
|
|
|
|
<a-date-picker
|
|
|
|
|
|
v-model:value="formData.scheduledDate"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
placeholder="选择日期"
|
2026-03-19 18:31:54 +08:00
|
|
|
|
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
2026-03-19 18:14:25 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
|
|
|
|
|
|
<a-form-item label="时间段" required>
|
|
|
|
|
|
<a-time-range-picker
|
|
|
|
|
|
v-model:value="formData.scheduledTimeRange"
|
|
|
|
|
|
format="HH:mm"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
:placeholder="['开始时间', '结束时间']"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="confirm-info">
|
|
|
|
|
|
<a-alert type="info" show-icon>
|
|
|
|
|
|
<template #message>
|
|
|
|
|
|
<div>将为班级 <strong>{{ selectedClassName }}</strong> 创建排课</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template #description>
|
|
|
|
|
|
<div>课程类型: {{ getSelectedLessonTypeName() }}</div>
|
|
|
|
|
|
<div>排课日期: {{ formData.scheduledDate?.format('YYYY-MM-DD') || '-' }}</div>
|
|
|
|
|
|
<div>时间段: {{ getSelectedTimeRange() || '-' }}</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-alert>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<div class="modal-footer">
|
|
|
|
|
|
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
2026-03-23 14:31:48 +08:00
|
|
|
|
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
|
2026-03-19 18:14:25 +08:00
|
|
|
|
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
|
|
|
|
|
<a-button @click="handleCancel">取消</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-modal>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { ref, reactive, computed } from 'vue';
|
|
|
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
|
import dayjs, { Dayjs } from 'dayjs';
|
|
|
|
|
|
import {
|
|
|
|
|
|
BookOutlined,
|
2026-03-19 18:31:54 +08:00
|
|
|
|
CalendarOutlined,
|
2026-03-19 18:14:25 +08:00
|
|
|
|
TeamOutlined,
|
|
|
|
|
|
HeartOutlined,
|
|
|
|
|
|
SoundOutlined,
|
|
|
|
|
|
UsergroupAddOutlined,
|
|
|
|
|
|
ExperimentOutlined,
|
|
|
|
|
|
BgColorsOutlined,
|
|
|
|
|
|
} from '@ant-design/icons-vue';
|
|
|
|
|
|
import {
|
|
|
|
|
|
getCourseCollections,
|
|
|
|
|
|
getCourseCollectionPackages,
|
|
|
|
|
|
getCoursePackageLessonTypes,
|
|
|
|
|
|
type CourseCollection,
|
|
|
|
|
|
type LessonTypeInfo,
|
|
|
|
|
|
} from '@/api/school';
|
|
|
|
|
|
import { getTeacherClasses, createTeacherSchedule, type TeacherClass } from '@/api/teacher';
|
|
|
|
|
|
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
(e: 'success'): void;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
|
|
|
|
|
|
const visible = ref(false);
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
const loadingLessonTypes = ref(false);
|
|
|
|
|
|
const currentStep = ref(0);
|
2026-03-23 10:20:24 +08:00
|
|
|
|
/** 从课程详情进入时,跳过套餐与课程包选择 */
|
|
|
|
|
|
const isPresetMode = ref(false);
|
2026-03-19 18:14:25 +08:00
|
|
|
|
|
|
|
|
|
|
const collections = ref<CourseCollection[]>([]);
|
2026-03-19 18:31:54 +08:00
|
|
|
|
const loadingPackages = ref(false);
|
2026-03-19 18:14:25 +08:00
|
|
|
|
const lessonTypes = ref<LessonTypeInfo[]>([]);
|
|
|
|
|
|
const myClasses = ref<TeacherClass[]>([]);
|
|
|
|
|
|
|
2026-03-19 18:31:54 +08:00
|
|
|
|
// 排课计划参考数据
|
|
|
|
|
|
const scheduleRefData = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 排课计划参考表格列(与学校端一致)
|
|
|
|
|
|
const scheduleRefColumns = [
|
|
|
|
|
|
{ title: '时间', dataIndex: 'dayOfWeek', key: 'dayOfWeek', width: 80 },
|
|
|
|
|
|
{ title: '课程类型', dataIndex: 'lessonType', key: 'lessonType', width: 100 },
|
|
|
|
|
|
{ title: '课程名称', dataIndex: 'lessonName', key: 'lessonName' },
|
|
|
|
|
|
{ title: '区域活动', dataIndex: 'activity', key: 'activity' },
|
|
|
|
|
|
{ title: '备注', dataIndex: 'note', key: 'note' },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const weekDayNames: Record<number, string> = {
|
|
|
|
|
|
1: '周一',
|
|
|
|
|
|
2: '周二',
|
|
|
|
|
|
3: '周三',
|
|
|
|
|
|
4: '周四',
|
|
|
|
|
|
5: '周五',
|
|
|
|
|
|
6: '周六',
|
|
|
|
|
|
0: '周日',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatDayOfWeek = (val: number | string | undefined): string => {
|
|
|
|
|
|
if (val === undefined || val === null) return '-';
|
|
|
|
|
|
if (typeof val === 'string') {
|
|
|
|
|
|
if (/^[一二三四五六日]/.test(val) || val.startsWith('周')) return val;
|
|
|
|
|
|
const n = parseInt(val, 10);
|
|
|
|
|
|
if (!isNaN(n)) return weekDayNames[n] ?? weekDayNames[n as 0] ?? '-';
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof val === 'number') return weekDayNames[val as 0] ?? '-';
|
|
|
|
|
|
return '-';
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeScheduleRefData = (raw: any[]): any[] => {
|
|
|
|
|
|
if (!Array.isArray(raw) || raw.length === 0) return [];
|
|
|
|
|
|
const first = raw[0];
|
|
|
|
|
|
const isWeeklyFormat = 'dayOfWeek' in first || 'lessonName' in first || 'activity' in first;
|
|
|
|
|
|
const isLessonMetaFormat = 'title' in first && ('suggestedOrder' in first || 'description' in first);
|
|
|
|
|
|
if (isWeeklyFormat && !isLessonMetaFormat) {
|
|
|
|
|
|
return raw.map((r, i) => ({
|
|
|
|
|
|
key: r.key ?? `row_${i}`,
|
|
|
|
|
|
dayOfWeek: r.dayOfWeek,
|
|
|
|
|
|
lessonType: r.lessonType,
|
|
|
|
|
|
lessonName: r.lessonName ?? r.title ?? '-',
|
|
|
|
|
|
activity: r.activity ?? r.description ?? '-',
|
|
|
|
|
|
note: r.note ?? r.tips ?? r.frequency ?? '-',
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
return raw.map((r, i) => ({
|
|
|
|
|
|
key: r.key ?? `row_${i}`,
|
|
|
|
|
|
dayOfWeek: r.dayOfWeek ?? '-',
|
|
|
|
|
|
lessonType: r.lessonType,
|
|
|
|
|
|
lessonName: r.title ?? r.lessonName ?? '-',
|
|
|
|
|
|
activity: r.description ?? r.activity ?? (Array.isArray(r.keyPoints) ? r.keyPoints.join(';') : '-'),
|
|
|
|
|
|
note: r.tips ?? r.frequency ?? r.note ?? '-',
|
|
|
|
|
|
}));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const scheduleRefDisplay = computed(() => normalizeScheduleRefData(scheduleRefData.value));
|
|
|
|
|
|
|
2026-03-19 18:14:25 +08:00
|
|
|
|
interface FormData {
|
|
|
|
|
|
collectionId?: number;
|
|
|
|
|
|
packageId?: number;
|
|
|
|
|
|
courseId?: number;
|
|
|
|
|
|
lessonType?: string;
|
|
|
|
|
|
classId?: number;
|
|
|
|
|
|
scheduledDate?: Dayjs;
|
|
|
|
|
|
scheduledTimeRange?: [Dayjs, Dayjs];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const formData = reactive<FormData>({});
|
|
|
|
|
|
|
|
|
|
|
|
const selectedCollection = computed(() => {
|
|
|
|
|
|
if (!formData.collectionId) return null;
|
|
|
|
|
|
return collections.value.find(c => c.id === formData.collectionId) || null;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const selectedClassName = computed(() => {
|
|
|
|
|
|
if (!formData.classId) return '-';
|
|
|
|
|
|
const cls = myClasses.value.find(c => c.id === formData.classId);
|
|
|
|
|
|
return cls?.name || '-';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** 从课程中心打开时的预设(跳过步骤1、2) */
|
|
|
|
|
|
export interface SchedulePreset {
|
|
|
|
|
|
packageId: number;
|
|
|
|
|
|
courseId: number;
|
|
|
|
|
|
lessonType: string;
|
|
|
|
|
|
classId?: number;
|
2026-03-23 10:20:24 +08:00
|
|
|
|
/** 可选,若已知所属套餐可传入以节省请求 */
|
|
|
|
|
|
collectionId?: number;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const open = () => {
|
2026-03-23 10:20:24 +08:00
|
|
|
|
isPresetMode.value = false;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
visible.value = true;
|
|
|
|
|
|
currentStep.value = 0;
|
|
|
|
|
|
resetForm();
|
|
|
|
|
|
loadCollections();
|
|
|
|
|
|
loadMyClasses();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 10:20:24 +08:00
|
|
|
|
/** 从课程详情打开:跳过套餐与课程包,从选择课程类型开始 */
|
2026-03-19 18:14:25 +08:00
|
|
|
|
const openWithPreset = async (preset: SchedulePreset) => {
|
2026-03-23 10:20:24 +08:00
|
|
|
|
isPresetMode.value = true;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
visible.value = true;
|
|
|
|
|
|
resetForm();
|
|
|
|
|
|
formData.packageId = preset.packageId;
|
|
|
|
|
|
formData.courseId = preset.courseId;
|
|
|
|
|
|
formData.lessonType = preset.lessonType;
|
|
|
|
|
|
formData.classId = preset.classId;
|
|
|
|
|
|
|
|
|
|
|
|
await loadMyClasses();
|
2026-03-23 10:20:24 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await loadLessonTypes(preset.packageId);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// 校本课程等场景可能无对应课程包接口,使用预设类型填充
|
|
|
|
|
|
lessonTypes.value = preset.lessonType
|
2026-03-23 14:31:48 +08:00
|
|
|
|
? [{ lessonType: preset.lessonType, lessonTypeName: getLessonTypeName(preset.lessonType), count: 1 }]
|
2026-03-23 10:20:24 +08:00
|
|
|
|
: [];
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
2026-03-23 10:20:24 +08:00
|
|
|
|
|
2026-03-23 14:31:48 +08:00
|
|
|
|
// 从选择课程类型开始,跳过课程包选择
|
|
|
|
|
|
currentStep.value = 1;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const resetForm = () => {
|
|
|
|
|
|
formData.collectionId = undefined;
|
|
|
|
|
|
formData.packageId = undefined;
|
|
|
|
|
|
formData.courseId = undefined;
|
|
|
|
|
|
formData.lessonType = undefined;
|
|
|
|
|
|
formData.classId = undefined;
|
|
|
|
|
|
formData.scheduledDate = undefined;
|
|
|
|
|
|
formData.scheduledTimeRange = undefined;
|
2026-03-19 18:31:54 +08:00
|
|
|
|
scheduleRefData.value = [];
|
2026-03-19 18:14:25 +08:00
|
|
|
|
lessonTypes.value = [];
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 10:20:24 +08:00
|
|
|
|
// 加载课程套餐列表(租户可拥有多个套餐)
|
2026-03-19 18:14:25 +08:00
|
|
|
|
const loadCollections = async () => {
|
2026-03-19 18:31:54 +08:00
|
|
|
|
loadingPackages.value = true;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
try {
|
|
|
|
|
|
collections.value = await getCourseCollections();
|
2026-03-23 14:09:03 +08:00
|
|
|
|
if (collections.value.length > 0) {
|
|
|
|
|
|
const first = collections.value[0];
|
|
|
|
|
|
formData.collectionId = first.id as number;
|
2026-03-23 14:31:48 +08:00
|
|
|
|
// 若仅有一个套餐,自动加载其课程包
|
|
|
|
|
|
if (collections.value.length === 1) {
|
|
|
|
|
|
await selectCollection(first);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await loadPackagesForCollection(first.id);
|
2026-03-23 14:09:03 +08:00
|
|
|
|
}
|
2026-03-19 18:31:54 +08:00
|
|
|
|
}
|
2026-03-19 18:14:25 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载课程套餐失败:', error);
|
|
|
|
|
|
message.error('加载课程套餐失败');
|
2026-03-19 18:31:54 +08:00
|
|
|
|
} finally {
|
|
|
|
|
|
loadingPackages.value = false;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-23 10:20:24 +08:00
|
|
|
|
// 选择套餐,并加载该套餐下的课程包
|
|
|
|
|
|
const selectCollection = async (coll: CourseCollection) => {
|
|
|
|
|
|
formData.collectionId = coll.id as number;
|
|
|
|
|
|
formData.packageId = undefined;
|
|
|
|
|
|
formData.courseId = undefined;
|
|
|
|
|
|
scheduleRefData.value = [];
|
|
|
|
|
|
lessonTypes.value = [];
|
|
|
|
|
|
|
|
|
|
|
|
if (!coll.id) return;
|
|
|
|
|
|
loadingPackages.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const packages = await getCourseCollectionPackages(coll.id);
|
|
|
|
|
|
(coll as any).packages = packages || [];
|
|
|
|
|
|
if (!packages || packages.length === 0) {
|
|
|
|
|
|
message.warning('该套餐暂无课程包');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('加载套餐课程包失败:', error);
|
|
|
|
|
|
message.error('加载课程包失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingPackages.value = false;
|
2026-03-23 14:31:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 加载指定套餐下的课程包
|
|
|
|
|
|
const loadPackagesForCollection = async (collectionId: number | string) => {
|
|
|
|
|
|
const col = collections.value.find(c => c.id === collectionId);
|
|
|
|
|
|
if (!col) return;
|
|
|
|
|
|
const packages = await getCourseCollectionPackages(collectionId);
|
|
|
|
|
|
(col as any).packages = packages;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换套餐时重新加载课程包并清空已选课程包(步骤2 多套餐时的选择器)
|
|
|
|
|
|
const onCollectionChange = async () => {
|
|
|
|
|
|
formData.packageId = undefined;
|
|
|
|
|
|
formData.courseId = undefined;
|
|
|
|
|
|
scheduleRefData.value = [];
|
|
|
|
|
|
lessonTypes.value = [];
|
2026-03-23 14:09:03 +08:00
|
|
|
|
const colId = formData.collectionId;
|
|
|
|
|
|
if (colId) {
|
|
|
|
|
|
loadingPackages.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await loadPackagesForCollection(colId);
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingPackages.value = false;
|
|
|
|
|
|
}
|
2026-03-23 10:20:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-19 18:14:25 +08:00
|
|
|
|
const loadMyClasses = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
myClasses.value = await getTeacherClasses();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('加载班级失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectPackage = async (packageId: number) => {
|
|
|
|
|
|
formData.packageId = packageId;
|
|
|
|
|
|
|
|
|
|
|
|
const collection = selectedCollection.value;
|
|
|
|
|
|
if (collection?.packages) {
|
2026-03-23 14:31:48 +08:00
|
|
|
|
const pkg = collection.packages.find((p: any) => p.id == packageId || Number(p.id) === packageId);
|
2026-03-19 18:14:25 +08:00
|
|
|
|
if (pkg?.courses && pkg.courses.length > 0) {
|
|
|
|
|
|
formData.courseId = pkg.courses[0].id;
|
2026-03-19 18:31:54 +08:00
|
|
|
|
|
|
|
|
|
|
// 加载排课计划参考(从课程包中任一课程的 scheduleRefData 获取)
|
|
|
|
|
|
const courseWithRef = pkg.courses.find((c: any) => c.scheduleRefData);
|
|
|
|
|
|
const rawRef = courseWithRef?.scheduleRefData ?? (pkg as any).scheduleRefData;
|
|
|
|
|
|
if (rawRef) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = typeof rawRef === 'string' ? JSON.parse(rawRef) : rawRef;
|
|
|
|
|
|
scheduleRefData.value = Array.isArray(parsed) ? parsed : [];
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('解析排课计划参考失败:', e);
|
|
|
|
|
|
scheduleRefData.value = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
scheduleRefData.value = [];
|
|
|
|
|
|
}
|
2026-03-19 18:14:25 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
formData.courseId = undefined;
|
2026-03-19 18:31:54 +08:00
|
|
|
|
scheduleRefData.value = [];
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await loadLessonTypes(packageId);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const loadLessonTypes = async (packageId: number) => {
|
|
|
|
|
|
loadingLessonTypes.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
lessonTypes.value = await getCoursePackageLessonTypes(packageId);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('加载课程类型失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loadingLessonTypes.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const selectLessonType = (lessonType: string) => {
|
|
|
|
|
|
formData.lessonType = lessonType;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const lessonTypeIconMap: Record<string, any> = {
|
|
|
|
|
|
INTRODUCTION: BookOutlined,
|
|
|
|
|
|
INTRO: BookOutlined,
|
|
|
|
|
|
COLLECTIVE: TeamOutlined,
|
|
|
|
|
|
LANGUAGE: SoundOutlined,
|
|
|
|
|
|
DOMAIN_LANGUAGE: SoundOutlined,
|
|
|
|
|
|
SOCIETY: UsergroupAddOutlined,
|
|
|
|
|
|
SOCIAL: UsergroupAddOutlined,
|
|
|
|
|
|
DOMAIN_SOCIAL: UsergroupAddOutlined,
|
|
|
|
|
|
SCIENCE: ExperimentOutlined,
|
|
|
|
|
|
DOMAIN_SCIENCE: ExperimentOutlined,
|
|
|
|
|
|
ART: BgColorsOutlined,
|
|
|
|
|
|
DOMAIN_ART: BgColorsOutlined,
|
|
|
|
|
|
HEALTH: HeartOutlined,
|
|
|
|
|
|
DOMAIN_HEALTH: HeartOutlined,
|
|
|
|
|
|
MUSIC: SoundOutlined,
|
|
|
|
|
|
SPORT: HeartOutlined,
|
|
|
|
|
|
OUTDOOR: TeamOutlined,
|
|
|
|
|
|
};
|
|
|
|
|
|
const getLessonTypeIconComponent = (type: string) => lessonTypeIconMap[type] || BookOutlined;
|
|
|
|
|
|
|
|
|
|
|
|
const getSelectedLessonTypeName = (): string => {
|
|
|
|
|
|
if (!formData.lessonType) return '-';
|
|
|
|
|
|
return getLessonTypeName(formData.lessonType);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const getSelectedTimeRange = (): string => {
|
|
|
|
|
|
if (!formData.scheduledTimeRange) return '';
|
|
|
|
|
|
return `${formData.scheduledTimeRange[0].format('HH:mm')}-${formData.scheduledTimeRange[1].format('HH:mm')}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const validateStep = (): boolean => {
|
|
|
|
|
|
switch (currentStep.value) {
|
|
|
|
|
|
case 0:
|
2026-03-23 10:20:24 +08:00
|
|
|
|
if (!formData.collectionId) {
|
|
|
|
|
|
message.warning('请选择套餐');
|
2026-03-19 18:14:25 +08:00
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!formData.packageId) {
|
|
|
|
|
|
message.warning('请选择课程包');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!formData.courseId) {
|
|
|
|
|
|
message.warning('课程包数据异常,请联系管理员');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-03-23 14:31:48 +08:00
|
|
|
|
case 1:
|
2026-03-19 18:14:25 +08:00
|
|
|
|
if (!formData.lessonType) {
|
|
|
|
|
|
message.warning('请选择课程类型');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-03-23 14:31:48 +08:00
|
|
|
|
case 2:
|
2026-03-19 18:14:25 +08:00
|
|
|
|
if (!formData.classId) {
|
|
|
|
|
|
message.warning('请选择班级');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-03-23 14:31:48 +08:00
|
|
|
|
case 3:
|
2026-03-19 18:14:25 +08:00
|
|
|
|
if (!formData.scheduledDate) {
|
|
|
|
|
|
message.warning('请选择排课日期');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!formData.scheduledTimeRange) {
|
|
|
|
|
|
message.warning('请选择时间段');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const nextStep = () => {
|
|
|
|
|
|
if (validateStep()) currentStep.value++;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const prevStep = () => {
|
2026-03-23 14:31:48 +08:00
|
|
|
|
// 预设模式下从选择课程类型点击上一步时关闭弹窗(不展示课程包选择)
|
|
|
|
|
|
if (isPresetMode.value && currentStep.value === 1) {
|
2026-03-23 10:20:24 +08:00
|
|
|
|
handleCancel();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-19 18:14:25 +08:00
|
|
|
|
currentStep.value--;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
|
if (!validateStep()) return;
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
let scheduledTime: string | undefined;
|
|
|
|
|
|
if (formData.scheduledTimeRange && formData.scheduledTimeRange.length === 2) {
|
|
|
|
|
|
scheduledTime = `${formData.scheduledTimeRange[0].format('HH:mm')}-${formData.scheduledTimeRange[1].format('HH:mm')}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dateStr = formData.scheduledDate!.format('YYYY-MM-DD');
|
|
|
|
|
|
const lessonTypeName = getSelectedLessonTypeName();
|
|
|
|
|
|
const name = `${lessonTypeName} - ${dateStr}`;
|
|
|
|
|
|
|
|
|
|
|
|
await createTeacherSchedule({
|
|
|
|
|
|
name,
|
|
|
|
|
|
classId: formData.classId!,
|
|
|
|
|
|
courseId: formData.courseId!,
|
|
|
|
|
|
coursePackageId: formData.packageId,
|
|
|
|
|
|
lessonType: formData.lessonType,
|
|
|
|
|
|
scheduledDate: dateStr,
|
|
|
|
|
|
scheduledTime,
|
|
|
|
|
|
repeatType: 'NONE',
|
|
|
|
|
|
note: undefined,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
message.success('预约成功');
|
|
|
|
|
|
visible.value = false;
|
|
|
|
|
|
emit('success');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
message.error('预约失败');
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
visible.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
defineExpose({ open, openWithPreset });
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
|
.steps-navigator {
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-content {
|
|
|
|
|
|
min-height: 400px;
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.step-panel {
|
|
|
|
|
|
h3 {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
color: #2D3436;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
h4 {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-23 14:09:03 +08:00
|
|
|
|
.collection-selector {
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #FAFAFA;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.selector-label {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collection-radio-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 18:14:25 +08:00
|
|
|
|
.collection-option {
|
|
|
|
|
|
.collection-name { font-weight: 500; }
|
|
|
|
|
|
.collection-info { font-size: 12px; color: #999; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.packages-section { margin-top: 24px; }
|
|
|
|
|
|
|
2026-03-23 10:20:24 +08:00
|
|
|
|
.collections-grid {
|
|
|
|
|
|
.collection-card {
|
2026-03-23 14:18:49 +08:00
|
|
|
|
.package-count { color: #FF8C42; }
|
2026-03-23 10:20:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 18:14:25 +08:00
|
|
|
|
.packages-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-card {
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 2px solid #E0E0E0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { border-color: #BDBDBD; }
|
|
|
|
|
|
&.active {
|
2026-03-23 14:18:49 +08:00
|
|
|
|
border-color: #FF8C42;
|
|
|
|
|
|
background: #FFF0E6;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.package-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
|
|
|
|
|
.package-grade { font-size: 12px; color: #999; margin-bottom: 2px; }
|
2026-03-23 14:18:49 +08:00
|
|
|
|
.package-count { font-size: 11px; color: #FF8C42; }
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 18:31:54 +08:00
|
|
|
|
.schedule-ref-card {
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
padding: 16px;
|
2026-03-23 14:18:49 +08:00
|
|
|
|
background: #FFF8F0;
|
2026-03-19 18:31:54 +08:00
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
.ref-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
.ref-icon {
|
2026-03-23 14:18:49 +08:00
|
|
|
|
color: #FF8C42;
|
2026-03-19 18:31:54 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.ref-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #2D3436;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 18:14:25 +08:00
|
|
|
|
.lesson-type-empty {
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lesson-type-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.lesson-type-card {
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
border: 2px solid transparent;
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.3s;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover {
|
|
|
|
|
|
border-color: rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&.active {
|
|
|
|
|
|
border-color: #43e97b !important;
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(67, 233, 123, 0.4) !important;
|
|
|
|
|
|
transform: scale(1.02);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.type-icon { font-size: 32px; margin-bottom: 8px; opacity: 0.9; }
|
|
|
|
|
|
.type-name { font-weight: 500; margin-bottom: 4px; }
|
|
|
|
|
|
.type-count { font-size: 12px; opacity: 0.85; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.class-select-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.class-card {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border: 2px solid #E0E0E0;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
|
|
|
|
|
|
&:hover { border-color: #BDBDBD; }
|
|
|
|
|
|
&.active {
|
2026-03-23 14:18:49 +08:00
|
|
|
|
border-color: #FF8C42;
|
|
|
|
|
|
background: #FFF0E6;
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.class-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
|
|
|
|
|
.class-detail { font-size: 12px; color: #999; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-hint {
|
|
|
|
|
|
padding: 40px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: #999;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.confirm-info {
|
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #F5F5F5;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
|
|
|
|
div { margin-bottom: 4px; }
|
|
|
|
|
|
div:last-child { margin-bottom: 0; }
|
2026-03-23 14:18:49 +08:00
|
|
|
|
strong { color: #FF8C42; }
|
2026-03-19 18:14:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.modal-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|