Merge remote-tracking branch 'origin/master'
This commit is contained in:
commit
fb4d63ec99
@ -14,6 +14,13 @@ export interface CourseCollection {
|
||||
endDate?: string;
|
||||
}
|
||||
|
||||
/** 课程包中的课程项(用于提取课程配置) */
|
||||
export interface CoursePackageCourseItem {
|
||||
id?: number;
|
||||
name?: string;
|
||||
lessonType?: string;
|
||||
}
|
||||
|
||||
/** 课程包信息 */
|
||||
export interface CoursePackage {
|
||||
id: number;
|
||||
@ -22,7 +29,8 @@ export interface CoursePackage {
|
||||
coverImagePath?: string;
|
||||
pictureBookName?: string;
|
||||
gradeTags: string[];
|
||||
domainTags?: string[];
|
||||
domainTags?: string[]; // 不再展示,由课程配置替代
|
||||
courses?: CoursePackageCourseItem[]; // 用于课程配置展示
|
||||
themeId?: number;
|
||||
themeName?: string;
|
||||
durationMinutes?: number;
|
||||
@ -37,9 +45,9 @@ export interface GradeOption {
|
||||
count: number;
|
||||
}
|
||||
|
||||
/** 筛选元数据 - 主题选项 */
|
||||
export interface ThemeOption {
|
||||
id: number;
|
||||
/** 筛选元数据 - 课程配置选项 */
|
||||
export interface LessonTypeOption {
|
||||
lessonType: string;
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
@ -47,7 +55,7 @@ export interface ThemeOption {
|
||||
/** 筛选元数据响应 */
|
||||
export interface FilterMetaResponse {
|
||||
grades: GradeOption[];
|
||||
themes: ThemeOption[];
|
||||
lessonTypes: LessonTypeOption[];
|
||||
}
|
||||
|
||||
// ============= API 接口 =============
|
||||
@ -66,7 +74,7 @@ export function getPackages(
|
||||
collectionId: number,
|
||||
params?: {
|
||||
grade?: string;
|
||||
themeId?: number;
|
||||
lessonType?: string;
|
||||
keyword?: string;
|
||||
}
|
||||
): Promise<CoursePackage[]> {
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import { getReadingPlatformAPI } from './generated';
|
||||
import { axios, customMutator } from './generated/mutator';
|
||||
import { getReadingPlatformAPI } from "./generated";
|
||||
import { axios, customMutator } from "./generated/mutator";
|
||||
|
||||
// 创建 API 实例
|
||||
const api = getReadingPlatformAPI();
|
||||
|
||||
// 封装 http 方法(兼容原有代码)
|
||||
export const http = {
|
||||
get: <T = any>(url: string, config?: any) => customMutator<T>({ url, method: 'get', ...config }),
|
||||
post: <T = any>(url: string, data?: any, config?: any) => customMutator<T>({ url, method: 'post', data, ...config }),
|
||||
put: <T = any>(url: string, data?: any, config?: any) => customMutator<T>({ url, method: 'put', data, ...config }),
|
||||
delete: <T = any>(url: string, config?: any) => customMutator<T>({ url, method: 'delete', ...config }),
|
||||
get: <T = any>(url: string, config?: any) =>
|
||||
customMutator<T>({ url, method: "get", ...config }),
|
||||
post: <T = any>(url: string, data?: any, config?: any) =>
|
||||
customMutator<T>({ url, method: "post", data, ...config }),
|
||||
put: <T = any>(url: string, data?: any, config?: any) =>
|
||||
customMutator<T>({ url, method: "put", data, ...config }),
|
||||
delete: <T = any>(url: string, config?: any) =>
|
||||
customMutator<T>({ url, method: "delete", ...config }),
|
||||
};
|
||||
|
||||
// ============= 类型定义(保持向后兼容) =============
|
||||
@ -149,13 +153,20 @@ function normalizePageResult(raw: any): {
|
||||
|
||||
// 获取课程包列表(7 步流程创建的教学资源)
|
||||
// 注意:这里的 Course 实际对应后端的 CoursePackage(课程包)
|
||||
// 使用扁平 query 参数,确保 gradeTags(适用年级)正确传给后端
|
||||
export function getCourses(params: CourseQueryParams): Promise<{
|
||||
items: Course[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> {
|
||||
return api.getCoursePage1(toFindAllParams(params)).then(normalizePageResult) as any;
|
||||
const flatParams = toFindAllParams(params);
|
||||
return http
|
||||
.get<{ list?: Course[]; items?: Course[]; total?: number; pageNum?: number; pageSize?: number }>(
|
||||
"/v1/admin/packages",
|
||||
{ params: flatParams }
|
||||
)
|
||||
.then(normalizePageResult) as any;
|
||||
}
|
||||
|
||||
// 获取审核列表(仅返回待审核和已驳回,不含已通过)
|
||||
@ -165,7 +176,13 @@ export function getReviewList(params: CourseQueryParams): Promise<{
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}> {
|
||||
return api.getCoursePage1(toFindAllParams({ ...params, reviewOnly: true })).then(normalizePageResult) as any;
|
||||
const flatParams = toFindAllParams({ ...params, reviewOnly: true });
|
||||
return http
|
||||
.get<{ list?: Course[]; items?: Course[]; total?: number; pageNum?: number; pageSize?: number }>(
|
||||
"/v1/admin/packages",
|
||||
{ params: flatParams }
|
||||
)
|
||||
.then(normalizePageResult) as any;
|
||||
}
|
||||
|
||||
// 获取课程包详情(id 支持 number | string,避免大整数精度丢失)
|
||||
@ -197,8 +214,14 @@ export function validateCourse(_id: number): Promise<ValidationResult> {
|
||||
export function submitCourse(id: number | string): Promise<any> {
|
||||
return http.post(`/v1/admin/packages/${id}/submit`).then((res: any) => {
|
||||
const body = res;
|
||||
if (body && typeof body === 'object' && 'code' in body && body.code !== 200 && body.code !== 0) {
|
||||
throw new Error(body.message || '提交失败');
|
||||
if (
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
"code" in body &&
|
||||
body.code !== 200 &&
|
||||
body.code !== 0
|
||||
) {
|
||||
throw new Error(body.message || "提交失败");
|
||||
}
|
||||
return body?.data;
|
||||
});
|
||||
@ -206,27 +229,42 @@ export function submitCourse(id: number | string): Promise<any> {
|
||||
|
||||
// 撤销审核 (暂时使用更新接口,需要确认后端是否有此功能)
|
||||
export function withdrawCourse(id: number): Promise<any> {
|
||||
return api.updateCourse(id, { status: 'DRAFT' }) as any;
|
||||
return api.updateCourse(id, { status: "DRAFT" }) as any;
|
||||
}
|
||||
|
||||
// 审核通过
|
||||
export function approveCourse(id: number, _data: { checklist?: any; comment?: string }): Promise<any> {
|
||||
export function approveCourse(
|
||||
id: number,
|
||||
_data: { checklist?: any; comment?: string },
|
||||
): Promise<any> {
|
||||
return api.publishCourse(id) as any;
|
||||
}
|
||||
|
||||
// 审核驳回(课程专用,调用 POST /v1/admin/packages/{id}/reject)
|
||||
export function rejectCourse(id: number | string, data: { comment: string }): Promise<any> {
|
||||
export function rejectCourse(
|
||||
id: number | string,
|
||||
data: { comment: string },
|
||||
): Promise<any> {
|
||||
return http.post(`/v1/admin/packages/${id}/reject`, data).then((res: any) => {
|
||||
const body = res;
|
||||
if (body && typeof body === 'object' && 'code' in body && body.code !== 200 && body.code !== 0) {
|
||||
throw new Error(body.message || '驳回失败');
|
||||
if (
|
||||
body &&
|
||||
typeof body === "object" &&
|
||||
"code" in body &&
|
||||
body.code !== 200 &&
|
||||
body.code !== 0
|
||||
) {
|
||||
throw new Error(body.message || "驳回失败");
|
||||
}
|
||||
return body?.data;
|
||||
});
|
||||
}
|
||||
|
||||
// 直接发布(超级管理员)
|
||||
export function directPublishCourse(id: number, _skipValidation?: boolean): Promise<any> {
|
||||
export function directPublishCourse(
|
||||
id: number,
|
||||
_skipValidation?: boolean,
|
||||
): Promise<any> {
|
||||
return api.publishCourse(id) as any;
|
||||
}
|
||||
|
||||
@ -258,15 +296,18 @@ export function getCourseVersions(_id: number): Promise<any[]> {
|
||||
// ============= 常量 =============
|
||||
|
||||
// 课程状态映射
|
||||
export const COURSE_STATUS_MAP: Record<string, { label: string; color: string }> = {
|
||||
DRAFT: { label: '草稿', color: 'default' },
|
||||
PENDING: { label: '审核中', color: 'processing' },
|
||||
REJECTED: { label: '已驳回', color: 'error' },
|
||||
PUBLISHED: { label: '已发布', color: 'success' },
|
||||
ARCHIVED: { label: '已下架', color: 'warning' },
|
||||
export const COURSE_STATUS_MAP: Record<
|
||||
string,
|
||||
{ label: string; color: string }
|
||||
> = {
|
||||
DRAFT: { label: "草稿", color: "default" },
|
||||
PENDING: { label: "审核中", color: "processing" },
|
||||
REJECTED: { label: "已驳回", color: "error" },
|
||||
PUBLISHED: { label: "已发布", color: "success" },
|
||||
ARCHIVED: { label: "已下架", color: "warning" },
|
||||
};
|
||||
|
||||
// 获取状态显示信息
|
||||
export function getCourseStatusInfo(status: string) {
|
||||
return COURSE_STATUS_MAP[status] || { label: status, color: 'default' };
|
||||
return COURSE_STATUS_MAP[status] || { label: status, color: "default" };
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<a-row :gutter="16" align="middle">
|
||||
<a-col :span="16">
|
||||
<a-space>
|
||||
<a-select v-model:value="filters.gradeTags" placeholder="年级" style="width: 150px" allow-clear
|
||||
<a-select v-model:value="filters.gradeTags" placeholder="适用年级" style="width: 180px" allow-clear
|
||||
mode="multiple" @change="fetchCourses">
|
||||
<a-select-option value="小班">小班</a-select-option>
|
||||
<a-select-option value="中班">中班</a-select-option>
|
||||
@ -57,6 +57,15 @@
|
||||
{{ record.pictureBookName }}
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'lessonConfig'">
|
||||
<div class="lesson-config-tags">
|
||||
<a-tag v-for="lt in getLessonTypesFromRecord(record)" :key="lt" size="small" :style="getLessonTagStyle(lt)">
|
||||
{{ getLessonTypeName(lt) }}
|
||||
</a-tag>
|
||||
<span v-if="!getLessonTypesFromRecord(record).length" class="empty-text">-</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-tag :style="getCourseStatusStyle(record.status)">
|
||||
{{ translateCourseStatus(record.status) }}
|
||||
@ -195,8 +204,21 @@ import {
|
||||
getGradeTagStyle,
|
||||
translateCourseStatus,
|
||||
getCourseStatusStyle,
|
||||
getLessonTypeName,
|
||||
getLessonTagStyle,
|
||||
} from '@/utils/tagMaps';
|
||||
|
||||
// 从课程包记录提取课程类型列表(去重,与学校端排课一致)
|
||||
const getLessonTypesFromRecord = (record: any): string[] => {
|
||||
const lessons = record.courseLessons || record.lessons || [];
|
||||
const types = new Set<string>();
|
||||
for (const l of lessons) {
|
||||
const t = l.lessonType;
|
||||
if (t) types.add(t);
|
||||
}
|
||||
return Array.from(types);
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const loading = ref(false);
|
||||
@ -218,6 +240,7 @@ const pagination = reactive({
|
||||
const columns = [
|
||||
{ title: '课程包名称', key: 'name', width: 250 },
|
||||
{ title: '关联绘本', key: 'pictureBook', width: 120 },
|
||||
{ title: '课程配置', key: 'lessonConfig', width: 200 },
|
||||
{ title: '状态', key: 'status', width: 90 },
|
||||
{ title: '版本', key: 'version', width: 70 },
|
||||
{ title: '数据统计', key: 'stats', width: 130 },
|
||||
@ -521,5 +544,17 @@ const formatDate = (dateStr: string) => {
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.lesson-config-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
|
||||
.empty-text {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,12 +7,9 @@
|
||||
</div>
|
||||
<a-spin :spinning="loadingCollections">
|
||||
<div class="collection-list">
|
||||
<div
|
||||
v-for="collection in collections"
|
||||
:key="collection.id"
|
||||
<div v-for="collection in collections" :key="collection.id"
|
||||
:class="['collection-item', { active: selectedCollectionId === collection.id }]"
|
||||
@click="selectCollection(collection)"
|
||||
>
|
||||
@click="selectCollection(collection)">
|
||||
<div class="collection-name">{{ collection.name }}</div>
|
||||
<div class="collection-count">{{ collection.packageCount || 0 }}个课程包</div>
|
||||
</div>
|
||||
@ -34,11 +31,7 @@
|
||||
<div ref="descRef" :class="['desc-text', { expanded: descExpanded }]">
|
||||
{{ selectedCollection.description }}
|
||||
</div>
|
||||
<button
|
||||
v-if="showExpandBtn"
|
||||
class="expand-btn"
|
||||
@click="descExpanded = !descExpanded"
|
||||
>
|
||||
<button v-if="showExpandBtn" class="expand-btn" @click="descExpanded = !descExpanded">
|
||||
{{ descExpanded ? '收起' : '展开更多' }}
|
||||
<DownOutlined :class="{ rotated: descExpanded }" />
|
||||
</button>
|
||||
@ -52,18 +45,12 @@
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">年级:</span>
|
||||
<div class="grade-tags">
|
||||
<span
|
||||
:class="['grade-tag', { active: !selectedGrade }]"
|
||||
@click="selectedGrade = ''"
|
||||
>
|
||||
<span :class="['grade-tag', { active: !selectedGrade }]" @click="selectedGrade = ''">
|
||||
全部
|
||||
</span>
|
||||
<span
|
||||
v-for="grade in filterMeta.grades"
|
||||
:key="grade.label"
|
||||
<span v-for="grade in filterMeta.grades" :key="grade.label"
|
||||
:class="['grade-tag', { active: selectedGrade === grade.label }]"
|
||||
@click="selectedGrade = grade.label"
|
||||
>
|
||||
@click="selectedGrade = grade.label">
|
||||
{{ grade.label }}
|
||||
<span class="count">({{ grade.count }})</span>
|
||||
</span>
|
||||
@ -72,36 +59,23 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-row">
|
||||
<!-- 主题筛选 -->
|
||||
<!-- 课程配置筛选 -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">主题:</span>
|
||||
<a-select
|
||||
v-model:value="selectedThemeId"
|
||||
placeholder="全部主题"
|
||||
style="width: 180px"
|
||||
allowClear
|
||||
@change="loadPackages"
|
||||
>
|
||||
<a-select-option :value="undefined">全部主题</a-select-option>
|
||||
<a-select-option
|
||||
v-for="theme in filterMeta.themes"
|
||||
:key="theme.id"
|
||||
:value="theme.id"
|
||||
>
|
||||
{{ theme.name }} ({{ theme.count }})
|
||||
<span class="filter-label">课程配置:</span>
|
||||
<a-select v-model:value="selectedLessonType" placeholder="全部课程配置" style="width: 180px" allowClear
|
||||
@change="loadPackages">
|
||||
<a-select-option :value="undefined">全部课程配置</a-select-option>
|
||||
<a-select-option v-for="opt in (filterMeta.lessonTypes || [])" :key="opt.lessonType"
|
||||
:value="opt.lessonType">
|
||||
{{ opt.name }} ({{ opt.count }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- 搜索 -->
|
||||
<div class="filter-group search-group">
|
||||
<a-input-search
|
||||
v-model:value="searchKeyword"
|
||||
placeholder="搜索课程包..."
|
||||
style="width: 220px"
|
||||
allowClear
|
||||
@search="loadPackages"
|
||||
/>
|
||||
<a-input-search v-model:value="searchKeyword" placeholder="搜索课程包..." style="width: 220px" allowClear
|
||||
@search="loadPackages" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -110,13 +84,8 @@
|
||||
<section class="packages-section">
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="packages.length > 0" class="packages-grid">
|
||||
<CoursePackageCard
|
||||
v-for="pkg in packages"
|
||||
:key="pkg.id"
|
||||
:pkg="pkg"
|
||||
@click="handlePackageClick"
|
||||
@view="handlePackageView"
|
||||
/>
|
||||
<CoursePackageCard v-for="pkg in packages" :key="pkg.id" :pkg="pkg" @click="handlePackageClick"
|
||||
@view="handlePackageView" />
|
||||
</div>
|
||||
<div v-else class="empty-packages">
|
||||
<InboxOutlined class="empty-icon" />
|
||||
@ -200,13 +169,13 @@ const loadCollections = async () => {
|
||||
loadingCollections.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedLessonType = ref<string | undefined>(undefined);
|
||||
// 选择套餐
|
||||
const selectCollection = async (collection: CourseCollection) => {
|
||||
selectedCollectionId.value = collection.id;
|
||||
// 重置筛选条件
|
||||
selectedGrade.value = '';
|
||||
selectedThemeId.value = undefined;
|
||||
selectedLessonType.value = undefined;
|
||||
searchKeyword.value = '';
|
||||
descExpanded.value = false;
|
||||
|
||||
@ -236,7 +205,7 @@ const loadFilterMeta = async () => {
|
||||
filterMeta.value = await getFilterMeta(selectedCollectionId.value);
|
||||
} catch (error) {
|
||||
console.error('获取筛选元数据失败', error);
|
||||
filterMeta.value = { grades: [], themes: [] };
|
||||
filterMeta.value = { grades: [], lessonTypes: [] };
|
||||
}
|
||||
};
|
||||
|
||||
@ -247,7 +216,7 @@ const loadPackages = async () => {
|
||||
try {
|
||||
packages.value = await getPackages(selectedCollectionId.value, {
|
||||
grade: selectedGrade.value || undefined,
|
||||
themeId: selectedThemeId.value,
|
||||
lessonType: selectedLessonType.value,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -258,8 +227,8 @@ const loadPackages = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 监听年级变化
|
||||
watch(selectedGrade, () => {
|
||||
// 监听年级、课程配置变化
|
||||
watch([selectedGrade, selectedLessonType], () => {
|
||||
loadPackages();
|
||||
});
|
||||
|
||||
|
||||
@ -28,10 +28,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 主题标签行 -->
|
||||
<div v-if="pkg.themeName" class="tag-row theme-row">
|
||||
<span class="theme-tag">
|
||||
{{ pkg.themeName }}
|
||||
<!-- 课程配置标签行(参考管理端) -->
|
||||
<div v-if="lessonTypes.length > 0" class="tag-row config-row">
|
||||
<span
|
||||
v-for="lt in lessonTypes"
|
||||
:key="lt"
|
||||
class="config-tag"
|
||||
:style="getLessonTagStyle(lt)"
|
||||
>
|
||||
{{ getLessonTypeName(lt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -66,6 +71,7 @@ import {
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { CoursePackage } from '@/api/course-center';
|
||||
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
|
||||
|
||||
const props = defineProps<{
|
||||
pkg: CoursePackage;
|
||||
@ -82,6 +88,17 @@ const gradeText = computed(() => {
|
||||
return grades.join(' · ');
|
||||
});
|
||||
|
||||
// 从 courses 提取课程类型列表(去重,与管理端一致)
|
||||
const lessonTypes = computed(() => {
|
||||
const courses = props.pkg.courses || [];
|
||||
const types = new Set<string>();
|
||||
for (const c of courses) {
|
||||
const t = c.lessonType;
|
||||
if (t) types.add(t);
|
||||
}
|
||||
return Array.from(types);
|
||||
});
|
||||
|
||||
// 获取图片完整 URL
|
||||
const getImageUrl = (path: string) => {
|
||||
if (!path) return '';
|
||||
@ -199,14 +216,17 @@ const handleView = () => {
|
||||
border: 1px solid #91d5ff;
|
||||
}
|
||||
|
||||
.theme-tag {
|
||||
.config-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.config-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: #f6ffed;
|
||||
color: #52c41a;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #b7eb8f;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
|
||||
@ -137,16 +137,9 @@
|
||||
|
||||
<!-- 分页组件 -->
|
||||
<div class="pagination-wrapper" v-if="!loading && pagination.total > 0">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
show-size-changer
|
||||
show-quick-jumper
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange"
|
||||
@showSizeChange="handlePageSizeChange"
|
||||
/>
|
||||
<a-pagination v-model:current="pagination.current" v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total" show-size-changer show-quick-jumper :show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange" @showSizeChange="handlePageSizeChange" />
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
||||
@ -182,10 +182,10 @@
|
||||
<a-form-item label="图片">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:custom-request="handleCustomUpload"
|
||||
list-type="picture-card"
|
||||
:max-count="9"
|
||||
accept="image/*"
|
||||
@change="handleUploadChange"
|
||||
>
|
||||
<div v-if="fileList.length < 9">
|
||||
@ -273,6 +273,7 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { FormInstance } from 'ant-design-vue';
|
||||
import { fileApi, validateFileType } from '@/api/file';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
@ -404,10 +405,41 @@ const handleModalOk = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (info: any) => {
|
||||
if (info.file.status === 'done') {
|
||||
formState.images.push(info.file.response.url);
|
||||
/** OSS 直传:自定义上传 */
|
||||
const handleCustomUpload = async (options: any) => {
|
||||
const { file, onSuccess, onError, onProgress } = options;
|
||||
const uploadFile = file instanceof File ? file : (file?.originFileObj ?? file);
|
||||
|
||||
const isImage = uploadFile.type?.startsWith('image/');
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件');
|
||||
onError?.(new Error('只能上传图片'));
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validateFileType(uploadFile, 'POSTER');
|
||||
if (!validation.valid) {
|
||||
message.error(validation.error);
|
||||
onError?.(new Error(validation.error));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fileApi.uploadFile(uploadFile, 'resource', {
|
||||
onProgress: (percent) => onProgress?.({ percent }),
|
||||
});
|
||||
onSuccess?.({ url: result.filePath });
|
||||
} catch (err: any) {
|
||||
const msg = err?.message || '上传失败';
|
||||
message.error(msg);
|
||||
onError?.(new Error(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (info: any) => {
|
||||
formState.images = (info.fileList || [])
|
||||
.filter((f: any) => f.response?.url)
|
||||
.map((f: any) => f.response.url);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -528,6 +560,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
@ -542,20 +576,30 @@ onMounted(() => {
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 160px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.cover-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-count {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
@ -581,6 +625,7 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
@ -605,7 +650,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
@ -639,16 +688,24 @@ onMounted(() => {
|
||||
font-size: 13px;
|
||||
color: #636E72;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
margin: 0 0 12px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #F0F0F0;
|
||||
background: #FAFAFA;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
@ -39,6 +39,14 @@
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.collections-grid {
|
||||
.collection-card {
|
||||
.package-count {
|
||||
color: #FF8C42;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.packages-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
destroy-on-close
|
||||
>
|
||||
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
||||
<a-step title="选择套餐" />
|
||||
<a-step title="选择课程包" />
|
||||
<a-step title="选择课程类型" />
|
||||
<a-step title="选择班级" />
|
||||
@ -16,8 +17,38 @@
|
||||
</a-steps>
|
||||
|
||||
<div class="step-content">
|
||||
<!-- 步骤1: 选择课程包(租户仅一个套餐,直接展示套餐下课程包) -->
|
||||
<!-- 步骤1: 选择套餐(租户可拥有多个套餐,一对多) -->
|
||||
<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>
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
||||
@ -34,11 +65,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!loadingPackages && collections.length > 0" class="packages-section">
|
||||
<a-alert message="暂无课程包" type="warning" show-icon />
|
||||
<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 && collections.length === 0" class="packages-section">
|
||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
||||
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
|
||||
<a-alert message="请先选择套餐" type="info" show-icon />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
@ -70,8 +101,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 选择课程类型 -->
|
||||
<div v-show="currentStep === 1" class="step-panel">
|
||||
<!-- 步骤3: 选择课程类型 -->
|
||||
<div v-show="currentStep === 2" class="step-panel">
|
||||
<h3>选择课程类型</h3>
|
||||
<a-alert
|
||||
message="请选择一个课程类型进行排课"
|
||||
@ -101,8 +132,8 @@
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 选择班级并分配教师 -->
|
||||
<div v-show="currentStep === 2" class="step-panel">
|
||||
<!-- 步骤4: 选择班级并分配教师 -->
|
||||
<div v-show="currentStep === 3" class="step-panel">
|
||||
<h3>选择班级并分配教师</h3>
|
||||
<a-alert
|
||||
message="选择班级后,为每个班级指定授课教师"
|
||||
@ -151,8 +182,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4: 设置时间 -->
|
||||
<div v-show="currentStep === 3" class="step-panel">
|
||||
<!-- 步骤5: 设置时间 -->
|
||||
<div v-show="currentStep === 4" class="step-panel">
|
||||
<h3>设置时间</h3>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="排课日期" required>
|
||||
@ -212,7 +243,7 @@
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
||||
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
|
||||
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
|
||||
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</div>
|
||||
@ -397,21 +428,14 @@ 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('该套餐暂无课程包');
|
||||
}
|
||||
// 若仅有一个套餐,自动选中并加载其课程包,提升体验
|
||||
if (collections.value.length === 1) {
|
||||
await selectCollection(collections.value[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载课程套餐失败:', error);
|
||||
@ -421,6 +445,30 @@ const loadCollections = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 选择套餐,并加载该套餐下的课程包
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载班级列表
|
||||
const loadClasses = async () => {
|
||||
try {
|
||||
@ -568,7 +616,13 @@ const validateStep = (): boolean => {
|
||||
switch (currentStep.value) {
|
||||
case 0:
|
||||
if (!formData.collectionId) {
|
||||
message.warning('请选择课程套餐');
|
||||
message.warning('请选择套餐');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (!formData.collectionId) {
|
||||
message.warning('请选择套餐');
|
||||
return false;
|
||||
}
|
||||
if (!formData.packageId) {
|
||||
@ -580,13 +634,13 @@ const validateStep = (): boolean => {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
if (!formData.lessonType) {
|
||||
message.warning('请选择课程类型');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
if (formData.classIds.length === 0) {
|
||||
message.warning('请至少选择一个班级');
|
||||
return false;
|
||||
@ -600,7 +654,7 @@ const validateStep = (): boolean => {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
if (!formData.scheduledDate) {
|
||||
message.warning('请选择排课日期');
|
||||
return false;
|
||||
|
||||
@ -72,23 +72,23 @@
|
||||
</div>
|
||||
|
||||
<div class="filter-row">
|
||||
<!-- 主题筛选 -->
|
||||
<!-- 课程配置筛选 -->
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">主题:</span>
|
||||
<span class="filter-label">课程配置:</span>
|
||||
<a-select
|
||||
v-model:value="selectedThemeId"
|
||||
placeholder="全部主题"
|
||||
v-model:value="selectedLessonType"
|
||||
placeholder="全部课程配置"
|
||||
style="width: 180px"
|
||||
allowClear
|
||||
@change="loadPackages"
|
||||
>
|
||||
<a-select-option :value="undefined">全部主题</a-select-option>
|
||||
<a-select-option :value="undefined">全部课程配置</a-select-option>
|
||||
<a-select-option
|
||||
v-for="theme in filterMeta.themes"
|
||||
:key="theme.id"
|
||||
:value="theme.id"
|
||||
v-for="opt in (filterMeta.lessonTypes || [])"
|
||||
:key="opt.lessonType"
|
||||
:value="opt.lessonType"
|
||||
>
|
||||
{{ theme.name }} ({{ theme.count }})
|
||||
{{ opt.name }} ({{ opt.count }})
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</div>
|
||||
@ -166,7 +166,7 @@ const selectedCollection = computed(() =>
|
||||
);
|
||||
|
||||
// 筛选元数据
|
||||
const filterMeta = ref<FilterMetaResponse>({ grades: [], themes: [] });
|
||||
const filterMeta = ref<FilterMetaResponse>({ grades: [], lessonTypes: [] });
|
||||
|
||||
// 课程包列表
|
||||
const packages = ref<CoursePackage[]>([]);
|
||||
@ -174,7 +174,7 @@ const loadingPackages = ref(false);
|
||||
|
||||
// 筛选条件
|
||||
const selectedGrade = ref('');
|
||||
const selectedThemeId = ref<number | undefined>(undefined);
|
||||
const selectedLessonType = ref<string | undefined>(undefined);
|
||||
const searchKeyword = ref('');
|
||||
|
||||
// 描述展开
|
||||
@ -204,7 +204,7 @@ const selectCollection = async (collection: CourseCollection) => {
|
||||
selectedCollectionId.value = collection.id;
|
||||
// 重置筛选条件
|
||||
selectedGrade.value = '';
|
||||
selectedThemeId.value = undefined;
|
||||
selectedLessonType.value = undefined;
|
||||
searchKeyword.value = '';
|
||||
descExpanded.value = false;
|
||||
|
||||
@ -234,7 +234,7 @@ const loadFilterMeta = async () => {
|
||||
filterMeta.value = await getFilterMeta(selectedCollectionId.value);
|
||||
} catch (error) {
|
||||
console.error('获取筛选元数据失败', error);
|
||||
filterMeta.value = { grades: [], themes: [] };
|
||||
filterMeta.value = { grades: [], lessonTypes: [] };
|
||||
}
|
||||
};
|
||||
|
||||
@ -245,7 +245,7 @@ const loadPackages = async () => {
|
||||
try {
|
||||
packages.value = await getPackages(selectedCollectionId.value, {
|
||||
grade: selectedGrade.value || undefined,
|
||||
themeId: selectedThemeId.value,
|
||||
lessonType: selectedLessonType.value,
|
||||
keyword: searchKeyword.value || undefined,
|
||||
});
|
||||
} catch (error: any) {
|
||||
@ -266,8 +266,8 @@ const handlePrepare = (pkg: CoursePackage) => {
|
||||
router.push(`/teacher/courses/${pkg.id}/prepare`);
|
||||
};
|
||||
|
||||
// 监听年级变化
|
||||
watch(selectedGrade, () => {
|
||||
// 监听年级、课程配置变化
|
||||
watch([selectedGrade, selectedLessonType], () => {
|
||||
loadPackages();
|
||||
});
|
||||
|
||||
|
||||
@ -28,10 +28,15 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 主题标签行 -->
|
||||
<div v-if="pkg.themeName" class="tag-row theme-row">
|
||||
<span class="theme-tag">
|
||||
{{ pkg.themeName }}
|
||||
<!-- 课程配置标签行(参考管理端) -->
|
||||
<div v-if="lessonTypes.length > 0" class="tag-row config-row">
|
||||
<span
|
||||
v-for="lt in lessonTypes"
|
||||
:key="lt"
|
||||
class="config-tag"
|
||||
:style="getLessonTagStyle(lt)"
|
||||
>
|
||||
{{ getLessonTypeName(lt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -66,6 +71,7 @@ import {
|
||||
EditOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import type { CoursePackage } from '@/api/course-center';
|
||||
import { getLessonTypeName, getLessonTagStyle } from '@/utils/tagMaps';
|
||||
|
||||
const props = defineProps<{
|
||||
pkg: CoursePackage;
|
||||
@ -82,6 +88,17 @@ const gradeText = computed(() => {
|
||||
return grades.join(' · ');
|
||||
});
|
||||
|
||||
// 从 courses 提取课程类型列表(去重,与管理端一致)
|
||||
const lessonTypes = computed(() => {
|
||||
const courses = props.pkg.courses || [];
|
||||
const types = new Set<string>();
|
||||
for (const c of courses) {
|
||||
const t = c.lessonType;
|
||||
if (t) types.add(t);
|
||||
}
|
||||
return Array.from(types);
|
||||
});
|
||||
|
||||
// 获取图片完整 URL
|
||||
const getImageUrl = (path: string) => {
|
||||
if (!path) return '';
|
||||
@ -203,18 +220,17 @@ const handlePrepare = () => {
|
||||
border: 1px solid #FFD591;
|
||||
}
|
||||
|
||||
.theme-row {
|
||||
/* 主题标签样式 */
|
||||
.config-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.theme-tag {
|
||||
.config-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: #E6F7FF;
|
||||
color: #096DD9;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #91D5FF;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
|
||||
@ -124,6 +124,10 @@ const lessons = ref<any[]>([]);
|
||||
const classes = ref<any[]>([]);
|
||||
const selectedClassId = ref<number>();
|
||||
|
||||
/** 预约上课时使用的 courseId(Course.id)和 packageId,从加载数据中解析 */
|
||||
const scheduleCourseId = ref<number>();
|
||||
const schedulePackageId = ref<number>();
|
||||
|
||||
// 预约上课相关
|
||||
const scheduleModalRef = ref<InstanceType<typeof TeacherCreateScheduleModal>>();
|
||||
|
||||
@ -192,6 +196,10 @@ const loadCourseData = async () => {
|
||||
const res = await getTeacherSchoolCourseFullDetail(courseId.value as any);
|
||||
data = res.data || res;
|
||||
|
||||
// 预约上课:校本课程使用 sourceCourseId 作为 courseId
|
||||
scheduleCourseId.value = data.sourceCourseId ?? data.sourceCourse?.id;
|
||||
schedulePackageId.value = data.id;
|
||||
|
||||
// 转换校本课程包数据结构
|
||||
course.value = {
|
||||
id: data.id,
|
||||
@ -246,6 +254,10 @@ const loadCourseData = async () => {
|
||||
} else {
|
||||
// 加载标准课程包(id 传 string 避免 Long 精度丢失)
|
||||
data = await teacherApi.getTeacherCourse(courseId.value);
|
||||
// 预约上课:标准课程包,data.id 为 Course.id,首课的 courseId 与之相同
|
||||
scheduleCourseId.value = data.courseLessons?.[0]?.courseId ?? data.id;
|
||||
schedulePackageId.value = data.id;
|
||||
|
||||
course.value = {
|
||||
...data,
|
||||
courseLessons: data.courseLessons || [],
|
||||
@ -383,14 +395,15 @@ const showScheduleModal = () => {
|
||||
message.warning('课程数据异常,暂无课程');
|
||||
return;
|
||||
}
|
||||
const pkgId = parseInt(courseId.value, 10);
|
||||
if (isNaN(pkgId)) {
|
||||
message.warning('课程 ID 无效');
|
||||
const pkgId = schedulePackageId.value ?? parseInt(courseId.value, 10);
|
||||
const cid = scheduleCourseId.value ?? firstLesson.courseId ?? firstLesson.id;
|
||||
if (!cid) {
|
||||
message.warning('课程数据异常,无法预约');
|
||||
return;
|
||||
}
|
||||
scheduleModalRef.value?.openWithPreset({
|
||||
packageId: pkgId,
|
||||
courseId: firstLesson.id,
|
||||
courseId: cid,
|
||||
lessonType: firstLesson.lessonType || 'INTRODUCTION',
|
||||
classId: selectedClassId.value,
|
||||
});
|
||||
|
||||
@ -24,24 +24,14 @@
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<div class="filters">
|
||||
<a-select
|
||||
v-model:value="filters.classId"
|
||||
placeholder="选择班级"
|
||||
allow-clear
|
||||
style="width: 150px;"
|
||||
@change="handleFilter"
|
||||
>
|
||||
<a-select v-model:value="filters.classId" placeholder="选择班级" allow-clear style="width: 150px;"
|
||||
@change="handleFilter">
|
||||
<a-select-option v-for="cls in myClasses" :key="cls.id" :value="cls.id">
|
||||
{{ cls.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-input-search
|
||||
v-model:value="filters.keyword"
|
||||
placeholder="搜索标题"
|
||||
style="width: 200px;"
|
||||
@search="handleFilter"
|
||||
allow-clear
|
||||
/>
|
||||
<a-input-search v-model:value="filters.keyword" placeholder="搜索标题" style="width: 200px;" @search="handleFilter"
|
||||
allow-clear />
|
||||
</div>
|
||||
<a-button type="primary" class="add-btn" @click="showAddModal">
|
||||
<PlusOutlined class="btn-icon" />
|
||||
@ -51,14 +41,10 @@
|
||||
|
||||
<!-- 档案卡片网格 -->
|
||||
<div class="record-grid" v-if="!loading && records.length > 0">
|
||||
<div
|
||||
v-for="record in records"
|
||||
:key="record.id"
|
||||
class="record-card"
|
||||
>
|
||||
<div v-for="record in records" :key="record.id" class="record-card">
|
||||
<div class="card-cover">
|
||||
<div v-if="record.images?.length" class="cover-image ">
|
||||
<img :src="getImageUrl(record.images[0])" alt="cover" />
|
||||
<img :src="getImageUrl(record.images[0])" class="pos-absolute !object-contain " />
|
||||
<div class="image-count" v-if="record.images.length > 1">
|
||||
<CameraOutlined /> {{ record.images.length }}
|
||||
</div>
|
||||
@ -88,7 +74,8 @@
|
||||
<span>{{ formatDate(record.recordDate) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="record-content">{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }}</p>
|
||||
<p class="record-content">{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a-button type="link" size="small" @click="handleView(record)">
|
||||
@ -124,12 +111,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑档案弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
width="700px"
|
||||
@ok="handleModalOk"
|
||||
:confirm-loading="submitting"
|
||||
>
|
||||
<a-modal v-model:open="modalVisible" width="700px" @ok="handleModalOk" :confirm-loading="submitting">
|
||||
<template #title>
|
||||
<span class="modal-title">
|
||||
<EditOutlined v-if="isEdit" class="modal-title-icon" />
|
||||
@ -137,20 +119,10 @@
|
||||
{{ isEdit ? ' 编辑档案' : ' 添加档案' }}
|
||||
</span>
|
||||
</template>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formState"
|
||||
:rules="rules"
|
||||
:label-col="{ span: 4 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
|
||||
<a-form-item label="学生" name="studentId" v-if="!isEdit">
|
||||
<a-select
|
||||
v-model:value="formState.studentId"
|
||||
placeholder="请选择学生"
|
||||
show-search
|
||||
:filter-option="filterStudentOption"
|
||||
>
|
||||
<a-select v-model:value="formState.studentId" placeholder="请选择学生" show-search
|
||||
:filter-option="filterStudentOption">
|
||||
<a-select-option v-for="student in students" :key="student.id" :value="student.id">
|
||||
{{ student.name }} - {{ student.className }}
|
||||
</a-select-option>
|
||||
@ -158,36 +130,26 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="档案类型" name="recordType" v-if="!isEdit">
|
||||
<a-radio-group v-model:value="formState.recordType">
|
||||
<a-radio value="STUDENT"><UserOutlined class="radio-icon" /> 个人档案</a-radio>
|
||||
<a-radio value="CLASS"><TeamOutlined class="radio-icon" /> 班级档案</a-radio>
|
||||
<a-radio value="STUDENT">
|
||||
<UserOutlined class="radio-icon" /> 个人档案
|
||||
</a-radio>
|
||||
<a-radio value="CLASS">
|
||||
<TeamOutlined class="radio-icon" /> 班级档案
|
||||
</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="标题" name="title">
|
||||
<a-input v-model:value="formState.title" placeholder="请输入档案标题" />
|
||||
</a-form-item>
|
||||
<a-form-item label="记录日期" name="recordDate">
|
||||
<a-date-picker
|
||||
v-model:value="formState.recordDateValue"
|
||||
style="width: 100%;"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
<a-date-picker v-model:value="formState.recordDateValue" style="width: 100%;" value-format="YYYY-MM-DD" />
|
||||
</a-form-item>
|
||||
<a-form-item label="内容" name="content">
|
||||
<a-textarea
|
||||
v-model:value="formState.content"
|
||||
placeholder="请输入档案内容"
|
||||
:rows="4"
|
||||
/>
|
||||
<a-textarea v-model:value="formState.content" placeholder="请输入档案内容" :rows="4" />
|
||||
</a-form-item>
|
||||
<a-form-item label="图片">
|
||||
<a-upload
|
||||
v-model:file-list="fileList"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
list-type="picture-card"
|
||||
:max-count="9"
|
||||
@change="handleUploadChange"
|
||||
>
|
||||
<a-upload v-model:file-list="fileList" :custom-request="handleCustomUpload" list-type="picture-card"
|
||||
:max-count="9" accept="image/*" @change="handleUploadChange">
|
||||
<div v-if="fileList.length < 9">
|
||||
<PlusOutlined />
|
||||
<div style="margin-top: 8px">上传</div>
|
||||
@ -198,11 +160,7 @@
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看档案详情弹窗 -->
|
||||
<a-modal
|
||||
v-model:open="detailModalVisible"
|
||||
width="700px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-modal v-model:open="detailModalVisible" width="700px" :footer="null">
|
||||
<template #title>
|
||||
<span class="modal-title">
|
||||
<CameraOutlined class="modal-title-icon" />
|
||||
@ -231,12 +189,8 @@
|
||||
|
||||
<div v-if="currentRecord.images?.length" class="image-gallery">
|
||||
<a-image-preview-group>
|
||||
<a-image
|
||||
v-for="(img, index) in currentRecord.images"
|
||||
:key="index"
|
||||
:src="getImageUrl(img)"
|
||||
style="width: 100px; height: 100px; object-fit: cover; margin-right: 8px; border-radius: 8px;"
|
||||
/>
|
||||
<a-image v-for="(img, index) in currentRecord.images" :key="index" :src="getImageUrl(img)"
|
||||
style="width: 100px; height: 100px; object-fit: cover; margin-right: 8px; border-radius: 8px;" />
|
||||
</a-image-preview-group>
|
||||
</div>
|
||||
</div>
|
||||
@ -245,14 +199,9 @@
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrapper" v-if="records.length > 0">
|
||||
<a-pagination
|
||||
v-model:current="pagination.current"
|
||||
v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:show-size-changer="true"
|
||||
:show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
<a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
|
||||
:total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
|
||||
@change="handlePageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -283,6 +232,7 @@ import {
|
||||
type UpdateGrowthRecordDto,
|
||||
} from '@/api/growth';
|
||||
import { getTeacherClasses, getTeacherStudents } from '@/api/teacher';
|
||||
import { fileApi, validateFileType } from '@/api/file';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
@ -309,9 +259,6 @@ const records = ref<GrowthRecord[]>([]);
|
||||
const currentRecord = ref<GrowthRecord | null>(null);
|
||||
const fileList = ref<any[]>([]);
|
||||
|
||||
const uploadUrl = '/api/upload';
|
||||
const uploadHeaders = {};
|
||||
|
||||
const formState = reactive<CreateGrowthRecordDto & { recordDateValue?: string }>({
|
||||
studentId: undefined as any,
|
||||
classId: undefined,
|
||||
@ -476,10 +423,41 @@ const handleModalOk = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (info: any) => {
|
||||
if (info.file.status === 'done') {
|
||||
formState.images.push(info.file.response.url);
|
||||
/** OSS 直传:自定义上传 */
|
||||
const handleCustomUpload = async (options: any) => {
|
||||
const { file, onSuccess, onError, onProgress } = options;
|
||||
const uploadFile = file instanceof File ? file : (file?.originFileObj ?? file);
|
||||
|
||||
const isImage = uploadFile.type?.startsWith('image/');
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件');
|
||||
onError?.(new Error('只能上传图片'));
|
||||
return;
|
||||
}
|
||||
|
||||
const validation = validateFileType(uploadFile, 'POSTER');
|
||||
if (!validation.valid) {
|
||||
message.error(validation.error);
|
||||
onError?.(new Error(validation.error));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await fileApi.uploadFile(uploadFile, 'resource', {
|
||||
onProgress: (percent) => onProgress?.({ percent }),
|
||||
});
|
||||
onSuccess?.({ url: result.filePath });
|
||||
} catch (err: any) {
|
||||
const msg = err?.message || '上传失败';
|
||||
message.error(msg);
|
||||
onError?.(new Error(msg));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadChange = (info: any) => {
|
||||
formState.images = (info.fileList || [])
|
||||
.filter((f: any) => f.response?.url)
|
||||
.map((f: any) => f.response.url);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -602,6 +580,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
@ -616,20 +596,30 @@ onMounted(() => {
|
||||
|
||||
.card-cover {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
height: 160px;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
}
|
||||
|
||||
.cover-image img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-count {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 8px;
|
||||
z-index: 2;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
@ -655,6 +645,7 @@ onMounted(() => {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 11px;
|
||||
@ -679,7 +670,11 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.card-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.record-title {
|
||||
@ -713,16 +708,24 @@ onMounted(() => {
|
||||
font-size: 13px;
|
||||
color: #636E72;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
margin: 0 0 12px 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #F0F0F0;
|
||||
background: #FAFAFA;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
destroy-on-close
|
||||
>
|
||||
<a-steps :current="currentStep" size="small" class="steps-navigator">
|
||||
<a-step title="选择套餐" />
|
||||
<a-step title="选择课程包" />
|
||||
<a-step title="选择课程类型" />
|
||||
<a-step title="选择班级" />
|
||||
@ -16,8 +17,38 @@
|
||||
</a-steps>
|
||||
|
||||
<div class="step-content">
|
||||
<!-- 步骤1: 选择课程包(租户仅一个套餐,直接展示套餐下课程包) -->
|
||||
<!-- 步骤1: 选择套餐(租户可拥有多个套餐,一对多) -->
|
||||
<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>
|
||||
<a-spin :spinning="loadingPackages">
|
||||
<div v-if="selectedCollection && selectedCollection.packages && selectedCollection.packages.length > 0" class="packages-section">
|
||||
@ -34,11 +65,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!loadingPackages && collections.length > 0" class="packages-section">
|
||||
<a-alert message="暂无课程包" type="warning" show-icon />
|
||||
<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 && collections.length === 0" class="packages-section">
|
||||
<a-alert message="暂无课程套餐,请联系管理员" type="info" show-icon />
|
||||
<div v-else-if="!loadingPackages && !selectedCollection" class="packages-section">
|
||||
<a-alert message="请先选择套餐" type="info" show-icon />
|
||||
</div>
|
||||
</a-spin>
|
||||
|
||||
@ -70,8 +101,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2: 选择课程类型 -->
|
||||
<div v-show="currentStep === 1" class="step-panel">
|
||||
<!-- 步骤3: 选择课程类型 -->
|
||||
<div v-show="currentStep === 2" class="step-panel">
|
||||
<h3>选择课程类型</h3>
|
||||
<a-alert
|
||||
message="请选择一个课程类型进行排课"
|
||||
@ -101,8 +132,8 @@
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3: 选择班级(教师端单选) -->
|
||||
<div v-show="currentStep === 2" class="step-panel">
|
||||
<!-- 步骤4: 选择班级(教师端单选) -->
|
||||
<div v-show="currentStep === 3" class="step-panel">
|
||||
<h3>选择班级</h3>
|
||||
<a-alert
|
||||
message="请选择要排课的班级"
|
||||
@ -124,8 +155,8 @@
|
||||
<div v-if="myClasses.length === 0" class="empty-hint">暂无可用班级</div>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4: 设置时间 -->
|
||||
<div v-show="currentStep === 3" class="step-panel">
|
||||
<!-- 步骤5: 设置时间 -->
|
||||
<div v-show="currentStep === 4" class="step-panel">
|
||||
<h3>设置时间</h3>
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="排课日期" required>
|
||||
@ -165,7 +196,7 @@
|
||||
<template #footer>
|
||||
<div class="modal-footer">
|
||||
<a-button v-if="currentStep > 0" @click="prevStep">上一步</a-button>
|
||||
<a-button v-if="currentStep < 3" type="primary" @click="nextStep">下一步</a-button>
|
||||
<a-button v-if="currentStep < 4" type="primary" @click="nextStep">下一步</a-button>
|
||||
<a-button v-else type="primary" :loading="loading" @click="handleSubmit">创建排课</a-button>
|
||||
<a-button @click="handleCancel">取消</a-button>
|
||||
</div>
|
||||
@ -206,6 +237,8 @@ const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const loadingLessonTypes = ref(false);
|
||||
const currentStep = ref(0);
|
||||
/** 从课程详情进入时,跳过套餐与课程包选择 */
|
||||
const isPresetMode = ref(false);
|
||||
|
||||
const collections = ref<CourseCollection[]>([]);
|
||||
const loadingPackages = ref(false);
|
||||
@ -301,9 +334,12 @@ export interface SchedulePreset {
|
||||
courseId: number;
|
||||
lessonType: string;
|
||||
classId?: number;
|
||||
/** 可选,若已知所属套餐可传入以节省请求 */
|
||||
collectionId?: number;
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
isPresetMode.value = false;
|
||||
visible.value = true;
|
||||
currentStep.value = 0;
|
||||
resetForm();
|
||||
@ -311,8 +347,9 @@ const open = () => {
|
||||
loadMyClasses();
|
||||
};
|
||||
|
||||
/** 从课程中心打开,预填课程包、课程类型、班级,直接进入选择班级或设置时间 */
|
||||
/** 从课程详情打开:跳过套餐与课程包,从选择课程类型开始 */
|
||||
const openWithPreset = async (preset: SchedulePreset) => {
|
||||
isPresetMode.value = true;
|
||||
visible.value = true;
|
||||
resetForm();
|
||||
formData.packageId = preset.packageId;
|
||||
@ -321,13 +358,17 @@ const openWithPreset = async (preset: SchedulePreset) => {
|
||||
formData.classId = preset.classId;
|
||||
|
||||
await loadMyClasses();
|
||||
try {
|
||||
await loadLessonTypes(preset.packageId);
|
||||
|
||||
if (preset.classId) {
|
||||
currentStep.value = 3; // 直接到设置时间
|
||||
} else {
|
||||
currentStep.value = 2; // 到选择班级
|
||||
} catch {
|
||||
// 校本课程等场景可能无对应课程包接口,使用预设类型填充
|
||||
lessonTypes.value = preset.lessonType
|
||||
? [{ lessonType: preset.lessonType, count: 1 }]
|
||||
: [];
|
||||
}
|
||||
|
||||
// 从选择课程类型(步骤2)开始,跳过套餐与课程包
|
||||
currentStep.value = 2;
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
@ -342,21 +383,14 @@ const resetForm = () => {
|
||||
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('该套餐暂无课程包');
|
||||
}
|
||||
// 若仅有一个套餐,自动选中并加载其课程包,提升体验
|
||||
if (collections.value.length === 1) {
|
||||
await selectCollection(collections.value[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载课程套餐失败:', error);
|
||||
@ -366,6 +400,30 @@ const loadCollections = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 选择套餐,并加载该套餐下的课程包
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const loadMyClasses = async () => {
|
||||
try {
|
||||
myClasses.value = await getTeacherClasses();
|
||||
@ -456,7 +514,13 @@ const validateStep = (): boolean => {
|
||||
switch (currentStep.value) {
|
||||
case 0:
|
||||
if (!formData.collectionId) {
|
||||
message.warning('请选择课程套餐');
|
||||
message.warning('请选择套餐');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (!formData.collectionId) {
|
||||
message.warning('请选择套餐');
|
||||
return false;
|
||||
}
|
||||
if (!formData.packageId) {
|
||||
@ -468,19 +532,19 @@ const validateStep = (): boolean => {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
case 2:
|
||||
if (!formData.lessonType) {
|
||||
message.warning('请选择课程类型');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
if (!formData.classId) {
|
||||
message.warning('请选择班级');
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
case 4:
|
||||
if (!formData.scheduledDate) {
|
||||
message.warning('请选择排课日期');
|
||||
return false;
|
||||
@ -499,6 +563,11 @@ const nextStep = () => {
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
// 预设模式下从步骤2点击上一步时关闭弹窗(不展示套餐/课程包选择)
|
||||
if (isPresetMode.value && currentStep.value === 2) {
|
||||
handleCancel();
|
||||
return;
|
||||
}
|
||||
currentStep.value--;
|
||||
};
|
||||
|
||||
@ -580,6 +649,12 @@ defineExpose({ open, openWithPreset });
|
||||
|
||||
.packages-section { margin-top: 24px; }
|
||||
|
||||
.collections-grid {
|
||||
.collection-card {
|
||||
.package-count { color: #722ed1; }
|
||||
}
|
||||
}
|
||||
|
||||
.packages-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
|
||||
@ -3,6 +3,7 @@ package com.reading.platform.common.mapper;
|
||||
import com.reading.platform.dto.response.GrowthRecordResponse;
|
||||
import com.reading.platform.entity.GrowthRecord;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
import java.util.List;
|
||||
@ -16,8 +17,9 @@ public interface GrowthRecordMapper {
|
||||
GrowthRecordMapper INSTANCE = Mappers.getMapper(GrowthRecordMapper.class);
|
||||
|
||||
/**
|
||||
* Entity 转 Response
|
||||
* Entity 转 Response(images 从 JSON 字符串解析为 List)
|
||||
*/
|
||||
@Mapping(target = "images", expression = "java(java.util.Arrays.asList(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getImages())))")
|
||||
GrowthRecordResponse toVO(GrowthRecord entity);
|
||||
|
||||
/**
|
||||
@ -26,7 +28,8 @@ public interface GrowthRecordMapper {
|
||||
List<GrowthRecordResponse> toVO(List<GrowthRecord> entities);
|
||||
|
||||
/**
|
||||
* Response 转 Entity(用于创建/更新时)
|
||||
* Response 转 Entity(images 从 List 转为 JSON 字符串)
|
||||
*/
|
||||
@Mapping(target = "images", expression = "java(vo.getImages() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getImages()) : null)")
|
||||
GrowthRecord toEntity(GrowthRecordResponse vo);
|
||||
}
|
||||
|
||||
@ -50,13 +50,13 @@ public class SchoolPackageController {
|
||||
public Result<List<CoursePackageResponse>> getPackagesByCollection(
|
||||
@PathVariable Long collectionId,
|
||||
@RequestParam(required = false) String grade,
|
||||
@RequestParam(required = false) Long themeId,
|
||||
@RequestParam(required = false) String lessonType,
|
||||
@RequestParam(required = false) String keyword) {
|
||||
return Result.success(collectionService.getPackagesByCollection(collectionId, grade, themeId, keyword));
|
||||
return Result.success(collectionService.getPackagesByCollection(collectionId, grade, lessonType, keyword));
|
||||
}
|
||||
|
||||
@GetMapping("/{collectionId}/filter-meta")
|
||||
@Operation(summary = "获取套餐筛选元数据(年级、主题选项)")
|
||||
@Operation(summary = "获取套餐筛选元数据(年级、课程配置选项)")
|
||||
@RequireRole({UserRole.SCHOOL, UserRole.TEACHER})
|
||||
public Result<PackageFilterMetaResponse> getFilterMeta(@PathVariable Long collectionId) {
|
||||
return Result.success(collectionService.getPackageFilterMeta(collectionId));
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package com.reading.platform.controller.teacher;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.reading.platform.common.mapper.GrowthRecordMapper;
|
||||
import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.security.SecurityUtils;
|
||||
import com.reading.platform.dto.request.GrowthRecordCreateRequest;
|
||||
import com.reading.platform.dto.request.GrowthRecordUpdateRequest;
|
||||
import com.reading.platform.dto.response.GrowthRecordResponse;
|
||||
import com.reading.platform.entity.GrowthRecord;
|
||||
import com.reading.platform.service.GrowthRecordService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@ -14,6 +16,8 @@ import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "教师端 - 成长记录", description = "教师端成长记录 API")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/teacher/growth-records")
|
||||
@ -21,37 +25,42 @@ import org.springframework.web.bind.annotation.*;
|
||||
public class TeacherGrowthController {
|
||||
|
||||
private final GrowthRecordService growthRecordService;
|
||||
private final GrowthRecordMapper growthRecordMapper;
|
||||
|
||||
@Operation(summary = "创建成长记录")
|
||||
@PostMapping
|
||||
public Result<GrowthRecord> createGrowthRecord(@Valid @RequestBody GrowthRecordCreateRequest request) {
|
||||
public Result<GrowthRecordResponse> createGrowthRecord(@Valid @RequestBody GrowthRecordCreateRequest request) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Long userId = SecurityUtils.getCurrentUserId();
|
||||
return Result.success(growthRecordService.createGrowthRecord(tenantId, userId, "teacher", request));
|
||||
GrowthRecord record = growthRecordService.createGrowthRecord(tenantId, userId, "teacher", request);
|
||||
return Result.success(growthRecordMapper.toVO(record));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新成长记录")
|
||||
@PutMapping("/{id}")
|
||||
public Result<GrowthRecord> updateGrowthRecord(@PathVariable Long id, @RequestBody GrowthRecordUpdateRequest request) {
|
||||
return Result.success(growthRecordService.updateGrowthRecord(id, request));
|
||||
public Result<GrowthRecordResponse> updateGrowthRecord(@PathVariable Long id, @RequestBody GrowthRecordUpdateRequest request) {
|
||||
GrowthRecord record = growthRecordService.updateGrowthRecord(id, request);
|
||||
return Result.success(growthRecordMapper.toVO(record));
|
||||
}
|
||||
|
||||
@Operation(summary = "根据 ID 获取成长记录")
|
||||
@GetMapping("/{id}")
|
||||
public Result<GrowthRecord> getGrowthRecord(@PathVariable Long id) {
|
||||
return Result.success(growthRecordService.getGrowthRecordById(id));
|
||||
public Result<GrowthRecordResponse> getGrowthRecord(@PathVariable Long id) {
|
||||
GrowthRecord record = growthRecordService.getGrowthRecordById(id);
|
||||
return Result.success(growthRecordMapper.toVO(record));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取成长记录分页列表")
|
||||
@GetMapping
|
||||
public Result<PageResult<GrowthRecord>> getGrowthRecordPage(
|
||||
public Result<PageResult<GrowthRecordResponse>> getGrowthRecordPage(
|
||||
@RequestParam(required = false, defaultValue = "1") Integer pageNum,
|
||||
@RequestParam(required = false, defaultValue = "10") Integer pageSize,
|
||||
@RequestParam(required = false) Long studentId,
|
||||
@RequestParam(required = false) String type) {
|
||||
Long tenantId = SecurityUtils.getCurrentTenantId();
|
||||
Page<GrowthRecord> page = growthRecordService.getGrowthRecordPage(tenantId, pageNum, pageSize, studentId, type);
|
||||
return Result.success(PageResult.of(page));
|
||||
List<GrowthRecordResponse> voList = growthRecordMapper.toVO(page.getRecords());
|
||||
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除成长记录")
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.reading.platform.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
@ -17,7 +18,8 @@ public class GrowthRecordCreateRequest {
|
||||
private Long studentId;
|
||||
|
||||
@NotBlank(message = "类型不能为空")
|
||||
@Schema(description = "类型:reading-阅读,behavior-行为,achievement-成就,milestone-里程碑")
|
||||
@JsonAlias("recordType")
|
||||
@Schema(description = "类型:reading-阅读,behavior-行为,achievement-成就,milestone-里程碑,STUDENT-学生记录")
|
||||
private String type;
|
||||
|
||||
@NotBlank(message = "标题不能为空")
|
||||
@ -27,8 +29,8 @@ public class GrowthRecordCreateRequest {
|
||||
@Schema(description = "内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "图片(JSON 数组)")
|
||||
private String images;
|
||||
@Schema(description = "图片 URL 列表")
|
||||
private List<String> images;
|
||||
|
||||
@Schema(description = "记录日期")
|
||||
private LocalDate recordDate;
|
||||
|
||||
@ -19,8 +19,8 @@ public class GrowthRecordUpdateRequest {
|
||||
@Schema(description = "内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "图片(JSON 数组)")
|
||||
private String images;
|
||||
@Schema(description = "图片 URL 列表")
|
||||
private List<String> images;
|
||||
|
||||
@Schema(description = "记录日期")
|
||||
private LocalDate recordDate;
|
||||
|
||||
@ -6,6 +6,7 @@ import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 成长记录响应
|
||||
@ -34,8 +35,8 @@ public class GrowthRecordResponse {
|
||||
@Schema(description = "内容")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "图片")
|
||||
private String images;
|
||||
@Schema(description = "图片URL列表")
|
||||
private List<String> images;
|
||||
|
||||
@Schema(description = "记录人 ID")
|
||||
private Long recordedBy;
|
||||
|
||||
@ -10,7 +10,7 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* 套餐筛选元数据响应
|
||||
* 用于返回套餐下课程包的筛选选项(年级、主题)
|
||||
* 用于返回套餐下课程包的筛选选项(年级、课程配置)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ -22,8 +22,8 @@ public class PackageFilterMetaResponse {
|
||||
@Schema(description = "年级选项列表")
|
||||
private List<GradeOption> grades;
|
||||
|
||||
@Schema(description = "主题选项列表")
|
||||
private List<ThemeOption> themes;
|
||||
@Schema(description = "课程配置选项列表(导入课、集体课、健康、科学等)")
|
||||
private List<LessonTypeOption> lessonTypes;
|
||||
|
||||
/**
|
||||
* 年级选项
|
||||
@ -42,21 +42,21 @@ public class PackageFilterMetaResponse {
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题选项
|
||||
* 课程配置选项(课程环节类型)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "主题选项")
|
||||
public static class ThemeOption {
|
||||
@Schema(description = "主题ID")
|
||||
private Long id;
|
||||
@Schema(description = "课程配置选项")
|
||||
public static class LessonTypeOption {
|
||||
@Schema(description = "课程类型编码,如 INTRODUCTION、COLLECTIVE、HEALTH")
|
||||
private String lessonType;
|
||||
|
||||
@Schema(description = "主题名称")
|
||||
@Schema(description = "课程类型中文名称,如 导入课、集体课、健康")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "该主题下的课程包数量")
|
||||
@Schema(description = "包含该类型环节的课程包数量")
|
||||
private Integer count;
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,14 +39,14 @@ public interface CourseCollectionService extends IService<CourseCollection> {
|
||||
* 获取课程套餐下的课程包列表(支持筛选)
|
||||
* @param collectionId 套餐ID
|
||||
* @param grade 年级筛选
|
||||
* @param themeId 主题ID筛选
|
||||
* @param lessonType 课程配置筛选(INTRODUCTION、COLLECTIVE、HEALTH、LANGUAGE、SCIENCE、SOCIAL、ART)
|
||||
* @param keyword 关键词搜索
|
||||
* @return 课程包列表
|
||||
*/
|
||||
List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword);
|
||||
List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword);
|
||||
|
||||
/**
|
||||
* 获取套餐的筛选元数据(年级、主题选项)
|
||||
* 获取套餐的筛选元数据(年级、课程配置选项)
|
||||
* @param collectionId 套餐ID
|
||||
* @return 筛选元数据
|
||||
*/
|
||||
|
||||
@ -11,7 +11,12 @@ import com.reading.platform.common.response.PageResult;
|
||||
import com.reading.platform.dto.response.CourseCollectionResponse;
|
||||
import com.reading.platform.dto.response.CoursePackageResponse;
|
||||
import com.reading.platform.dto.response.PackageFilterMetaResponse;
|
||||
import com.reading.platform.entity.*;
|
||||
import com.reading.platform.entity.CourseCollection;
|
||||
import com.reading.platform.entity.CourseCollectionPackage;
|
||||
import com.reading.platform.entity.CourseLesson;
|
||||
import com.reading.platform.entity.CoursePackage;
|
||||
import com.reading.platform.entity.TenantPackage;
|
||||
import com.reading.platform.entity.Theme;
|
||||
import com.reading.platform.mapper.*;
|
||||
import com.reading.platform.service.CourseCollectionService;
|
||||
import com.reading.platform.service.CourseLessonService;
|
||||
@ -208,8 +213,8 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
* 获取课程套餐下的课程包列表(支持筛选)
|
||||
*/
|
||||
@Override
|
||||
public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, Long themeId, String keyword) {
|
||||
log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, themeId={}, keyword={}", collectionId, grade, themeId, keyword);
|
||||
public List<CoursePackageResponse> getPackagesByCollection(Long collectionId, String grade, String lessonType, String keyword) {
|
||||
log.info("获取课程套餐的课程包列表(筛选),collectionId={}, grade={}, lessonType={}, keyword={}", collectionId, grade, lessonType, keyword);
|
||||
|
||||
// 查询关联关系
|
||||
List<CourseCollectionPackage> associations = collectionPackageMapper.selectList(
|
||||
@ -227,6 +232,16 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
.map(CourseCollectionPackage::getPackageId)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 课程配置筛选:仅保留包含该课程环节类型的课程包
|
||||
if (StringUtils.hasText(lessonType)) {
|
||||
List<Long> idsWithLesson = courseLessonService.findCourseIdsByLessonType(lessonType);
|
||||
Set<Long> idSet = new HashSet<>(idsWithLesson);
|
||||
packageIds = packageIds.stream().filter(idSet::contains).collect(Collectors.toList());
|
||||
if (packageIds.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
LambdaQueryWrapper<CoursePackage> wrapper = new LambdaQueryWrapper<CoursePackage>()
|
||||
.in(CoursePackage::getId, packageIds)
|
||||
@ -237,11 +252,6 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
wrapper.apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\"");
|
||||
}
|
||||
|
||||
// 主题筛选
|
||||
if (themeId != null) {
|
||||
wrapper.eq(CoursePackage::getThemeId, themeId);
|
||||
}
|
||||
|
||||
// 关键词搜索
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(w -> w
|
||||
@ -304,7 +314,7 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
if (associations.isEmpty()) {
|
||||
return PackageFilterMetaResponse.builder()
|
||||
.grades(new ArrayList<>())
|
||||
.themes(new ArrayList<>())
|
||||
.lessonTypes(new ArrayList<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -337,36 +347,70 @@ public class CourseCollectionServiceImpl extends ServiceImpl<CourseCollectionMap
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 统计主题分布
|
||||
Map<Long, Integer> themeCountMap = new HashMap<>();
|
||||
Set<Long> themeIds = new HashSet<>();
|
||||
for (CoursePackage pkg : packages) {
|
||||
if (pkg.getThemeId() != null) {
|
||||
themeCountMap.merge(pkg.getThemeId(), 1, Integer::sum);
|
||||
themeIds.add(pkg.getThemeId());
|
||||
// 统计课程配置(课程环节类型)分布:查询所有课程环节,按规范化类型统计课程包数量
|
||||
List<CourseLesson> allLessons = courseLessonService.list(
|
||||
new LambdaQueryWrapper<CourseLesson>()
|
||||
.in(CourseLesson::getCourseId, packageIds)
|
||||
.select(CourseLesson::getCourseId, CourseLesson::getLessonType)
|
||||
);
|
||||
// 规范化类型 -> 包含该类型的课程包ID集合
|
||||
Map<String, Set<Long>> typeToPackageIds = new HashMap<>();
|
||||
for (CourseLesson lesson : allLessons) {
|
||||
String canonical = normalizeLessonTypeForFilter(lesson.getLessonType());
|
||||
if (canonical != null) {
|
||||
typeToPackageIds.computeIfAbsent(canonical, k -> new HashSet<>()).add(lesson.getCourseId());
|
||||
}
|
||||
}
|
||||
|
||||
// 批量查询主题名称
|
||||
List<PackageFilterMetaResponse.ThemeOption> themes = new ArrayList<>();
|
||||
if (!themeIds.isEmpty()) {
|
||||
List<Theme> themeList = themeMapper.selectBatchIds(themeIds);
|
||||
themes = themeList.stream()
|
||||
.filter(t -> themeCountMap.containsKey(t.getId()))
|
||||
.map(t -> PackageFilterMetaResponse.ThemeOption.builder()
|
||||
.id(t.getId())
|
||||
.name(t.getName())
|
||||
.count(themeCountMap.get(t.getId()))
|
||||
// 按固定顺序生成课程配置选项(与管理端一致)
|
||||
List<String> lessonTypeOrder = List.of("INTRODUCTION", "COLLECTIVE", "HEALTH", "LANGUAGE", "SCIENCE", "SOCIAL", "ART");
|
||||
Map<String, String> typeToName = getLessonTypeDisplayNames();
|
||||
List<PackageFilterMetaResponse.LessonTypeOption> lessonTypes = lessonTypeOrder.stream()
|
||||
.filter(typeToPackageIds::containsKey)
|
||||
.map(type -> PackageFilterMetaResponse.LessonTypeOption.builder()
|
||||
.lessonType(type)
|
||||
.name(typeToName.getOrDefault(type, type))
|
||||
.count(typeToPackageIds.get(type).size())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return PackageFilterMetaResponse.builder()
|
||||
.grades(grades)
|
||||
.themes(themes)
|
||||
.lessonTypes(lessonTypes)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将课程环节类型规范化为筛选用的标准编码(与管理端、前端 LESSON_TYPE_NAMES 一致)
|
||||
*/
|
||||
private String normalizeLessonTypeForFilter(String lessonType) {
|
||||
if (!StringUtils.hasText(lessonType)) return null;
|
||||
return switch (lessonType.toUpperCase()) {
|
||||
case "INTRODUCTION", "INTRO" -> "INTRODUCTION";
|
||||
case "COLLECTIVE" -> "COLLECTIVE";
|
||||
case "LANGUAGE", "DOMAIN_LANGUAGE" -> "LANGUAGE";
|
||||
case "HEALTH", "DOMAIN_HEALTH" -> "HEALTH";
|
||||
case "SCIENCE", "DOMAIN_SCIENCE" -> "SCIENCE";
|
||||
case "SOCIAL", "SOCIETY", "DOMAIN_SOCIAL" -> "SOCIAL";
|
||||
case "ART", "DOMAIN_ART" -> "ART";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 课程类型编码 -> 中文名称(与管理端、前端 tagMaps 一致)
|
||||
*/
|
||||
private Map<String, String> getLessonTypeDisplayNames() {
|
||||
Map<String, String> m = new HashMap<>();
|
||||
m.put("INTRODUCTION", "导入课");
|
||||
m.put("COLLECTIVE", "集体课");
|
||||
m.put("LANGUAGE", "语言");
|
||||
m.put("HEALTH", "健康");
|
||||
m.put("SCIENCE", "科学");
|
||||
m.put("SOCIAL", "社会");
|
||||
m.put("ART", "艺术");
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析年级标签
|
||||
*/
|
||||
|
||||
@ -172,9 +172,9 @@ public class CoursePackageServiceImpl extends ServiceImpl<CoursePackageMapper, C
|
||||
String grade = selectedGrades[i].trim();
|
||||
if (StringUtils.hasText(grade)) {
|
||||
if (i == 0) {
|
||||
w.apply("JSON_CONTAINS(grade_tags, ?)", "\"" + grade + "\"");
|
||||
w.apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\"");
|
||||
} else {
|
||||
w.or().apply("JSON_CONTAINS(grade_tags, ?)", "\"" + grade + "\"");
|
||||
w.or().apply("JSON_CONTAINS(grade_tags, {0})", "\"" + grade + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,7 +39,13 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
|
||||
record.setType(request.getType());
|
||||
record.setTitle(request.getTitle());
|
||||
record.setContent(request.getContent());
|
||||
record.setImages(request.getImages());
|
||||
if (request.getImages() != null) {
|
||||
try {
|
||||
record.setImages(objectMapper.writeValueAsString(request.getImages()));
|
||||
} catch (JsonProcessingException e) {
|
||||
record.setImages("[]");
|
||||
}
|
||||
}
|
||||
record.setRecordedBy(recorderId);
|
||||
record.setRecorderRole(recorderRole);
|
||||
record.setRecordDate(request.getRecordDate() != null ? request.getRecordDate() : LocalDate.now());
|
||||
@ -72,7 +78,11 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
|
||||
record.setContent(request.getContent());
|
||||
}
|
||||
if (request.getImages() != null) {
|
||||
record.setImages(request.getImages());
|
||||
try {
|
||||
record.setImages(objectMapper.writeValueAsString(request.getImages()));
|
||||
} catch (JsonProcessingException e) {
|
||||
record.setImages("[]");
|
||||
}
|
||||
}
|
||||
if (request.getRecordDate() != null) {
|
||||
record.setRecordDate(request.getRecordDate());
|
||||
@ -127,7 +137,8 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
|
||||
if (StringUtils.hasText(type)) {
|
||||
wrapper.eq(GrowthRecord::getType, type);
|
||||
}
|
||||
wrapper.orderByDesc(GrowthRecord::getRecordDate);
|
||||
// 按最新修改时间、最新创建时间倒序
|
||||
wrapper.orderByDesc(GrowthRecord::getUpdatedAt).orderByDesc(GrowthRecord::getCreatedAt);
|
||||
|
||||
return growthRecordMapper.selectPage(page, wrapper);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user