zlwu 1 rok temu
rodzic
commit
1b117de6cf

+ 300 - 0
docs/问题跟踪/后端文件上传数据丢失问题.md

@@ -0,0 +1,300 @@
+# 后端文件上传数据丢失问题
+
+**发现日期**: 2025-01-24  
+**问题模块**: 文件上传/OSS服务  
+**影响范围**: 所有使用附件上传的业务模块  
+**严重程度**: ⚠️ 中等(影响用户体验,但有前端兜底)
+
+---
+
+## 问题描述
+
+后端在保存文件时丢失了真实的文件名和扩展名信息,导致前端回显时无法正确识别文件类型。
+
+### 问题表现
+
+从 `/resource/oss/listByIds/{ossIds}` API 返回的数据中:
+
+```javascript
+{
+    ossId: "1981738218223079425",
+    originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9",  // ❌ UUID,不是用户上传的真实文件名
+    fileSuffix: "9",                                        // ❌ 数字字符,不是真实扩展名(应该是 jpg/png 等)
+    fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139", // ❌ 没有扩展名
+    url: "/resource/oss/download/1981738218223079425"
+}
+```
+
+### 期望的数据格式
+
+```javascript
+{
+    ossId: "1981738218223079425",
+    originalName: "违纪现场照片.jpg",     // ✅ 用户上传时的真实文件名
+    fileSuffix: "jpg",                    // ✅ 真实的文件扩展名
+    fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139.jpg", // ✅ 包含扩展名
+    url: "/resource/oss/download/1981738218223079425"
+}
+```
+
+---
+
+## 问题分析
+
+### 可能的原因
+
+1. **文件上传时未保存原始文件名**
+   - 上传接口 `/resource/oss/upload` 可能未正确提取文件名
+   - `originalName` 字段被UUID覆盖
+
+2. **扩展名提取错误**
+   - `fileSuffix` 字段提取逻辑有误
+   - 从文件路径末尾提取了一个字符(如 `9`、`e`、`d`)而不是真实扩展名
+
+3. **数据库保存逻辑问题**
+   - `sys_oss` 表中的字段未正确保存
+   - 可能是某个字段映射错误
+
+### 影响的业务场景
+
+- ✅ **新增上传**:正常工作(文件可以上传)
+- ❌ **回显展示**:文件类型识别错误,图片无法正常显示
+- ❌ **下载功能**:下载的文件名显示为UUID,用户体验差
+- ❌ **文件管理**:管理员无法识别文件内容
+
+---
+
+## 复现步骤
+
+1. 在移动端进入"宿舍卫生检查"模块
+2. 点击"录入违纪",上传一张图片(如 `photo.jpg`)
+3. 保存违纪记录
+4. 刷新页面或重新进入
+5. 点击"编辑"查看违纪记录
+6. **问题**:附件无法显示为图片,文件名显示为UUID
+
+---
+
+## 调试信息
+
+### 前端上传请求
+
+```javascript
+// 调用上传接口
+POST /resource/oss/upload
+Content-Type: multipart/form-data
+
+// FormData:
+file: [Blob] photo.jpg
+bizType: "dorm_violation_attachment"
+```
+
+### 后端返回(上传成功)
+
+```javascript
+{
+    code: 200,
+    data: {
+        ossId: "1981738218223079425",
+        url: "/resource/oss/download/1981738218223079425",
+        fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139"  // ⚠️ 缺少扩展名
+    }
+}
+```
+
+### 后端返回(回显查询)
+
+```javascript
+// GET /resource/oss/listByIds/1981738218223079425
+{
+    code: 200,
+    data: [{
+        ossId: "1981738218223079425",
+        originalName: "12038a7c-48bd-4978-bcbb-1119a201a6e9",  // ❌ UUID
+        fileSuffix: "9",                                        // ❌ 错误
+        fileName: "2025/10/24/aa991fe4d6114995a0562d783cea56139",
+        url: "/resource/oss/download/1981738218223079425"
+    }]
+}
+```
+
+---
+
+## 建议的修复方案
+
+### 方案1:修复上传接口(推荐)⭐
+
+**修改文件**:`backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java`
+
+**修复点**:
+1. 上传时正确提取并保存 `originalName`(用户上传的文件名)
+2. 正确提取文件扩展名保存到 `fileSuffix`
+3. 保存到OSS的文件名应包含扩展名
+
+```java
+@PostMapping("/upload")
+public R<SysOss> upload(@RequestParam("file") MultipartFile file, 
+                        @RequestParam(required = false) String bizType) {
+    // 获取原始文件名
+    String originalFilename = file.getOriginalFilename();
+    
+    // 提取真实扩展名
+    String extension = "";
+    if (originalFilename != null && originalFilename.contains(".")) {
+        extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
+    }
+    
+    // 生成OSS文件名(包含扩展名)
+    String ossFileName = generateOssFileName() + "." + extension;
+    
+    // 保存到数据库
+    SysOss oss = new SysOss();
+    oss.setOriginalName(originalFilename);  // ✅ 保存真实文件名
+    oss.setFileSuffix(extension);           // ✅ 保存真实扩展名
+    oss.setFileName(ossFileName);           // ✅ 包含扩展名的OSS文件名
+    
+    // ... 其他逻辑
+}
+```
+
+### 方案2:修复查询接口
+
+**修改文件**:`backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java`
+
+**修复点**:
+1. 从数据库正确读取 `originalName` 和 `fileSuffix`
+2. 确保返回的VO类包含正确的字段值
+
+```java
+public List<SysOssVo> listByIds(String ossIds) {
+    // ... 查询逻辑 ...
+    
+    List<SysOssVo> voList = new ArrayList<>();
+    for (SysOss oss : ossList) {
+        SysOssVo vo = new SysOssVo();
+        vo.setOssId(oss.getOssId());
+        vo.setOriginalName(oss.getOriginalName());  // ✅ 确保读取正确
+        vo.setFileSuffix(oss.getFileSuffix());      // ✅ 确保读取正确
+        vo.setFileName(oss.getFileName());
+        vo.setUrl(oss.getUrl());
+        voList.add(vo);
+    }
+    
+    return voList;
+}
+```
+
+### 方案3:数据库表结构检查
+
+**检查表**:`sys_oss`
+
+**需要确认的字段**:
+```sql
+-- 检查字段定义
+SHOW COLUMNS FROM sys_oss LIKE '%name%';
+SHOW COLUMNS FROM sys_oss LIKE '%suffix%';
+
+-- 检查数据示例
+SELECT oss_id, original_name, file_suffix, file_name 
+FROM sys_oss 
+WHERE oss_id = '1981738218223079425';
+```
+
+**期望的字段定义**:
+- `original_name` VARCHAR(255) - 用户上传的原始文件名
+- `file_suffix` VARCHAR(10) - 文件扩展名(jpg/png/pdf等)
+- `file_name` VARCHAR(500) - OSS中的文件路径(应包含扩展名)
+
+---
+
+## 临时解决方案(前端兜底)
+
+**已实施**:在 `ViolationRecordDialog.vue` 中添加了兜底逻辑
+
+```javascript
+// 检测 fileSuffix 是否为有效扩展名
+if (item.fileSuffix && item.fileSuffix.length <= 5 && /^[a-zA-Z]+$/.test(item.fileSuffix)) {
+    // 使用 fileSuffix
+    realExtension = item.fileSuffix.toLowerCase();
+    fileType = this.getFileTypeBySuffix(realExtension);
+} else {
+    // 默认按图片处理(违纪证明材料大概率是图片)
+    realExtension = 'jpg';
+    fullFileName = `违纪证明_${timestamp + index}.jpg`;
+    fileType = 'image/jpeg';
+}
+```
+
+**局限性**:
+- ✅ 可以临时解决图片回显问题
+- ❌ 无法恢复真实的文件名
+- ❌ 非图片文件可能被错误识别
+- ❌ 用户体验不佳
+
+---
+
+## 验证方法
+
+### 1. 后端修复后的验证
+
+**步骤**:
+1. 上传一张图片 `test.jpg`
+2. 调用 `/resource/oss/listByIds/{ossId}` 查询
+3. 检查返回数据:
+   - `originalName` 应该是 `test.jpg`
+   - `fileSuffix` 应该是 `jpg`
+   - `fileName` 应该包含 `.jpg` 扩展名
+
+**期望日志**:
+```
+违纪记录:原始数据 - originalName=test.jpg, fileSuffix=jpg, fileName=2025/10/24/xxx.jpg
+违纪记录:最终文件名=test.jpg, 扩展名=jpg, 文件类型=image/jpeg
+```
+
+### 2. 前端验证
+
+**步骤**:
+1. 上传图片后编辑违纪记录
+2. 检查附件回显是否正常
+3. 检查文件名是否正确显示
+
+**期望结果**:
+- ✅ 图片正常显示(不是灰白背景)
+- ✅ 文件名显示为真实文件名(不是UUID)
+- ✅ 下载文件名正确
+
+---
+
+## 相关文件
+
+**后端**:
+- `backend/src/main/java/com/ruoyi/resource/controller/SysOssController.java`
+- `backend/src/main/java/com/ruoyi/resource/service/impl/SysOssServiceImpl.java`
+- `backend/src/main/java/com/ruoyi/resource/domain/vo/SysOssVo.java`
+- `backend/src/main/java/com/ruoyi/resource/domain/SysOss.java`
+
+**前端**:
+- `mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue`(已添加兜底逻辑)
+
+**数据库**:
+- `sys_oss` 表
+
+---
+
+## 优先级和影响
+
+**优先级**: P2(中等)
+
+**影响评估**:
+- **用户体验**: ⬇️ 下降(文件名显示为UUID,无法识别内容)
+- **功能可用性**: ✅ 正常(有前端兜底,基本功能可用)
+- **数据完整性**: ⚠️ 受影响(历史数据无法恢复真实文件名)
+
+**建议处理时间**: 2周内修复
+
+---
+
+**创建时间**: 2025-01-24  
+**创建人**: Cursor AI  
+**状态**: 🔴 待修复
+

+ 598 - 56
mobile/src/pages/xgss/wsjc/components/SingleRoomCheck.vue

@@ -519,6 +519,28 @@
             @select="handleUploadActionSelect"
             @close="showUploadActionSheet = false"
         ></u-action-sheet>
+
+        <!-- 删除违纪记录确认对话框 -->
+        <u-modal
+            :show="showDeleteConfirm"
+            title="确认删除"
+            content="确认删除这条违纪记录吗?"
+            :show-cancel-button="true"
+            confirm-color="#ff5722"
+            @confirm="confirmDeleteViolation"
+            @cancel="cancelDeleteViolation"
+        ></u-modal>
+
+        <!-- 删除附件确认对话框 -->
+        <u-modal
+            :show="showDeleteImageConfirm"
+            title="确认删除"
+            content="确定要删除这张图片吗?删除后无法恢复。"
+            :show-cancel-button="true"
+            confirm-color="#ff5722"
+            @confirm="confirmDeleteImage"
+            @cancel="cancelDeleteImage"
+        ></u-modal>
     </view>
 </template>
 
@@ -590,6 +612,11 @@ export default {
             currentViolation: null,
             currentViolationIndex: -1,
 
+            // 删除违纪记录确认对话框
+            showDeleteConfirm: false,
+            deleteViolationStudentId: null,
+            deleteViolationIndex: -1,
+
             // 附件数据
             attachmentList: [],
 
@@ -601,6 +628,10 @@ export default {
             // 附件上传操作选择框
             showUploadActionSheet: false,
             uploadActions: [{ name: '选择图片' }],
+
+            // 删除附件确认弹窗
+            showDeleteImageConfirm: false,
+            deleteImageTarget: null,
         };
     },
     computed: {
@@ -712,6 +743,10 @@ export default {
         await this.checkWechatEnvironment();
         this.initComponent();
     },
+    beforeDestroy() {
+        // 清理blob URL,防止内存泄漏
+        this.cleanupBlobUrls();
+    },
     methods: {
         // 检查微信环境并自动初始化SDK
         async checkWechatEnvironment() {
@@ -881,9 +916,425 @@ export default {
         },
 
         // 加载编辑数据
-        loadEditData() {
-            // TODO: 加载现有检查数据进行编辑
-            console.log('加载编辑数据:', this.existingData);
+        async loadEditData() {
+            console.log('📝 加载编辑数据:', this.existingData);
+
+            if (!this.existingData) {
+                console.warn('⚠️ 没有现有数据');
+                return;
+            }
+
+            try {
+                // 1️⃣ 回显表单基础数据
+                this.formData.disciplineTypeId = this.existingData.disciplineTypeId || null;
+                this.formData.disciplineTypeName = this.existingData.disciplineTypeName || '';
+                this.formData.totalScore = this.existingData.totalScore || 0;
+                this.formData.hygieneGrade = this.existingData.hygieneGrade || 'A';
+                this.formData.checkRemarks = this.existingData.checkRemarks || '';
+                this.formData.attachmentIds = this.existingData.attachmentIds || '';
+
+                console.log('✅ 表单基础数据回显完成:', this.formData);
+
+                // 2️⃣ 回显附件数据
+                if (this.existingData.attachmentIds) {
+                    await this.loadAttachments(this.existingData.attachmentIds);
+                }
+
+                // 3️⃣ 从后端接口加载学生评分数据、违纪记录和请假信息(复用Web端接口)
+                // 注意:loadStudentScoresFromAPI 内部会自动调用 loadViolationRecordsFromAPI 和 loadStudentLeaves
+                if (this.existingData.id) {
+                    await this.loadStudentScoresFromAPI(this.existingData.id);
+                }
+
+                console.log('✅ 所有数据回显完成');
+            } catch (error) {
+                console.error('❌ 加载编辑数据失败:', error);
+                uni.showToast({ title: '加载数据失败', icon: 'none' });
+            }
+        },
+
+        // 从后端API加载学生评分数据(复用Web端接口)
+        async loadStudentScoresFromAPI(checkId) {
+            if (!checkId) {
+                console.warn('⚠️ 没有检查ID,无法加载学生评分数据');
+                return;
+            }
+
+            this.studentsLoading = true;
+            try {
+                console.log('🔄 从API加载学生评分数据,检查ID:', checkId);
+
+                const res = await request({
+                    url: `/dormHygieneStudentScore/dormHygieneStudentScore/getByCheckId/${checkId}`,
+                    method: 'GET',
+                });
+
+                if (res.code === 200 && res.data && res.data.length > 0) {
+                    console.log('✅ 成功获取学生评分数据:', res.data.length, '条');
+
+                    // 将后端返回的学生评分数据映射到roomStudents
+                    this.roomStudents = res.data.map((studentScore) => ({
+                        // 学生基本信息
+                        studentId: studentScore.studentId,
+                        studentName: studentScore.studentName,
+                        studentNo: studentScore.studentNo,
+                        className: studentScore.className,
+                        majorName: studentScore.majorName,
+                        collegeName: studentScore.collegeName,
+                        bedId: studentScore.bedId,
+                        bedName: studentScore.bedCode,
+                        bedCode: studentScore.bedCode,
+                        gender: studentScore.gender || '1',
+                        // 评分数据
+                        student_score: studentScore.studentScore || 0,
+                        attendance_status: studentScore.attendanceStatus || '1',
+                        student_grade: studentScore.studentGrade || 'A',
+                        isResponsiblePerson: studentScore.isResponsiblePerson || 0,
+                        scoreRemarks: studentScore.scoreRemarks || '',
+                        improvementNotes: studentScore.improvementNotes || '',
+                        // UI状态
+                        showAttendancePicker: false,
+                        showGradePicker: false,
+                    }));
+
+                    console.log('✅ 学生评分数据映射完成');
+
+                    // 🔴 同时加载违纪记录和请假信息(复用Web端逻辑)
+                    await Promise.all([
+                        this.loadViolationRecordsFromAPI(checkId),
+                        this.loadStudentLeaves(),
+                    ]);
+                } else {
+                    console.warn('⚠️ 未获取到学生评分数据,保持当前学生列表');
+                }
+            } catch (error) {
+                console.error('❌ 加载学生评分数据失败:', error);
+                uni.showToast({ title: '加载学生评分数据失败', icon: 'none' });
+            } finally {
+                this.studentsLoading = false;
+            }
+        },
+
+        // 从后端API加载违纪记录数据(复用Web端接口)
+        async loadViolationRecordsFromAPI(checkId) {
+            if (!checkId) {
+                console.warn('⚠️ 没有检查ID,无法加载违纪记录数据');
+                return;
+            }
+
+            try {
+                console.log('🔄 从API加载违纪记录数据,检查ID:', checkId);
+
+                const res = await request({
+                    url: `/dormHygieneCheck/dormHygieneCheck/getViolations/${checkId}`,
+                    method: 'GET',
+                });
+
+                if (res.code === 200 && res.data) {
+                    console.log('✅ 成功获取违纪记录数据:', res.data.length, '条');
+
+                    // 清空现有违纪记录
+                    this.violationRecords.clear();
+
+                    // 按学生ID分组违纪记录
+                    res.data.forEach((violation) => {
+                        const studentId = violation.studentId;
+                        if (!studentId) return;
+
+                        const violations = this.violationRecords.get(studentId) || [];
+                        violations.push({
+                            disciplineTypeId: violation.disciplineTypeId,
+                            disciplineTypeName: violation.disciplineTypeName,
+                            scoreDeduction: violation.scoreDeduction,
+                            violationDescription:
+                                violation.disciplineContent || violation.disciplineReason || '',
+                            // 🔴 统一使用 attachmentOssIds 字段名
+                            attachmentOssIds: violation.attachmentOssIds || '',
+                        });
+
+                        this.violationRecords.set(studentId, violations);
+                        console.log(
+                            `✅ 学生ID ${studentId} 违纪记录加载完成,数量:`,
+                            violations.length,
+                        );
+                    });
+
+                    console.log(
+                        '✅ 所有违纪记录数据加载完成,总学生数:',
+                        this.violationRecords.size,
+                    );
+                } else {
+                    console.warn('⚠️ 未获取到违纪记录数据');
+                }
+            } catch (error) {
+                console.error('❌ 加载违纪记录数据失败:', error);
+                // 不显示错误提示,因为可能确实没有违纪记录
+            }
+        },
+
+        // 加载附件数据
+        async loadAttachments(attachmentIds) {
+            if (!attachmentIds) {
+                console.log('📎 没有附件数据');
+                return;
+            }
+
+            try {
+                console.log('📎 开始加载附件数据, IDs:', attachmentIds);
+
+                const ossIds = attachmentIds.toString().trim();
+                const response = await request({
+                    url: `/resource/oss/listByIds/${ossIds}`,
+                    method: 'GET',
+                });
+
+                if (response.code === 200 && response.data) {
+                    console.log('📎 附件数据加载成功,数量:', response.data.length);
+
+                    const attachmentFiles = response.data.map((item, index) => {
+                        // 使用fileSuffix字段判断文件类型
+                        const fileType = this.getFileTypeBySuffix(item.fileSuffix);
+                        const isImage = this.isImageFile({
+                            name: item.originalName,
+                            type: fileType,
+                        });
+
+                        // 处理图片URL - 确保有正确的API前缀
+                        let processedUrl = item.url;
+                        if (isImage && item.url && item.url.startsWith('/resource/')) {
+                            // 图片文件需要添加/api前缀
+                            processedUrl = `/api${item.url}`;
+                        }
+
+                        // 构造完整的文件名(包含扩展名)
+                        let fullFileName = item.originalName || `附件${index + 1}`;
+                        if (
+                            item.fileSuffix &&
+                            !fullFileName
+                                .toLowerCase()
+                                .endsWith(`.${item.fileSuffix.toLowerCase()}`)
+                        ) {
+                            fullFileName = `${fullFileName}.${item.fileSuffix}`;
+                        }
+
+                        const fileObj = {
+                            uid: `existing_${index}_${Date.now()}`,
+                            name: fullFileName,
+                            url: processedUrl,
+                            ossId: item.ossId,
+                            status: 'success',
+                            type: fileType,
+                            size: item.fileSize || 0,
+                        };
+
+                        console.log(`📎 附件${index + 1}:`, fileObj);
+                        return fileObj;
+                    });
+
+                    this.attachmentList = attachmentFiles;
+
+                    // 为图片文件获取blob数据以确保正确显示
+                    await this.processImageFiles();
+
+                    // 强制刷新组件显示
+                    this.$nextTick(() => {
+                        // 预加载图片以确保能正确显示
+                        this.preloadImages();
+                    });
+
+                    console.log('✅ 附件数据处理完成');
+                } else {
+                    console.warn('⚠️ 获取附件信息失败:', response.msg);
+                    this.attachmentList = [];
+                }
+            } catch (error) {
+                console.error('❌ 获取附件失败:', error);
+                this.attachmentList = [];
+            }
+        },
+
+        // 根据文件后缀获取文件类型
+        getFileTypeBySuffix(fileSuffix) {
+            if (!fileSuffix) return '';
+
+            // 移除点号,转换为小写
+            const ext = fileSuffix.replace('.', '').toLowerCase();
+
+            const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+            if (imageTypes.includes(ext)) {
+                const mimeTypeMap = {
+                    jpg: 'image/jpeg',
+                    jpeg: 'image/jpeg',
+                    png: 'image/png',
+                    gif: 'image/gif',
+                    bmp: 'image/bmp',
+                    webp: 'image/webp',
+                };
+                return mimeTypeMap[ext] || `image/${ext}`;
+            }
+
+            // 其他文件类型
+            const docTypes = {
+                pdf: 'application/pdf',
+                doc: 'application/msword',
+                docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+                xls: 'application/vnd.ms-excel',
+                xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+            };
+
+            return docTypes[ext] || `file/${ext}`;
+        },
+
+        // 处理图片文件,获取blob数据以确保正确显示
+        async processImageFiles() {
+            console.log('=== 处理图片文件 ===');
+            const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
+
+            for (const file of imageFiles) {
+                if (file.url && !file.blobUrl) {
+                    try {
+                        console.log(`处理图片文件: ${file.name}`);
+                        const blobUrl = await this.getImageBlobUrl(file.url);
+                        if (blobUrl) {
+                            // 更新文件对象,添加blob URL,并标记为图片
+                            file.blobUrl = blobUrl;
+                            file.url = blobUrl; // 🔴 关键:使用blob URL作为显示URL,解决灰白背景问题
+                            file.fileType = 'image';
+                            file.type =
+                                file.type && file.type.startsWith('image') ? file.type : 'image';
+                            file.isImage = true;
+                            console.log(`图片文件处理成功: ${file.name} - ${blobUrl}`);
+                        }
+                    } catch (error) {
+                        console.error(`处理图片文件失败: ${file.name}`, error);
+                    }
+                }
+            }
+        },
+
+        // 获取图片blob URL
+        async getImageBlobUrl(imageUrl) {
+            try {
+                console.log(`获取图片blob数据: ${imageUrl}`);
+
+                // 处理图片URL路径
+                let apiUrl = imageUrl;
+                if (imageUrl.startsWith('/api/')) {
+                    // 移除/api前缀,因为request会自动添加baseURL
+                    apiUrl = imageUrl.replace('/api', '');
+                } else if (imageUrl.startsWith('/resource/')) {
+                    // 直接使用相对路径
+                    apiUrl = imageUrl;
+                } else if (imageUrl.startsWith('http')) {
+                    // 完整URL,需要特殊处理
+                    console.log('检测到完整URL,直接返回原URL');
+                    return imageUrl;
+                }
+
+                console.log(`API请求URL: ${apiUrl}`);
+
+                // 使用request方法获取图片数据
+                const response = await request({
+                    url: apiUrl,
+                    method: 'GET',
+                    responseType: 'arraybuffer', // 设置响应类型为arraybuffer
+                    header: {
+                        isToken: true,
+                    },
+                });
+
+                // 将arraybuffer转换为blob
+                const blob = new Blob([response], { type: 'image/*' });
+                const blobUrl = URL.createObjectURL(blob);
+
+                console.log(`成功创建blob URL: ${blobUrl}`);
+                return blobUrl;
+            } catch (error) {
+                console.error(`获取图片blob数据失败: ${imageUrl}`, error);
+                // 如果请求失败,返回原始URL作为备用方案
+                return imageUrl;
+            }
+        },
+
+        // 预加载图片以确保能正确显示
+        async preloadImages() {
+            console.log('=== 预加载图片 ===');
+            const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
+
+            for (const file of imageFiles) {
+                if (file.blobUrl || file.url) {
+                    try {
+                        const imgUrl = file.blobUrl || file.url;
+                        console.log(`预加载图片: ${file.name} - ${imgUrl}`);
+                        // 注意:在uni-app中,可能需要使用不同的预加载方式
+                        // 这里只是简单的日志记录,实际预加载由image标签自动完成
+                    } catch (error) {
+                        console.error(`预加载图片失败: ${file.name}`, error);
+                    }
+                }
+            }
+        },
+
+        // 回显学生评分数据
+        loadStudentScores(studentScores) {
+            console.log('👥 回显学生评分数据:', studentScores);
+
+            studentScores.forEach((scoreData) => {
+                const student = this.roomStudents.find((s) => s.studentId === scoreData.studentId);
+                if (student) {
+                    student.student_score = scoreData.studentScore || 0;
+                    student.attendance_status = scoreData.attendanceStatus || '1';
+                    student.student_grade = scoreData.studentGrade || 'A';
+                    student.scoreRemarks = scoreData.scoreRemarks || '';
+                    student.improvementNotes = scoreData.improvementNotes || '';
+                    console.log(`✅ 学生 ${student.studentName} 评分数据回显完成`);
+                }
+            });
+        },
+
+        // 回显违纪记录数据
+        loadViolationRecords(violationData) {
+            console.log('⚠️ 回显违纪记录数据:', violationData);
+
+            this.violationRecords.clear();
+
+            violationData.forEach((violation) => {
+                const studentId = violation.studentId;
+                if (!studentId) return;
+
+                const violations = this.violationRecords.get(studentId) || [];
+                violations.push({
+                    disciplineTypeId: violation.disciplineTypeId,
+                    disciplineTypeName: violation.disciplineTypeName,
+                    scoreDeduction: violation.scoreDeduction,
+                    violationDescription: violation.violationDescription,
+                    // 🔴 修复字段名不一致问题:统一使用 attachmentOssIds
+                    // 兼容后端可能返回的两种字段名
+                    attachmentOssIds:
+                        violation.violationAttachmentIds || violation.attachmentOssIds || '',
+                });
+
+                this.violationRecords.set(studentId, violations);
+                console.log(
+                    `✅ 学生ID ${studentId} 违纪记录回显完成,附件IDs:`,
+                    violation.violationAttachmentIds || violation.attachmentOssIds,
+                );
+            });
+        },
+
+        // 清理blob URL,防止内存泄漏
+        cleanupBlobUrls() {
+            console.log('=== 清理blob URL ===');
+            this.attachmentList.forEach((file) => {
+                if (file.blobUrl && file.blobUrl.startsWith('blob:')) {
+                    try {
+                        URL.revokeObjectURL(file.blobUrl);
+                        console.log(`已清理blob URL: ${file.name}`);
+                    } catch (error) {
+                        console.error(`清理blob URL失败: ${file.name}`, error);
+                    }
+                }
+            });
         },
 
         // 违纪类型确认处理
@@ -1150,39 +1601,109 @@ export default {
         // 上传文件到服务器
         async uploadFileToServer(file) {
             try {
+                console.log('🚀 === 开始上传文件到服务器 ===');
+                console.log('📁 待上传文件对象:', file);
+                console.log('📁 文件对象属性:', Object.keys(file));
+
+                // 更新文件状态
                 file.status = 'uploading';
 
                 // 🔴 关键修复:保存原始文件名,防止被后端UUID覆盖
                 const originalName = file.name;
                 console.log('📝 上传前文件名:', originalName);
 
+                // 确定正确的文件路径 - uview2通常使用url属性
+                const filePath = file.url || file.tempFilePath || file.path;
+                console.log('🎯 文件路径优先级选择:', {
+                    url: file.url,
+                    tempFilePath: file.tempFilePath,
+                    path: file.path,
+                    selected: filePath,
+                });
+
+                if (!filePath) {
+                    const error = new Error('文件路径不存在');
+                    console.error('❌ 上传失败 - 文件路径为空:', file);
+                    file.status = 'error';
+                    throw error;
+                }
+
+                // 显示上传进度
+                uni.showLoading({
+                    title: `上传${file.name}...`,
+                    mask: true,
+                });
+                console.log(`💫 开始上传文件: ${file.name}`);
+
+                // 调用智能上传API(自动适配微信环境)
+                console.log('📡 调用智能上传API...');
                 const response = await smartUploadFile('/resource/oss/upload', file, {
                     bizType: 'dorm_hygiene_check_attachment',
                 });
 
-                console.log('📡 上传响应:', response);
-                console.log('🔍 上传后file.name:', file.name);
+                console.log('📡 上传API响应:', response);
+                uni.hideLoading();
 
                 if (response && response.code === 200) {
+                    // 上传成功
                     file.url = response.data.url;
                     file.ossId = response.data.ossId;
                     file.status = 'success';
                     // 🔴 恢复原始文件名,不使用后端返回的fileName(UUID)
                     file.name = originalName;
-                    console.log('✅ 恢复文件名为:', file.name);
-                    uni.showToast({ title: '上传成功', icon: 'success', duration: 1500 });
+
+                    console.log('✅ 文件上传成功:', {
+                        fileName: file.name,
+                        ossId: file.ossId,
+                        url: file.url,
+                    });
+
+                    uni.showToast({
+                        title: '上传成功',
+                        icon: 'success',
+                        duration: 1500,
+                    });
+
+                    return response;
                 } else {
+                    // 上传失败
                     file.status = 'error';
+                    const errorMsg = (response && response.msg) || '上传失败';
+                    console.error('❌ 上传失败 - API返回错误:', {
+                        response: response,
+                        fileName: file.name,
+                        errorMsg: errorMsg,
+                    });
+
                     uni.showToast({
-                        title: (response && response.msg) || '上传失败',
+                        title: errorMsg,
                         icon: 'none',
-                        duration: 2000,
+                        duration: 3000,
                     });
+
+                    throw new Error(errorMsg);
                 }
             } catch (error) {
-                console.error('上传文件失败:', error);
+                console.error('❌ 上传文件失败:', error);
                 file.status = 'error';
-                uni.showToast({ title: '上传失败', icon: 'none', duration: 2000 });
+                uni.hideLoading();
+
+                // 如果是已知错误,直接显示
+                if (error.message && error.message !== 'undefined') {
+                    uni.showToast({
+                        title: error.message,
+                        icon: 'none',
+                        duration: 2000,
+                    });
+                } else {
+                    uni.showToast({
+                        title: '上传失败,请重试',
+                        icon: 'none',
+                        duration: 2000,
+                    });
+                }
+
+                throw error;
             }
         },
 
@@ -1401,41 +1922,47 @@ export default {
                 event.preventDefault();
             }
 
-            uni.showModal({
-                title: '确认删除',
-                content: '确定要删除这张图片吗?删除后无法恢复。',
-                confirmColor: '#ff5722',
-                success: (res) => {
-                    if (res.confirm) {
-                        // 从附件列表中删除
-                        const attachmentIndex = this.attachmentList.findIndex(
-                            (file) =>
-                                file.uid === image.uid ||
-                                (file.ossId && file.ossId === image.ossId) ||
-                                (file.url && file.url === image.url),
-                        );
+            // 🔴 修复:使用 u-modal 组件替代 uni.showModal,解决 z-index 层级冲突
+            this.showDeleteImageConfirm = true;
+            this.deleteImageTarget = { image, index };
+        },
 
-                        if (attachmentIndex >= 0) {
-                            this.attachmentList.splice(attachmentIndex, 1);
-                        }
+        // 确认删除图片
+        confirmDeleteImage() {
+            const { image, index } = this.deleteImageTarget;
 
-                        // 清理blob URL,防止内存泄漏
-                        if (image.blobUrl && image.blobUrl.startsWith('blob:')) {
-                            URL.revokeObjectURL(image.blobUrl);
-                        }
+            // 从附件列表中删除
+            const attachmentIndex = this.attachmentList.findIndex(
+                (file) =>
+                    file.uid === image.uid ||
+                    (file.ossId && file.ossId === image.ossId) ||
+                    (file.url && file.url === image.url),
+            );
 
-                        uni.showToast({
-                            title: '删除成功',
-                            icon: 'success',
-                        });
-                    }
-                },
-                fail: (err) => {
-                    console.error('删除确认对话框显示失败:', err);
-                },
+            if (attachmentIndex >= 0) {
+                this.attachmentList.splice(attachmentIndex, 1);
+            }
+
+            // 清理blob URL,防止内存泄漏
+            if (image.blobUrl && image.blobUrl.startsWith('blob:')) {
+                URL.revokeObjectURL(image.blobUrl);
+            }
+
+            this.showDeleteImageConfirm = false;
+            this.deleteImageTarget = null;
+
+            uni.showToast({
+                title: '删除成功',
+                icon: 'success',
             });
         },
 
+        // 取消删除图片
+        cancelDeleteImage() {
+            this.showDeleteImageConfirm = false;
+            this.deleteImageTarget = null;
+        },
+
         // 获取文件名短格式
         getShortFileName(fileName) {
             if (!fileName) return '';
@@ -1574,24 +2101,39 @@ export default {
 
         // 删除违纪记录
         removeViolationRecord(studentId, index) {
-            uni.showModal({
-                title: '确认删除',
-                content: '确认删除这条违纪记录吗?',
-                success: (res) => {
-                    if (res.confirm) {
-                        const violations = this.violationRecords.get(studentId) || [];
-                        violations.splice(index, 1);
+            // 保存要删除的记录信息
+            this.deleteViolationStudentId = studentId;
+            this.deleteViolationIndex = index;
+            // 显示确认对话框
+            this.showDeleteConfirm = true;
+        },
 
-                        if (violations.length === 0) {
-                            this.violationRecords.delete(studentId);
-                        } else {
-                            this.violationRecords.set(studentId, violations);
-                        }
+        // 确认删除违纪记录
+        confirmDeleteViolation() {
+            if (this.deleteViolationStudentId !== null && this.deleteViolationIndex >= 0) {
+                const violations = this.violationRecords.get(this.deleteViolationStudentId) || [];
+                violations.splice(this.deleteViolationIndex, 1);
 
-                        uni.showToast({ title: '违纪记录删除成功', icon: 'success' });
-                    }
-                },
-            });
+                if (violations.length === 0) {
+                    this.violationRecords.delete(this.deleteViolationStudentId);
+                } else {
+                    this.violationRecords.set(this.deleteViolationStudentId, violations);
+                }
+
+                uni.showToast({ title: '违纪记录删除成功', icon: 'success' });
+            }
+
+            // 重置状态
+            this.showDeleteConfirm = false;
+            this.deleteViolationStudentId = null;
+            this.deleteViolationIndex = -1;
+        },
+
+        // 取消删除违纪记录
+        cancelDeleteViolation() {
+            this.showDeleteConfirm = false;
+            this.deleteViolationStudentId = null;
+            this.deleteViolationIndex = -1;
         },
 
         // 获取学生违纪记录

+ 345 - 43
mobile/src/pages/xgss/wsjc/components/ViolationRecordDialog.vue

@@ -312,6 +312,17 @@
             @select="handleUploadActionSelect"
             @close="showUploadActionSheet = false"
         ></u-action-sheet>
+
+        <!-- 删除确认弹窗(使用 u-modal 解决 z-index 层级问题) -->
+        <u-modal
+            :show="showDeleteConfirm"
+            title="确认删除"
+            content="确定要删除这张图片吗?删除后无法恢复。"
+            :show-cancel-button="true"
+            confirm-color="#ff5722"
+            @confirm="confirmDelete"
+            @cancel="cancelDelete"
+        ></u-modal>
     </u-popup>
 </template>
 
@@ -368,7 +379,25 @@ export default {
                     { required: true, message: '请选择违纪类型', trigger: 'change' },
                 ],
                 disciplineDate: [{ required: true, message: '请选择违纪日期', trigger: 'change' }],
-                scoreDeduction: [{ required: true, message: '请输入扣分分值', trigger: 'blur' }],
+                scoreDeduction: [
+                    {
+                        required: true,
+                        message: '请输入扣分分值',
+                        trigger: 'blur',
+                        validator: (rule, value, callback) => {
+                            // 允许数字0,但不允许null、undefined、空字符串
+                            if (value === null || value === undefined || value === '') {
+                                callback(new Error('请输入扣分分值'));
+                            } else if (isNaN(value)) {
+                                callback(new Error('扣分分值必须是数字'));
+                            } else if (value < 0) {
+                                callback(new Error('扣分分值不能小于0'));
+                            } else {
+                                callback();
+                            }
+                        },
+                    },
+                ],
                 disciplineReason: [{ required: true, message: '请输入违纪原因', trigger: 'blur' }],
             },
 
@@ -399,6 +428,10 @@ export default {
             // 附件上传操作选择框
             showUploadActionSheet: false,
             uploadActions: [{ name: '选择图片' }],
+
+            // 删除确认弹窗
+            showDeleteConfirm: false,
+            deleteTarget: null,
         };
     },
     computed: {
@@ -458,6 +491,10 @@ export default {
             this.initDialog();
         }
     },
+    beforeUnmount() {
+        // 组件销毁前清理blob URL,防止内存泄漏
+        this.cleanupBlobUrls();
+    },
     methods: {
         // === 微信SDK相关方法 ===
 
@@ -582,8 +619,8 @@ export default {
                 this.allViolationTypes.length,
             );
 
-            // 初始化表单
-            this.initForm();
+            // 🔴 关键修复:等待表单初始化完成(包括附件数据加载)
+            await this.initForm();
 
             console.log('✅ 违纪记录弹窗初始化完成,最终数据量:', this.allViolationTypes.length);
         },
@@ -656,7 +693,7 @@ export default {
         },
 
         // 初始化表单数据
-        initForm() {
+        async initForm() {
             // 先设置默认日期(确保无论什么情况都有默认日期)
             this.disciplineDateValue = Number(new Date());
             this.disciplineDateDisplay = dayjs().format('YYYY-MM-DD');
@@ -717,13 +754,14 @@ export default {
                     });
                 }
 
-                // 如果是编辑模式,处理附件数据
+                // 🔴 关键修复:如果是编辑模式,异步等待附件数据加载完成
                 if (this.violationData.attachmentOssIds) {
                     console.log(
                         '违纪记录:发现附件数据,准备回显:',
                         this.violationData.attachmentOssIds,
                     );
-                    this.handleAttachmentData(this.violationData.attachmentOssIds);
+                    await this.handleAttachmentData(this.violationData.attachmentOssIds);
+                    console.log('违纪记录:附件数据回显完成');
                 } else {
                     console.log('违纪记录:编辑模式但无附件数据');
                     this.attachmentList = [];
@@ -763,17 +801,94 @@ export default {
                 if (response.code === 200 && response.data) {
                     console.log('违纪记录:获取到的附件信息:', response.data);
 
-                    const attachmentFiles = response.data.map((item, index) => ({
-                        uid: `existing_${index}_${Date.now()}`,
-                        name: item.originalName || `附件${index + 1}`,
-                        url: item.url,
-                        ossId: item.ossId,
-                        status: 'success',
-                        type: this.getFileTypeByName(item.originalName),
-                    }));
+                    const attachmentFiles = response.data.map((item, index) => {
+                        console.log(`违纪记录:处理第${index + 1}个附件:`, item);
+                        console.log(
+                            `违纪记录:原始数据 - originalName=${item.originalName}, fileSuffix=${item.fileSuffix}, fileName=${item.fileName}`,
+                        );
+
+                        // 处理URL路径
+                        let processedUrl = item.url || '';
+                        if (
+                            processedUrl &&
+                            !processedUrl.startsWith('http') &&
+                            !processedUrl.startsWith('/')
+                        ) {
+                            processedUrl = `/${processedUrl}`;
+                        }
+
+                        // 🔴 核心修复:后端数据质量问题导致无法获取真实文件名和扩展名
+                        // 问题分析:
+                        // 1. originalName 是 UUID(如 12038a7c-48bd-4978-bcbb-1119a201a6e9)
+                        // 2. fileSuffix 是数字字符(如 "9"、"e"、"d")
+                        // 3. fileName 也没有扩展名(如 2025/10/24/aa991fe4d6114995a0562d783cea56139)
+                        // 解决方案:通过 Content-Type 检测文件类型,默认按图片处理
+
+                        // 生成一个友好的文件名(不使用UUID)
+                        const timestamp = new Date().getTime();
+                        let fullFileName = `附件_${timestamp + index}`;
+                        let realExtension = '';
+                        let fileType = '';
+
+                        // 尝试从 fileSuffix 判断(如果是有效的扩展名)
+                        if (
+                            item.fileSuffix &&
+                            item.fileSuffix.length <= 5 &&
+                            /^[a-zA-Z]+$/.test(item.fileSuffix)
+                        ) {
+                            realExtension = item.fileSuffix.toLowerCase();
+                            fullFileName = `附件_${timestamp + index}.${realExtension}`;
+                            fileType = this.getFileTypeBySuffix(realExtension);
+                            console.log(
+                                `违纪记录:从fileSuffix识别 - 扩展名=${realExtension}, 类型=${fileType}`,
+                            );
+                        }
+
+                        // 如果fileSuffix无效,默认按图片处理(因为是违纪证明材料,大概率是图片)
+                        if (!fileType || !fileType.startsWith('image/')) {
+                            console.warn(`违纪记录:无法识别文件类型,默认按jpg图片处理`);
+                            realExtension = 'jpg';
+                            fullFileName = `违纪证明_${timestamp + index}.jpg`;
+                            fileType = 'image/jpeg';
+                        }
+
+                        console.log(
+                            `违纪记录:最终文件名=${fullFileName}, 扩展名=${realExtension}, 文件类型=${fileType}`,
+                        );
+
+                        const fileObj = {
+                            uid: `existing_${index}_${Date.now()}`,
+                            name: fullFileName,
+                            url: processedUrl,
+                            ossId: item.ossId,
+                            status: 'success',
+                            type: fileType,
+                            size: item.fileSize || 0,
+                            // 为uview2组件添加必要的属性
+                            file: {
+                                name: fullFileName,
+                                type: fileType,
+                                size: item.fileSize || 0,
+                                path: processedUrl,
+                                url: processedUrl,
+                            },
+                        };
+
+                        console.log(`违纪记录:构造的文件对象:`, fileObj);
+                        return fileObj;
+                    });
 
                     this.attachmentList = attachmentFiles;
                     console.log('违纪记录:设置附件列表成功:', this.attachmentList);
+
+                    // 为图片文件获取blob数据以确保正确显示
+                    await this.processImageFiles();
+
+                    // 强制刷新组件显示
+                    this.$nextTick(() => {
+                        // 预加载图片以确保能正确显示
+                        this.preloadImages();
+                    });
                 } else {
                     console.warn('违纪记录:获取附件信息失败:', response.msg);
                     this.attachmentList = [];
@@ -883,10 +998,19 @@ export default {
 
         // 扣分变化处理
         handleScoreChange() {
+            // 确保转换为数字类型
             const score = parseFloat(this.formData.scoreDeduction);
-            if (!isNaN(score) && score < 0) {
+            if (isNaN(score) || this.formData.scoreDeduction === '') {
+                // 如果不是有效数字或为空,保持为空字符串或设为0
+                return;
+            }
+
+            if (score < 0) {
                 uni.showToast({ title: '扣分分数不能小于0', icon: 'none' });
                 this.formData.scoreDeduction = 0;
+            } else {
+                // 确保是数字类型
+                this.formData.scoreDeduction = score;
             }
         },
 
@@ -1453,38 +1577,47 @@ export default {
                 event.preventDefault();
             }
 
-            uni.showModal({
-                title: '确认删除',
-                content: '确定要删除这张图片吗?删除后无法恢复。',
-                confirmColor: '#ff5722',
-                success: (res) => {
-                    if (res.confirm) {
-                        // 从附件列表中查找并删除
-                        const attachmentIndex = this.attachmentList.findIndex(
-                            (file) =>
-                                file.uid === image.uid ||
-                                (file.ossId && file.ossId === image.ossId) ||
-                                (file.url && file.url === image.url),
-                        );
+            // 🔴 修复:使用 u-modal 组件替代 uni.showModal,解决 z-index 层级冲突
+            this.showDeleteConfirm = true;
+            this.deleteTarget = { image, index };
+        },
 
-                        if (attachmentIndex >= 0) {
-                            this.attachmentList.splice(attachmentIndex, 1);
-                        }
+        // 确认删除图片
+        confirmDelete() {
+            const { image, index } = this.deleteTarget;
 
-                        // 释放blob URL(如果存在)
-                        if (image.blobUrl && image.blobUrl.startsWith('blob:')) {
-                            URL.revokeObjectURL(image.blobUrl);
-                        }
+            // 从附件列表中查找并删除
+            const attachmentIndex = this.attachmentList.findIndex(
+                (file) =>
+                    file.uid === image.uid ||
+                    (file.ossId && file.ossId === image.ossId) ||
+                    (file.url && file.url === image.url),
+            );
 
-                        uni.showToast({
-                            title: '删除成功',
-                            icon: 'success',
-                        });
-                    }
-                },
+            if (attachmentIndex >= 0) {
+                this.attachmentList.splice(attachmentIndex, 1);
+            }
+
+            // 释放blob URL(如果存在)
+            if (image.blobUrl && image.blobUrl.startsWith('blob:')) {
+                URL.revokeObjectURL(image.blobUrl);
+            }
+
+            this.showDeleteConfirm = false;
+            this.deleteTarget = null;
+
+            uni.showToast({
+                title: '删除成功',
+                icon: 'success',
             });
         },
 
+        // 取消删除
+        cancelDelete() {
+            this.showDeleteConfirm = false;
+            this.deleteTarget = null;
+        },
+
         // 获取文件名短格式
         getShortFileName(fileName) {
             if (!fileName) return '';
@@ -1524,6 +1657,165 @@ export default {
             return `file/${ext}`;
         },
 
+        // 根据文件后缀获取文件类型
+        getFileTypeBySuffix(fileSuffix) {
+            if (!fileSuffix) return '';
+
+            // 移除点号,转换为小写
+            const ext = fileSuffix.replace('.', '').toLowerCase();
+
+            // 图片类型判断
+            const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'];
+            if (imageTypes.includes(ext)) {
+                const mimeTypeMap = {
+                    jpg: 'image/jpeg',
+                    jpeg: 'image/jpeg',
+                    png: 'image/png',
+                    gif: 'image/gif',
+                    bmp: 'image/bmp',
+                    webp: 'image/webp',
+                };
+                return mimeTypeMap[ext] || `image/${ext}`;
+            }
+
+            // 文档类型判断
+            const documentTypes = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'];
+            if (documentTypes.includes(ext)) {
+                return `file/${ext}`;
+            }
+
+            return `file/${ext}`;
+        },
+
+        // 获取图片的blob数据(用于图片显示)
+        async getImageBlobUrl(imageUrl) {
+            try {
+                console.log(`获取图片blob数据: ${imageUrl}`);
+
+                // 处理图片URL路径
+                let apiUrl = imageUrl;
+                if (imageUrl.startsWith('/api/')) {
+                    // 移除/api前缀,因为request会自动添加baseURL
+                    apiUrl = imageUrl.replace('/api', '');
+                } else if (imageUrl.startsWith('/resource/')) {
+                    // 直接使用相对路径
+                    apiUrl = imageUrl;
+                } else if (imageUrl.startsWith('http')) {
+                    // 完整URL,需要特殊处理
+                    console.log('检测到完整URL,直接返回原URL');
+                    return imageUrl;
+                }
+
+                console.log(`API请求URL: ${apiUrl}`);
+
+                // 使用request方法获取图片数据
+                const response = await request({
+                    url: apiUrl,
+                    method: 'GET',
+                    responseType: 'arraybuffer', // 设置响应类型为arraybuffer
+                    header: {
+                        isToken: true,
+                    },
+                });
+
+                // 将arraybuffer转换为blob
+                const blob = new Blob([response], { type: 'image/*' });
+                const blobUrl = URL.createObjectURL(blob);
+
+                console.log(`成功创建blob URL: ${blobUrl}`);
+                return blobUrl;
+            } catch (error) {
+                console.error(`获取图片blob数据失败: ${imageUrl}`, error);
+                // 如果请求失败,返回原始URL作为备用方案
+                return imageUrl;
+            }
+        },
+
+        // 处理图片文件,获取blob数据以确保正确显示
+        async processImageFiles() {
+            console.log('=== 处理图片文件 ===');
+            console.log('违纪记录:当前附件列表:', this.attachmentList);
+
+            // 检查每个文件是否为图片
+            this.attachmentList.forEach((file, index) => {
+                const isImage = this.isImageFile(file);
+                console.log(
+                    `违纪记录:文件[${index}] ${file.name}, type=${file.type}, isImage=${isImage}`,
+                );
+            });
+
+            const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
+            console.log(`违纪记录:找到 ${imageFiles.length} 个图片文件`);
+
+            for (const file of imageFiles) {
+                if (file.url && !file.blobUrl) {
+                    try {
+                        console.log(`违纪记录:开始处理图片: ${file.name}, url=${file.url}`);
+                        const blobUrl = await this.getImageBlobUrl(file.url);
+                        if (blobUrl) {
+                            // 🔴 关键修复:同时更新url和blobUrl字段
+                            file.blobUrl = blobUrl;
+                            file.url = blobUrl; // 使用blob URL作为显示URL,解决灰白背景问题
+                            file.fileType = 'image';
+                            file.type =
+                                file.type && file.type.startsWith('image') ? file.type : 'image';
+                            file.isImage = true;
+                            console.log(`违纪记录:图片处理成功: ${file.name} - ${blobUrl}`);
+                        } else {
+                            console.warn(`违纪记录:blob URL 为空: ${file.name}`);
+                        }
+                    } catch (error) {
+                        console.error(`违纪记录:处理图片失败: ${file.name}`, error);
+                    }
+                } else {
+                    console.log(
+                        `违纪记录:跳过处理: ${file.name}, url=${file.url}, blobUrl=${file.blobUrl}`,
+                    );
+                }
+            }
+        },
+
+        // 预加载图片以确保能正确显示
+        async preloadImages() {
+            console.log('=== 预加载图片 ===');
+            const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
+
+            for (const file of imageFiles) {
+                if (file.blobUrl || file.url) {
+                    try {
+                        const imgUrl = file.blobUrl || file.url;
+                        console.log(`预加载图片: ${file.name} - ${imgUrl}`);
+                        // 创建一个Image对象来预加载图片
+                        const img = new Image();
+                        img.onload = () => {
+                            console.log(`图片预加载成功: ${file.name}`);
+                        };
+                        img.onerror = (error) => {
+                            console.error(`图片预加载失败: ${file.name}`, error);
+                        };
+                        img.src = imgUrl;
+                    } catch (error) {
+                        console.error(`预加载图片异常: ${file.name}`, error);
+                    }
+                }
+            }
+        },
+
+        // 清理blob URL,防止内存泄漏
+        cleanupBlobUrls() {
+            console.log('=== 清理blob URL ===');
+            this.attachmentList.forEach((file) => {
+                if (file.blobUrl && file.blobUrl.startsWith('blob:')) {
+                    try {
+                        URL.revokeObjectURL(file.blobUrl);
+                        console.log(`已清理blob URL: ${file.name}`);
+                    } catch (error) {
+                        console.error(`清理blob URL失败: ${file.name}`, error);
+                    }
+                }
+            });
+        },
+
         // 图片加载错误处理
         handleImageError(event) {
             console.warn('图片加载失败:', event);
@@ -1532,11 +1824,21 @@ export default {
         // 判断是否为图片文件
         isImageFile(file) {
             const imageTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp'];
+
+            // 检查 file.type
             if (file.type) {
-                return imageTypes.includes(file.type);
+                const isImageByType = imageTypes.includes(file.type);
+                console.log(
+                    `违纪记录:通过type判断: ${file.name}, type=${file.type}, 是否图片=${isImageByType}`,
+                );
+                return isImageByType;
             }
+
+            // 通过文件名判断
             const fileName = file.name || file.url || '';
-            return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
+            const isImageByName = /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName);
+            console.log(`违纪记录:通过文件名判断: ${fileName}, 是否图片=${isImageByName}`);
+            return isImageByName;
         },
 
         // 确定