提交代码
This commit is contained in:
1 parent
e97c97aa0b
commit
1b117de6cf
3 files changed
+1246
-102
No files matched your search
@@ -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
|
||||
**状态**: 🔴 待修复
|
||||
|
||||
@@ -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 });
|
||||
} else {
|
||||
file.status = 'error';
|
||||
|
||||
console.log('✅ 文件上传成功:', {
|
||||
fileName: file.name,
|
||||
ossId: file.ossId,
|
||||
url: file.url,
|
||||
});
|
||||
|
||||
uni.showToast({
|
||||
title: (response && response.msg) || '上传失败',
|
||||
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: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 3000,
|
||||
});
|
||||
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 上传文件失败:', error);
|
||||
file.status = 'error';
|
||||
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,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
file.status = 'error';
|
||||
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;
|
||||
},
|
||||
|
||||
// 获取学生违纪记录
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
// 确定
|
||||
|
||||
Reference in new issue
Block a user