Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
36b8621060
@ -26910,8 +26910,9 @@
|
|||||||
"description": "内容"
|
"description": "内容"
|
||||||
},
|
},
|
||||||
"images": {
|
"images": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "图片(JSON 数组)"
|
"items": { "type": "string", "description": "图片 URL" },
|
||||||
|
"description": "图片 URL 列表"
|
||||||
},
|
},
|
||||||
"recordDate": {
|
"recordDate": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -30152,8 +30153,9 @@
|
|||||||
"description": "内容"
|
"description": "内容"
|
||||||
},
|
},
|
||||||
"images": {
|
"images": {
|
||||||
"type": "string",
|
"type": "array",
|
||||||
"description": "图片(JSON 数组)"
|
"items": { "type": "string", "description": "图片 URL" },
|
||||||
|
"description": "图片 URL 列表"
|
||||||
},
|
},
|
||||||
"recordDate": {
|
"recordDate": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
|||||||
@ -31,6 +31,8 @@ async function fetchAndFix() {
|
|||||||
if (spec.components?.schemas) {
|
if (spec.components?.schemas) {
|
||||||
delete spec.components.schemas['ResultObject[]'];
|
delete spec.components.schemas['ResultObject[]'];
|
||||||
}
|
}
|
||||||
|
// 修复成长记录 images 字段:统一为 array of string(避免 SpringDoc 误生成为 string)
|
||||||
|
fixGrowthRecordImagesSchema(spec.components?.schemas);
|
||||||
|
|
||||||
writeFileSync(OUTPUT, JSON.stringify(spec, null, 2));
|
writeFileSync(OUTPUT, JSON.stringify(spec, null, 2));
|
||||||
console.log('OpenAPI spec written to:', OUTPUT);
|
console.log('OpenAPI spec written to:', OUTPUT);
|
||||||
@ -61,6 +63,22 @@ function fixSchema(schema) {
|
|||||||
if (schema.items) fixSchema(schema.items);
|
if (schema.items) fixSchema(schema.items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将 GrowthRecordCreateRequest/GrowthRecordUpdateRequest 的 images 统一为 array of string */
|
||||||
|
function fixGrowthRecordImagesSchema(schemas) {
|
||||||
|
if (!schemas) return;
|
||||||
|
const arrayOfString = {
|
||||||
|
type: 'array',
|
||||||
|
items: { type: 'string', description: '图片 URL' },
|
||||||
|
description: '图片 URL 列表',
|
||||||
|
};
|
||||||
|
for (const name of ['GrowthRecordCreateRequest', 'GrowthRecordUpdateRequest']) {
|
||||||
|
const s = schemas[name];
|
||||||
|
if (s?.properties?.images?.type === 'string') {
|
||||||
|
schemas[name].properties.images = arrayOfString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function inlineResultObjectArrayRef(paths) {
|
function inlineResultObjectArrayRef(paths) {
|
||||||
const inlineSchema = {
|
const inlineSchema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|||||||
@ -18,8 +18,8 @@ export interface GrowthRecordCreateRequest {
|
|||||||
title: string;
|
title: string;
|
||||||
/** 内容 */
|
/** 内容 */
|
||||||
content?: string;
|
content?: string;
|
||||||
/** 图片(JSON 数组) */
|
/** 图片 URL 列表 */
|
||||||
images?: string;
|
images?: string[];
|
||||||
/** 记录日期 */
|
/** 记录日期 */
|
||||||
recordDate?: string;
|
recordDate?: string;
|
||||||
/** 标签 */
|
/** 标签 */
|
||||||
|
|||||||
@ -16,8 +16,8 @@ export interface GrowthRecordUpdateRequest {
|
|||||||
title?: string;
|
title?: string;
|
||||||
/** 内容 */
|
/** 内容 */
|
||||||
content?: string;
|
content?: string;
|
||||||
/** 图片(JSON 数组) */
|
/** 图片 URL 列表 */
|
||||||
images?: string;
|
images?: string[];
|
||||||
/** 记录日期 */
|
/** 记录日期 */
|
||||||
recordDate?: string;
|
recordDate?: string;
|
||||||
/** 标签 */
|
/** 标签 */
|
||||||
|
|||||||
@ -25,6 +25,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.collection-option {
|
.collection-option {
|
||||||
.collection-name {
|
.collection-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|||||||
@ -1,15 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-modal
|
<a-modal v-model:open="visible" title="新建排课" :confirm-loading="loading" @ok="handleSubmit" @cancel="handleCancel"
|
||||||
v-model:open="visible"
|
width="700px" destroy-on-close>
|
||||||
title="新建排课"
|
|
||||||
:confirm-loading="loading"
|
|
||||||
@ok="handleSubmit"
|
|
||||||
@cancel="handleCancel"
|
|
||||||
width="700px"
|
|
||||||
destroy-on-close
|
|
||||||
>
|
|
||||||
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
||||||
<a-step title="选择套餐" />
|
|
||||||
<a-step title="选择课程包" />
|
<a-step title="选择课程包" />
|
||||||
<a-step title="选择课程类型" />
|
<a-step title="选择课程类型" />
|
||||||
<a-step title="选择班级" />
|
<a-step title="选择班级" />
|
||||||
@ -17,60 +9,41 @@
|
|||||||
</a-steps>
|
</a-steps>
|
||||||
|
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<!-- 步骤1: 选择套餐(租户可拥有多个套餐,一对多) -->
|
<!-- 步骤1: 选择课程包(多套餐时顶部展示套餐选择器) -->
|
||||||
<div v-show="currentStep === 0" class="step-panel">
|
<div v-show="currentStep === 0" class="step-panel">
|
||||||
<h3>选择套餐</h3>
|
<h3>选择课程包</h3>
|
||||||
<a-alert
|
<!-- 多套餐时:先选套餐 -->
|
||||||
message="请先选择要排课的套餐,再选择套餐下的课程包"
|
<div v-if="collections.length > 1" class="collection-selector">
|
||||||
type="info"
|
<div class="selector-label">选择课程套餐</div>
|
||||||
show-icon
|
<a-radio-group v-model:value="formData.collectionId" button-style="solid" class="collection-radio-group"
|
||||||
style="margin-bottom: 16px"
|
@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>
|
||||||
<a-spin :spinning="loadingPackages">
|
<a-spin :spinning="loadingPackages">
|
||||||
<div v-if="collections.length > 0" class="packages-section">
|
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0"
|
||||||
<div class="packages-grid collections-grid">
|
class="packages-section">
|
||||||
<div
|
<div v-if="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
|
||||||
v-for="coll in collections"
|
<div class="packages-grid">
|
||||||
:key="coll.id"
|
<div v-for="pkg in selectedCollection.packages" :key="pkg.id"
|
||||||
:class="['package-card collection-card', { active: formData.collectionId === coll.id }]"
|
:class="['package-card', { active: formData.packageId == pkg.id }]" @click="selectPackage(Number(pkg.id) || pkg.id)">
|
||||||
@click="selectCollection(coll)"
|
<div class="package-name">{{ pkg.name }}</div>
|
||||||
>
|
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') :
|
||||||
<div class="package-name">{{ coll.name }}</div>
|
pkg.gradeLevels }}</div>
|
||||||
<div class="package-grade">{{ Array.isArray(coll.gradeLevels) ? coll.gradeLevels.join(', ') : (coll.gradeLevels || '-') }}</div>
|
<div class="package-count">{{ pkg.courseCount }} 门课程</div>
|
||||||
<div class="package-count">{{ coll.packageCount ?? 0 }} 个课程包</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!loadingPackages" class="packages-section">
|
<div
|
||||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
v-else-if="!loadingPackages && selectedCollection && (!selectedCollection.packages || selectedCollection.packages.length === 0)"
|
||||||
|
class="packages-section">
|
||||||
|
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
|
||||||
</div>
|
<a-alert message="请先选择套餐" type="info" show-icon />
|
||||||
|
|
||||||
<!-- 步骤2: 选择课程包 -->
|
|
||||||
<div v-show="currentStep === 1" class="step-panel">
|
|
||||||
<h3>选择课程包</h3>
|
|
||||||
<a-spin :spinning="loadingPackages">
|
|
||||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
|
||||||
<div class="packages-grid">
|
|
||||||
<div
|
|
||||||
v-for="pkg in selectedCollection.packages"
|
|
||||||
:key="pkg.id"
|
|
||||||
:class="['package-card', { active: formData.packageId === pkg.id }]"
|
|
||||||
@click="selectPackage(pkg.id)"
|
|
||||||
>
|
|
||||||
<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>
|
||||||
</div>
|
|
||||||
<div v-else-if="!loadingPackages && selectedCollection && (!selectedCollection.packages || selectedCollection.packages.length === 0)" class="packages-section">
|
|
||||||
<a-alert message="该套餐暂无课程包" type="warning" show-icon />
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
|
|
||||||
<a-alert message="请先选择套餐" type="info" show-icon />
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
||||||
<!-- 排课计划参考(与管理端课程包详情一致) -->
|
<!-- 排课计划参考(与管理端课程包详情一致) -->
|
||||||
@ -79,13 +52,8 @@
|
|||||||
<CalendarOutlined class="ref-icon" />
|
<CalendarOutlined class="ref-icon" />
|
||||||
<span class="ref-title">排课计划参考</span>
|
<span class="ref-title">排课计划参考</span>
|
||||||
</div>
|
</div>
|
||||||
<a-table
|
<a-table :columns="scheduleRefColumns" :data-source="scheduleRefDisplay" :pagination="false" size="small"
|
||||||
:columns="scheduleRefColumns"
|
bordered>
|
||||||
:data-source="scheduleRefDisplay"
|
|
||||||
:pagination="false"
|
|
||||||
size="small"
|
|
||||||
bordered
|
|
||||||
>
|
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'dayOfWeek'">
|
<template v-if="column.key === 'dayOfWeek'">
|
||||||
{{ formatDayOfWeek(record.dayOfWeek) }}
|
{{ formatDayOfWeek(record.dayOfWeek) }}
|
||||||
@ -101,27 +69,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤3: 选择课程类型 -->
|
<!-- 步骤2: 选择课程类型 -->
|
||||||
<div v-show="currentStep === 2" class="step-panel">
|
<div v-show="currentStep === 1" class="step-panel">
|
||||||
<h3>选择课程类型</h3>
|
<h3>选择课程类型</h3>
|
||||||
<a-alert
|
<a-alert message="请选择一个课程类型进行排课" type="info" show-icon style="margin-bottom: 16px" />
|
||||||
message="请选择一个课程类型进行排课"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
/>
|
|
||||||
<a-spin :spinning="loadingLessonTypes">
|
<a-spin :spinning="loadingLessonTypes">
|
||||||
<div v-if="!loadingLessonTypes && lessonTypes.length === 0" class="lesson-type-empty">
|
<div v-if="!loadingLessonTypes && lessonTypes.length === 0" class="lesson-type-empty">
|
||||||
该课程包暂无课程类型,请先选择其他课程包
|
该课程包暂无课程类型,请先选择其他课程包
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="lesson-type-grid">
|
<div v-else class="lesson-type-grid">
|
||||||
<div
|
<div v-for="type in lessonTypes" :key="type.lessonType"
|
||||||
v-for="type in lessonTypes"
|
|
||||||
:key="type.lessonType"
|
|
||||||
:class="['lesson-type-card', { active: formData.lessonType === type.lessonType }]"
|
:class="['lesson-type-card', { active: formData.lessonType === type.lessonType }]"
|
||||||
:style="getLessonTagStyle(type.lessonType)"
|
:style="getLessonTagStyle(type.lessonType)" @click="selectLessonType(type.lessonType)">
|
||||||
@click="selectLessonType(type.lessonType)"
|
|
||||||
>
|
|
||||||
<div class="type-icon">
|
<div class="type-icon">
|
||||||
<component :is="getLessonTypeIconComponent(type.lessonType)" />
|
<component :is="getLessonTypeIconComponent(type.lessonType)" />
|
||||||
</div>
|
</div>
|
||||||
@ -132,15 +91,10 @@
|
|||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤4: 选择班级并分配教师 -->
|
<!-- 步骤3: 选择班级并分配教师 -->
|
||||||
<div v-show="currentStep === 3" class="step-panel">
|
<div v-show="currentStep === 2" class="step-panel">
|
||||||
<h3>选择班级并分配教师</h3>
|
<h3>选择班级并分配教师</h3>
|
||||||
<a-alert
|
<a-alert message="选择班级后,为每个班级指定授课教师" type="info" show-icon style="margin-bottom: 16px" />
|
||||||
message="选择班级后,为每个班级指定授课教师"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
/>
|
|
||||||
<div class="grade-selector">
|
<div class="grade-selector">
|
||||||
<a-radio-group v-model:value="selectedGrade" button-style="solid">
|
<a-radio-group v-model:value="selectedGrade" button-style="solid">
|
||||||
<a-radio-button value="">全部</a-radio-button>
|
<a-radio-button value="">全部</a-radio-button>
|
||||||
@ -150,11 +104,8 @@
|
|||||||
</a-radio-group>
|
</a-radio-group>
|
||||||
</div>
|
</div>
|
||||||
<div class="class-teacher-grid">
|
<div class="class-teacher-grid">
|
||||||
<div
|
<div v-for="cls in filteredClasses" :key="cls.id"
|
||||||
v-for="cls in filteredClasses"
|
:class="['class-teacher-card', { selected: isClassSelected(cls.id) }]">
|
||||||
:key="cls.id"
|
|
||||||
:class="['class-teacher-card', { selected: isClassSelected(cls.id) }]"
|
|
||||||
>
|
|
||||||
<div class="class-header" @click="toggleClass(cls.id)">
|
<div class="class-header" @click="toggleClass(cls.id)">
|
||||||
<a-checkbox :checked="isClassSelected(cls.id)" @click.stop />
|
<a-checkbox :checked="isClassSelected(cls.id)" @click.stop />
|
||||||
<div class="class-info">
|
<div class="class-info">
|
||||||
@ -163,13 +114,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isClassSelected(cls.id)" class="teacher-selector">
|
<div v-if="isClassSelected(cls.id)" class="teacher-selector">
|
||||||
<a-select
|
<a-select v-model:value="classTeacherMap[cls.id]" placeholder="选择教师" style="width: 100%" show-search
|
||||||
v-model:value="classTeacherMap[cls.id]"
|
:filter-option="filterTeacher">
|
||||||
placeholder="选择教师"
|
|
||||||
style="width: 100%"
|
|
||||||
show-search
|
|
||||||
:filter-option="filterTeacher"
|
|
||||||
>
|
|
||||||
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
|
<a-select-option v-for="teacher in teachers" :key="teacher.id" :value="teacher.id">
|
||||||
{{ teacher.name }}
|
{{ teacher.name }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
@ -182,26 +128,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤5: 设置时间 -->
|
<!-- 步骤4: 设置时间 -->
|
||||||
<div v-show="currentStep === 4" class="step-panel">
|
<div v-show="currentStep === 3" class="step-panel">
|
||||||
<h3>设置时间</h3>
|
<h3>设置时间</h3>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="排课日期" required>
|
<a-form-item label="排课日期" required>
|
||||||
<a-date-picker
|
<a-date-picker v-model:value="formData.scheduledDate" style="width: 100%" placeholder="选择日期"
|
||||||
v-model:value="formData.scheduledDate"
|
:disabled-date="(current) => current && current < dayjs().startOf('day')" />
|
||||||
style="width: 100%"
|
|
||||||
placeholder="选择日期"
|
|
||||||
:disabled-date="(current) => current && current < dayjs().startOf('day')"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|
||||||
<a-form-item label="时间段" required>
|
<a-form-item label="时间段" required>
|
||||||
<a-time-range-picker
|
<a-time-range-picker v-model:value="formData.scheduledTimeRange" format="HH:mm" style="width: 100%"
|
||||||
v-model:value="formData.scheduledTimeRange"
|
:placeholder="['开始时间', '结束时间']" />
|
||||||
format="HH:mm"
|
|
||||||
style="width: 100%"
|
|
||||||
:placeholder="['开始时间', '结束时间']"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
||||||
@ -243,7 +181,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
||||||
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
|
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
|
||||||
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
||||||
<a-button @click="handleCancel">取消</a-button>
|
<a-button @click="handleCancel">取消</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -433,9 +371,14 @@ const loadCollections = async () => {
|
|||||||
loadingPackages.value = true;
|
loadingPackages.value = true;
|
||||||
try {
|
try {
|
||||||
collections.value = await getCourseCollections();
|
collections.value = await getCourseCollections();
|
||||||
// 若仅有一个套餐,自动选中并加载其课程包,提升体验
|
if (collections.value.length > 0) {
|
||||||
if (collections.value.length === 1) {
|
const first = collections.value[0];
|
||||||
await selectCollection(collections.value[0]);
|
formData.collectionId = first.id as number;
|
||||||
|
if (collections.value.length === 1) {
|
||||||
|
await selectCollection(first);
|
||||||
|
} else {
|
||||||
|
await loadPackagesForCollection(first.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 加载课程套餐失败:', error);
|
console.error('❌ 加载课程套餐失败:', error);
|
||||||
@ -469,6 +412,31 @@ const selectCollection = async (coll: CourseCollection) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载指定套餐下的课程包
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换套餐时重新加载课程包并清空已选课程包
|
||||||
|
const onCollectionChange = async () => {
|
||||||
|
formData.packageId = undefined;
|
||||||
|
formData.courseId = undefined;
|
||||||
|
scheduleRefData.value = [];
|
||||||
|
lessonTypes.value = [];
|
||||||
|
const colId = formData.collectionId;
|
||||||
|
if (colId) {
|
||||||
|
loadingPackages.value = true;
|
||||||
|
try {
|
||||||
|
await loadPackagesForCollection(colId);
|
||||||
|
} finally {
|
||||||
|
loadingPackages.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 加载班级列表
|
// 加载班级列表
|
||||||
const loadClasses = async () => {
|
const loadClasses = async () => {
|
||||||
try {
|
try {
|
||||||
@ -494,7 +462,7 @@ const selectPackage = async (packageId: number) => {
|
|||||||
|
|
||||||
// 自动选择第一门课程(用于后端API)
|
// 自动选择第一门课程(用于后端API)
|
||||||
if (selectedCollection.value?.packages) {
|
if (selectedCollection.value?.packages) {
|
||||||
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id === packageId);
|
const selectedPkg = selectedCollection.value.packages.find((p: any) => p.id == packageId || Number(p.id) === packageId);
|
||||||
if (selectedPkg?.courses && selectedPkg.courses.length > 0) {
|
if (selectedPkg?.courses && selectedPkg.courses.length > 0) {
|
||||||
// 自动设置为第一门课程
|
// 自动设置为第一门课程
|
||||||
formData.courseId = selectedPkg.courses[0].id;
|
formData.courseId = selectedPkg.courses[0].id;
|
||||||
@ -615,12 +583,6 @@ const getSelectedClassesWithTeachers = () => {
|
|||||||
const validateStep = (): boolean => {
|
const validateStep = (): boolean => {
|
||||||
switch (currentStep.value) {
|
switch (currentStep.value) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!formData.collectionId) {
|
|
||||||
message.warning('请选择套餐');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
if (!formData.collectionId) {
|
if (!formData.collectionId) {
|
||||||
message.warning('请选择套餐');
|
message.warning('请选择套餐');
|
||||||
return false;
|
return false;
|
||||||
@ -634,13 +596,13 @@ const validateStep = (): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 1:
|
||||||
if (!formData.lessonType) {
|
if (!formData.lessonType) {
|
||||||
message.warning('请选择课程类型');
|
message.warning('请选择课程类型');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 2:
|
||||||
if (formData.classIds.length === 0) {
|
if (formData.classIds.length === 0) {
|
||||||
message.warning('请至少选择一个班级');
|
message.warning('请至少选择一个班级');
|
||||||
return false;
|
return false;
|
||||||
@ -654,7 +616,7 @@ const validateStep = (): boolean => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 3:
|
||||||
if (!formData.scheduledDate) {
|
if (!formData.scheduledDate) {
|
||||||
message.warning('请选择排课日期');
|
message.warning('请选择排课日期');
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -94,7 +94,7 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
<div class="schedule-source">
|
<div class="schedule-source">
|
||||||
<a-tag v-if="schedule.source === 'SCHOOL'" color="orange" size="small">学校排课</a-tag>
|
<a-tag v-if="schedule.source === 'SCHOOL'" color="orange" size="small">学校排课</a-tag>
|
||||||
<a-tag v-else color="purple" size="small">自主预约</a-tag>
|
<a-tag v-else color="orange" size="small">自主预约</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="schedule.hasLesson" class="lesson-status">
|
<div v-if="schedule.hasLesson" class="lesson-status">
|
||||||
<a-tag :color="getLessonStatusColor(schedule.lessonStatus)" size="small">
|
<a-tag :color="getLessonStatusColor(schedule.lessonStatus)" size="small">
|
||||||
@ -136,7 +136,7 @@
|
|||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="来源">
|
<a-descriptions-item label="来源">
|
||||||
<a-tag v-if="selectedSchedule.source === 'SCHOOL'" color="orange">学校排课</a-tag>
|
<a-tag v-if="selectedSchedule.source === 'SCHOOL'" color="orange">学校排课</a-tag>
|
||||||
<a-tag v-else color="purple">自主预约</a-tag>
|
<a-tag v-else color="orange">自主预约</a-tag>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="状态">
|
<a-descriptions-item label="状态">
|
||||||
<a-tag :color="getLessonStatusColor(selectedSchedule.status)">
|
<a-tag :color="getLessonStatusColor(selectedSchedule.status)">
|
||||||
@ -480,7 +480,7 @@ onMounted(() => {
|
|||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s;
|
transition: all 0.3s;
|
||||||
border-left: 3px solid #722ed1;
|
border-left: 3px solid #FF8C42;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
|||||||
@ -9,7 +9,6 @@
|
|||||||
destroy-on-close
|
destroy-on-close
|
||||||
>
|
>
|
||||||
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
||||||
<a-step title="选择套餐" />
|
|
||||||
<a-step title="选择课程包" />
|
<a-step title="选择课程包" />
|
||||||
<a-step title="选择课程类型" />
|
<a-step title="选择课程类型" />
|
||||||
<a-step title="选择班级" />
|
<a-step title="选择班级" />
|
||||||
@ -17,47 +16,31 @@
|
|||||||
</a-steps>
|
</a-steps>
|
||||||
|
|
||||||
<div class="step-content">
|
<div class="step-content">
|
||||||
<!-- 步骤1: 选择套餐(租户可拥有多个套餐,一对多) -->
|
<!-- 步骤1: 选择课程包(多套餐时顶部展示套餐选择器) -->
|
||||||
<div v-show="currentStep === 0" class="step-panel">
|
<div v-show="currentStep === 0" class="step-panel">
|
||||||
<h3>选择套餐</h3>
|
|
||||||
<a-alert
|
|
||||||
message="请先选择要预约的套餐,再选择套餐下的课程包"
|
|
||||||
type="info"
|
|
||||||
show-icon
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
/>
|
|
||||||
<a-spin :spinning="loadingPackages">
|
|
||||||
<div v-if="collections.length > 0" class="packages-section">
|
|
||||||
<div class="packages-grid collections-grid">
|
|
||||||
<div
|
|
||||||
v-for="coll in collections"
|
|
||||||
:key="coll.id"
|
|
||||||
:class="['package-card collection-card', { active: formData.collectionId === coll.id }]"
|
|
||||||
@click="selectCollection(coll)"
|
|
||||||
>
|
|
||||||
<div class="package-name">{{ coll.name }}</div>
|
|
||||||
<div class="package-grade">{{ Array.isArray(coll.gradeLevels) ? coll.gradeLevels.join(', ') : (coll.gradeLevels || '-') }}</div>
|
|
||||||
<div class="package-count">{{ coll.packageCount ?? 0 }} 个课程包</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="!loadingPackages" class="packages-section">
|
|
||||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
|
||||||
</div>
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 步骤2: 选择课程包 -->
|
|
||||||
<div v-show="currentStep === 1" class="step-panel">
|
|
||||||
<h3>选择课程包</h3>
|
<h3>选择课程包</h3>
|
||||||
|
<!-- 多套餐时:先选套餐 -->
|
||||||
|
<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>
|
||||||
<a-spin :spinning="loadingPackages">
|
<a-spin :spinning="loadingPackages">
|
||||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
||||||
|
<div v-if="collections.length > 1" class="selector-label" style="margin-bottom: 12px">选择课程包</div>
|
||||||
<div class="packages-grid">
|
<div class="packages-grid">
|
||||||
<div
|
<div
|
||||||
v-for="pkg in selectedCollection.packages"
|
v-for="pkg in selectedCollection.packages"
|
||||||
:key="pkg.id"
|
:key="pkg.id"
|
||||||
:class="['package-card', { active: formData.packageId === pkg.id }]"
|
:class="['package-card', { active: formData.packageId == pkg.id }]"
|
||||||
@click="selectPackage(pkg.id)"
|
@click="selectPackage(Number(pkg.id))"
|
||||||
>
|
>
|
||||||
<div class="package-name">{{ pkg.name }}</div>
|
<div class="package-name">{{ pkg.name }}</div>
|
||||||
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
|
<div class="package-grade">{{ Array.isArray(pkg.gradeLevels) ? pkg.gradeLevels.join(', ') : pkg.gradeLevels }}</div>
|
||||||
@ -101,8 +84,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤3: 选择课程类型 -->
|
<!-- 步骤2: 选择课程类型 -->
|
||||||
<div v-show="currentStep === 2" class="step-panel">
|
<div v-show="currentStep === 1" class="step-panel">
|
||||||
<h3>选择课程类型</h3>
|
<h3>选择课程类型</h3>
|
||||||
<a-alert
|
<a-alert
|
||||||
message="请选择一个课程类型进行排课"
|
message="请选择一个课程类型进行排课"
|
||||||
@ -132,8 +115,8 @@
|
|||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤4: 选择班级(教师端单选) -->
|
<!-- 步骤3: 选择班级(教师端单选) -->
|
||||||
<div v-show="currentStep === 3" class="step-panel">
|
<div v-show="currentStep === 2" class="step-panel">
|
||||||
<h3>选择班级</h3>
|
<h3>选择班级</h3>
|
||||||
<a-alert
|
<a-alert
|
||||||
message="请选择要排课的班级"
|
message="请选择要排课的班级"
|
||||||
@ -155,8 +138,8 @@
|
|||||||
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
|
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 步骤5: 设置时间 -->
|
<!-- 步骤4: 设置时间 -->
|
||||||
<div v-show="currentStep === 4" class="step-panel">
|
<div v-show="currentStep === 3" class="step-panel">
|
||||||
<h3>设置时间</h3>
|
<h3>设置时间</h3>
|
||||||
<a-form layout="vertical">
|
<a-form layout="vertical">
|
||||||
<a-form-item label="排课日期" required>
|
<a-form-item label="排课日期" required>
|
||||||
@ -196,7 +179,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
||||||
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
|
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
|
||||||
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
||||||
<a-button @click="handleCancel">取消</a-button>
|
<a-button @click="handleCancel">取消</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -223,7 +206,6 @@ import {
|
|||||||
getCourseCollectionPackages,
|
getCourseCollectionPackages,
|
||||||
getCoursePackageLessonTypes,
|
getCoursePackageLessonTypes,
|
||||||
type CourseCollection,
|
type CourseCollection,
|
||||||
type CoursePackageItem,
|
|
||||||
type LessonTypeInfo,
|
type LessonTypeInfo,
|
||||||
} from '@/api/school';
|
} from '@/api/school';
|
||||||
import { getTeacherClasses, createTeacherSchedule, type TeacherClass } from '@/api/teacher';
|
import { getTeacherClasses, createTeacherSchedule, type TeacherClass } from '@/api/teacher';
|
||||||
@ -363,12 +345,12 @@ const openWithPreset = async (preset: SchedulePreset) => {
|
|||||||
} catch {
|
} catch {
|
||||||
// 校本课程等场景可能无对应课程包接口,使用预设类型填充
|
// 校本课程等场景可能无对应课程包接口,使用预设类型填充
|
||||||
lessonTypes.value = preset.lessonType
|
lessonTypes.value = preset.lessonType
|
||||||
? [{ lessonType: preset.lessonType, count: 1 }]
|
? [{ lessonType: preset.lessonType, lessonTypeName: getLessonTypeName(preset.lessonType), count: 1 }]
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从选择课程类型(步骤2)开始,跳过套餐与课程包
|
// 从选择课程类型开始,跳过课程包选择
|
||||||
currentStep.value = 2;
|
currentStep.value = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
@ -388,9 +370,15 @@ const loadCollections = async () => {
|
|||||||
loadingPackages.value = true;
|
loadingPackages.value = true;
|
||||||
try {
|
try {
|
||||||
collections.value = await getCourseCollections();
|
collections.value = await getCourseCollections();
|
||||||
// 若仅有一个套餐,自动选中并加载其课程包,提升体验
|
if (collections.value.length > 0) {
|
||||||
if (collections.value.length === 1) {
|
const first = collections.value[0];
|
||||||
await selectCollection(collections.value[0]);
|
formData.collectionId = first.id as number;
|
||||||
|
// 若仅有一个套餐,自动加载其课程包
|
||||||
|
if (collections.value.length === 1) {
|
||||||
|
await selectCollection(first);
|
||||||
|
} else {
|
||||||
|
await loadPackagesForCollection(first.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载课程套餐失败:', error);
|
console.error('加载课程套餐失败:', error);
|
||||||
@ -424,6 +412,31 @@ const selectCollection = async (coll: CourseCollection) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 加载指定套餐下的课程包
|
||||||
|
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 = [];
|
||||||
|
const colId = formData.collectionId;
|
||||||
|
if (colId) {
|
||||||
|
loadingPackages.value = true;
|
||||||
|
try {
|
||||||
|
await loadPackagesForCollection(colId);
|
||||||
|
} finally {
|
||||||
|
loadingPackages.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadMyClasses = async () => {
|
const loadMyClasses = async () => {
|
||||||
try {
|
try {
|
||||||
myClasses.value = await getTeacherClasses();
|
myClasses.value = await getTeacherClasses();
|
||||||
@ -437,7 +450,7 @@ const selectPackage = async (packageId: number) => {
|
|||||||
|
|
||||||
const collection = selectedCollection.value;
|
const collection = selectedCollection.value;
|
||||||
if (collection?.packages) {
|
if (collection?.packages) {
|
||||||
const pkg = collection.packages.find((p: any) => p.id === packageId);
|
const pkg = collection.packages.find((p: any) => p.id == packageId || Number(p.id) === packageId);
|
||||||
if (pkg?.courses && pkg.courses.length > 0) {
|
if (pkg?.courses && pkg.courses.length > 0) {
|
||||||
formData.courseId = pkg.courses[0].id;
|
formData.courseId = pkg.courses[0].id;
|
||||||
|
|
||||||
@ -513,12 +526,6 @@ const getSelectedTimeRange = (): string => {
|
|||||||
const validateStep = (): boolean => {
|
const validateStep = (): boolean => {
|
||||||
switch (currentStep.value) {
|
switch (currentStep.value) {
|
||||||
case 0:
|
case 0:
|
||||||
if (!formData.collectionId) {
|
|
||||||
message.warning('请选择套餐');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
if (!formData.collectionId) {
|
if (!formData.collectionId) {
|
||||||
message.warning('请选择套餐');
|
message.warning('请选择套餐');
|
||||||
return false;
|
return false;
|
||||||
@ -532,19 +539,19 @@ const validateStep = (): boolean => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 1:
|
||||||
if (!formData.lessonType) {
|
if (!formData.lessonType) {
|
||||||
message.warning('请选择课程类型');
|
message.warning('请选择课程类型');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 2:
|
||||||
if (!formData.classId) {
|
if (!formData.classId) {
|
||||||
message.warning('请选择班级');
|
message.warning('请选择班级');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 3:
|
||||||
if (!formData.scheduledDate) {
|
if (!formData.scheduledDate) {
|
||||||
message.warning('请选择排课日期');
|
message.warning('请选择排课日期');
|
||||||
return false;
|
return false;
|
||||||
@ -563,8 +570,8 @@ const nextStep = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const prevStep = () => {
|
const prevStep = () => {
|
||||||
// 预设模式下从步骤2点击上一步时关闭弹窗(不展示套餐/课程包选择)
|
// 预设模式下从选择课程类型点击上一步时关闭弹窗(不展示课程包选择)
|
||||||
if (isPresetMode.value && currentStep.value === 2) {
|
if (isPresetMode.value && currentStep.value === 1) {
|
||||||
handleCancel();
|
handleCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -642,6 +649,26 @@ defineExpose({ open, openWithPreset });
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.collection-option {
|
.collection-option {
|
||||||
.collection-name { font-weight: 500; }
|
.collection-name { font-weight: 500; }
|
||||||
.collection-info { font-size: 12px; color: #999; }
|
.collection-info { font-size: 12px; color: #999; }
|
||||||
@ -651,7 +678,7 @@ defineExpose({ open, openWithPreset });
|
|||||||
|
|
||||||
.collections-grid {
|
.collections-grid {
|
||||||
.collection-card {
|
.collection-card {
|
||||||
.package-count { color: #722ed1; }
|
.package-count { color: #FF8C42; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -671,19 +698,19 @@ defineExpose({ open, openWithPreset });
|
|||||||
|
|
||||||
&:hover { border-color: #BDBDBD; }
|
&:hover { border-color: #BDBDBD; }
|
||||||
&.active {
|
&.active {
|
||||||
border-color: #722ed1;
|
border-color: #FF8C42;
|
||||||
background: #f9f0ff;
|
background: #FFF0E6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.package-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
.package-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
||||||
.package-grade { font-size: 12px; color: #999; margin-bottom: 2px; }
|
.package-grade { font-size: 12px; color: #999; margin-bottom: 2px; }
|
||||||
.package-count { font-size: 11px; color: #722ed1; }
|
.package-count { font-size: 11px; color: #FF8C42; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-ref-card {
|
.schedule-ref-card {
|
||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #f9f0ff;
|
background: #FFF8F0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|
||||||
.ref-header {
|
.ref-header {
|
||||||
@ -693,7 +720,7 @@ defineExpose({ open, openWithPreset });
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
.ref-icon {
|
.ref-icon {
|
||||||
color: #722ed1;
|
color: #FF8C42;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ref-title {
|
.ref-title {
|
||||||
@ -757,8 +784,8 @@ defineExpose({ open, openWithPreset });
|
|||||||
|
|
||||||
&:hover { border-color: #BDBDBD; }
|
&:hover { border-color: #BDBDBD; }
|
||||||
&.active {
|
&.active {
|
||||||
border-color: #722ed1;
|
border-color: #FF8C42;
|
||||||
background: #f9f0ff;
|
background: #FFF0E6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.class-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
.class-name { font-weight: 500; color: #2D3436; margin-bottom: 4px; }
|
||||||
@ -779,7 +806,7 @@ defineExpose({ open, openWithPreset });
|
|||||||
|
|
||||||
div { margin-bottom: 4px; }
|
div { margin-bottom: 4px; }
|
||||||
div:last-child { margin-bottom: 0; }
|
div:last-child { margin-bottom: 0; }
|
||||||
strong { color: #722ed1; }
|
strong { color: #FF8C42; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
.modal-footer {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ public enum ErrorCode {
|
|||||||
|
|
||||||
// Package Errors (3100+)
|
// Package Errors (3100+)
|
||||||
PACKAGE_NOT_FOUND(3101, "Package not found"),
|
PACKAGE_NOT_FOUND(3101, "Package not found"),
|
||||||
|
REMOVE_PACKAGE_HAS_SCHEDULES(3102, "该套餐下有排课计划"),
|
||||||
|
|
||||||
// User Errors (4000+)
|
// User Errors (4000+)
|
||||||
USER_NOT_FOUND(4001, "User not found"),
|
USER_NOT_FOUND(4001, "User not found"),
|
||||||
|
|||||||
@ -11,29 +11,41 @@ public class BusinessException extends RuntimeException {
|
|||||||
|
|
||||||
private final Integer code;
|
private final Integer code;
|
||||||
private final String message;
|
private final String message;
|
||||||
|
private final Object data;
|
||||||
|
|
||||||
public BusinessException(String message) {
|
public BusinessException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = 500;
|
this.code = 500;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
this.data = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BusinessException(Integer code, String message) {
|
public BusinessException(Integer code, String message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
this.data = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BusinessException(ErrorCode errorCode) {
|
public BusinessException(ErrorCode errorCode) {
|
||||||
super(errorCode.getMessage());
|
super(errorCode.getMessage());
|
||||||
this.code = errorCode.getCode();
|
this.code = errorCode.getCode();
|
||||||
this.message = errorCode.getMessage();
|
this.message = errorCode.getMessage();
|
||||||
|
this.data = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BusinessException(ErrorCode errorCode, String message) {
|
public BusinessException(ErrorCode errorCode, String message) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = errorCode.getCode();
|
this.code = errorCode.getCode();
|
||||||
this.message = message;
|
this.message = message;
|
||||||
|
this.data = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BusinessException(ErrorCode errorCode, String message, Object data) {
|
||||||
|
super(message);
|
||||||
|
this.code = errorCode.getCode();
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static BusinessException of(String message) {
|
public static BusinessException of(String message) {
|
||||||
|
|||||||
@ -33,9 +33,9 @@ public class GlobalExceptionHandler {
|
|||||||
private String activeProfile;
|
private String activeProfile;
|
||||||
|
|
||||||
@ExceptionHandler(BusinessException.class)
|
@ExceptionHandler(BusinessException.class)
|
||||||
public Result<Void> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
public Result<Object> handleBusinessException(BusinessException e, HttpServletRequest request) {
|
||||||
log.warn("业务异常 at {}: {}", request.getRequestURI(), e.getMessage());
|
log.warn("业务异常 at {}: {}", request.getRequestURI(), e.getMessage());
|
||||||
return Result.error(e.getCode(), e.getMessage());
|
return Result.error(e.getCode(), e.getMessage(), e.getData());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
|||||||
@ -84,6 +84,17 @@ public class Result<T> implements Serializable {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误响应(带错误码、消息和附加数据)
|
||||||
|
*/
|
||||||
|
public static <T> Result<T> error(Integer code, String message, T data) {
|
||||||
|
Result<T> result = new Result<>();
|
||||||
|
result.setCode(code);
|
||||||
|
result.setMessage(message);
|
||||||
|
result.setData(data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 错误响应(默认 500)
|
* 错误响应(默认 500)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -29,13 +29,13 @@ public class GrowthRecordCreateRequest {
|
|||||||
@Schema(description = "内容")
|
@Schema(description = "内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@Schema(description = "图片 URL 列表")
|
@Schema(description = "图片 URL 列表", type = "array", implementation = String.class)
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
@Schema(description = "记录日期")
|
@Schema(description = "记录日期")
|
||||||
private LocalDate recordDate;
|
private LocalDate recordDate;
|
||||||
|
|
||||||
@Schema(description = "标签")
|
@Schema(description = "标签", type = "array", implementation = String.class)
|
||||||
private List<String> tags;
|
private List<String> tags;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,13 +19,13 @@ public class GrowthRecordUpdateRequest {
|
|||||||
@Schema(description = "内容")
|
@Schema(description = "内容")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
||||||
@Schema(description = "图片 URL 列表")
|
@Schema(description = "图片 URL 列表", type = "array", implementation = String.class)
|
||||||
private List<String> images;
|
private List<String> images;
|
||||||
|
|
||||||
@Schema(description = "记录日期")
|
@Schema(description = "记录日期")
|
||||||
private LocalDate recordDate;
|
private LocalDate recordDate;
|
||||||
|
|
||||||
@Schema(description = "标签")
|
@Schema(description = "标签", type = "array", implementation = String.class)
|
||||||
private List<String> tags;
|
private List<String> tags;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,9 @@ public class TenantUpdateRequest {
|
|||||||
@Schema(description = "课程套餐ID(用于三层架构)")
|
@Schema(description = "课程套餐ID(用于三层架构)")
|
||||||
private List<Long> collectionIds;
|
private List<Long> collectionIds;
|
||||||
|
|
||||||
|
@Schema(description = "是否强制移除套餐(即便套餐下有排课计划)")
|
||||||
|
private Boolean forceRemove;
|
||||||
|
|
||||||
@Schema(description = "教师配额")
|
@Schema(description = "教师配额")
|
||||||
private Integer teacherQuota;
|
private Integer teacherQuota;
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user