feat: 排课/预约优化与国际化
- main.ts: dayjs 时间国际化使用中文 - 排课日期禁止选择过去时间(学校端、教师端、校本课程预约) - 移除选择课程套餐,租户仅一个套餐直接展示课程包 - 教师端预约上课增加排课计划参考表格 Made-with: Cursor
This commit is contained in:
parent
824ce7ad80
commit
ed9371b21f
@ -2,6 +2,10 @@ import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import Antd from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
import 'virtual:uno.css';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
@ -137,7 +137,11 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="排课日期" name="scheduledDate">
|
||||
<a-date-picker v-model:value="formState.scheduledDate" style="width: 100%" />
|
||||
<a-date-picker
|
||||
v-model:value="formState.scheduledDate"
|
||||
style="width: 100%"
|
||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="时间段" name="scheduledTimeRange">
|
||||
<a-time-range-picker
|
||||
|
||||
@ -16,28 +16,11 @@
|
||||
</a-steps>
|
||||
|
||||
<div class="step-content">
|
||||
<!-- 步骤1: 选择课程套餐和课程包 -->
|
||||
<!-- 步骤1: 选择课程包(租户仅一个套餐,直接展示套餐下课程包) -->
|
||||
<div v-show="currentStep === 0" class="step-panel">
|
||||
<h3>选择课程套餐</h3>
|
||||
<a-select
|
||||
v-model:value="formData.collectionId"
|
||||
placeholder="请选择课程套餐"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterCollection"
|
||||
@change="handleCollectionChange"
|
||||
>
|
||||
<a-select-option v-for="collection in collections" :key="collection.id" :value="collection.id">
|
||||
<div class="collection-option">
|
||||
<div class="collection-name">{{ collection.name }}</div>
|
||||
<div class="collection-info">{{ collection.packageCount }} 个课程包 · {{ collection.gradeLevels?.join(', ') }}</div>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<!-- 选择课程包 -->
|
||||
<h3>选择课程包</h3>
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
||||
<h4>选择课程包</h4>
|
||||
<div class="packages-grid">
|
||||
<div
|
||||
v-for="pkg in selectedCollection.packages"
|
||||
@ -51,11 +34,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 调试信息:如果没有课程包,显示提示 -->
|
||||
<div v-else-if="selectedCollection" class="packages-section">
|
||||
<h4>选择课程包</h4>
|
||||
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
|
||||
<div v-else-if="!loadingPackages && collections.length > 0" class="packages-section">
|
||||
<a-alert message="暂无课程包" type="warning" show-icon />
|
||||
</div>
|
||||
<div v-else-if="!loadingPackages && collections.length === 0" class="packages-section">
|
||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
<!-- 排课计划参考(与管理端课程包详情一致) -->
|
||||
<div v-if="scheduleRefDisplay.length > 0" class="schedule-ref-card">
|
||||
@ -175,6 +160,7 @@
|
||||
v-model:value="formData.scheduledDate"
|
||||
style="width: 100%"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@ -275,6 +261,7 @@ const currentStep = ref(0);
|
||||
|
||||
// 课程套餐列表
|
||||
const collections = ref<CourseCollection[]>([]);
|
||||
const loadingPackages = ref(false);
|
||||
const selectedGrade = ref('');
|
||||
|
||||
// 课程类型列表
|
||||
@ -410,13 +397,27 @@ const resetForm = () => {
|
||||
classTeacherMap.value = {};
|
||||
};
|
||||
|
||||
// 加载课程套餐列表
|
||||
// 加载课程套餐列表,租户仅一个套餐时自动加载其课程包
|
||||
const loadCollections = async () => {
|
||||
loadingPackages.value = true;
|
||||
try {
|
||||
collections.value = await getCourseCollections();
|
||||
if (collections.value.length > 0) {
|
||||
const first = collections.value[0];
|
||||
formData.collectionId = first.id as number;
|
||||
const packages = await getCourseCollectionPackages(first.id);
|
||||
if (first) {
|
||||
(first as any).packages = packages;
|
||||
}
|
||||
if (!packages || packages.length === 0) {
|
||||
message.warning('该套餐暂无课程包');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载课程套餐失败:', error);
|
||||
message.error('加载课程套餐失败');
|
||||
} finally {
|
||||
loadingPackages.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -439,35 +440,6 @@ const loadTeachers = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 课程套餐变化 - 加载课程包列表
|
||||
const handleCollectionChange = async (collectionId: number) => {
|
||||
// 重置后续选择
|
||||
formData.packageId = undefined;
|
||||
formData.courseId = undefined;
|
||||
scheduleRefData.value = [];
|
||||
|
||||
if (!collectionId) return;
|
||||
|
||||
try {
|
||||
// 获取课程包列表(API: GET /v1/school/packages/{collectionId}/packages)
|
||||
const packages = await getCourseCollectionPackages(collectionId);
|
||||
|
||||
if (!packages || packages.length === 0) {
|
||||
message.warning('该套餐暂无课程包');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新当前套餐的课程包列表
|
||||
const collection = collections.value.find(c => c.id === collectionId);
|
||||
if (collection) {
|
||||
collection.packages = packages;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载课程包失败:', error);
|
||||
message.error('加载课程包失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 选择课程包
|
||||
const selectPackage = async (packageId: number) => {
|
||||
formData.packageId = packageId;
|
||||
@ -536,12 +508,6 @@ const isClassSelected = (classId: number): boolean => {
|
||||
return formData.classIds.includes(classId);
|
||||
};
|
||||
|
||||
// 过滤课程套餐
|
||||
const filterCollection = (input: string, option: any) => {
|
||||
const collection = collections.value.find(c => c.id === option.value);
|
||||
return collection?.name?.toLowerCase().includes(input.toLowerCase()) || false;
|
||||
};
|
||||
|
||||
// 过滤教师
|
||||
const filterTeacher = (input: string, option: any) => {
|
||||
const teacher = teachers.value.find(t => t.id === option.value);
|
||||
|
||||
@ -220,6 +220,7 @@
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
placeholder="选择预约时间"
|
||||
style="width: 100%"
|
||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
@ -235,6 +236,7 @@
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CalendarOutlined,
|
||||
|
||||
@ -180,6 +180,7 @@
|
||||
format="YYYY-MM-DD HH:mm"
|
||||
placeholder="选择预约时间"
|
||||
style="width: 100%"
|
||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="备注">
|
||||
|
||||
@ -16,27 +16,11 @@
|
||||
</a-steps>
|
||||
|
||||
<div class="step-content">
|
||||
<!-- 步骤1: 选择课程套餐和课程包 -->
|
||||
<!-- 步骤1: 选择课程包(租户仅一个套餐,直接展示套餐下课程包) -->
|
||||
<div v-show="currentStep === 0" class="step-panel">
|
||||
<h3>选择课程套餐</h3>
|
||||
<a-select
|
||||
v-model:value="formData.collectionId"
|
||||
placeholder="请选择课程套餐"
|
||||
style="width: 100%"
|
||||
show-search
|
||||
:filter-option="filterCollection"
|
||||
@change="handleCollectionChange"
|
||||
>
|
||||
<a-select-option v-for="collection in collections" :key="collection.id" :value="collection.id">
|
||||
<div class="collection-option">
|
||||
<div class="collection-name">{{ collection.name }}</div>
|
||||
<div class="collection-info">{{ collection.packageCount }} 个课程包 · {{ collection.gradeLevels?.join(', ') }}</div>
|
||||
</div>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
||||
<h3>选择课程包</h3>
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
||||
<h4>选择课程包</h4>
|
||||
<div class="packages-grid">
|
||||
<div
|
||||
v-for="pkg in selectedCollection.packages"
|
||||
@ -50,9 +34,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="selectedCollection" class="packages-section">
|
||||
<h4>选择课程包</h4>
|
||||
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
|
||||
<div v-else-if="!loadingPackages && collections.length > 0" class="packages-section">
|
||||
<a-alert message="暂无课程包" type="warning" show-icon />
|
||||
</div>
|
||||
<div v-else-if="!loadingPackages && collections.length === 0" class="packages-section">
|
||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -119,6 +133,7 @@
|
||||
v-model:value="formData.scheduledDate"
|
||||
style="width: 100%"
|
||||
placeholder="选择日期"
|
||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
@ -164,6 +179,7 @@ import { message } from 'ant-design-vue';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
BookOutlined,
|
||||
CalendarOutlined,
|
||||
TeamOutlined,
|
||||
HeartOutlined,
|
||||
SoundOutlined,
|
||||
@ -192,9 +208,70 @@ const loadingLessonTypes = ref(false);
|
||||
const currentStep = ref(0);
|
||||
|
||||
const collections = ref<CourseCollection[]>([]);
|
||||
const loadingPackages = ref(false);
|
||||
const lessonTypes = ref<LessonTypeInfo[]>([]);
|
||||
const myClasses = ref<TeacherClass[]>([]);
|
||||
|
||||
// 排课计划参考数据
|
||||
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));
|
||||
|
||||
interface FormData {
|
||||
collectionId?: number;
|
||||
packageId?: number;
|
||||
@ -261,15 +338,31 @@ const resetForm = () => {
|
||||
formData.classId = undefined;
|
||||
formData.scheduledDate = undefined;
|
||||
formData.scheduledTimeRange = undefined;
|
||||
scheduleRefData.value = [];
|
||||
lessonTypes.value = [];
|
||||
};
|
||||
|
||||
// 租户仅一个套餐,自动加载其课程包
|
||||
const loadCollections = async () => {
|
||||
loadingPackages.value = true;
|
||||
try {
|
||||
collections.value = await getCourseCollections();
|
||||
if (collections.value.length > 0) {
|
||||
const first = collections.value[0];
|
||||
formData.collectionId = first.id as number;
|
||||
const packages = await getCourseCollectionPackages(first.id);
|
||||
if (first) {
|
||||
(first as any).packages = packages;
|
||||
}
|
||||
if (!packages || packages.length === 0) {
|
||||
message.warning('该套餐暂无课程包');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载课程套餐失败:', error);
|
||||
message.error('加载课程套餐失败');
|
||||
} finally {
|
||||
loadingPackages.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@ -281,29 +374,6 @@ const loadMyClasses = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCollectionChange = async (collectionId: number) => {
|
||||
formData.packageId = undefined;
|
||||
formData.courseId = undefined;
|
||||
formData.lessonType = undefined;
|
||||
lessonTypes.value = [];
|
||||
|
||||
if (!collectionId) return;
|
||||
|
||||
try {
|
||||
const packages = await getCourseCollectionPackages(collectionId);
|
||||
const collection = collections.value.find(c => c.id === collectionId);
|
||||
if (collection) {
|
||||
collection.packages = packages as CoursePackageItem[];
|
||||
}
|
||||
if (!packages || packages.length === 0) {
|
||||
message.warning('该套餐暂无课程包');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载课程包失败:', error);
|
||||
message.error('加载课程包失败');
|
||||
}
|
||||
};
|
||||
|
||||
const selectPackage = async (packageId: number) => {
|
||||
formData.packageId = packageId;
|
||||
|
||||
@ -312,8 +382,24 @@ const selectPackage = async (packageId: number) => {
|
||||
const pkg = collection.packages.find((p: any) => p.id === packageId);
|
||||
if (pkg?.courses && pkg.courses.length > 0) {
|
||||
formData.courseId = pkg.courses[0].id;
|
||||
|
||||
// 加载排课计划参考(从课程包中任一课程的 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 = [];
|
||||
}
|
||||
} else {
|
||||
formData.courseId = undefined;
|
||||
scheduleRefData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
@ -335,11 +421,6 @@ const selectLessonType = (lessonType: string) => {
|
||||
formData.lessonType = lessonType;
|
||||
};
|
||||
|
||||
const filterCollection = (input: string, option: any) => {
|
||||
const collection = collections.value.find(c => c.id === option.value);
|
||||
return collection?.name?.toLowerCase().includes(input.toLowerCase()) || false;
|
||||
};
|
||||
|
||||
const lessonTypeIconMap: Record<string, any> = {
|
||||
INTRODUCTION: BookOutlined,
|
||||
INTRO: BookOutlined,
|
||||
@ -524,6 +605,29 @@ defineExpose({ open, openWithPreset });
|
||||
.package-count { font-size: 11px; color: #722ed1; }
|
||||
}
|
||||
|
||||
.schedule-ref-card {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f0ff;
|
||||
border-radius: 8px;
|
||||
|
||||
.ref-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.ref-icon {
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.ref-title {
|
||||
font-weight: 600;
|
||||
color: #2D3436;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-type-empty {
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user