fix: 成长档案 images JSON 格式化、排序与卡片排版

- 后端:GrowthRecordResponse.images 改为 List<String>,Mapper 解析 JSON
- 后端:TeacherGrowthController 返回 GrowthRecordResponse 统一 images 格式
- 后端:分页按 updatedAt、createdAt 倒序排序
- 前端:教师/学校端成长档案卡片封面与排版修复(cover 约束、flex 布局)

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-23 11:58:10 +08:00
parent ac8e07c784
commit 7d659e87c8
8 changed files with 199 additions and 113 deletions

View File

@ -182,10 +182,10 @@
<a-form-item label="图片"> <a-form-item label="图片">
<a-upload <a-upload
v-model:file-list="fileList" v-model:file-list="fileList"
:action="uploadUrl" :custom-request="handleCustomUpload"
:headers="uploadHeaders"
list-type="picture-card" list-type="picture-card"
:max-count="9" :max-count="9"
accept="image/*"
@change="handleUploadChange" @change="handleUploadChange"
> >
<div v-if="fileList.length < 9"> <div v-if="fileList.length < 9">
@ -273,6 +273,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { FormInstance } from 'ant-design-vue'; import type { FormInstance } from 'ant-design-vue';
import { fileApi, validateFileType } from '@/api/file';
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); const submitting = ref(false);
@ -404,10 +405,41 @@ const handleModalOk = async () => {
} }
}; };
const handleUploadChange = (info: any) => { /** OSS 直传:自定义上传 */
if (info.file.status === 'done') { const handleCustomUpload = async (options: any) => {
formState.images.push(info.file.response.url); 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(() => { onMounted(() => {
@ -528,6 +560,8 @@ onMounted(() => {
} }
.record-card { .record-card {
display: flex;
flex-direction: column;
background: white; background: white;
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
@ -542,20 +576,30 @@ onMounted(() => {
.card-cover { .card-cover {
position: relative; position: relative;
flex-shrink: 0;
height: 160px; height: 160px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); 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 { .cover-image img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block;
} }
.image-count { .image-count {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
z-index: 2;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
color: white; color: white;
padding: 4px 8px; padding: 4px 8px;
@ -581,6 +625,7 @@ onMounted(() => {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
z-index: 2;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-size: 11px; font-size: 11px;
@ -605,7 +650,11 @@ onMounted(() => {
} }
.card-body { .card-body {
flex: 1;
min-height: 0;
padding: 16px; padding: 16px;
display: flex;
flex-direction: column;
} }
.record-title { .record-title {
@ -639,16 +688,24 @@ onMounted(() => {
font-size: 13px; font-size: 13px;
color: #636E72; color: #636E72;
line-height: 1.6; 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 { .card-actions {
flex-shrink: 0;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
padding: 12px 16px; padding: 12px 16px;
border-top: 1px solid #F0F0F0; border-top: 1px solid #F0F0F0;
background: #FAFAFA; background: #FAFAFA;
margin-top: auto;
} }
.empty-state { .empty-state {

View File

@ -24,24 +24,14 @@
<!-- 操作栏 --> <!-- 操作栏 -->
<div class="action-bar"> <div class="action-bar">
<div class="filters"> <div class="filters">
<a-select <a-select v-model:value="filters.classId" placeholder="选择班级" allow-clear style="width: 150px;"
v-model:value="filters.classId" @change="handleFilter">
placeholder="选择班级"
allow-clear
style="width: 150px;"
@change="handleFilter"
>
<a-select-option v-for="cls in myClasses" :key="cls.id" :value="cls.id"> <a-select-option v-for="cls in myClasses" :key="cls.id" :value="cls.id">
{{ cls.name }} {{ cls.name }}
</a-select-option> </a-select-option>
</a-select> </a-select>
<a-input-search <a-input-search v-model:value="filters.keyword" placeholder="搜索标题" style="width: 200px;" @search="handleFilter"
v-model:value="filters.keyword" allow-clear />
placeholder="搜索标题"
style="width: 200px;"
@search="handleFilter"
allow-clear
/>
</div> </div>
<a-button type="primary" class="add-btn" @click="showAddModal"> <a-button type="primary" class="add-btn" @click="showAddModal">
<PlusOutlined class="btn-icon" /> <PlusOutlined class="btn-icon" />
@ -51,14 +41,10 @@
<!-- 档案卡片网格 --> <!-- 档案卡片网格 -->
<div class="record-grid" v-if="!loading && records.length > 0"> <div class="record-grid" v-if="!loading && records.length > 0">
<div <div v-for="record in records" :key="record.id" class="record-card">
v-for="record in records"
:key="record.id"
class="record-card"
>
<div class="card-cover"> <div class="card-cover">
<div v-if="record.images?.length" class="cover-image"> <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"> <div class="image-count" v-if="record.images.length > 1">
<CameraOutlined /> {{ record.images.length }} <CameraOutlined /> {{ record.images.length }}
</div> </div>
@ -88,7 +74,8 @@
<span>{{ formatDate(record.recordDate) }}</span> <span>{{ formatDate(record.recordDate) }}</span>
</div> </div>
</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>
<div class="card-actions"> <div class="card-actions">
<a-button type="link" size="small" @click="handleView(record)"> <a-button type="link" size="small" @click="handleView(record)">
@ -124,12 +111,7 @@
</div> </div>
<!-- 添加/编辑档案弹窗 --> <!-- 添加/编辑档案弹窗 -->
<a-modal <a-modal v-model:open="modalVisible" width="700px" @ok="handleModalOk" :confirm-loading="submitting">
v-model:open="modalVisible"
width="700px"
@ok="handleModalOk"
:confirm-loading="submitting"
>
<template #title> <template #title>
<span class="modal-title"> <span class="modal-title">
<EditOutlined v-if="isEdit" class="modal-title-icon" /> <EditOutlined v-if="isEdit" class="modal-title-icon" />
@ -137,20 +119,10 @@
{{ isEdit ? ' 编辑档案' : ' 添加档案' }} {{ isEdit ? ' 编辑档案' : ' 添加档案' }}
</span> </span>
</template> </template>
<a-form <a-form ref="formRef" :model="formState" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
ref="formRef"
:model="formState"
:rules="rules"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="学生" name="studentId" v-if="!isEdit"> <a-form-item label="学生" name="studentId" v-if="!isEdit">
<a-select <a-select v-model:value="formState.studentId" placeholder="请选择学生" show-search
v-model:value="formState.studentId" :filter-option="filterStudentOption">
placeholder="请选择学生"
show-search
:filter-option="filterStudentOption"
>
<a-select-option v-for="student in students" :key="student.id" :value="student.id"> <a-select-option v-for="student in students" :key="student.id" :value="student.id">
{{ student.name }} - {{ student.className }} {{ student.name }} - {{ student.className }}
</a-select-option> </a-select-option>
@ -158,36 +130,26 @@
</a-form-item> </a-form-item>
<a-form-item label="档案类型" name="recordType" v-if="!isEdit"> <a-form-item label="档案类型" name="recordType" v-if="!isEdit">
<a-radio-group v-model:value="formState.recordType"> <a-radio-group v-model:value="formState.recordType">
<a-radio value="STUDENT"><UserOutlined class="radio-icon" /> 个人档案</a-radio> <a-radio value="STUDENT">
<a-radio value="CLASS"><TeamOutlined class="radio-icon" /> 班级档案</a-radio> <UserOutlined class="radio-icon" /> 个人档案
</a-radio>
<a-radio value="CLASS">
<TeamOutlined class="radio-icon" /> 班级档案
</a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="标题" name="title"> <a-form-item label="标题" name="title">
<a-input v-model:value="formState.title" placeholder="请输入档案标题" /> <a-input v-model:value="formState.title" placeholder="请输入档案标题" />
</a-form-item> </a-form-item>
<a-form-item label="记录日期" name="recordDate"> <a-form-item label="记录日期" name="recordDate">
<a-date-picker <a-date-picker v-model:value="formState.recordDateValue" style="width: 100%;" value-format="YYYY-MM-DD" />
v-model:value="formState.recordDateValue"
style="width: 100%;"
value-format="YYYY-MM-DD"
/>
</a-form-item> </a-form-item>
<a-form-item label="内容" name="content"> <a-form-item label="内容" name="content">
<a-textarea <a-textarea v-model:value="formState.content" placeholder="请输入档案内容" :rows="4" />
v-model:value="formState.content"
placeholder="请输入档案内容"
:rows="4"
/>
</a-form-item> </a-form-item>
<a-form-item label="图片"> <a-form-item label="图片">
<a-upload <a-upload v-model:file-list="fileList" :custom-request="handleCustomUpload" list-type="picture-card"
v-model:file-list="fileList" :max-count="9" accept="image/*" @change="handleUploadChange">
:action="uploadUrl"
:headers="uploadHeaders"
list-type="picture-card"
:max-count="9"
@change="handleUploadChange"
>
<div v-if="fileList.length < 9"> <div v-if="fileList.length < 9">
<PlusOutlined /> <PlusOutlined />
<div style="margin-top: 8px">上传</div> <div style="margin-top: 8px">上传</div>
@ -198,11 +160,7 @@
</a-modal> </a-modal>
<!-- 查看档案详情弹窗 --> <!-- 查看档案详情弹窗 -->
<a-modal <a-modal v-model:open="detailModalVisible" width="700px" :footer="null">
v-model:open="detailModalVisible"
width="700px"
:footer="null"
>
<template #title> <template #title>
<span class="modal-title"> <span class="modal-title">
<CameraOutlined class="modal-title-icon" /> <CameraOutlined class="modal-title-icon" />
@ -231,12 +189,8 @@
<div v-if="currentRecord.images?.length" class="image-gallery"> <div v-if="currentRecord.images?.length" class="image-gallery">
<a-image-preview-group> <a-image-preview-group>
<a-image <a-image v-for="(img, index) in currentRecord.images" :key="index" :src="getImageUrl(img)"
v-for="(img, index) in currentRecord.images" style="width: 100px; height: 100px; object-fit: cover; margin-right: 8px; border-radius: 8px;" />
:key="index"
:src="getImageUrl(img)"
style="width: 100px; height: 100px; object-fit: cover; margin-right: 8px; border-radius: 8px;"
/>
</a-image-preview-group> </a-image-preview-group>
</div> </div>
</div> </div>
@ -245,14 +199,9 @@
<!-- 分页 --> <!-- 分页 -->
<div class="pagination-wrapper" v-if="records.length > 0"> <div class="pagination-wrapper" v-if="records.length > 0">
<a-pagination <a-pagination v-model:current="pagination.current" v-model:pageSize="pagination.pageSize"
v-model:current="pagination.current" :total="pagination.total" :show-size-changer="true" :show-total="(total: number) => `共 ${total} 条`"
v-model:pageSize="pagination.pageSize" @change="handlePageChange" />
:total="pagination.total"
:show-size-changer="true"
:show-total="(total: number) => `共 ${total} 条`"
@change="handlePageChange"
/>
</div> </div>
</div> </div>
</template> </template>
@ -283,6 +232,7 @@ import {
type UpdateGrowthRecordDto, type UpdateGrowthRecordDto,
} from '@/api/growth'; } from '@/api/growth';
import { getTeacherClasses, getTeacherStudents } from '@/api/teacher'; import { getTeacherClasses, getTeacherStudents } from '@/api/teacher';
import { fileApi, validateFileType } from '@/api/file';
const loading = ref(false); const loading = ref(false);
const submitting = ref(false); const submitting = ref(false);
@ -309,9 +259,6 @@ const records = ref<GrowthRecord[]>([]);
const currentRecord = ref<GrowthRecord | null>(null); const currentRecord = ref<GrowthRecord | null>(null);
const fileList = ref<any[]>([]); const fileList = ref<any[]>([]);
const uploadUrl = '/api/upload';
const uploadHeaders = {};
const formState = reactive<CreateGrowthRecordDto & { recordDateValue?: string }>({ const formState = reactive<CreateGrowthRecordDto & { recordDateValue?: string }>({
studentId: undefined as any, studentId: undefined as any,
classId: undefined, classId: undefined,
@ -476,10 +423,41 @@ const handleModalOk = async () => {
} }
}; };
const handleUploadChange = (info: any) => { /** OSS 直传:自定义上传 */
if (info.file.status === 'done') { const handleCustomUpload = async (options: any) => {
formState.images.push(info.file.response.url); 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(() => { onMounted(() => {
@ -602,6 +580,8 @@ onMounted(() => {
} }
.record-card { .record-card {
display: flex;
flex-direction: column;
background: white; background: white;
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
@ -616,20 +596,30 @@ onMounted(() => {
.card-cover { .card-cover {
position: relative; position: relative;
flex-shrink: 0;
height: 160px; height: 160px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); 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 { .cover-image img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block;
} }
.image-count { .image-count {
position: absolute; position: absolute;
bottom: 8px; bottom: 8px;
right: 8px; right: 8px;
z-index: 2;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.6);
color: white; color: white;
padding: 4px 8px; padding: 4px 8px;
@ -655,6 +645,7 @@ onMounted(() => {
position: absolute; position: absolute;
top: 12px; top: 12px;
right: 12px; right: 12px;
z-index: 2;
padding: 4px 12px; padding: 4px 12px;
border-radius: 12px; border-radius: 12px;
font-size: 11px; font-size: 11px;
@ -679,7 +670,11 @@ onMounted(() => {
} }
.card-body { .card-body {
flex: 1;
min-height: 0;
padding: 16px; padding: 16px;
display: flex;
flex-direction: column;
} }
.record-title { .record-title {
@ -713,16 +708,24 @@ onMounted(() => {
font-size: 13px; font-size: 13px;
color: #636E72; color: #636E72;
line-height: 1.6; 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 { .card-actions {
flex-shrink: 0;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
gap: 8px; gap: 8px;
padding: 12px 16px; padding: 12px 16px;
border-top: 1px solid #F0F0F0; border-top: 1px solid #F0F0F0;
background: #FAFAFA; background: #FAFAFA;
margin-top: auto;
} }
.empty-state { .empty-state {

View File

@ -3,6 +3,7 @@ package com.reading.platform.common.mapper;
import com.reading.platform.dto.response.GrowthRecordResponse; import com.reading.platform.dto.response.GrowthRecordResponse;
import com.reading.platform.entity.GrowthRecord; import com.reading.platform.entity.GrowthRecord;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import java.util.List; import java.util.List;
@ -16,8 +17,9 @@ public interface GrowthRecordMapper {
GrowthRecordMapper INSTANCE = Mappers.getMapper(GrowthRecordMapper.class); GrowthRecordMapper INSTANCE = Mappers.getMapper(GrowthRecordMapper.class);
/** /**
* Entity Response * Entity Responseimages JSON 字符串解析为 List
*/ */
@Mapping(target = "images", expression = "java(java.util.Arrays.asList(com.reading.platform.common.util.JsonUtils.parseStringArray(entity.getImages())))")
GrowthRecordResponse toVO(GrowthRecord entity); GrowthRecordResponse toVO(GrowthRecord entity);
/** /**
@ -26,7 +28,8 @@ public interface GrowthRecordMapper {
List<GrowthRecordResponse> toVO(List<GrowthRecord> entities); List<GrowthRecordResponse> toVO(List<GrowthRecord> entities);
/** /**
* Response Entity用于创建/更新时 * Response Entityimages List 转为 JSON 字符串
*/ */
@Mapping(target = "images", expression = "java(vo.getImages() != null ? com.reading.platform.common.util.JsonUtils.toJson(vo.getImages()) : null)")
GrowthRecord toEntity(GrowthRecordResponse vo); GrowthRecord toEntity(GrowthRecordResponse vo);
} }

View File

@ -1,11 +1,13 @@
package com.reading.platform.controller.teacher; package com.reading.platform.controller.teacher;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.PageResult;
import com.reading.platform.common.response.Result; import com.reading.platform.common.response.Result;
import com.reading.platform.common.security.SecurityUtils; import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.dto.request.GrowthRecordCreateRequest; import com.reading.platform.dto.request.GrowthRecordCreateRequest;
import com.reading.platform.dto.request.GrowthRecordUpdateRequest; import com.reading.platform.dto.request.GrowthRecordUpdateRequest;
import com.reading.platform.dto.response.GrowthRecordResponse;
import com.reading.platform.entity.GrowthRecord; import com.reading.platform.entity.GrowthRecord;
import com.reading.platform.service.GrowthRecordService; import com.reading.platform.service.GrowthRecordService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -14,6 +16,8 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "教师端 - 成长记录", description = "教师端成长记录 API") @Tag(name = "教师端 - 成长记录", description = "教师端成长记录 API")
@RestController @RestController
@RequestMapping("/api/v1/teacher/growth-records") @RequestMapping("/api/v1/teacher/growth-records")
@ -21,37 +25,42 @@ import org.springframework.web.bind.annotation.*;
public class TeacherGrowthController { public class TeacherGrowthController {
private final GrowthRecordService growthRecordService; private final GrowthRecordService growthRecordService;
private final GrowthRecordMapper growthRecordMapper;
@Operation(summary = "创建成长记录") @Operation(summary = "创建成长记录")
@PostMapping @PostMapping
public Result<GrowthRecord> createGrowthRecord(@Valid @RequestBody GrowthRecordCreateRequest request) { public Result<GrowthRecordResponse> createGrowthRecord(@Valid @RequestBody GrowthRecordCreateRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Long userId = SecurityUtils.getCurrentUserId(); 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 = "更新成长记录") @Operation(summary = "更新成长记录")
@PutMapping("/{id}") @PutMapping("/{id}")
public Result<GrowthRecord> updateGrowthRecord(@PathVariable Long id, @RequestBody GrowthRecordUpdateRequest request) { public Result<GrowthRecordResponse> updateGrowthRecord(@PathVariable Long id, @RequestBody GrowthRecordUpdateRequest request) {
return Result.success(growthRecordService.updateGrowthRecord(id, request)); GrowthRecord record = growthRecordService.updateGrowthRecord(id, request);
return Result.success(growthRecordMapper.toVO(record));
} }
@Operation(summary = "根据 ID 获取成长记录") @Operation(summary = "根据 ID 获取成长记录")
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<GrowthRecord> getGrowthRecord(@PathVariable Long id) { public Result<GrowthRecordResponse> getGrowthRecord(@PathVariable Long id) {
return Result.success(growthRecordService.getGrowthRecordById(id)); GrowthRecord record = growthRecordService.getGrowthRecordById(id);
return Result.success(growthRecordMapper.toVO(record));
} }
@Operation(summary = "获取成长记录分页列表") @Operation(summary = "获取成长记录分页列表")
@GetMapping @GetMapping
public Result<PageResult<GrowthRecord>> getGrowthRecordPage( public Result<PageResult<GrowthRecordResponse>> getGrowthRecordPage(
@RequestParam(required = false, defaultValue = "1") Integer pageNum, @RequestParam(required = false, defaultValue = "1") Integer pageNum,
@RequestParam(required = false, defaultValue = "10") Integer pageSize, @RequestParam(required = false, defaultValue = "10") Integer pageSize,
@RequestParam(required = false) Long studentId, @RequestParam(required = false) Long studentId,
@RequestParam(required = false) String type) { @RequestParam(required = false) String type) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Page<GrowthRecord> page = growthRecordService.getGrowthRecordPage(tenantId, pageNum, pageSize, studentId, type); 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 = "删除成长记录") @Operation(summary = "删除成长记录")

View File

@ -1,5 +1,6 @@
package com.reading.platform.dto.request; package com.reading.platform.dto.request;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@ -17,7 +18,8 @@ public class GrowthRecordCreateRequest {
private Long studentId; private Long studentId;
@NotBlank(message = "类型不能为空") @NotBlank(message = "类型不能为空")
@Schema(description = "类型reading-阅读behavior-行为achievement-成就milestone-里程碑") @JsonAlias("recordType")
@Schema(description = "类型reading-阅读behavior-行为achievement-成就milestone-里程碑STUDENT-学生记录")
private String type; private String type;
@NotBlank(message = "标题不能为空") @NotBlank(message = "标题不能为空")
@ -27,8 +29,8 @@ public class GrowthRecordCreateRequest {
@Schema(description = "内容") @Schema(description = "内容")
private String content; private String content;
@Schema(description = "图片JSON 数组)") @Schema(description = "图片 URL 列表")
private String images; private List<String> images;
@Schema(description = "记录日期") @Schema(description = "记录日期")
private LocalDate recordDate; private LocalDate recordDate;

View File

@ -19,8 +19,8 @@ public class GrowthRecordUpdateRequest {
@Schema(description = "内容") @Schema(description = "内容")
private String content; private String content;
@Schema(description = "图片JSON 数组)") @Schema(description = "图片 URL 列表")
private String images; private List<String> images;
@Schema(description = "记录日期") @Schema(description = "记录日期")
private LocalDate recordDate; private LocalDate recordDate;

View File

@ -6,6 +6,7 @@ import lombok.Data;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
/** /**
* 成长记录响应 * 成长记录响应
@ -34,8 +35,8 @@ public class GrowthRecordResponse {
@Schema(description = "内容") @Schema(description = "内容")
private String content; private String content;
@Schema(description = "图片") @Schema(description = "图片URL列表")
private String images; private List<String> images;
@Schema(description = "记录人 ID") @Schema(description = "记录人 ID")
private Long recordedBy; private Long recordedBy;

View File

@ -39,7 +39,13 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
record.setType(request.getType()); record.setType(request.getType());
record.setTitle(request.getTitle()); record.setTitle(request.getTitle());
record.setContent(request.getContent()); 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.setRecordedBy(recorderId);
record.setRecorderRole(recorderRole); record.setRecorderRole(recorderRole);
record.setRecordDate(request.getRecordDate() != null ? request.getRecordDate() : LocalDate.now()); record.setRecordDate(request.getRecordDate() != null ? request.getRecordDate() : LocalDate.now());
@ -72,7 +78,11 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
record.setContent(request.getContent()); record.setContent(request.getContent());
} }
if (request.getImages() != null) { if (request.getImages() != null) {
record.setImages(request.getImages()); try {
record.setImages(objectMapper.writeValueAsString(request.getImages()));
} catch (JsonProcessingException e) {
record.setImages("[]");
}
} }
if (request.getRecordDate() != null) { if (request.getRecordDate() != null) {
record.setRecordDate(request.getRecordDate()); record.setRecordDate(request.getRecordDate());
@ -127,7 +137,8 @@ public class GrowthRecordServiceImpl extends ServiceImpl<GrowthRecordMapper, Gro
if (StringUtils.hasText(type)) { if (StringUtils.hasText(type)) {
wrapper.eq(GrowthRecord::getType, type); wrapper.eq(GrowthRecord::getType, type);
} }
wrapper.orderByDesc(GrowthRecord::getRecordDate); // 按最新修改时间最新创建时间倒序
wrapper.orderByDesc(GrowthRecord::getUpdatedAt).orderByDesc(GrowthRecord::getCreatedAt);
return growthRecordMapper.selectPage(page, wrapper); return growthRecordMapper.selectPage(page, wrapper);
} }