diff --git a/docs/开发文档/学生考核申请附件功能改造说明.md b/docs/开发文档/学生考核申请附件功能改造说明.md
index 2f1ea644..747c1c1e 100644
--- a/docs/开发文档/学生考核申请附件功能改造说明.md
+++ b/docs/开发文档/学生考核申请附件功能改造说明.md
@@ -177,6 +177,17 @@ interface XskhAssessmentBonusApprovalForm {
- `ElMessageBox`: 确认对话框
- `ElLoading`: 加载状态
+## 2025-11-29 移动端违纪记录适配
+
+最新一轮改造已经将移动端违纪记录录入页(`mobile/src/pages/xskh/wjjlEntry/disciplineRecordForm.vue`)迁移到与宿舍卫生检查新增页一致的附件上传方案,主要收益如下:
+
+1. **统一的上传能力**:支持 `uni.chooseFile`、相册/相机、备用 action sheet,多端体验保持一致。
+2. **多类型文件**:除图片外,可上传 PDF、Word 等文档并在界面上以卡片方式区分展示。
+3. **微信优化**:在微信内置浏览器自动初始化 JS-SDK,保留进度条与后端 `/system/wechat/downloadToOss` 的桥接能力。
+4. **一致的交互**:图片列表提供预览/下载/删除/重试,文档列表提供下载/删除/重试,同步复用 15 个文件、10MB 大小的限制说明。
+
+后续若有新的移动端页面需要接入文件上传,可直接参考此实现或复用该文档中的“技术实现”章节配置,确保多端体验保持一致。
+
## 总结
本次改造成功将学生考核申请的附件功能升级为现代化的文件管理系统,提供了:
diff --git a/mobile/.env.development b/mobile/.env.development
index fe205555..c0c8de03 100644
--- a/mobile/.env.development
+++ b/mobile/.env.development
@@ -5,7 +5,7 @@ NODE_ENV=development
# API 服务器配置
VUE_APP_API_BASE_URL=/api
-VUE_APP_DEV_SERVER_URL=http://localhost:8080
+VUE_APP_DEV_SERVER_URL=http://localhost:8081
VUE_APP_REQUEST_TIMEOUT=300000
VUE_APP_REPEAT_SUBMIT_INTERVAL=500
diff --git a/mobile/.env.production-local b/mobile/.env.production-local
index 4c4f17b1..7ffcf94d 100644
--- a/mobile/.env.production-local
+++ b/mobile/.env.production-local
@@ -9,7 +9,7 @@ NODE_ENV=production
VUE_APP_API_BASE_URL=/prod-api
# 如果生产环境需要直接访问完整URL,请修改为:
# VUE_APP_API_BASE_URL=https://api.example.com
-VUE_APP_DEV_SERVER_URL=http://hzdyjsapp.camasort.com/
+VUE_APP_DEV_SERVER_URL=http://hzdyjsapp.camasort.com
VUE_APP_REQUEST_TIMEOUT=300000
VUE_APP_REPEAT_SUBMIT_INTERVAL=500
diff --git a/mobile/src/pages/xskh/wjjlEntry/disciplineRecordForm.vue b/mobile/src/pages/xskh/wjjlEntry/disciplineRecordForm.vue
index 2e96c724..1fca0d1f 100644
--- a/mobile/src/pages/xskh/wjjlEntry/disciplineRecordForm.vue
+++ b/mobile/src/pages/xskh/wjjlEntry/disciplineRecordForm.vue
@@ -187,9 +187,8 @@
v-for="(image, index) in imageFiles"
:key="image.uid || index"
class="image-item"
- @click="handleImageClick(image, index)"
+ @click="handleImageClick(image, index, $event)"
>
-
-
-
上传失败
-
-
-
+
-
-
-
- 下载
-
-
-
-
{{ getShortFileName(image.name) }}
@@ -306,25 +280,157 @@
+
+
+
+
+ 微信图片上传进度: {{ uploadProgress }}%
+
+
+
+
+
+ 文档附件
+
+
+
+
+
+
+
+
+ {{ file.progress || 0 }}%
+
+
+
+
+
+ 上传失败
+
+
+
+
+
+
+
+
+
+ 下载
+
+
+ 重试
+
+
+ 删除
+
+
+
+
+ {{ getShortFileName(file.name) }}
+
+
+
+
+
-
+
- 添加附件(图片)
+ 添加附件(图片/文档)
-
- • 支持格式:JPG、PNG等
- • 单个文件不超过10MB
- • 最多上传15个文件
+
+ • 支持图片、视频、PDF、Word等格式,单个文件不超过10MB,最多15个
+
+ • 图片提供缩略图预览,其它文件卡片展示
+
+ • 当前为微信浏览器,已启用原生选图与上传能力
+
@@ -459,6 +565,7 @@
import { request, smartUploadFile, downloadFile } from '@/common/request.js';
import { getDictLabel, getDictTagType, getDict } from '@/common/dict.js';
import dayjs from 'dayjs';
+import weChatSDK from '@/utils/wechat.js';
export default {
name: 'DisciplineRecordForm',
@@ -555,7 +662,13 @@ export default {
statusDict: [],
// 附件相关
- imageFiles: [],
+ attachmentList: [],
+ uploadProgress: 0,
+
+ // 微信SDK
+ wechatSDK: weChatSDK,
+ isWechatBrowser: false,
+ wechatInitializing: false,
};
},
computed: {
@@ -570,12 +683,23 @@ export default {
isReadonly() {
return this.mode === 'view';
},
+ // 图片附件
+ imageFiles() {
+ return this.attachmentList.filter((file) => this.isImageFile(file));
+ },
+ // 非图片附件
+ nonImageFiles() {
+ return this.attachmentList.filter((file) => !this.isImageFile(file));
+ },
},
async onLoad(options) {
// 获取URL参数
this.mode = options.mode || 'add';
this.recordId = options.id || null;
+ // 检查微信环境
+ await this.checkWechatEnvironment();
+
// 加载字典数据
await this.loadDictData();
@@ -599,6 +723,84 @@ export default {
uni.navigateBack();
},
+ // === 微信SDK相关 ===
+ async checkWechatEnvironment() {
+ try {
+ this.isWechatBrowser = this.wechatSDK.isWeChatBrowser();
+ if (this.isWechatBrowser) {
+ await this.initWechatSDK();
+ }
+ } catch (error) {
+ console.error('微信环境检查失败:', error);
+ }
+ },
+
+ async initWechatSDK() {
+ if (this.wechatInitializing) {
+ return;
+ }
+ this.wechatInitializing = true;
+ try {
+ await this.wechatSDK.init({
+ debug: process.env.NODE_ENV === 'development',
+ jsApiList: ['chooseImage', 'previewImage', 'getLocation', 'openLocation'],
+ });
+ try {
+ await this.wechatSDK.checkJsApi(['chooseImage']);
+ } catch (apiError) {
+ console.warn('API支持检查失败:', apiError.message);
+ }
+ } catch (error) {
+ console.error('微信SDK初始化失败:', error);
+ } finally {
+ this.wechatInitializing = false;
+ }
+ },
+
+ async reinitWechatSDK() {
+ this.wechatSDK.isInitialized = false;
+ await this.initWechatSDK();
+ },
+
+ async uploadWechatImageToOSS(localId) {
+ try {
+ this.uploadProgress = 10;
+ const wxUploadRes = await this.wechatSDK.uploadImage(localId);
+ if (!wxUploadRes.serverId) {
+ throw new Error('微信服务器上传失败,未获取到serverId');
+ }
+ const mediaId = wxUploadRes.serverId;
+ this.uploadProgress = 50;
+
+ const ossResponse = await request({
+ url: '/system/wechat/downloadToOss',
+ method: 'GET',
+ params: { mediaId },
+ timeout: 30000,
+ });
+
+ if (ossResponse.code === 200) {
+ const ossResult = ossResponse.data;
+ this.uploadProgress = 100;
+ return {
+ ossId: ossResult.ossId,
+ url: ossResult.url,
+ fileName: ossResult.fileName,
+ fileSize: ossResult.fileSize || 0,
+ };
+ }
+
+ throw new Error(ossResponse.msg || '保存图片到云端存储失败');
+ } catch (error) {
+ this.uploadProgress = 0;
+ uni.showToast({
+ title: error.message || '图片上传失败,请重试',
+ icon: 'none',
+ });
+ throw error;
+ }
+ },
+
// 加载字典数据
async loadDictData() {
try {
@@ -679,7 +881,7 @@ export default {
try {
if (!this.form.attachmentOssIds || !this.form.attachmentOssIds.toString().trim()) {
console.log('⚠️ attachmentOssIds 为空');
- this.imageFiles = [];
+ this.attachmentList = [];
return;
}
@@ -693,7 +895,7 @@ export default {
if (ossIdsArray.length === 0) {
console.log('⚠️ OSS IDs 数组为空');
- this.imageFiles = [];
+ this.attachmentList = [];
return;
}
@@ -707,19 +909,19 @@ export default {
if (response.code === 200 && response.data) {
console.log(`✅ 获取到 ${response.data.length} 个附件`);
-
- this.imageFiles = response.data.map((item, index) => {
+
+ this.attachmentList = response.data.map((item, index) => {
console.log(`\n📄 处理附件 ${index + 1}/${response.data.length}:`, {
originalName: item.originalName,
fileSuffix: item.fileSuffix,
url: item.url,
- ossId: item.ossId
+ ossId: item.ossId,
});
// 使用fileSuffix字段判断文件类型
const fileType = this.getFileTypeBySuffix(item.fileSuffix);
console.log(` 📝 fileType: ${fileType}`);
-
+
const isImage = this.isImageFile({
name: item.originalName,
type: fileType,
@@ -743,7 +945,9 @@ export default {
.endsWith(`.${item.fileSuffix.toLowerCase()}`)
) {
fullFileName = `${fullFileName}.${item.fileSuffix}`;
- console.log(` 📎 文件名处理: ${item.originalName} -> ${fullFileName}`);
+ console.log(
+ ` 📎 文件名处理: ${item.originalName} -> ${fullFileName}`,
+ );
}
const fileObj = {
@@ -783,11 +987,11 @@ export default {
});
} else {
console.log('❌ 响应数据无效');
- this.imageFiles = [];
+ this.attachmentList = [];
}
} catch (error) {
console.error('❌ 处理附件数据失败:', error);
- this.imageFiles = [];
+ this.attachmentList = [];
}
},
@@ -884,7 +1088,7 @@ export default {
// 预加载图片以确保能正确显示
async preloadImages() {
console.log('=== 预加载图片 ===');
- const imageFiles = this.imageFiles.filter((file) => this.isImageFile(file));
+ const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
for (const file of imageFiles) {
if (file.url) {
@@ -909,9 +1113,9 @@ export default {
// 处理图片文件,获取blob数据以确保正确显示
async processImageFiles() {
console.log('\n🎨 ===== processImageFiles 开始 =====');
- console.log(`📊 总文件数: ${this.imageFiles.length}`);
-
- const imageFiles = this.imageFiles.filter((file) => this.isImageFile(file));
+ console.log(`📊 总文件数: ${this.attachmentList.length}`);
+
+ const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file));
console.log(`🖼️ 图片文件数: ${imageFiles.length}`);
for (let i = 0; i < imageFiles.length; i++) {
@@ -920,13 +1124,13 @@ export default {
console.log(` 📝 name: ${file.name}`);
console.log(` 🔗 url (处理前): ${file.url}`);
console.log(` 🔗 blobUrl (处理前): ${file.blobUrl || '无'}`);
-
+
if (file.url && !file.blobUrl) {
try {
console.log(` 🌐 开始获取blob URL...`);
const blobUrl = await this.getImageBlobUrl(file.url);
console.log(` ✅ 获取到blob URL: ${blobUrl}`);
-
+
if (blobUrl) {
// 更新文件对象,添加blob URL,并标记为图片
const oldUrl = file.url;
@@ -936,7 +1140,7 @@ export default {
file.type =
file.type && file.type.startsWith('image') ? file.type : 'image';
file.isImage = true;
-
+
console.log(` ✅ 图片文件处理成功:`);
console.log(` - 原URL: ${oldUrl}`);
console.log(` - 新URL: ${file.url}`);
@@ -958,15 +1162,15 @@ export default {
}
}
}
-
+
console.log('\n✅ ===== processImageFiles 完成 =====');
- console.log(`📊 最终imageFiles数组:`, this.imageFiles);
+ console.log(`📊 最终attachmentList数组:`, this.attachmentList);
},
// 清理blob URL,防止内存泄漏
cleanupBlobUrls() {
console.log('=== 清理blob URL ===');
- this.imageFiles.forEach((file) => {
+ this.attachmentList.forEach((file) => {
if (file.blobUrl && file.blobUrl.startsWith('blob:')) {
try {
URL.revokeObjectURL(file.blobUrl);
@@ -1117,129 +1321,35 @@ export default {
this.form.scoreDeduction = e;
},
- // 图片上传
- handleImageUpload() {
- if (this.imageFiles.length >= 15) {
- uni.showToast({
- title: '最多上传15个文件',
- icon: 'none',
- });
- return;
- }
-
- uni.chooseImage({
- count: Math.min(5, 15 - this.imageFiles.length),
- sizeType: ['original', 'compressed'],
- sourceType: ['album', 'camera'],
- success: (res) => {
- console.log('选择图片成功:', res);
- res.tempFilePaths.forEach((path, index) => {
- const tempFile = res.tempFiles?.[index];
- this.uploadImage(path, tempFile);
- });
- },
- fail: (err) => {
- console.error('选择图片失败:', err);
- uni.showToast({
- title: '选择图片失败',
- icon: 'none',
- });
- },
- });
- },
-
- // 上传图片
- async uploadImage(filePath, tempFile) {
- const fileName = filePath.split('/').pop() || `附件_${Date.now()}.jpg`;
- const uid = `upload_${Date.now()}_${Math.random()}`;
-
- const fileObj = {
- uid: uid,
- name: fileName,
- url: filePath, // 用于预览
- blobUrl: filePath, // 用于预览
- tempFilePath: filePath, // 保留原始路径用于重试
- status: 'uploading',
- progress: 0,
- ossId: '',
- size: tempFile?.size || 0,
- };
-
- this.imageFiles.push(fileObj);
-
- try {
- const response = await smartUploadFile('/resource/oss/upload', filePath, {
- bizType: 'discipline_record_attachment',
- });
-
- if (response.code === 200) {
- fileObj.status = 'success';
- fileObj.ossId = response.data.ossId;
- fileObj.url = response.data.url;
- fileObj.name = response.data.fileName || fileName;
- fileObj.size = response.data.fileSize || fileObj.size;
- fileObj.progress = 100;
-
- uni.showToast({
- title: '上传成功',
- icon: 'success',
- duration: 1500,
- });
- } else {
- fileObj.status = 'error';
- uni.showToast({
- title: response.msg || '上传失败',
- icon: 'none',
- });
- }
- } catch (error) {
- fileObj.status = 'error';
- console.error('上传失败:', error);
- uni.showToast({
- title: '上传失败',
- icon: 'error',
- });
- }
- },
-
// 重试上传
- async retryUploadFile(file) {
+ async retryUploadFile(file, event) {
console.log('=== 重试上传文件 ===', file);
- // 重置文件状态
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
file.status = 'uploading';
file.progress = 0;
try {
- // 使用原始的tempFilePath或url进行上传
- const filePath = file.tempFilePath || file.url;
- if (!filePath) {
- throw new Error('文件路径无效');
- }
-
- const response = await smartUploadFile('/resource/oss/upload', filePath, {
- bizType: 'discipline_record_attachment',
- });
-
- if (response.code === 200) {
+ if (file.source === 'wechat' && file.localId) {
+ const uploadResult = await this.uploadWechatImageToOSS(file.localId);
file.status = 'success';
- file.ossId = response.data.ossId;
- file.url = response.data.url;
- file.name = response.data.fileName || file.name;
+ file.ossId = uploadResult.ossId;
+ file.url = uploadResult.url;
+ file.name = uploadResult.fileName || file.name;
+ file.size = uploadResult.fileSize || 0;
file.progress = 100;
-
- uni.showToast({
- title: '重新上传成功',
- icon: 'success',
- duration: 1500,
- });
} else {
- file.status = 'error';
- uni.showToast({
- title: response.msg || '上传失败',
- icon: 'none',
- });
+ await this.uploadFileToServer(file);
}
+
+ uni.showToast({
+ title: '重新上传成功',
+ icon: 'success',
+ });
} catch (error) {
console.error('重试上传失败:', error);
file.status = 'error';
@@ -1248,21 +1358,322 @@ export default {
uni.showToast({
title: '重试失败,请稍后再试',
icon: 'none',
- duration: 2000,
});
}
},
// 图片点击
- handleImageClick(image, index) {
- if (image.status === 'success') {
- const urls = this.imageFiles
- .filter((f) => f.status === 'success')
- .map((f) => f.url);
- uni.previewImage({
- urls: urls,
- current: image.url,
+ handleImageClick(image, index, event) {
+ if (event && event.target) {
+ const target = event.target;
+ const isActionButton =
+ (typeof target.closest === 'function' &&
+ (target.closest('.image-actions') || target.closest('u-button'))) ||
+ target.tagName === 'BUTTON';
+ if (isActionButton) {
+ return;
+ }
+ }
+
+ if (image.status !== 'success') {
+ uni.showToast({
+ title: '图片尚未上传完成',
+ icon: 'none',
});
+ return;
+ }
+
+ const urls = this.imageFiles.filter((f) => f.status === 'success').map((f) => f.url);
+ uni.previewImage({
+ urls,
+ current: image.url,
+ });
+ },
+
+ // 统一附件上传入口
+ handleDocUpload() {
+ console.log('=== 统一附件上传方法 ===');
+
+ if (this.attachmentList.length >= 15) {
+ uni.showToast({
+ title: '最多只能上传15个文件',
+ icon: 'none',
+ });
+ return;
+ }
+
+ uni.showActionSheet({
+ itemList: ['选择图片'],
+ success: (res) => {
+ switch (res.tapIndex) {
+ case 0:
+ this.chooseDocuments();
+ break;
+ }
+ },
+ fail: () => {
+ console.log('用户取消选择');
+ },
+ });
+ },
+
+ // 选择文件(优先使用chooseFile)
+ chooseDocuments() {
+ if (typeof uni.chooseFile === 'function') {
+ uni.chooseFile({
+ count: Math.min(5, 15 - this.attachmentList.length),
+ success: (res) => {
+ if (res.tempFiles && res.tempFiles.length > 0) {
+ this.handleFileUpload({
+ tempFiles: res.tempFiles,
+ });
+ }
+ },
+ fail: (err) => {
+ console.error('选择文档失败:', err);
+ uni.showToast({
+ title: '选择失败,请重试',
+ icon: 'none',
+ });
+ },
+ });
+ } else {
+ this.handleBackupUpload();
+ }
+ },
+
+ // 备用上传方案
+ handleBackupUpload() {
+ uni.showActionSheet({
+ itemList: ['选择图片', '选择视频', '选择文档'],
+ success: (res) => {
+ if (res.tapIndex === 0) {
+ uni.chooseImage({
+ count: Math.min(5, 15 - this.attachmentList.length),
+ sizeType: ['original', 'compressed'],
+ sourceType: ['album', 'camera'],
+ success: (imageRes) => {
+ const files = imageRes.tempFilePaths.map((path, index) => {
+ const fileName =
+ path.split('/').pop() || `image_${Date.now()}_${index}.jpg`;
+ const tempFile = imageRes.tempFiles?.[index];
+ return {
+ name: fileName,
+ path,
+ url: path,
+ tempFilePath: path,
+ size: tempFile?.size || 0,
+ type: tempFile?.type || 'image/jpeg',
+ };
+ });
+
+ const validFiles = files.filter((file) => this.checkFileSize(file));
+ if (validFiles.length > 0) {
+ this.handleFileUpload({ tempFiles: validFiles });
+ }
+ },
+ fail: (err) => {
+ console.error('选择图片失败:', err);
+ uni.showToast({
+ title: '选择图片失败',
+ icon: 'none',
+ });
+ },
+ });
+ } else if (res.tapIndex === 1) {
+ uni.chooseVideo({
+ sourceType: ['album', 'camera'],
+ maxDuration: 60,
+ success: (videoRes) => {
+ const file = {
+ name: `video_${Date.now()}.mp4`,
+ path: videoRes.tempFilePath,
+ url: videoRes.tempFilePath,
+ tempFilePath: videoRes.tempFilePath,
+ size: videoRes.size || 0,
+ type: 'video/mp4',
+ };
+
+ if (this.checkFileSize(file)) {
+ this.handleFileUpload({ tempFiles: [file] });
+ }
+ },
+ fail: (err) => {
+ console.error('选择视频失败:', err);
+ uni.showToast({
+ title: '选择视频失败',
+ icon: 'none',
+ });
+ },
+ });
+ } else if (res.tapIndex === 2) {
+ this.handleDocUpload();
+ }
+ },
+ });
+ },
+
+ // 通用文件上传处理
+ handleFileUpload(event) {
+ uni.showToast({
+ title: '文件选择成功,开始上传...',
+ icon: 'loading',
+ duration: 1500,
+ });
+
+ let files = [];
+ if (event.file) {
+ files = Array.isArray(event.file) ? event.file : [event.file];
+ } else if (event.tempFiles) {
+ files = event.tempFiles;
+ } else if (event.tempFilePaths) {
+ files = event.tempFilePaths.map((path, index) => ({
+ url: path,
+ path: path,
+ tempFilePath: path,
+ name: `文件${index + 1}`,
+ type: '',
+ }));
+ } else if (event.url || event.path || event.tempFilePath) {
+ files = [event];
+ }
+
+ if (files.length === 0) {
+ uni.showToast({
+ title: '未获取到文件,请重试',
+ icon: 'none',
+ });
+ return;
+ }
+
+ if (this.attachmentList.length + files.length > 15) {
+ uni.showToast({
+ title: '最多只能上传15个文件',
+ icon: 'none',
+ });
+ return;
+ }
+
+ const validFiles = files.filter((file) => this.checkFileSize(file));
+ if (validFiles.length === 0) {
+ return;
+ }
+
+ if (validFiles.length < files.length) {
+ uni.showToast({
+ title: `已过滤 ${files.length - validFiles.length} 个超限文件`,
+ icon: 'none',
+ });
+ }
+
+ const newFiles = validFiles.map((file, index) => {
+ const filePath = file.url || file.tempFilePath || file.path;
+ const fileName =
+ file.name || filePath?.split('/').pop() || `附件${Date.now()}_${index}.dat`;
+ return {
+ ...file,
+ uid: `upload_${Date.now()}_${index}`,
+ name: fileName,
+ url: filePath,
+ path: filePath,
+ tempFilePath: filePath,
+ blobUrl: filePath,
+ status: 'uploading',
+ ossId: null,
+ type: file.type || this.getFileTypeByName(fileName),
+ };
+ });
+
+ this.attachmentList.push(...newFiles);
+ newFiles.forEach((file) => {
+ this.uploadFileToServer(file).catch((error) => {
+ console.error(`文件 ${file.name} 上传失败:`, error);
+ });
+ });
+ },
+
+ // 文件大小校验
+ checkFileSize(file) {
+ const maxSize = 10 * 1024 * 1024;
+ const fileSize = file.size || 0;
+ if (fileSize > maxSize) {
+ const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
+ uni.showToast({
+ title: `文件大小 ${fileSizeMB}MB,超过10MB限制`,
+ icon: 'none',
+ });
+ return false;
+ }
+ return true;
+ },
+
+ // 上传文件到服务器
+ async uploadFileToServer(file) {
+ try {
+ file.status = 'uploading';
+ const filePath = file.url || file.tempFilePath || file.path;
+
+ if (!filePath) {
+ file.status = 'error';
+ throw new Error('文件路径不存在');
+ }
+
+ uni.showLoading({
+ title: `上传${file.name}...`,
+ mask: true,
+ });
+
+ const response = await smartUploadFile('/resource/oss/upload', file, {
+ bizType: 'discipline_record_attachment',
+ });
+
+ uni.hideLoading();
+
+ if (response && response.code === 200) {
+ file.url = response.data.url;
+ file.ossId = response.data.ossId;
+ file.status = 'success';
+ file.name = response.data.fileName || file.name;
+ file.size = response.data.fileSize || file.size;
+ file.progress = 100;
+ file.blobUrl = file.url;
+
+ uni.showToast({
+ title: '上传成功',
+ icon: 'success',
+ });
+
+ return response;
+ } else {
+ file.status = 'error';
+ uni.showToast({
+ title: response?.msg || '上传失败',
+ icon: 'none',
+ });
+ throw new Error(response?.msg || '上传失败');
+ }
+ } catch (error) {
+ uni.hideLoading();
+ file.status = 'error';
+ uni.showModal({
+ title: '上传失败',
+ content: `文件"${file.name}"上传失败:${error.message || '请重试'}`,
+ showCancel: true,
+ cancelText: '取消',
+ confirmText: '重试',
+ success: (res) => {
+ if (res.confirm) {
+ file.status = 'pending';
+ this.uploadFileToServer(file);
+ } else {
+ const index = this.attachmentList.findIndex((f) => f.uid === file.uid);
+ if (index > -1) {
+ this.attachmentList.splice(index, 1);
+ }
+ }
+ },
+ });
+ throw error;
}
},
@@ -1299,8 +1710,17 @@ export default {
URL.revokeObjectURL(image.blobUrl);
}
- // 从图片列表中删除
- this.imageFiles.splice(index, 1);
+ // 从附件列表中删除
+ const attachmentIndex = this.attachmentList.findIndex(
+ (file) =>
+ file.uid === image.uid ||
+ (file.ossId && file.ossId === image.ossId) ||
+ (file.url && file.url === image.url),
+ );
+
+ if (attachmentIndex > -1) {
+ this.attachmentList.splice(attachmentIndex, 1);
+ }
uni.showToast({
title: '删除成功',
@@ -1420,6 +1840,153 @@ export default {
}
},
+ // 文档/非图片文件下载
+ async handleDownloadFile(file, event, retryCount = 0) {
+ if (file.status !== 'success' || !file.ossId) {
+ uni.showToast({
+ title: '文件尚未上传完成',
+ icon: 'none',
+ });
+ return;
+ }
+
+ uni.showLoading({
+ title: `正在下载${file.name}...`,
+ mask: true,
+ });
+
+ try {
+ const downloadUrl = `/resource/oss/download/${file.ossId}`;
+ if (this.isImageFile(file)) {
+ await this.downloadAndSaveImage(downloadUrl, file.name);
+ } else {
+ await this.downloadAndOpenDocument(downloadUrl, file.name);
+ }
+ } catch (error) {
+ uni.hideLoading();
+ if (retryCount < 2) {
+ uni.showToast({
+ title: `下载失败,正在重试(${retryCount + 1}/2)...`,
+ icon: 'none',
+ });
+ setTimeout(() => {
+ this.handleDownloadFile(file, event, retryCount + 1);
+ }, 1500);
+ return;
+ }
+
+ uni.showModal({
+ title: '下载失败',
+ content: error.message || '请稍后重试',
+ showCancel: true,
+ cancelText: '取消',
+ confirmText: '重试',
+ success: (res) => {
+ if (res.confirm) {
+ this.handleDownloadFile(file);
+ }
+ },
+ });
+ }
+ },
+
+ async downloadAndSaveImage(downloadUrl, fileName) {
+ const response = await downloadFile(downloadUrl, fileName);
+ if (response.statusCode === 200) {
+ uni.hideLoading();
+ uni.saveFile({
+ tempFilePath: response.tempFilePath,
+ success: () => {
+ uni.showToast({
+ title: '下载成功',
+ icon: 'success',
+ });
+ },
+ fail: () => {
+ uni.showToast({
+ title: '保存失败',
+ icon: 'none',
+ });
+ },
+ });
+ } else {
+ uni.hideLoading();
+ throw new Error('下载失败');
+ }
+ },
+
+ async downloadAndOpenDocument(downloadUrl, fileName) {
+ const response = await downloadFile(downloadUrl, fileName);
+ if (response.statusCode === 200) {
+ uni.hideLoading();
+ uni.saveFile({
+ tempFilePath: response.tempFilePath,
+ success: () => {
+ uni.showToast({
+ title: '下载成功',
+ icon: 'success',
+ });
+ },
+ fail: () => {
+ uni.showToast({
+ title: '保存失败',
+ icon: 'none',
+ });
+ },
+ });
+ } else {
+ uni.hideLoading();
+ throw new Error('下载失败');
+ }
+ },
+
+ handleDownloadFileClick(file, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ this.handleDownloadFile(file, event);
+ },
+
+ handleRemoveFile(file, index, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+
+ uni.showModal({
+ title: '确认删除',
+ content: '确定要删除这个文件吗?',
+ success: (res) => {
+ if (res.confirm) {
+ const fileIndex = this.attachmentList.findIndex((f) => f.uid === file.uid);
+ if (fileIndex > -1) {
+ this.attachmentList.splice(fileIndex, 1);
+
+ if (file.blobUrl && file.blobUrl.startsWith('blob:')) {
+ URL.revokeObjectURL(file.blobUrl);
+ }
+
+ uni.showToast({
+ title: '文件删除成功',
+ icon: 'success',
+ });
+ }
+ }
+ },
+ });
+ },
+
+ handleRemoveFileClick(file, index, event) {
+ if (event) {
+ event.stopPropagation();
+ event.preventDefault();
+ event.stopImmediatePropagation();
+ }
+ this.handleRemoveFile(file, index);
+ },
+
// 获取缩写的文件名(保留后缀)
getShortFileName(fileName) {
if (!fileName) return '';
@@ -1449,7 +2016,7 @@ export default {
this.buttonLoading = true;
// 处理附件数据
- const attachmentOssIds = this.imageFiles
+ const attachmentOssIds = this.attachmentList
.filter((file) => file.status === 'success' && file.ossId)
.map((file) => file.ossId);
this.form.attachmentOssIds = attachmentOssIds.join(',');
@@ -1512,7 +2079,7 @@ export default {
status: '1',
attachmentOssIds: '',
};
- this.imageFiles = [];
+ this.attachmentList = [];
this.$refs.formRef?.resetFields();
},
},
@@ -1678,6 +2245,86 @@ export default {
margin: 16rpx 0;
}
+.doc-upload-section {
+ margin: 20rpx 0;
+ padding: 16rpx 0;
+ border-top: 2rpx dashed #e0e0e0;
+}
+
+.upload-progress {
+ margin-top: 20rpx;
+ padding: 20rpx;
+ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
+ border-radius: 12rpx;
+ border: 1px solid #e1e8ed;
+ box-shadow: 0 2px 8px rgba(41, 121, 255, 0.1);
+}
+
+.progress-text {
+ display: block;
+ text-align: center;
+ font-size: 26rpx;
+ color: #2979ff;
+ font-weight: 500;
+ margin-top: 12rpx;
+}
+
+.custom-file-list {
+ margin-top: 20rpx;
+}
+
+.file-title {
+ font-size: 28rpx;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 20rpx;
+ padding-left: 10rpx;
+ border-left: 4rpx solid #2979ff;
+}
+
+.file-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280rpx, 1fr));
+ gap: 20rpx;
+}
+
+.file-card {
+ position: relative;
+ background: #fff;
+ border-radius: 12rpx;
+ overflow: hidden;
+ border: 2rpx solid #f0f0f0;
+ transition: all 0.3s ease;
+}
+
+.file-card:active {
+ transform: scale(0.98);
+ border-color: #2979ff;
+}
+
+.file-icon-container {
+ position: relative;
+ width: 100%;
+ height: 200rpx;
+ border-radius: 8rpx;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: #f8f9fa;
+}
+
+.file-name-display {
+ padding: 8rpx 12rpx;
+ background: #fff;
+ font-size: 22rpx;
+ color: #666;
+ text-align: center;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
.upload-tips {
display: flex;
flex-direction: column;
@@ -1693,6 +2340,11 @@ export default {
color: #909399;
}
+.wechat-tip {
+ color: #2979ff;
+ font-weight: 500;
+}
+
/* 修复uni-scroll-view宽度溢出问题 */
::v-deep .popup-content {
.uni-scroll-view {