diff --git a/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue b/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue index 979d8ba..5e78c8f 100644 --- a/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue +++ b/reading-platform-frontend/src/views/school/growth/GrowthRecordView.vue @@ -182,10 +182,10 @@
@@ -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 { diff --git a/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue b/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue index 8ad77d8..9c1a285 100644 --- a/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue +++ b/reading-platform-frontend/src/views/teacher/growth/GrowthRecordView.vue @@ -24,24 +24,14 @@
- + {{ cls.name }} - +
@@ -51,14 +41,10 @@
-
+
-
- cover +
+
{{ record.images.length }}
@@ -88,7 +74,8 @@ {{ formatDate(record.recordDate) }}
-

{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }}

+

{{ record.content?.substring(0, 80) }}{{ record.content?.length > 80 ? '...' : '' }} +

@@ -124,12 +111,7 @@
- + - + - + {{ student.name }} - {{ student.className }} @@ -158,36 +130,26 @@ - 个人档案 - 班级档案 + + 个人档案 + + + 班级档案 + - + - + - +
上传
@@ -198,11 +160,7 @@ - + @@ -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([]); const currentRecord = ref(null); const fileList = ref([]); -const uploadUrl = '/api/upload'; -const uploadHeaders = {}; - const formState = reactive({ 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 { diff --git a/reading-platform-java/src/main/java/com/reading/platform/common/mapper/GrowthRecordMapper.java b/reading-platform-java/src/main/java/com/reading/platform/common/mapper/GrowthRecordMapper.java index 2e1defe..e125234 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/common/mapper/GrowthRecordMapper.java +++ b/reading-platform-java/src/main/java/com/reading/platform/common/mapper/GrowthRecordMapper.java @@ -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 toVO(List 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); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherGrowthController.java b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherGrowthController.java index a884069..d8c4227 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherGrowthController.java +++ b/reading-platform-java/src/main/java/com/reading/platform/controller/teacher/TeacherGrowthController.java @@ -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 createGrowthRecord(@Valid @RequestBody GrowthRecordCreateRequest request) { + public Result 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 updateGrowthRecord(@PathVariable Long id, @RequestBody GrowthRecordUpdateRequest request) { - return Result.success(growthRecordService.updateGrowthRecord(id, request)); + public Result 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 getGrowthRecord(@PathVariable Long id) { - return Result.success(growthRecordService.getGrowthRecordById(id)); + public Result getGrowthRecord(@PathVariable Long id) { + GrowthRecord record = growthRecordService.getGrowthRecordById(id); + return Result.success(growthRecordMapper.toVO(record)); } @Operation(summary = "获取成长记录分页列表") @GetMapping - public Result> getGrowthRecordPage( + public Result> 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 page = growthRecordService.getGrowthRecordPage(tenantId, pageNum, pageSize, studentId, type); - return Result.success(PageResult.of(page)); + List voList = growthRecordMapper.toVO(page.getRecords()); + return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); } @Operation(summary = "删除成长记录") diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordCreateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordCreateRequest.java index 2957526..f7d83bb 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordCreateRequest.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordCreateRequest.java @@ -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 images; @Schema(description = "记录日期") private LocalDate recordDate; diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordUpdateRequest.java b/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordUpdateRequest.java index f7f73c6..8a54127 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordUpdateRequest.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/request/GrowthRecordUpdateRequest.java @@ -19,8 +19,8 @@ public class GrowthRecordUpdateRequest { @Schema(description = "内容") private String content; - @Schema(description = "图片(JSON 数组)") - private String images; + @Schema(description = "图片 URL 列表") + private List images; @Schema(description = "记录日期") private LocalDate recordDate; diff --git a/reading-platform-java/src/main/java/com/reading/platform/dto/response/GrowthRecordResponse.java b/reading-platform-java/src/main/java/com/reading/platform/dto/response/GrowthRecordResponse.java index 0f40078..56d8409 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/dto/response/GrowthRecordResponse.java +++ b/reading-platform-java/src/main/java/com/reading/platform/dto/response/GrowthRecordResponse.java @@ -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 images; @Schema(description = "记录人 ID") private Long recordedBy; diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/GrowthRecordServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/GrowthRecordServiceImpl.java index a822266..7c3c389 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/GrowthRecordServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/GrowthRecordServiceImpl.java @@ -39,7 +39,13 @@ public class GrowthRecordServiceImpl extends ServiceImpl