From f6c8e6805d5b0714a8c44eb3357127cec7dea56f Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 10 Aug 2025 13:03:53 +0800 Subject: [PATCH 01/23] =?UTF-8?q?=E9=87=8D=E8=A6=81=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=EF=BC=8C=E5=A4=87=E4=BB=BD=E6=96=87=E6=A1=A3=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9A=84=E8=83=BD=E5=8A=9B=EF=BC=8C=E5=9B=A0=E4=B8=BA=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E4=B8=8D=E6=94=AF=E6=8C=81=E5=9C=A8=E7=BA=BF=E6=89=93?= =?UTF-8?q?=E5=BC=80=E6=96=87=E6=A1=A3=E3=80=82=E6=89=80=E4=BB=A5=E4=B8=8B?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=E7=89=88=E6=9C=AC=E4=B8=AD=E4=BC=9A=E5=8E=BB?= =?UTF-8?q?=E9=99=A4=E8=BF=99=E4=B8=AA=E6=96=87=E6=A1=A3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 3187 +++++++++++------ 1 file changed, 2171 insertions(+), 1016 deletions(-) diff --git a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue index 6097c8ac..413fe352 100644 --- a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue +++ b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue @@ -31,39 +31,79 @@ - - - - 🔧 开发调试工具(正式发布前请隐藏) + + + + 微信SDK调试 + + + {{ wechatInitializing ? '初始化中' : '重新初始化' }} + + + {{ debugMode ? '关闭调试' : '开启调试' }} + + 清空日志 + - - - 文件大小检查 - - - 调试图片文件 - - - 处理图片文件 - - 审批记录 - 🧪测试上传事件 - - 📷真实选择 - + + + + + 微信环境: + + {{ isWechatBrowser ? '是' : '否' }} + + + + SDK状态: + + {{ wechatSDK.isInitialized ? '已初始化' : '未初始化' }} + + + + 版本信息: + {{ wechatSDK.getVersion() }} + + + 可用API: + {{ wechatSDK.getAvailableApis() }} + + + + + + + {{ log.time }} + {{ log.message }} + + {{ log.detail }} + + + + 暂无调试日志 + + + + + + + + - 定位 + + {{ isWechatBrowser ? '微信定位' : '定位' }} + + + + + + 坐标: {{ geoLatitude.toFixed(6) }}, {{ geoLongitude.toFixed(6) }} + + + + 查看位置 @@ -236,125 +293,234 @@ 附件上传 - - + + + 图片附件 + + + + + - + + + + {{ image.progress || 0 }}% + + + + + + 上传失败 + + + + + + + + + + + + 下载 + + + 重试 + + + 删除 + + + + + + {{ getShortFileName(image.name) }} + + + + + + + + + 微信图片上传进度: {{ uploadProgress }}% + + + + 文档附件 + + + + + + + + + + {{ file.progress || 0 }}% + + + + + + 上传失败 + + + + + + + + + + + {{ getFileExt(file.name) }} + + {{ formatFileSize(file.size) }} + + + + + + + 下载 + + + 重试 + + + 删除 + + + + + + {{ getShortFileName(file.name) }} + + + + + - 上传文档附件 + 添加附件(图片) - + - - - - 直接选择文件 - - - 测试上传逻辑 - - - 测试uview2事件 - - - - - - - - - - {{ getShortFileName(file.name) }} - {{ getFileExt(file.name) }} - - - - - 下载 - - - 删除 - - - - - 上传中... - - - 上传失败 - - + + + + • 支持格式:JPG、PNG等 • 单个文件不超过10MB • 最多上传15个文件 + + • 图片文件将以图片形式展示,支持预览和下载 + + • 在微信中使用可获得更好的图片选择体验 + @@ -438,6 +604,7 @@ import { REQUEST_CONFIG } from '@/common/config.js'; import ApprovalButton from '@/components/Process/approvalButton.vue'; import SubmitVerify from '@/components/Process/submitVerify.vue'; import ApprovalRecord from '@/components/Process/approvalRecord.vue'; +import weChatSDK from '@/utils/wechat.js'; export default { name: 'LeaveEdit', @@ -461,6 +628,19 @@ export default { { label: '销假流程', value: 'hzdyjs-xj' }, ], + // 微信SDK相关 + wechatSDK: weChatSDK, + isWechatBrowser: false, + wechatInitializing: false, + wechatImageLoading: false, + debugMode: false, + wechatLogs: [], + latestLogId: '', + + // 微信图片相关 + selectedWechatImages: [], + uploadProgress: 0, // 上传进度 + form: { id: undefined, studentId: '', @@ -548,46 +728,6 @@ export default { return index >= 0 ? index : 0; }, - // uview2 upload组件所需的文件列表格式 - uploadFileList() { - const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); - console.log('uploadFileList - 图片文件列表:', imageFiles); - - return imageFiles.map((file) => { - // 确保图片文件有正确的显示属性 - const displayUrl = file.blobUrl || file.url || file.path; - const uploadFile = { - ...file, - url: displayUrl, - status: file.status || 'success', - message: file.status === 'error' ? '上传失败' : '', - // 明确告诉 u-upload 这是图片文件 - fileType: 'image', - type: 'image', - isImage: true, - thumb: displayUrl, - name: file.name || 'image.jpg', - }; - - // 对于从服务器获取的图片文件,确保有正确的缩略图显示 - if (file.ossId && displayUrl) { - // uview2 upload组件需要的额外属性 - if (!uploadFile.file) { - uploadFile.file = { - name: uploadFile.name, - type: 'image', - size: file.size || 0, - path: displayUrl, - url: displayUrl, - }; - } - } - - console.log('处理后的upload文件:', uploadFile); - return uploadFile; - }); - }, - // 图片文件(用于内部处理) imageFiles() { return this.attachmentList.filter((file) => this.isImageFile(file)); @@ -598,8 +738,12 @@ export default { return this.attachmentList.filter((file) => !this.isImageFile(file)); }, }, - onLoad(options) { + async onLoad(options) { this.routeParams = options || {}; + + // 初始化微信环境检查并自动初始化SDK + await this.checkWechatEnvironment(); + this.initPageData(); }, onUnload() { @@ -607,6 +751,462 @@ export default { this.cleanupBlobUrls(); }, methods: { + // === 微信SDK相关方法 === + + // 检查微信环境并自动初始化SDK + async checkWechatEnvironment() { + this.addWechatLog('开始检查微信环境...', 'info'); + + try { + // 检查是否在微信浏览器中 + this.isWechatBrowser = this.wechatSDK.isWeChatBrowser(); + this.addWechatLog( + `微信环境检测结果: ${this.isWechatBrowser ? '是' : '否'}`, + this.isWechatBrowser ? 'success' : 'warning', + ); + + if (this.isWechatBrowser) { + // 在微信环境中,自动初始化SDK + this.addWechatLog('检测到微信环境,开始自动初始化SDK...', 'info'); + await this.initWechatSDK(); + } else { + this.addWechatLog('非微信环境,跳过SDK初始化', 'warning'); + } + + // 在开发环境或微信环境中默认开启调试模式 + if (process.env.NODE_ENV === 'development' || this.isWechatBrowser) { + this.debugMode = false; // 默认关闭,用户可手动开启 + this.addWechatLog('调试模式已准备就绪', 'info'); + } + } catch (error) { + console.error('微信环境检查失败:', error); + this.addWechatLog(`微信环境检查失败: ${error.message}`, 'error'); + } + }, + + // 初始化微信SDK + async initWechatSDK() { + if (this.wechatInitializing) { + this.addWechatLog('SDK正在初始化中,请等待...', 'warning'); + return; + } + + this.wechatInitializing = true; + this.addWechatLog('开始初始化微信SDK...', 'info'); + + try { + // 使用微信SDK工具类进行初始化 + await this.wechatSDK.init({ + debug: process.env.NODE_ENV === 'development', + jsApiList: ['chooseImage', 'previewImage', 'getLocation', 'openLocation'], + }); + + this.addWechatLog('微信SDK初始化成功', 'success'); + this.addWechatLog(`SDK版本: ${this.wechatSDK.getVersion()}`, 'info'); + this.addWechatLog(`可用API: ${this.wechatSDK.getAvailableApis()}`, 'info'); + + // 检查关键API支持情况 + try { + const apiCheckResult = await this.wechatSDK.checkJsApi([ + 'chooseImage', + 'getLocation', + ]); + this.addWechatLog( + `API支持检查完成`, + 'success', + JSON.stringify(apiCheckResult, null, 2), + ); + } catch (apiError) { + this.addWechatLog(`API支持检查失败: ${apiError.message}`, 'warning'); + } + } catch (error) { + console.error('微信SDK初始化失败:', error); + this.addWechatLog(`微信SDK初始化失败: ${error.message}`, 'error'); + + // 显示用户友好的错误提示 + if (error.message.includes('invalid signature')) { + this.addWechatLog('可能原因: 签名验证失败,请检查域名配置', 'error'); + } else if (error.message.includes('invalid url domain')) { + this.addWechatLog('可能原因: 当前域名未在微信公众平台配置', 'error'); + } + } finally { + this.wechatInitializing = false; + } + }, + + // 重新初始化微信SDK + async reinitWechatSDK() { + this.addWechatLog('用户触发重新初始化SDK...', 'info'); + this.wechatSDK.isInitialized = false; + await this.initWechatSDK(); + }, + + // 添加调试日志 + addWechatLog(message, type = 'info', detail = '') { + const log = { + time: new Date().toLocaleTimeString(), + message: message, + type: type, // 'info', 'success', 'warning', 'error' + detail: detail, + }; + + this.wechatLogs.push(log); + + // 控制日志数量,避免内存溢出 + if (this.wechatLogs.length > 100) { + this.wechatLogs.shift(); + } + + // 更新最新日志ID,用于自动滚动 + this.latestLogId = `wechat-log-${this.wechatLogs.length - 1}`; + + // 同时输出到控制台 + const consoleMethod = + { + info: 'log', + success: 'log', + warning: 'warn', + error: 'error', + }[type] || 'log'; + + console[consoleMethod](`[微信SDK] ${message}`, detail ? detail : ''); + }, + + // 清空调试日志 + clearWechatLogs() { + this.wechatLogs = []; + this.latestLogId = ''; + this.addWechatLog('调试日志已清空', 'info'); + }, + + // 切换调试模式 + toggleDebugMode() { + this.debugMode = !this.debugMode; + this.addWechatLog(`调试模式已${this.debugMode ? '开启' : '关闭'}`, 'info'); + + if (this.debugMode) { + uni.showToast({ + title: '调试模式已开启', + icon: 'success', + }); + } + }, + + // 微信选择图片 + async chooseWechatImage() { + if (!this.isWechatBrowser) { + this.addWechatLog('非微信环境,无法使用微信选择图片功能', 'error'); + uni.showToast({ + title: '请在微信中打开', + icon: 'none', + }); + return; + } + + if (!this.wechatSDK.isInitialized) { + this.addWechatLog('微信SDK未初始化,尝试重新初始化...', 'warning'); + await this.initWechatSDK(); + + if (!this.wechatSDK.isInitialized) { + uni.showToast({ + title: '微信SDK未就绪', + icon: 'none', + }); + return; + } + } + + // 检查文件数量限制 + if (this.attachmentList.length >= 15) { + uni.showToast({ + title: '最多只能上传15个文件', + icon: 'none', + }); + return; + } + + this.wechatImageLoading = true; + this.uploadProgress = 0; + this.addWechatLog('开始选择微信图片...', 'info'); + + try { + // 计算剩余可选择的图片数量 + const remainingCount = 5 - this.attachmentList.length; + const selectCount = Math.min(remainingCount, 9); + + this.addWechatLog(`可选择图片数量: ${selectCount}`, 'info'); + + const result = await this.wechatSDK.chooseImage({ + count: selectCount, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + }); + + this.addWechatLog(`成功选择了 ${result.localIds.length} 张图片`, 'success'); + + // 处理选中的图片 + if (result.localIds && result.localIds.length > 0) { + await this.processWechatImages(result.localIds); + } + } catch (error) { + console.error('微信选择图片失败:', error); + this.addWechatLog(`选择图片失败: ${error.message}`, 'error'); + + uni.showToast({ + title: '选择图片失败', + icon: 'none', + }); + } finally { + this.wechatImageLoading = false; + this.uploadProgress = 0; + } + }, + + // 上传微信图片到OSS(参考stuMsgDialog.vue的实现) + async uploadWechatImageToOSS(localId) { + try { + this.addWechatLog('🚀 开始上传微信图片到OSS...', 'info'); + this.uploadProgress = 10; + + // 步骤1:上传到微信服务器(20%进度) + this.addWechatLog('📤 正在上传到微信服务器...', 'info'); + const wxUploadRes = await this.wechatSDK.uploadImage(localId); + + if (!wxUploadRes.serverId) { + throw new Error('微信服务器上传失败,未获取到serverId'); + } + + const mediaId = wxUploadRes.serverId; + this.uploadProgress = 30; + this.addWechatLog(`✅ 微信服务器上传成功,mediaId: ${mediaId}`, 'success'); + + // 步骤2:保存到OSS(80%进度) + this.uploadProgress = 50; + this.addWechatLog('💾 正在保存到云端存储...', 'info'); + + const ossResponse = await request({ + url: '/system/wechat/downloadToOss', + method: 'GET', + params: { + mediaId: mediaId, + }, + timeout: 30000, // 30秒超时 + }); + + if (ossResponse.code === 200) { + const ossResult = ossResponse.data; + this.uploadProgress = 90; + + this.addWechatLog( + '🎉 图片保存成功!', + 'success', + `文件名: ${ossResult.fileName}\nOSS ID: ${ossResult.ossId}`, + ); + + this.uploadProgress = 100; + return { + ossId: ossResult.ossId, + url: ossResult.url, + fileName: ossResult.fileName, + fileSize: ossResult.fileSize || 0, + }; + } else { + throw new Error(ossResponse.msg || '保存图片到云端存储失败'); + } + } catch (error) { + console.error('上传微信图片失败:', error); + this.addWechatLog(`❌ 图片上传失败: ${error.message}`, 'error'); + + // 上传失败时重置进度 + this.uploadProgress = 0; + + // 显示详细错误信息 + let errorMsg = '图片上传失败,请重试'; + if (error.message.includes('timeout')) { + errorMsg = '上传超时,请检查网络连接后重试'; + } else if ( + error.message.includes('mediaId') || + error.message.includes('serverId') + ) { + errorMsg = '微信服务器上传失败,请重试'; + } else if (error.message.includes('OSS')) { + errorMsg = '保存到云端存储失败,请重试'; + } + + uni.showToast({ + title: errorMsg, + icon: 'none', + duration: 2000, + }); + + throw error; + } + }, + + // 处理微信选中的图片(支持多文件上传) + async processWechatImages(localIds) { + this.addWechatLog(`开始处理 ${localIds.length} 张微信图片...`, 'info'); + + // 检查文件数量限制 + if (this.attachmentList.length + localIds.length > 15) { + const canUpload = 15 - this.attachmentList.length; + if (canUpload <= 0) { + uni.showToast({ + title: '最多只能上传15个文件', + icon: 'none', + }); + return; + } + + // 只处理允许上传的数量 + localIds = localIds.slice(0, canUpload); + uni.showToast({ + title: `已选择前${canUpload}张图片`, + icon: 'none', + duration: 2000, + }); + } + + const totalFiles = localIds.length; + let successCount = 0; + let failureCount = 0; + + for (let i = 0; i < totalFiles; i++) { + const localId = localIds[i]; + + // 计算总体进度 + const overallProgress = Math.round((i / totalFiles) * 100); + this.addWechatLog( + `正在处理第 ${i + 1}/${totalFiles} 张图片... (${overallProgress}%)`, + 'info', + ); + + try { + // 创建文件对象并添加到列表(显示上传中状态) + const file = { + uid: `wechat_${Date.now()}_${i}`, + name: `微信图片_${Date.now()}_${i + 1}.jpg`, + url: localId, + localId: localId, + status: 'uploading', + type: 'image/jpeg', + size: 0, // 微信API无法直接获取文件大小 + source: 'wechat', + progress: 0, + }; + + // 添加到附件列表(立即显示) + this.attachmentList.push(file); + + // 上传到OSS + const uploadResult = await this.uploadWechatImageToOSS(localId); + + // 更新文件信息 + file.status = 'success'; + file.ossId = uploadResult.ossId; + file.url = uploadResult.url; + file.name = uploadResult.fileName || file.name; + file.size = uploadResult.fileSize || 0; + file.progress = 100; + + successCount++; + this.addWechatLog(`✅ 第 ${i + 1} 张图片上传成功: ${file.name}`, 'success'); + } catch (error) { + console.error(`处理微信图片失败:`, error); + + // 更新失败的文件状态 + const failedFile = this.attachmentList.find((f) => f.localId === localId); + if (failedFile) { + failedFile.status = 'error'; + failedFile.progress = 0; + } + + failureCount++; + this.addWechatLog(`❌ 第 ${i + 1} 张图片上传失败: ${error.message}`, 'error'); + } + + // 重置单个文件的进度条 + this.uploadProgress = 0; + } + + // 显示最终结果 + const resultMessage = `上传完成: 成功${successCount}个,失败${failureCount}个`; + this.addWechatLog(resultMessage, successCount > 0 ? 'success' : 'warning'); + + if (successCount > 0) { + uni.showToast({ + title: `成功上传${successCount}个文件`, + icon: 'success', + duration: 2000, + }); + } + + if (failureCount > 0) { + uni.showModal({ + title: '部分文件上传失败', + content: `${failureCount}个文件上传失败,请检查网络后重试`, + showCancel: false, + confirmText: '知道了', + }); + } + }, + + // 打开地图查看位置 + async openLocationInMap() { + if (!this.geoLatitude || !this.geoLongitude) { + uni.showToast({ + title: '暂无位置信息', + icon: 'none', + }); + return; + } + + if (this.isWechatBrowser && this.wechatSDK.isInitialized) { + try { + this.addWechatLog('使用微信打开地图...', 'info'); + await this.wechatSDK.openLocation({ + latitude: this.geoLatitude, + longitude: this.geoLongitude, + name: '请假地点', + address: this.form.leaveLocation || '当前位置', + scale: 16, + }); + this.addWechatLog('微信地图打开成功', 'success'); + } catch (error) { + this.addWechatLog(`微信地图打开失败: ${error.message}`, 'error'); + this.fallbackOpenMap(); + } + } else { + this.fallbackOpenMap(); + } + }, + + // 备用地图打开方式 + fallbackOpenMap() { + const url = `https://apis.map.qq.com/uri/v1/marker?marker=coord:${this.geoLatitude},${ + this.geoLongitude + };title:请假地点;addr:${encodeURIComponent( + this.form.leaveLocation || '当前位置', + )}&referer=智慧校园`; + + // #ifdef H5 + window.open(url, '_blank'); + // #endif + + // #ifndef H5 + uni.showModal({ + title: '查看地图', + content: '是否打开外部地图应用查看位置?', + success: (res) => { + if (res.confirm) { + // 在非H5环境中,可以尝试使用系统的地图应用 + plus.runtime.openURL(url); + } + }, + }); + // #endif + }, + // 初始化页面数据 async initPageData() { try { @@ -804,9 +1404,20 @@ export default { console.log(`图片URL处理: ${item.url} -> ${processedUrl}`); } + // 构造完整的文件名(包含扩展名) + 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: item.originalName || `附件${index + 1}`, + name: fullFileName, url: processedUrl, ossId: item.ossId, status: 'success', @@ -815,7 +1426,7 @@ export default { size: item.fileSize || 0, // 为uview2 upload组件添加必要的属性 file: { - name: item.originalName || `附件${index + 1}`, + name: fullFileName, type: fileType, size: item.fileSize || 0, path: processedUrl, @@ -826,6 +1437,7 @@ export default { console.log(`附件 ${index + 1} 处理结果:`, { 原始文件名: item.originalName, 文件后缀: item.fileSuffix, + 完整文件名: fullFileName, 文件类型: fileType, 是否为图片: isImage, 图片URL: item.url, @@ -843,17 +1455,9 @@ export default { // 为图片文件获取blob数据以确保正确显示 await this.processImageFiles(); - // 强制刷新upload组件显示 + // 强制刷新组件显示 this.$nextTick(() => { - console.log('强制刷新upload组件显示'); - if (this.$refs.uploadRef) { - // 触发upload组件的重新渲染 - this.$refs.uploadRef.$forceUpdate(); - } - - // 调试:检查图片文件的处理情况 - this.debugImageFiles(); - + console.log('强制刷新图片组件显示'); // 预加载图片以确保能正确显示 this.preloadImages(); }); @@ -1179,85 +1783,159 @@ export default { }); }, - // 智能定位:根据环境自动选择最适合的定位方式 + // 智能定位:根据环境自动选择最适合的定位方式(优化版) async handleGetLocation() { try { uni.showLoading({ title: '定位中...', mask: true }); + this.addWechatLog('开始智能定位...', 'info'); - console.log('=== 开始智能定位 ==='); - console.log('当前环境检测结果:', { - isWeChat: this.isWeChat(), + const envInfo = { + isWechatBrowser: this.isWechatBrowser, + isWechatSdkReady: this.wechatSDK.isInitialized, isH5Web: this.isH5Web(), isBrowserGeolocationSupported: this.isBrowserGeolocationSupported(), - }); + }; - // 优先级1:微信端定位(如果在微信环境中) - if (this.isWeChat()) { - console.log('检测到微信环境,尝试微信JSSDK定位...'); - const wxLocationSuccess = await this.tryWeChatLocation(); + this.addWechatLog('当前环境检测结果', 'info', JSON.stringify(envInfo, null, 2)); + + // 优先级1:微信SDK定位(如果在微信环境且SDK已就绪) + if (this.isWechatBrowser && this.wechatSDK.isInitialized) { + this.addWechatLog('尝试微信SDK定位...', 'info'); + const wxLocationSuccess = await this.tryWeChatLocationWithSDK(); if (wxLocationSuccess) { - console.log('微信定位成功'); + this.addWechatLog('微信SDK定位成功', 'success'); return; } - console.log('微信定位失败,尝试下一种方式...'); + this.addWechatLog('微信SDK定位失败,尝试下一种方式...', 'warning'); } - // 优先级2:H5 web端原生定位(如果在H5环境且浏览器支持) + // 优先级2:微信环境但SDK未就绪,尝试初始化后重试 + if (this.isWechatBrowser && !this.wechatSDK.isInitialized) { + this.addWechatLog('微信SDK未初始化,尝试快速初始化...', 'warning'); + try { + await this.initWechatSDK(); + if (this.wechatSDK.isInitialized) { + const wxLocationSuccess = await this.tryWeChatLocationWithSDK(); + if (wxLocationSuccess) { + this.addWechatLog('微信SDK初始化并定位成功', 'success'); + return; + } + } + } catch (error) { + this.addWechatLog(`微信SDK初始化失败: ${error.message}`, 'error'); + } + } + + // 优先级3:H5 web端原生定位 if (this.isH5Web() && this.isBrowserGeolocationSupported()) { - console.log('检测到H5环境且支持原生定位,尝试浏览器原生定位...'); + this.addWechatLog('尝试H5浏览器原生定位...', 'info'); const h5LocationSuccess = await this.handleH5WebLocation(); if (h5LocationSuccess) { - console.log('H5原生定位成功'); + this.addWechatLog('H5原生定位成功', 'success'); return; } - console.log('H5原生定位失败,尝试下一种方式...'); + this.addWechatLog('H5原生定位失败,尝试最后的备用方案...', 'warning'); } - // 优先级3:uni-app通用定位(最后的回退方案) - console.log('尝试uni-app通用定位...'); + // 优先级4:uni-app通用定位(最后的回退方案) + this.addWechatLog('尝试uni-app通用定位...', 'info'); await this.handleFallbackLocation(); } catch (error) { console.error('智能定位流程出错:', error); + this.addWechatLog(`智能定位流程异常: ${error.message}`, 'error'); + uni.hideLoading(); - uni.showToast({ - title: '定位功能暂时不可用', - icon: 'none', - duration: 2000, + uni.showModal({ + title: '定位失败', + content: '定位功能暂时不可用,您可以手动输入位置信息。是否查看详细错误信息?', + showCancel: true, + cancelText: '取消', + confirmText: '查看详情', + success: (res) => { + if (res.confirm && this.isWechatBrowser) { + this.debugMode = true; // 自动开启调试模式以查看详情 + } + }, }); } }, - // 尝试微信定位(独立方法,便于错误处理) - async tryWeChatLocation() { + // 使用微信SDK进行定位(新版本,集成调试日志) + async tryWeChatLocationWithSDK() { try { - const wxInitSuccess = await this.initWeChatJssdk(); - if (wxInitSuccess && typeof wx !== 'undefined') { - return new Promise((resolve) => { - wx.getLocation({ - type: 'gcj02', - success: (res) => { - console.log('微信定位成功:', res); - this.geoLatitude = res.latitude; - this.geoLongitude = res.longitude; - this.form.leaveLocation = `${res.latitude},${res.longitude}`; - uni.hideLoading(); - uni.showToast({ title: '微信定位成功', icon: 'success' }); - resolve(true); - }, - fail: (error) => { - console.error('微信定位失败:', error); - resolve(false); - }, - }); + this.addWechatLog('开始微信SDK定位...', 'info'); + + const result = await this.wechatSDK.getLocation({ + type: 'gcj02', // 火星坐标系,适合国内地图 + }); + + this.addWechatLog( + '微信定位API调用成功', + 'success', + JSON.stringify(result, null, 2), + ); + + if (result.latitude && result.longitude) { + this.geoLatitude = parseFloat(result.latitude); + this.geoLongitude = parseFloat(result.longitude); + + // 格式化位置信息 + const locationStr = `${this.geoLatitude.toFixed(6)},${this.geoLongitude.toFixed( + 6, + )}`; + this.form.leaveLocation = locationStr; + + this.addWechatLog( + `定位成功 - 纬度: ${this.geoLatitude}, 经度: ${this.geoLongitude}`, + 'success', + ); + this.addWechatLog(`位置精度: ${result.accuracy || '未知'}米`, 'info'); + + uni.hideLoading(); + uni.showToast({ + title: '微信定位成功', + icon: 'success', + duration: 2000, }); + + return true; + } else { + this.addWechatLog( + '微信定位返回数据异常', + 'error', + JSON.stringify(result, null, 2), + ); + return false; } - return false; } catch (error) { - console.error('微信JSSDK初始化失败:', error); + console.error('微信SDK定位失败:', error); + this.addWechatLog(`微信SDK定位失败: ${error.message}`, 'error'); + + // 根据错误类型提供更友好的提示 + if (error.message && error.message.includes('denied')) { + this.addWechatLog('用户拒绝了位置权限请求', 'warning'); + uni.showModal({ + title: '定位权限', + content: '需要获取位置信息来记录请假地点,请在微信中允许位置权限。', + showCancel: false, + confirmText: '知道了', + }); + } else if (error.message && error.message.includes('unavailable')) { + this.addWechatLog('位置服务不可用,可能是设备问题', 'warning'); + } else if (error.message && error.message.includes('timeout')) { + this.addWechatLog('定位超时,网络或信号问题', 'warning'); + } + return false; } }, + // 保留原有方法作为备用(标记为废弃) + async tryWeChatLocation() { + console.warn('tryWeChatLocation 方法已废弃,请使用 tryWeChatLocationWithSDK 方法'); + return this.tryWeChatLocationWithSDK(); + }, + // 一键定位:优先使用微信JSSDK,失败回退到uni.getLocation async handleGetWeChatLocation() { console.warn('handleGetWeChatLocation 方法已废弃,请使用 handleGetLocation 方法'); @@ -1539,6 +2217,229 @@ export default { return /\.(jpg|jpeg|png|gif|bmp|webp)$/i.test(fileName); }, + // === 新增图片处理方法 === + + // 图片预览 + handleImagePreview(image, index) { + console.log('图片预览:', image); + + if (image.status !== 'success') { + uni.showToast({ + title: '图片尚未上传完成', + icon: 'none', + }); + return; + } + + // 获取所有成功上传的图片URL + const imageUrls = this.imageFiles + .filter((file) => file.status === 'success') + .map((file) => file.blobUrl || file.url); + + if (imageUrls.length === 0) { + uni.showToast({ + title: '暂无可预览的图片', + icon: 'none', + }); + return; + } + + // 找到当前图片在成功列表中的索引 + const currentUrl = image.blobUrl || image.url; + const currentIndex = imageUrls.findIndex((url) => url === currentUrl); + + uni.previewImage({ + urls: imageUrls, + current: currentIndex >= 0 ? currentIndex : 0, + success: () => { + console.log('图片预览成功'); + }, + fail: (error) => { + console.error('图片预览失败:', error); + uni.showToast({ + title: '预览失败', + icon: 'none', + }); + }, + }); + }, + + // 图片下载 + async handleImageDownload(image) { + console.log('图片下载:', image); + + if (image.status !== 'success') { + uni.showToast({ + title: '图片尚未上传完成', + icon: 'none', + }); + return; + } + + uni.showLoading({ + title: '准备下载...', + mask: true, + }); + + try { + // 如果有ossId,通过API下载 + if (image.ossId) { + const downloadUrl = `/api/resource/oss/download/${image.ossId}`; + + // 在H5环境中,直接打开下载链接 + // #ifdef H5 + const link = document.createElement('a'); + link.href = downloadUrl; + link.download = image.name || 'image.jpg'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + uni.hideLoading(); + uni.showToast({ + title: '下载已开始', + icon: 'success', + }); + // #endif + + // #ifndef H5 + // 在小程序或App中,使用downloadFile + uni.downloadFile({ + url: downloadUrl, + success: (res) => { + if (res.statusCode === 200) { + uni.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + uni.showToast({ + title: '保存成功', + icon: 'success', + }); + }, + fail: () => { + uni.showToast({ + title: '保存失败', + icon: 'none', + }); + }, + }); + } + }, + fail: (error) => { + console.error('下载失败:', error); + uni.showToast({ + title: '下载失败', + icon: 'none', + }); + }, + complete: () => { + uni.hideLoading(); + }, + }); + // #endif + } else { + // 没有ossId的情况,直接使用图片URL + const imageUrl = image.blobUrl || image.url; + if (imageUrl) { + // #ifdef H5 + const link = document.createElement('a'); + link.href = imageUrl; + link.download = image.name || 'image.jpg'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + uni.hideLoading(); + uni.showToast({ + title: '下载已开始', + icon: 'success', + }); + // #endif + + // #ifndef H5 + uni.downloadFile({ + url: imageUrl, + success: (res) => { + if (res.statusCode === 200) { + uni.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + uni.showToast({ + title: '保存成功', + icon: 'success', + }); + }, + fail: () => { + uni.showToast({ + title: '保存失败', + icon: 'none', + }); + }, + }); + } + }, + fail: (error) => { + console.error('下载失败:', error); + uni.showToast({ + title: '下载失败', + icon: 'none', + }); + }, + complete: () => { + uni.hideLoading(); + }, + }); + // #endif + } + } + } catch (error) { + console.error('下载过程出错:', error); + uni.hideLoading(); + uni.showToast({ + title: '下载失败', + icon: 'none', + }); + } + }, + + // 图片删除 + handleImageDelete(image, index) { + console.log('删除图片:', image, index); + + 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), + ); + + if (attachmentIndex >= 0) { + this.attachmentList.splice(attachmentIndex, 1); + } + + // 清理blob URL,防止内存泄漏 + if (image.blobUrl && image.blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(image.blobUrl); + } + + uni.showToast({ + title: '删除成功', + icon: 'success', + }); + + console.log('图片删除完成,当前附件数量:', this.attachmentList.length); + } + }, + }); + }, + // 获取文件扩展名 getFileExt(fileName) { if (!fileName) return 'FILE'; @@ -1567,19 +2468,145 @@ export default { return name.substring(0, 8) + '...' + ext; }, - // 文档上传方法 - handleDocUpload() { - console.log('=== 文档上传方法 ==='); + // 格式化文件大小 + formatFileSize(bytes) { + if (!bytes || bytes === 0) return '未知大小'; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + + if (i === 0) return bytes + ' ' + sizes[i]; + return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]; + }, + + // 重试上传文件 + async retryUploadFile(file) { + console.log('=== 重试上传文件 ===', file); + + // 重置文件状态 + file.status = 'uploading'; + file.progress = 0; + + try { + // 根据文件来源选择不同的上传方式 + if (file.source === 'wechat' && file.localId) { + // 微信图片重新上传 + const uploadResult = await this.uploadWechatImageToOSS(file.localId); + + // 更新文件信息 + file.status = 'success'; + file.ossId = uploadResult.ossId; + file.url = uploadResult.url; + file.name = uploadResult.fileName || file.name; + file.size = uploadResult.fileSize || 0; + file.progress = 100; + } else { + // 普通文件重新上传 + await this.uploadFileToServer(file); + } + + uni.showToast({ + title: '重新上传成功', + icon: 'success', + duration: 1500, + }); + } catch (error) { + console.error('重试上传失败:', error); + file.status = 'error'; + file.progress = 0; + + uni.showToast({ + title: '重试失败,请稍后再试', + icon: 'none', + duration: 2000, + }); + } + }, + + // 统一附件上传方法(支持图片和文档) + 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.chooseImages(); + break; + case 1: + this.chooseDocuments(); + break; + case 2: + if (this.isWechatBrowser) { + this.chooseWechatImage(); + } else { + uni.showToast({ + title: '请在微信中使用', + icon: 'none', + }); + } + break; + } + }, + fail: () => { + console.log('用户取消选择'); + }, + }); + }, + + // 选择图片文件 + chooseImages() { + uni.chooseImage({ + count: Math.min(9, 15 - this.attachmentList.length), + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + success: (res) => { + console.log('选择图片成功:', res); + const files = res.tempFilePaths.map((path, index) => { + const tempFile = res.tempFiles?.[index]; + return { + name: path.split('/').pop() || `image_${Date.now()}_${index}.jpg`, + path: path, + url: path, + tempFilePath: path, + size: tempFile?.size || 0, + type: tempFile?.type || 'image/jpeg', + }; + }); + + this.handleFileUpload({ tempFiles: files }); + }, + fail: (err) => { + console.error('选择图片失败:', err); + uni.showToast({ + title: '选择图片失败', + icon: 'none', + }); + }, + }); + }, + + // 选择文档文件 + chooseDocuments() { // 使用uni.chooseFile选择文档 if (typeof uni.chooseFile === 'function') { uni.chooseFile({ - count: 5, + count: Math.min(5, 15 - this.attachmentList.length), extension: ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.txt', '.zip', '.rar'], success: (res) => { console.log('选择文档成功:', res); if (res.tempFiles && res.tempFiles.length > 0) { - // 手动触发文件上传 this.handleFileUpload({ tempFiles: res.tempFiles, }); @@ -1599,95 +2626,13 @@ export default { } }, - // 测试uview2 upload组件事件(开发调试用) - testUview2Upload() { - console.log('=== 测试uview2 upload组件事件 ==='); - - // 模拟uview2 upload组件的after-read事件参数 - const mockEvent = { - file: { - url: '/static/logo.png', - size: 1024, - name: 'test-image.png', - type: 'image/png', - }, - }; - - console.log('模拟的uview2事件参数:', mockEvent); - this.handleFileUpload(mockEvent); - }, - - // 直接选择文件(调试用) - handleDirectChooseFile() { - console.log('=== 直接选择文件 ==='); - uni.chooseFile({ - count: 5, - extension: [ - '.jpg', - '.jpeg', - '.png', - '.gif', - '.pdf', - '.doc', - '.docx', - '.xls', - '.xlsx', - '.txt', - ], - success: (res) => { - console.log('选择文件成功:', res); - if (res.tempFiles && res.tempFiles.length > 0) { - // 检查文件大小 - const validFiles = res.tempFiles.filter((file) => { - if (!this.checkFileSize(file)) { - return false; - } - return true; - }); - - if (validFiles.length > 0) { - // 手动触发文件上传 - this.handleFileUpload({ - tempFiles: validFiles, - }); - } else { - console.log('所有文件都超过大小限制,取消上传'); - } - } - }, - fail: (err) => { - console.error('选择文件失败:', err); - uni.showToast({ - title: '选择文件失败', - icon: 'none', - }); - }, - }); - }, - - // 测试上传逻辑 - handleTestUpload() { - console.log('=== 测试上传逻辑 ==='); - const testFile = { - name: 'test.txt', - size: 1024, - type: 'text/plain', - url: '/static/logo.png', // 使用一个存在的静态文件测试 - path: '/static/logo.png', - }; - - this.handleFileUpload({ - tempFiles: [testFile], - }); - }, - // 备用上传方法(用户可见) handleBackupUpload() { console.log('=== 备用上传方法 ==='); // 先检查平台支持情况 uni.showActionSheet({ - itemList: ['选择图片', '选择视频', '选择文档', '测试上传(开发用)'], + itemList: ['选择图片', '选择视频', '选择文档'], success: (res) => { if (res.tapIndex === 0) { // 选择图片 @@ -1769,160 +2714,16 @@ export default { }, }); } else if (res.tapIndex === 2) { - // 选择文档(优先使用uni.chooseFile) - if (uni.chooseFile) { - this.handleDirectChooseFile(); - } else { - uni.showToast({ - title: '当前平台不支持文档选择', - icon: 'none', - }); - } - } else if (res.tapIndex === 3) { - // 测试上传功能(开发调试用) - this.testDirectUpload(); + // 选择文档 + this.handleDocUpload(); } }, }); }, - // 测试直接上传(开发调试用) - async testDirectUpload() { - console.log('=== 测试直接上传功能 ==='); - - try { - // 使用 uni.uploadFile 直接测试上传 - const token = uni.getStorageSync('access_token'); - const clientid = uni.getStorageSync('clientid') || REQUEST_CONFIG.clientId; - - // 先选择一个文件用于测试 - uni.chooseImage({ - count: 1, - success: (res) => { - console.log('测试选择图片结果:', res); - const filePath = res.tempFilePaths[0]; - console.log('测试文件路径:', filePath); - - uni.showLoading({ title: '测试上传中...' }); - - uni.uploadFile({ - url: `${REQUEST_CONFIG.serverUrl}/resource/oss/upload`, - filePath: filePath, - name: 'file', - formData: { - bizType: 'student_leave_attachment', - }, - header: { - authorization: `Bearer ${token}`, - clientid: clientid, - }, - success: (uploadRes) => { - uni.hideLoading(); - console.log('测试上传成功:', uploadRes); - try { - const data = JSON.parse(uploadRes.data); - console.log('解析后的响应数据:', data); - if (data.code === 200) { - uni.showToast({ - title: '测试上传成功', - icon: 'success', - }); - } else { - uni.showToast({ - title: `测试失败: ${data.msg}`, - icon: 'none', - }); - } - } catch (e) { - console.error('解析响应失败:', e); - uni.showToast({ - title: '解析响应失败', - icon: 'none', - }); - } - }, - fail: (err) => { - uni.hideLoading(); - console.error('测试上传失败:', err); - uni.showToast({ - title: `测试上传失败: ${err.errMsg}`, - icon: 'none', - }); - }, - }); - }, - fail: (err) => { - console.error('测试选择图片失败:', err); - uni.showToast({ - title: '选择图片失败', - icon: 'none', - }); - }, - }); - } catch (error) { - console.error('测试上传出错:', error); - uni.showToast({ - title: '测试上传出错', - icon: 'none', - }); - } - }, - - // 文件选择完成事件(用于调试) - handleChooseComplete(event) { - console.log('=== 文件选择完成事件 ===', event); - console.log('选择的文件列表:', event.tempFilePaths || event.tempFiles); - - // 显示选择成功提示 - uni.showToast({ - title: `已选择 ${ - event.tempFilePaths?.length || event.tempFiles?.length || 0 - } 个文件`, - icon: 'success', - }); - }, - // 文件上传 handleFileUpload(event) { - console.log('=== u-upload组件after-read事件触发 ==='); - console.log('🎯 事件对象完整信息:', JSON.stringify(event, null, 2)); - console.log('🎯 事件类型:', typeof event); - console.log('🎯 事件属性列表:', Object.keys(event)); - - // 增强调试:检查事件结构 - if (event.file) { - console.log('✅ 检测到event.file:', event.file); - console.log('📁 file类型:', typeof event.file); - console.log('📁 file是否为数组:', Array.isArray(event.file)); - - if (Array.isArray(event.file)) { - console.log('📁 文件数组长度:', event.file.length); - event.file.forEach((file, index) => { - console.log(`📁 文件${index + 1}:`, { - name: file.name, - url: file.url, - path: file.path, - tempFilePath: file.tempFilePath, - size: file.size, - type: file.type, - }); - }); - } else { - console.log('📁 单个文件对象:', { - name: event.file.name, - url: event.file.url, - path: event.file.path, - tempFilePath: event.file.tempFilePath, - size: event.file.size, - type: event.file.type, - }); - } - } else { - console.warn('⚠️ event.file不存在,可能的原因:'); - console.warn('1. u-upload组件版本问题'); - console.warn('2. 事件参数结构改变'); - console.warn('3. 组件配置问题'); - } + console.log('文件上传事件触发:', event); // 显示用户提示 uni.showToast({ @@ -1931,24 +2732,6 @@ export default { duration: 2000, }); - console.log('=== 开始处理文件上传 ===', event); - console.log('事件类型:', typeof event); - console.log('事件属性:', Object.keys(event)); - - // 详细分析uview2 upload组件的事件结构 - if (event.file) { - console.log('检测到event.file:', event.file); - console.log('event.file类型:', typeof event.file); - console.log('event.file是否为数组:', Array.isArray(event.file)); - if (Array.isArray(event.file)) { - console.log('event.file数组长度:', event.file.length); - console.log('event.file第一个元素:', event.file[0]); - } else { - console.log('event.file单个对象:', event.file); - console.log('event.file属性:', Object.keys(event.file)); - } - } - // 兼容不同的事件格式 let files = []; @@ -1996,10 +2779,10 @@ export default { } // 检查文件数量限制 - if (this.attachmentList.length + files.length > 5) { + if (this.attachmentList.length + files.length > 15) { console.warn('⚠️ 文件数量超过限制'); uni.showToast({ - title: '最多只能上传5个文件', + title: '最多只能上传15个文件', icon: 'none', }); return; @@ -2095,25 +2878,6 @@ export default { }); }, - // 图片过大处理(uview2 upload组件回调) - handleImageOversize(event) { - console.log('=== 图片过大事件 ===', event); - console.log('文件大小信息:', event); - - // 显示更详细的错误信息 - let errorMessage = '图片大小不能超过10MB'; - if (event.size) { - const fileSizeMB = (event.size / (1024 * 1024)).toFixed(2); - errorMessage = `文件大小 ${fileSizeMB}MB,超过10MB限制`; - } - - uni.showToast({ - title: errorMessage, - icon: 'none', - duration: 3000, - }); - }, - // 检查文件大小是否超过限制 checkFileSize(file) { const maxSize = 10 * 1024 * 1024; // 10MB @@ -2136,64 +2900,6 @@ export default { return true; }, - // 测试审批记录弹窗(开发调试用) - testApprovalRecord() { - console.log('=== 测试审批记录弹窗 ==='); - console.log('直接调用审批记录组件'); - console.log('approvalRecordRef是否存在:', !!this.$refs.approvalRecordRef); - - // 使用测试ID直接调用 - const testId = this.form.id || '123'; - console.log('使用测试ID:', testId); - - if (this.$refs.approvalRecordRef) { - this.$refs.approvalRecordRef.init(testId); - } else { - console.error('审批记录组件引用不存在'); - uni.showToast({ - title: '组件引用不存在', - icon: 'none', - }); - } - }, - - // 测试文件大小检查功能 - testFileSizeCheck() { - console.log('=== 测试文件大小检查功能 ==='); - - const testFiles = [ - { name: '小文件.jpg', size: 1024 * 1024 }, // 1MB - { name: '中等文件.jpg', size: 5 * 1024 * 1024 }, // 5MB - { name: '大文件.jpg', size: 15 * 1024 * 1024 }, // 15MB - { name: '超大文件.jpg', size: 50 * 1024 * 1024 }, // 50MB - ]; - - testFiles.forEach((file, index) => { - console.log(`测试文件 ${index + 1}:`, file.name); - const isValid = this.checkFileSize(file); - console.log(`文件 ${file.name} 检查结果:`, isValid ? '通过' : '拒绝'); - }); - }, - - // 调试图片文件处理情况 - debugImageFiles() { - console.log('=== 调试图片文件处理情况 ==='); - console.log('attachmentList:', this.attachmentList); - console.log('imageFiles:', this.imageFiles); - console.log('uploadFileList:', this.uploadFileList); - - // 检查每个图片文件的详细信息 - this.imageFiles.forEach((file, index) => { - console.log(`图片文件 ${index + 1}:`, { - 文件名: file.name, - 文件类型: file.type, - 文件URL: file.url, - 是否为图片: this.isImageFile(file), - 文件对象: file, - }); - }); - }, - // 预加载图片以确保能正确显示 async preloadImages() { console.log('=== 预加载图片 ==='); @@ -2305,22 +3011,6 @@ export default { }); }, - // 图片文件删除(uview2 upload组件回调) - handleImageFileDelete(event) { - console.log('=== 图片文件删除事件 ===', event); - const { index } = event; - const imageFiles = this.attachmentList.filter((file) => this.isImageFile(file)); - const fileToDelete = imageFiles[index]; - if (fileToDelete) { - const actualIndex = this.attachmentList.findIndex( - (file) => file.uid === fileToDelete.uid, - ); - if (actualIndex > -1) { - this.attachmentList.splice(actualIndex, 1); - } - } - }, - // 文件删除(通用方法,保留作为备用) handleFileDelete(event) { console.log('=== 文件删除事件 ===', event); @@ -2359,10 +3049,23 @@ export default { } }, - // 文件下载 + // 文件下载(微信小程序专用版本) async handleDownloadFile(file) { - console.log('=== 下载文件 ===', file); + console.log('=== 开始下载文件 ==='); + console.log('文件信息:', JSON.stringify(file, null, 2)); + console.log('当前时间:', new Date().toISOString()); + + // 检查环境信息 + const systemInfo = uni.getSystemInfoSync(); + console.log('运行环境信息:', { + platform: systemInfo.platform, + version: systemInfo.version, + model: systemInfo.model, + brand: systemInfo.brand, + }); + if (!file.ossId) { + console.warn('❌ 下载失败:文件ossId为空', file); uni.showToast({ title: '文件未上传完成', icon: 'none', @@ -2371,64 +3074,336 @@ export default { } try { + console.log('📤 开始显示下载Loading...'); uni.showLoading({ - title: '正在下载...', + title: `正在下载${file.name}...`, + mask: true, }); - const response = await request({ - url: `/resource/oss/download/${file.ossId}`, - method: 'GET', - responseType: 'arraybuffer', - }); + // 构建下载URL + const downloadUrl = `/resource/oss/download/${file.ossId}`; + console.log('下载URL:', downloadUrl); - console.log('下载响应:', response); + // 根据文件类型选择处理方式 + console.log('🔍 检查文件类型:', file.name, '是否为图片:', this.isImageFile(file)); - // 将arraybuffer转换为base64 - const bytes = new Uint8Array(response); - let binary = ''; - bytes.forEach((byte) => (binary += String.fromCharCode(byte))); - const base64 = btoa(binary); + if (this.isImageFile(file)) { + // 图片文件:下载并保存到相册 + console.log('🖼️ 图片文件,下载并保存到相册'); + await this.downloadAndSaveImage(downloadUrl, file.name); + } else { + // 非图片文件:下载并直接打开预览 + console.log('📄 非图片文件,下载并打开预览'); + await this.downloadAndOpenDocument(downloadUrl, file.name); + } + } catch (error) { + console.error('❌ 下载过程发生异常:', error); + uni.hideLoading(); - // 保存文件到本地 - const fs = uni.getFileSystemManager(); - const fileName = file.name || `attachment_${Date.now()}`; - const filePath = `${uni.env.USER_DATA_PATH}/${fileName}`; + let errorMsg = '下载失败,请重试'; + if (error.message.includes('timeout')) { + errorMsg = '下载超时,请检查网络后重试'; + } else if (error.message.includes('network')) { + errorMsg = '网络错误,请检查网络连接'; + } else if (error.message.includes('404')) { + errorMsg = '文件不存在,请联系管理员'; + } else if (error.message.includes('500')) { + errorMsg = '服务器错误,请稍后重试'; + } - fs.writeFile({ - filePath: filePath, - data: base64, - encoding: 'base64', - success: () => { - uni.hideLoading(); - uni.showToast({ - title: '下载完成', - icon: 'success', - }); - // 可以选择打开文件 - uni.openDocument({ - filePath: filePath, - success: () => { - console.log('打开文件成功'); - }, - fail: (err) => { - console.error('打开文件失败:', err); - }, - }); + uni.showModal({ + title: '下载失败', + content: errorMsg, + showCancel: true, + cancelText: '取消', + confirmText: '重试', + success: (res) => { + if (res.confirm) { + this.handleDownloadFile(file); + } }, - fail: (err) => { - console.error('保存文件失败:', err); + }); + } + + console.log('=== handleDownloadFile方法执行完成 ==='); + }, + + // 下载图片并保存到相册 + async downloadAndSaveImage(downloadUrl, fileName) { + return new Promise((resolve, reject) => { + // #ifdef MP-WEIXIN + // 微信小程序:使用downloadFile下载到临时文件,然后保存到相册 + uni.downloadFile({ + url: this.getFullApiUrl(downloadUrl), + header: this.getRequestHeaders(), + success: (res) => { + console.log('✅ 图片下载成功:', res); + if (res.statusCode === 200) { + // 保存到相册 + uni.saveImageToPhotosAlbum({ + filePath: res.tempFilePath, + success: () => { + console.log('✅ 图片保存到相册成功'); + uni.hideLoading(); + uni.showToast({ + title: '图片已保存到相册', + icon: 'success', + duration: 2000, + }); + resolve(); + }, + fail: (err) => { + console.error('❌ 保存图片到相册失败:', err); + uni.hideLoading(); + if (err.errMsg.includes('auth deny')) { + uni.showModal({ + title: '权限提示', + content: + '需要授权访问相册才能保存图片,请在设置中开启权限', + showCancel: false, + }); + } else { + uni.showToast({ + title: '保存图片失败', + icon: 'none', + }); + } + reject(err); + }, + }); + } else { + uni.hideLoading(); + uni.showToast({ + title: '图片下载失败', + icon: 'none', + }); + reject(new Error('下载失败')); + } + }, + fail: (error) => { + console.error('❌ 图片下载失败:', error); uni.hideLoading(); uni.showToast({ - title: '下载失败', + title: '图片下载失败', icon: 'none', }); + reject(error); }, }); - } catch (error) { - console.error('下载文件失败:', error); + // #endif + + // #ifdef H5 + // H5环境:直接使用浏览器下载 + this.downloadFileInBrowser(downloadUrl, fileName); + uni.hideLoading(); + resolve(); + // #endif + }); + }, + + // 下载文档并打开预览 + async downloadAndOpenDocument(downloadUrl, fileName) { + return new Promise((resolve, reject) => { + // #ifdef MP-WEIXIN + // 微信小程序:下载到临时文件并直接打开 + uni.downloadFile({ + url: this.getFullApiUrl(downloadUrl), + header: this.getRequestHeaders(), + success: (res) => { + console.log('✅ 文档下载成功:', res); + if (res.statusCode === 200) { + uni.hideLoading(); + // 直接打开文档预览 + uni.openDocument({ + filePath: res.tempFilePath, + fileType: this.getFileType(fileName), + success: () => { + console.log('✅ 文档打开成功'); + resolve(); + }, + fail: (err) => { + console.error('❌ 文档打开失败:', err); + // 如果无法直接打开,提供其他选项 + uni.showModal({ + title: '无法预览', + content: '该文件格式不支持预览,您可以选择其他操作', + showCancel: true, + cancelText: '取消', + confirmText: '分享文件', + success: (modalRes) => { + if (modalRes.confirm) { + // 可以通过分享功能让用户保存文件 + // #ifdef MP-WEIXIN + wx.shareFileMessage && + wx.shareFileMessage({ + filePath: res.tempFilePath, + fileName: fileName, + }); + // #endif + } + }, + }); + reject(err); + }, + }); + } else { + uni.hideLoading(); + uni.showToast({ + title: '文档下载失败', + icon: 'none', + }); + reject(new Error('下载失败')); + } + }, + fail: (error) => { + console.error('❌ 文档下载失败:', error); + uni.hideLoading(); + uni.showToast({ + title: '文档下载失败', + icon: 'none', + }); + reject(error); + }, + }); + // #endif + + // #ifdef H5 + // H5环境:直接使用浏览器下载 + this.downloadFileInBrowser(downloadUrl, fileName); + uni.hideLoading(); + resolve(); + // #endif + }); + }, + + // 获取完整的API地址 + getFullApiUrl(url) { + if (url.startsWith('http')) { + return url; + } + // 从配置中获取base URL + const baseURL = + process.env.NODE_ENV === 'development' + ? 'http://localhost:8080' + : 'https://your-api-domain.com'; + return baseURL + url; + }, + + // 获取请求头(包含认证信息) + getRequestHeaders() { + const headers = {}; + // 添加认证token + const token = uni.getStorageSync('token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + // 添加其他必要的请求头 + headers['Content-Type'] = 'application/json'; + return headers; + }, + + // 根据文件名获取文件类型 + getFileType(fileName) { + const ext = fileName.split('.').pop().toLowerCase(); + const typeMap = { + pdf: 'pdf', + doc: 'doc', + docx: 'docx', + xls: 'xls', + xlsx: 'xlsx', + ppt: 'ppt', + pptx: 'pptx', + txt: 'txt', + }; + return typeMap[ext] || ''; + }, + + // H5环境下的文件下载 + downloadFileInBrowser(downloadUrl, fileName) { + const link = document.createElement('a'); + link.href = this.getFullApiUrl(downloadUrl); + link.download = fileName; + link.target = '_blank'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }, + + // 预览图片文件 + previewImageFile(file) { + console.log('=== 预览图片文件 ===', file); + + // 收集所有成功上传的图片URL + const successImageFiles = this.attachmentList.filter( + (f) => this.isImageFile(f) && f.status === 'success' && f.url, + ); + + const imageUrls = successImageFiles.map((f) => f.blobUrl || f.url); + const currentIndex = successImageFiles.findIndex((f) => f.uid === file.uid); + + if (imageUrls.length > 0 && currentIndex >= 0) { + uni.previewImage({ + urls: imageUrls, + current: imageUrls[currentIndex], + longPressActions: { + itemList: ['保存到相册'], + success: (res) => { + if (res.tapIndex === 0) { + // 保存图片到相册 + this.saveImageToAlbum(file); + } + }, + }, + }); + } else { + uni.showToast({ + title: '图片加载中,请稍后', + icon: 'none', + }); + } + }, + + // 保存图片到相册 + async saveImageToAlbum(file) { + try { + uni.showLoading({ + title: '正在保存...', + }); + + // 下载图片到临时文件 + const tempFilePath = await new Promise((resolve, reject) => { + uni.downloadFile({ + url: file.url, + success: (res) => { + if (res.statusCode === 200) { + resolve(res.tempFilePath); + } else { + reject(new Error('下载失败')); + } + }, + fail: reject, + }); + }); + + // 保存到相册 + await new Promise((resolve, reject) => { + uni.saveImageToPhotosAlbum({ + filePath: tempFilePath, + success: resolve, + fail: reject, + }); + }); + uni.hideLoading(); uni.showToast({ - title: '下载失败', + title: '已保存到相册', + icon: 'success', + }); + } catch (error) { + console.error('保存图片失败:', error); + uni.hideLoading(); + uni.showToast({ + title: '保存失败', icon: 'none', }); } @@ -2859,398 +3834,36 @@ export default { } }, - // 测试审批记录弹窗(开发调试用) - testApprovalRecord() { - console.log('=== 测试审批记录弹窗 ==='); - console.log('直接调用审批记录组件'); - console.log('approvalRecordRef是否存在:', !!this.$refs.approvalRecordRef); + // // 文件选择前事件 - 应该在用户点击选择图片后首先触发 + // handleBeforeRead(event) { + // console.log('文件选择前事件触发:', event); + // uni.showToast({ + // title: '正在选择文件...', + // icon: 'loading', + // duration: 1000, + // }); + // return true; // 允许继续选择文件 + // }, - // 使用测试ID直接调用 - const testId = this.form.id || '123'; - console.log('使用测试ID:', testId); + // // 预览点击事件 + // handleClickPreview(event) { + // console.log('点击预览事件:', event); + // }, - if (this.$refs.approvalRecordRef) { - this.$refs.approvalRecordRef.init(testId); - } else { - console.error('审批记录组件引用不存在'); - uni.showToast({ - title: '组件引用不存在', - icon: 'none', - }); - } - }, + // // 上传进度事件 + // handleUploadProgress(event) { + // console.log('上传进度事件:', event); + // }, - // 测试上传事件(开发调试用)- 修复微信浏览器兼容性 - testUploadEvent() { - console.log('🧪 === 测试上传事件流程(微信浏览器兼容版本)==='); + // // 上传成功事件 + // handleUploadSuccess(event) { + // console.log('上传成功事件:', event); + // }, - // 检测当前环境 - const isWechat = /micromessenger/i.test(navigator.userAgent || ''); - console.log('📱 是否为微信浏览器:', isWechat); - - // 方法1:在微信浏览器中使用Canvas生成测试文件 - if (isWechat || true) { - // 统一使用Canvas方案,更稳定 - this.createTestFileWithCanvas(); - } else { - // 方法2:非微信环境使用原有的静态文件方案 - this.createMockFileEvent(); - } - }, - - // 使用Canvas创建真实的测试文件(微信浏览器兼容) - createTestFileWithCanvas() { - console.log('🎨 使用Canvas创建测试文件...'); - console.log('📱 当前环境:', { - isWeChat: isWeChat(), - userAgent: - typeof navigator !== 'undefined' - ? navigator.userAgent.substring(0, 50) - : 'unknown', - }); - - try { - // 创建Canvas元素 - const canvas = document.createElement('canvas'); - canvas.width = 300; - canvas.height = 200; - const ctx = canvas.getContext('2d'); - - // 绘制测试图片内容 - 增强版 - ctx.fillStyle = '#4CAF50'; - ctx.fillRect(0, 0, 300, 200); - - ctx.fillStyle = '#FFFFFF'; - ctx.font = '18px Arial'; - ctx.textAlign = 'center'; - ctx.fillText('微信环境测试图片', 150, 60); - ctx.fillText(new Date().toLocaleString(), 150, 90); - - // 添加环境标识 - if (isWeChat()) { - ctx.fillText('微信浏览器', 150, 120); - } else { - ctx.fillText('标准浏览器', 150, 120); - } - - // 添加版本标识 - ctx.font = '12px Arial'; - ctx.fillText(`V${Date.now()}`, 150, 150); - - console.log('✅ Canvas内容绘制完成'); - - // 将Canvas转换为Blob - canvas.toBlob( - (blob) => { - if (blob) { - console.log('✅ Canvas转换为Blob成功:', blob); - console.log('📊 Blob信息:', { - size: blob.size, - type: blob.type, - environment: isWeChat() ? 'WeChat' : 'Standard', - }); - - // 创建临时URL - const tempUrl = URL.createObjectURL(blob); - console.log('🔗 创建临时URL:', tempUrl); - - // 🔧 微信环境增强处理 - const fileName = `wechat-test-${Date.now()}.png`; - - // 模拟真实的文件选择结果 - const mockEvent = { - file: { - url: tempUrl, - size: blob.size, - name: fileName, - type: 'image/png', - path: tempUrl, - tempFilePath: tempUrl, - _blob: blob, // 保存原始blob引用 - 微信环境关键 - // 微信环境标识 - isWeChatTest: isWeChat(), - timestamp: Date.now(), - }, - }; - - console.log('🧪 Canvas生成的模拟事件参数:', mockEvent); - console.log('🧪 开始调用handleFileUpload方法...'); - - // 显示微信环境特定提示 - if (isWeChat()) { - uni.showToast({ - title: '微信环境测试中...', - icon: 'loading', - duration: 2000, - }); - } - - // 调用实际的文件上传处理方法 - this.handleFileUpload(mockEvent); - console.log('✅ Canvas测试调用成功'); - } else { - console.error('❌ Canvas转换为Blob失败'); - uni.showToast({ - title: isWeChat() ? '微信环境Canvas不支持' : 'Canvas转换失败', - icon: 'none', - duration: 3000, - }); - } - }, - 'image/png', - 0.9, // 提高质量 - ); - } catch (error) { - console.error('❌ Canvas测试失败:', error); - - let errorMsg = 'Canvas测试失败'; - if (isWeChat()) { - errorMsg = '微信环境Canvas异常'; - } - - uni.showToast({ - title: errorMsg + ': ' + error.message, - icon: 'none', - duration: 4000, - }); - - // 降级到触发真实选择 - console.log('🔄 降级到真实文件选择...'); - setTimeout(() => { - this.triggerRealFileSelection(); - }, 1000); - } - }, - - // 创建模拟文件事件(原有方案,用于非微信环境) - createMockFileEvent() { - console.log('📁 使用静态文件模拟方案(非微信环境)'); - - const mockEvent = { - file: { - url: '/static/logo.png', - size: 1024 * 50, // 50KB - name: 'test-static-image.png', - type: 'image/png', - path: '/static/logo.png', - tempFilePath: '/static/logo.png', - }, - }; - - console.log('🧪 静态文件模拟事件参数:', mockEvent); - - try { - this.handleFileUpload(mockEvent); - console.log('✅ 静态文件测试调用成功'); - } catch (error) { - console.error('❌ 静态文件测试调用失败:', error); - uni.showToast({ - title: '测试调用失败: ' + error.message, - icon: 'none', - duration: 3000, - }); - } - }, - - // 触发真实的文件选择(最终方案)- 微信环境优化版 - triggerRealFileSelection() { - console.log('📷 触发真实文件选择...'); - console.log('📱 微信环境状态:', { - isWeChat: isWeChat(), - userAgent: - typeof navigator !== 'undefined' - ? navigator.userAgent.includes('MicroMessenger') - : false, - }); - - // 微信环境特殊提示 - const actionItems = isWeChat() ? ['微信相册选择', '微信拍照'] : ['从相册选择', '拍照']; - - uni.showActionSheet({ - itemList: actionItems, - success: (res) => { - const sourceText = res.tapIndex === 0 ? '相册' : '拍照'; - console.log('用户选择了:', sourceText); - - // 微信环境显示特定加载提示 - if (isWeChat()) { - uni.showLoading({ - title: `微信${sourceText}处理中...`, - mask: true, - }); - } - - uni.chooseImage({ - count: 1, - sourceType: res.tapIndex === 0 ? ['album'] : ['camera'], - sizeType: ['original', 'compressed'], // 支持压缩以兼容微信 - success: (chooseRes) => { - console.log('✅ 真实文件选择成功:', chooseRes); - console.log('📊 选择结果详情:', { - tempFilePaths: chooseRes.tempFilePaths, - tempFiles: chooseRes.tempFiles, - environment: isWeChat() ? 'WeChat' : 'Standard', - }); - - if (isWeChat()) { - uni.hideLoading(); - } - - // 构建文件对象 - 微信环境增强 - const tempFile = chooseRes.tempFiles && chooseRes.tempFiles[0]; - const fileName = isWeChat() - ? `wechat-real-${Date.now()}.jpg` - : `real-test-${Date.now()}.jpg`; - - const realEvent = { - file: { - url: chooseRes.tempFilePaths[0], - size: tempFile ? tempFile.size : 0, - name: fileName, - type: tempFile ? tempFile.type || 'image/jpeg' : 'image/jpeg', - path: chooseRes.tempFilePaths[0], - tempFilePath: chooseRes.tempFilePaths[0], - // 微信环境标识 - isWeChatReal: isWeChat(), - source: sourceText, - timestamp: Date.now(), - }, - }; - - console.log('🧪 真实文件事件参数:', realEvent); - - // 显示成功提示 - uni.showToast({ - title: isWeChat() - ? '微信选择成功,开始上传...' - : '文件选择成功,开始上传...', - icon: 'success', - duration: 1500, - }); - - // 延迟调用,确保提示显示 - setTimeout(() => { - this.handleFileUpload(realEvent); - }, 500); - }, - fail: (err) => { - console.error('❌ 真实文件选择失败:', err); - - if (isWeChat()) { - uni.hideLoading(); - } - - // 微信环境特殊错误处理 - let errorMessage = '文件选择失败'; - if (isWeChat()) { - if (err.errMsg && err.errMsg.includes('cancel')) { - errorMessage = '用户取消了选择'; - } else if (err.errMsg && err.errMsg.includes('permission')) { - errorMessage = '请在微信中允许访问相册权限'; - } else { - errorMessage = '微信选择文件失败,请重试'; - } - } - - uni.showToast({ - title: errorMessage, - icon: 'none', - duration: 3000, - }); - }, - }); - }, - fail: (err) => { - console.error('❌ 显示选择菜单失败:', err); - - // 直接选择相册作为备用方案 - console.log('🔄 直接选择相册作为备用方案...'); - this.directChooseFromAlbum(); - }, - }); - }, - - // 直接从相册选择(备用方案) - directChooseFromAlbum() { - console.log('📱 直接从相册选择文件...'); - - uni.chooseImage({ - count: 1, - sourceType: ['album'], - sizeType: ['original', 'compressed'], - success: (chooseRes) => { - console.log('✅ 直接相册选择成功:', chooseRes); - - const tempFile = chooseRes.tempFiles && chooseRes.tempFiles[0]; - const realEvent = { - file: { - url: chooseRes.tempFilePaths[0], - size: tempFile ? tempFile.size : 0, - name: `direct-album-${Date.now()}.jpg`, - type: tempFile ? tempFile.type || 'image/jpeg' : 'image/jpeg', - path: chooseRes.tempFilePaths[0], - tempFilePath: chooseRes.tempFilePaths[0], - isDirect: true, - environment: isWeChat() ? 'WeChat' : 'Standard', - }, - }; - - this.handleFileUpload(realEvent); - }, - fail: (err) => { - console.error('❌ 直接相册选择失败:', err); - uni.showToast({ - title: '相册访问失败,请检查权限设置', - icon: 'none', - duration: 3000, - }); - }, - }); - }, - - // ======== 🔍 新增调试事件监听器 ======== - - // 文件选择前事件 - 应该在用户点击选择图片后首先触发 - handleBeforeRead(event) { - console.log('🟡 ===== @before-read 事件触发 ====='); - console.log('🟡 用户点击了选择图片按钮!'); - console.log('🟡 before-read事件参数:', event); - console.log('🟡 事件类型:', typeof event); - console.log('🟡 事件属性:', Object.keys(event || {})); - - // 显示用户提示 - uni.showToast({ - title: '正在选择文件...', - icon: 'loading', - duration: 1000, - }); - - return true; // 允许继续选择文件 - }, - - // 预览点击事件 - handleClickPreview(event) { - console.log('🔵 ===== @click-preview 事件触发 ====='); - console.log('🔵 点击预览事件参数:', event); - }, - - // 上传进度事件 - handleUploadProgress(event) { - console.log('🟣 ===== @progress 事件触发 ====='); - console.log('🟣 上传进度事件参数:', event); - }, - - // 上传成功事件 - handleUploadSuccess(event) { - console.log('🟢 ===== @success 事件触发 ====='); - console.log('🟢 上传成功事件参数:', event); - }, - - // 上传错误事件 - handleUploadError(event) { - console.log('🔴 ===== @error 事件触发 ====='); - console.log('🔴 上传错误事件参数:', event); - }, + // // 上传错误事件 + // handleUploadError(event) { + // console.log('上传错误事件:', event); + // }, }, }; @@ -3490,8 +4103,20 @@ export default { } .file-actions .u-button { - min-width: 60rpx; - padding: 8rpx 16rpx; + min-width: auto; + padding: 6rpx 10rpx; + font-size: 24rpx; +} + +.image-actions { + display: flex; + gap: 10rpx; +} + +.image-actions .u-button { + min-width: auto; + padding: 6rpx 10rpx; + font-size: 24rpx; } .upload-status { @@ -3536,4 +4161,534 @@ export default { align-items: center; gap: 16rpx; } + +.location-info { + margin-top: 10rpx; + padding: 10rpx; + background-color: #f8f9fa; + border-radius: 6rpx; + display: flex; + align-items: center; +} + +.location-coords { + font-size: 24rpx; + color: #666; +} + +/* 微信SDK调试样式 */ +.wechat-debug-section { + background: #fff; + border-radius: 12rpx; + padding: 30rpx; + margin-bottom: 20rpx; + border: 2rpx solid #2979ff; +} + +.debug-controls { + display: flex; + gap: 10rpx; + align-items: center; + margin-top: 10rpx; + flex-wrap: wrap; +} + +.sdk-status { + margin: 20rpx 0; + padding: 15rpx; + background: #f8f9fa; + border-radius: 8rpx; +} + +.status-item { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10rpx; +} + +.status-label { + font-size: 26rpx; + color: #666; +} + +.status-value { + font-size: 26rpx; + font-weight: 500; +} + +.status-value.success { + color: #52c41a; +} + +.status-value.error { + color: #f5222d; +} + +.debug-logs { + margin-top: 20rpx; + border: 2rpx solid #e8e8e8; + border-radius: 8rpx; + background: #fafafa; +} + +.log-content { + max-height: 400rpx; + padding: 15rpx; +} + +.log-item { + margin-bottom: 10rpx; + padding: 8rpx 12rpx; + border-radius: 6rpx; + font-size: 24rpx; + line-height: 1.4; +} + +.log-item.info { + background: #e6f7ff; + border-left: 4rpx solid #1890ff; +} + +.log-item.success { + background: #f6ffed; + border-left: 4rpx solid #52c41a; +} + +.log-item.warning { + background: #fffbe6; + border-left: 4rpx solid #faad14; +} + +.log-item.error { + background: #fff2f0; + border-left: 4rpx solid #f5222d; +} + +.log-time { + color: #999; + font-size: 22rpx; + margin-right: 10rpx; +} + +.log-text { + color: #333; +} + +.log-text.info { + color: #1890ff; +} + +.log-text.success { + color: #52c41a; +} + +.log-text.warning { + color: #faad14; +} + +.log-text.error { + color: #f5222d; +} + +.log-detail { + margin-top: 5rpx; + padding: 8rpx; + background: rgba(0, 0, 0, 0.03); + border-radius: 4rpx; +} + +.detail-text { + font-size: 20rpx; + color: #666; + font-family: monospace; + word-break: break-all; +} + +.empty-logs { + text-align: center; + color: #999; + padding: 40rpx 0; + font-size: 26rpx; +} + +/* 浮动调试按钮 */ +.debug-fab { + position: fixed; + right: 30rpx; + bottom: 200rpx; + width: 80rpx; + height: 80rpx; + background: #2979ff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 4rpx 12rpx rgba(41, 121, 255, 0.3); + z-index: 999; +} + +.debug-fab:active { + background: #1976d2; + transform: scale(0.95); +} + +/* 微信上传区域样式 */ +.wechat-upload-section { + margin-bottom: 20rpx; + padding: 20rpx; + background: #f0f9ff; + border: 2rpx dashed #2979ff; + border-radius: 10rpx; +} + +.upload-progress { + margin-top: 15rpx; +} + +.progress-text { + font-size: 24rpx; + color: #666; + margin-top: 8rpx; + text-align: center; + display: block; +} + +.upload-tips { + margin-top: 20rpx; + padding: 15rpx; + background: #f8f9fa; + border-radius: 8rpx; + border-left: 4rpx solid #17a2b8; +} + +.tip-item { + display: block; + font-size: 24rpx; + color: #666; + line-height: 1.5; + margin-bottom: 8rpx; +} + +.wechat-tip { + color: #2979ff; + font-weight: 500; +} + +/* 完善的上传进度条样式 */ +.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 !important; + color: #2979ff !important; + font-weight: 500; + margin-top: 12rpx; + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 0.8; + } + 50% { + opacity: 1; + } +} + +/* 自定义文件列表样式 */ +.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-info-card { + position: absolute; + bottom: 8rpx; + left: 8rpx; + right: 8rpx; + display: flex; + justify-content: space-between; + align-items: center; +} + +.file-ext-badge { + background: rgba(255, 255, 255, 0.9); + color: #333; + font-size: 22rpx; + font-weight: 600; + padding: 4rpx 8rpx; + border-radius: 4rpx; + backdrop-filter: blur(4rpx); +} + +.file-size { + background: rgba(0, 0, 0, 0.6); + color: #fff; + font-size: 20rpx; + padding: 2rpx 6rpx; + border-radius: 4rpx; +} + +.file-actions { + display: flex; + gap: 6rpx; + padding: 8rpx 12rpx; + background: #fafafa; + border-top: 1rpx solid #f0f0f0; + flex-wrap: nowrap; + justify-content: space-between; +} + +.file-actions .u-button { + flex: 1; + min-width: 60rpx; + font-size: 24rpx !important; +} + +.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-status { + display: flex; + align-items: center; + justify-content: center; + margin-top: 15rpx; + padding: 10rpx; + border-radius: 8rpx; + font-size: 24rpx; + font-weight: 500; +} + +.upload-status.success { + color: #19be6b; + background-color: rgba(25, 190, 107, 0.1); +} + +.upload-status.error { + color: #fa3534; + background-color: rgba(250, 53, 52, 0.1); +} + +.upload-progress-small { + width: 100%; + margin-bottom: 8rpx; +} + +.upload-text { + font-size: 22rpx; + color: #2979ff; + text-align: center; +} + +/* 图片展示区域样式 */ +.image-section { + margin-bottom: 30rpx; +} + +.image-title { + font-size: 28rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; + padding-left: 10rpx; + border-left: 4rpx solid #2979ff; +} + +.image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280rpx, 1fr)); + gap: 20rpx; +} + +.image-item { + position: relative; + background: #fff; + border-radius: 12rpx; + overflow: hidden; + border: 2rpx solid #f0f0f0; + transition: all 0.3s ease; +} + +.image-item:active { + transform: scale(0.98); + border-color: #2979ff; +} + +.image-container { + position: relative; + width: 100%; + height: 200rpx; + border-radius: 8rpx; + overflow: hidden; +} + +.attachment-image { + width: 100%; + height: 100%; + object-fit: cover; + background-color: #f5f5f5; +} + +.upload-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #fff; +} + +.upload-text { + font-size: 24rpx; + margin-top: 8rpx; + font-weight: 500; +} + +.error-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(250, 53, 52, 0.8); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + color: #fff; +} + +.error-text { + font-size: 24rpx; + margin-top: 8rpx; + font-weight: 500; +} + +.success-badge { + position: absolute; + top: 8rpx; + right: 8rpx; + background-color: rgba(255, 255, 255, 0.9); + border-radius: 50%; + padding: 4rpx; + box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1); +} + +.image-actions { + display: flex; + gap: 6rpx; + padding: 8rpx 12rpx; + background: #fafafa; + border-top: 1rpx solid #f0f0f0; + flex-wrap: nowrap; + justify-content: space-between; +} + +.image-actions .u-button { + flex: 1; + min-width: 60rpx; + font-size: 24rpx !important; +} + +.image-name { + padding: 8rpx 12rpx; + background: #fff; + font-size: 22rpx; + color: #666; + text-align: center; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +/* 响应式适配 */ +@media screen and (max-width: 750rpx) { + .file-grid { + grid-template-columns: repeat(auto-fill, minmax(240rpx, 1fr)); + gap: 15rpx; + } + + .file-icon-container { + height: 160rpx; + } + + .image-grid { + grid-template-columns: repeat(auto-fill, minmax(240rpx, 1fr)); + gap: 15rpx; + } + + .image-container { + height: 160rpx; + } + + .file-actions .u-button, + .image-actions .u-button { + font-size: 22rpx !important; + padding: 6rpx 12rpx; + } +} From 05739542215579b3825ed1263bb95e18b9035d73 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 10 Aug 2025 14:59:18 +0800 Subject: [PATCH 02/23] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 442 +++++++++++++++--- 1 file changed, 366 insertions(+), 76 deletions(-) diff --git a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue index 413fe352..4641d465 100644 --- a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue +++ b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue @@ -257,12 +257,7 @@ - + @@ -270,20 +265,34 @@ - - - 坐标: {{ geoLatitude.toFixed(6) }}, {{ geoLongitude.toFixed(6) }} - - - - 查看位置 - + + + + + + + + 经纬度: {{ form.leaveLocation || '未获取' }} + + + + + + 查看位置 + + @@ -301,7 +310,7 @@ v-for="(image, index) in imageFiles" :key="image.uid || index" class="image-item" - @click="handleImagePreview(image, index)" + @click="handleImageClick(image, index, $event)" > @@ -341,22 +350,25 @@ - - + 重试 @@ -364,7 +376,10 @@ size="mini" type="error" plain - @click.stop="handleImageDelete(image, index)" + @tap="handleImageDelete(image, index)" + @click.stop.prevent=" + handleImageDeleteClick(image, index, $event) + " > 删除 @@ -441,30 +456,31 @@ - + - - + 重试 @@ -472,12 +488,13 @@ size="mini" type="error" plain - @click=" + @click.stop.prevent=" handleRemoveFile( file, index + attachmentList.filter((f) => isImageFile(f)) .length, + $event, ) " > @@ -1153,7 +1170,7 @@ export default { // 打开地图查看位置 async openLocationInMap() { - if (!this.geoLatitude || !this.geoLongitude) { + if (!this.form.leaveLocation) { uni.showToast({ title: '暂无位置信息', icon: 'none', @@ -1161,12 +1178,33 @@ export default { return; } + // 从 form.leaveLocation 解析经纬度 + const coords = this.form.leaveLocation.split(','); + if (coords.length !== 2) { + uni.showToast({ + title: '位置信息格式错误', + icon: 'none', + }); + return; + } + + const latitude = parseFloat(coords[0]); + const longitude = parseFloat(coords[1]); + + if (isNaN(latitude) || isNaN(longitude)) { + uni.showToast({ + title: '位置信息无效', + icon: 'none', + }); + return; + } + if (this.isWechatBrowser && this.wechatSDK.isInitialized) { try { this.addWechatLog('使用微信打开地图...', 'info'); await this.wechatSDK.openLocation({ - latitude: this.geoLatitude, - longitude: this.geoLongitude, + latitude: latitude, + longitude: longitude, name: '请假地点', address: this.form.leaveLocation || '当前位置', scale: 16, @@ -1174,18 +1212,16 @@ export default { this.addWechatLog('微信地图打开成功', 'success'); } catch (error) { this.addWechatLog(`微信地图打开失败: ${error.message}`, 'error'); - this.fallbackOpenMap(); + this.fallbackOpenMap(latitude, longitude); } } else { - this.fallbackOpenMap(); + this.fallbackOpenMap(latitude, longitude); } }, // 备用地图打开方式 - fallbackOpenMap() { - const url = `https://apis.map.qq.com/uri/v1/marker?marker=coord:${this.geoLatitude},${ - this.geoLongitude - };title:请假地点;addr:${encodeURIComponent( + fallbackOpenMap(latitude, longitude) { + const url = `https://apis.map.qq.com/uri/v1/marker?marker=coord:${latitude},${longitude};title:请假地点;addr:${encodeURIComponent( this.form.leaveLocation || '当前位置', )}&referer=智慧校园`; @@ -2265,10 +2301,28 @@ export default { }, // 图片下载 - async handleImageDownload(image) { - console.log('图片下载:', image); + async handleImageDownload(image, event) { + this.addWechatLog('=== 图片下载方法开始执行 ===', 'info'); + this.addWechatLog( + '图片信息', + 'info', + JSON.stringify( + { + uid: image.uid, + ossId: image.ossId, + name: image.name, + url: image.url, + status: image.status, + }, + null, + 2, + ), + ); + + // 事件阻止已由handleImageDownloadClick方法处理 if (image.status !== 'success') { + this.addWechatLog('⚠️ 图片尚未上传完成,无法下载', 'warning'); uni.showToast({ title: '图片尚未上传完成', icon: 'none', @@ -2276,6 +2330,7 @@ export default { return; } + this.addWechatLog('📤 开始准备下载图片...', 'info'); uni.showLoading({ title: '准备下载...', mask: true, @@ -2285,6 +2340,7 @@ export default { // 如果有ossId,通过API下载 if (image.ossId) { const downloadUrl = `/api/resource/oss/download/${image.ossId}`; + this.addWechatLog(`📥 使用API下载图片,URL: ${downloadUrl}`, 'info'); // 在H5环境中,直接打开下载链接 // #ifdef H5 @@ -2295,6 +2351,7 @@ export default { link.click(); document.body.removeChild(link); + this.addWechatLog('✅ H5环境图片下载已开始', 'success'); uni.hideLoading(); uni.showToast({ title: '下载已开始', @@ -2326,7 +2383,11 @@ export default { } }, fail: (error) => { - console.error('下载失败:', error); + this.addWechatLog( + '❌ 图片下载失败', + 'error', + error.message || error.toString(), + ); uni.showToast({ title: '下载失败', icon: 'none', @@ -2379,7 +2440,11 @@ export default { } }, fail: (error) => { - console.error('下载失败:', error); + this.addWechatLog( + '❌ 图片下载失败', + 'error', + error.message || error.toString(), + ); uni.showToast({ title: '下载失败', icon: 'none', @@ -2393,25 +2458,111 @@ export default { } } } catch (error) { - console.error('下载过程出错:', error); + this.addWechatLog( + '❌ 图片下载过程出错', + 'error', + error.message || error.toString(), + ); uni.hideLoading(); uni.showToast({ title: '下载失败', icon: 'none', }); } + this.addWechatLog('=== 图片下载方法执行完成 ===', 'success'); + }, + + // 图片点击处理(智能判断是预览还是操作按钮) + handleImageClick(image, index, event) { + // 检查点击目标是否是操作按钮区域 + const target = event.target; + const isActionButton = + target.closest('.image-actions') || + target.closest('u-button') || + target.tagName === 'BUTTON'; + + // 如果点击的是操作按钮区域,不触发预览 + if (!isActionButton) { + this.handleImagePreview(image, index); + } + }, + + // 删除按钮点击处理(更强的事件处理) + handleImageDeleteClick(image, index, event) { + console.log('删除按钮被点击:', image, index); + + // 强制阻止事件冒泡和默认行为 + if (event) { + event.stopPropagation(); + event.preventDefault(); + event.stopImmediatePropagation(); + } + + // 调用实际的删除方法 + this.handleImageDelete(image, index); + }, + + // 图片下载按钮点击处理(更强的事件处理) + handleImageDownloadClick(image, event) { + this.addWechatLog('🔽 图片下载按钮被点击', 'info'); + this.addWechatLog(`图片信息: ${JSON.stringify(image)}`, 'info'); + + // 强制阻止事件冒泡和默认行为 + if (event) { + event.stopPropagation(); + event.preventDefault(); + event.stopImmediatePropagation(); + this.addWechatLog( + '✅ 事件冒泡已阻止 (stopPropagation + preventDefault + stopImmediatePropagation)', + 'success', + ); + } + + // 调用实际的下载方法 + this.handleImageDownload(image, event); }, // 图片删除 - handleImageDelete(image, index) { - console.log('删除图片:', image, index); + handleImageDelete(image, index, event) { + this.addWechatLog('=== 图片删除方法开始执行 ===', 'info'); + this.addWechatLog(`删除图片参数 - 索引: ${index}`, 'info'); + this.addWechatLog( + '图片信息', + 'info', + JSON.stringify( + { + uid: image.uid, + ossId: image.ossId, + name: image.name, + url: image.url, + status: image.status, + }, + null, + 2, + ), + ); + this.addWechatLog(`当前附件列表数量: ${this.attachmentList.length}`, 'info'); + // 强制阻止事件冒泡和默认行为 + if (event) { + this.addWechatLog('阻止事件冒泡和默认行为', 'info'); + event.stopPropagation(); + event.preventDefault(); + } + + this.addWechatLog('准备显示删除确认对话框', 'info'); uni.showModal({ title: '确认删除', content: '确定要删除这张图片吗?删除后无法恢复。', confirmColor: '#ff5722', success: (res) => { + this.addWechatLog( + `删除确认对话框回调 - 用户选择: ${res.confirm ? '确认' : '取消'}`, + 'info', + ); if (res.confirm) { + this.addWechatLog('用户确认删除,开始执行删除操作', 'success'); + // 从附件列表中删除 const attachmentIndex = this.attachmentList.findIndex( (file) => @@ -2420,12 +2571,44 @@ export default { (file.url && file.url === image.url), ); + this.addWechatLog(`查找到的附件索引: ${attachmentIndex}`, 'info'); + this.addWechatLog( + '匹配条件', + 'info', + `uid: ${image.uid}, ossId: ${image.ossId}, url: ${image.url}`, + ); + if (attachmentIndex >= 0) { + this.addWechatLog( + '删除前附件列表', + 'info', + JSON.stringify( + this.attachmentList.map((f) => ({ uid: f.uid, name: f.name })), + null, + 2, + ), + ); this.attachmentList.splice(attachmentIndex, 1); + this.addWechatLog( + '删除后附件列表', + 'info', + JSON.stringify( + this.attachmentList.map((f) => ({ uid: f.uid, name: f.name })), + null, + 2, + ), + ); + this.addWechatLog('✅ 图片从附件列表中删除成功', 'success'); + } else { + this.addWechatLog( + '⚠️ 警告:未找到要删除的附件,可能已被删除或索引错误', + 'warning', + ); } // 清理blob URL,防止内存泄漏 if (image.blobUrl && image.blobUrl.startsWith('blob:')) { + this.addWechatLog(`清理blob URL: ${image.blobUrl}`, 'info'); URL.revokeObjectURL(image.blobUrl); } @@ -2434,9 +2617,22 @@ export default { icon: 'success', }); - console.log('图片删除完成,当前附件数量:', this.attachmentList.length); + this.addWechatLog( + `图片删除完成,当前附件数量: ${this.attachmentList.length}`, + 'success', + ); + this.addWechatLog('=== 图片删除方法执行完成 ===', 'success'); + } else { + this.addWechatLog('用户取消删除操作', 'warning'); } }, + fail: (err) => { + this.addWechatLog( + '❌ 删除确认对话框显示失败', + 'error', + err.message || err.toString(), + ); + }, }); }, @@ -2480,9 +2676,15 @@ export default { }, // 重试上传文件 - async retryUploadFile(file) { + async retryUploadFile(file, event) { console.log('=== 重试上传文件 ===', file); + // 强制阻止事件冒泡和默认行为 + if (event) { + event.stopPropagation(); + event.preventDefault(); + } + // 重置文件状态 file.status = 'uploading'; file.progress = 0; @@ -2538,14 +2740,15 @@ export default { // 显示选择文件类型的操作菜单 uni.showActionSheet({ - itemList: ['选择图片', '选择文档', '微信选择图片'], + // itemList: ['选择图片', '选择文档', '微信选择图片'], + itemList: ['选择图片'], success: (res) => { switch (res.tapIndex) { case 0: - this.chooseImages(); + this.chooseDocuments(); break; case 1: - this.chooseDocuments(); + this.chooseImages(); break; case 2: if (this.isWechatBrowser) { @@ -3019,8 +3222,14 @@ export default { }, // 手动删除文件 - handleRemoveFile(file, index) { + handleRemoveFile(file, index, event) { console.log('=== 手动删除文件 ===', file, index); + + // 强制阻止事件冒泡和默认行为 + if (event) { + event.stopPropagation(); + event.preventDefault(); + } uni.showModal({ title: '确认删除', content: '确定要删除这个文件吗?', @@ -3049,23 +3258,57 @@ export default { } }, + // 文件下载按钮点击处理(更强的事件处理) + handleDownloadFileClick(file, event) { + this.addWechatLog('📁 文件下载按钮被点击', 'info'); + this.addWechatLog(`文件信息: ${JSON.stringify(file)}`, 'info'); + + // 强制阻止事件冒泡和默认行为 + if (event) { + event.stopPropagation(); + event.preventDefault(); + event.stopImmediatePropagation(); + this.addWechatLog( + '✅ 事件冒泡已阻止 (stopPropagation + preventDefault + stopImmediatePropagation)', + 'success', + ); + } + + // 调用实际的下载方法 + this.handleDownloadFile(file, event); + }, + // 文件下载(微信小程序专用版本) - async handleDownloadFile(file) { - console.log('=== 开始下载文件 ==='); - console.log('文件信息:', JSON.stringify(file, null, 2)); - console.log('当前时间:', new Date().toISOString()); + async handleDownloadFile(file, event) { + this.addWechatLog('=== 文件下载方法开始执行 ===', 'info'); + this.addWechatLog('文件信息', 'info', JSON.stringify(file, null, 2)); + this.addWechatLog(`当前时间: ${new Date().toISOString()}`, 'info'); + + // 事件阻止已由handleDownloadFileClick方法处理 // 检查环境信息 const systemInfo = uni.getSystemInfoSync(); - console.log('运行环境信息:', { - platform: systemInfo.platform, - version: systemInfo.version, - model: systemInfo.model, - brand: systemInfo.brand, - }); + this.addWechatLog( + '运行环境信息', + 'info', + JSON.stringify( + { + platform: systemInfo.platform, + version: systemInfo.version, + model: systemInfo.model, + brand: systemInfo.brand, + }, + null, + 2, + ), + ); if (!file.ossId) { - console.warn('❌ 下载失败:文件ossId为空', file); + this.addWechatLog( + '❌ 下载失败:文件ossId为空', + 'warning', + JSON.stringify(file, null, 2), + ); uni.showToast({ title: '文件未上传完成', icon: 'none', @@ -3074,7 +3317,7 @@ export default { } try { - console.log('📤 开始显示下载Loading...'); + this.addWechatLog('📤 开始显示下载Loading...', 'info'); uni.showLoading({ title: `正在下载${file.name}...`, mask: true, @@ -3082,22 +3325,29 @@ export default { // 构建下载URL const downloadUrl = `/resource/oss/download/${file.ossId}`; - console.log('下载URL:', downloadUrl); + this.addWechatLog(`下载URL: ${downloadUrl}`, 'info'); // 根据文件类型选择处理方式 - console.log('🔍 检查文件类型:', file.name, '是否为图片:', this.isImageFile(file)); + this.addWechatLog( + `🔍 检查文件类型: ${file.name}, 是否为图片: ${this.isImageFile(file)}`, + 'info', + ); if (this.isImageFile(file)) { // 图片文件:下载并保存到相册 - console.log('🖼️ 图片文件,下载并保存到相册'); + this.addWechatLog('🖼️ 图片文件,下载并保存到相册', 'info'); await this.downloadAndSaveImage(downloadUrl, file.name); } else { // 非图片文件:下载并直接打开预览 - console.log('📄 非图片文件,下载并打开预览'); + this.addWechatLog('📄 非图片文件,下载并打开预览', 'info'); await this.downloadAndOpenDocument(downloadUrl, file.name); } } catch (error) { - console.error('❌ 下载过程发生异常:', error); + this.addWechatLog( + '❌ 下载过程发生异常', + 'error', + error.message || error.toString(), + ); uni.hideLoading(); let errorMsg = '下载失败,请重试'; @@ -3125,7 +3375,7 @@ export default { }); } - console.log('=== handleDownloadFile方法执行完成 ==='); + this.addWechatLog('=== 文件下载方法执行完成 ===', 'success'); }, // 下载图片并保存到相册 @@ -4165,15 +4415,33 @@ export default { .location-info { margin-top: 10rpx; padding: 10rpx; - background-color: #f8f9fa; + /* background-color: #f8f9fa; */ border-radius: 6rpx; display: flex; align-items: center; + justify-content: space-between; + gap: 10rpx; +} + +.location-coords-container { + flex: 1; + display: flex; + align-items: center; + min-width: 0; } .location-coords { font-size: 24rpx; color: #666; + word-break: break-all; + overflow: hidden; + text-overflow: ellipsis; +} + +.location-action-container { + flex-shrink: 0; + display: flex; + align-items: center; } /* 微信SDK调试样式 */ @@ -4566,6 +4834,8 @@ export default { overflow: hidden; border: 2rpx solid #f0f0f0; transition: all 0.3s ease; + /* 确保图片容器的点击事件不影响按钮 */ + cursor: pointer; } .image-item:active { @@ -4579,6 +4849,7 @@ export default { height: 200rpx; border-radius: 8rpx; overflow: hidden; + cursor: pointer; } .attachment-image { @@ -4646,12 +4917,20 @@ export default { border-top: 1rpx solid #f0f0f0; flex-wrap: nowrap; justify-content: space-between; + /* 确保按钮区域能正确处理点击事件 */ + position: relative; + z-index: 10; + pointer-events: auto; } .image-actions .u-button { flex: 1; min-width: 60rpx; font-size: 24rpx !important; + /* 确保按钮可点击 */ + pointer-events: auto; + position: relative; + z-index: 11; } .image-name { @@ -4691,4 +4970,15 @@ export default { padding: 6rpx 12rpx; } } + +.location-coords-container { + display: flex; + align-items: center; +} + +.location-action-container { + display: flex; + align-items: center; + margin-left: 10rpx; +} From 3f2b92eb4ae0dc33fc4f263c948ef63643124fee Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 10 Aug 2025 15:23:44 +0800 Subject: [PATCH 03/23] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=B7=E5=81=87?= =?UTF-8?q?=E6=B5=81=E7=A8=8B=E4=B8=AD=E9=99=84=E4=BB=B6=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/views/xg/xsjcxx/xsqj/dialog.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/web/src/views/xg/xsjcxx/xsqj/dialog.vue b/web/src/views/xg/xsjcxx/xsqj/dialog.vue index d31e01ef..1a591cdd 100644 --- a/web/src/views/xg/xsjcxx/xsqj/dialog.vue +++ b/web/src/views/xg/xsjcxx/xsqj/dialog.vue @@ -594,8 +594,21 @@ const handleDownloadAttachment = async (file: any) => { if (!response) { throw new Error('响应数据为空'); } + console.log('response', response); - const url = window.URL.createObjectURL(response.data as Blob); + // 检查response的结构,判断是直接的Blob还是包含data属性的响应对象 + let blobData: Blob; + if (response instanceof Blob) { + // 如果response本身就是Blob对象 + blobData = response; + } else if (response.data instanceof Blob) { + // 如果response.data是Blob对象 + blobData = response.data; + } else { + throw new Error('无效的响应数据格式'); + } + + const url = window.URL.createObjectURL(blobData); const link = document.createElement('a'); link.href = url; From c79afe56e2c8a28ef5b61a82ee9000d1c39b33f4 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Sun, 10 Aug 2025 20:16:20 +0800 Subject: [PATCH 04/23] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/components/FileUpload/index.vue | 181 +++++++++--- .../src/components/Process/approvalButton.vue | 60 +++- .../src/components/Process/submitVerify.vue | 140 ++++++--- .../src/pages/xg/xsjcxx/xsqj/leave/index.vue | 16 ++ .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 269 ++++++++++++------ 5 files changed, 497 insertions(+), 169 deletions(-) diff --git a/mobile/src/components/FileUpload/index.vue b/mobile/src/components/FileUpload/index.vue index 29026487..48ed290c 100644 --- a/mobile/src/components/FileUpload/index.vue +++ b/mobile/src/components/FileUpload/index.vue @@ -37,7 +37,7 @@ - 下载 - + --> {{ uploadPercent }}% + + + + + + @@ -89,7 +118,7 @@ import { REQUEST_CONFIG } from '@/common/config.js'; export default { name: 'FileUpload', props: { - modelValue: { + value: { type: [String, Object, Array], default: () => [], }, @@ -119,12 +148,16 @@ export default { default: false, }, }, + emits: ['input', 'submit', 'cancel'], data() { return { fileList: [], uploading: false, showProgress: false, uploadPercent: 0, + showDeleteConfirm: false, + deleteIndex: -1, + showClearConfirm: false, }; }, computed: { @@ -133,7 +166,7 @@ export default { }, }, watch: { - modelValue: { + value: { handler: 'initFileList', immediate: true, deep: true, @@ -149,10 +182,10 @@ export default { } else if (typeof val === 'string' && val.trim()) { // 如果是字符串,调用API获取文件详情 try { + const ids = val.split(',').filter((id) => id.trim()); const response = await this.request({ - url: '/resource/oss/listByIds', - method: 'POST', - data: val.split(',').filter((id) => id.trim()), + url: `/resource/oss/listByIds/${encodeURIComponent(ids.join(','))}`, + method: 'GET', }); if (response.code === 200) { @@ -387,37 +420,54 @@ export default { }, // 删除文件 - async handleDelete(index) { + handleDelete(index) { + console.log('handleDelete', index); + this.deleteIndex = index; + this.showDeleteConfirm = true; + }, + + // 确认删除 + async confirmDelete() { try { - const result = await this.showConfirm('确定要删除这个文件吗?'); - if (result.confirm) { - const file = this.fileList[index]; + const file = this.fileList[this.deleteIndex]; - // 如果有ossId,调用删除API - if (file.ossId) { - try { - await this.request({ - url: `/resource/oss/${file.ossId}`, - method: 'DELETE', - }); - } catch (error) { - console.error('删除文件失败:', error); - } + // 如果有ossId,调用删除API + if (file.ossId) { + try { + await this.request({ + url: `/resource/oss/${file.ossId}`, + method: 'DELETE', + }); + } catch (error) { + console.error('删除文件失败:', error); } - - this.fileList.splice(index, 1); - this.emitChange(); - - uni.showToast({ - title: '删除成功', - icon: 'success', - }); } + + this.fileList.splice(this.deleteIndex, 1); + this.emitChange(); + + uni.showToast({ + title: '删除成功', + icon: 'success', + }); } catch (error) { console.error('删除文件失败:', error); + uni.showToast({ + title: '删除失败', + icon: 'none', + }); + } finally { + this.showDeleteConfirm = false; + this.deleteIndex = -1; } }, + // 取消删除 + cancelDelete() { + this.showDeleteConfirm = false; + this.deleteIndex = -1; + }, + // 获取文件名 getFileName(name) { if (!name) return '未知文件'; @@ -442,7 +492,8 @@ export default { // 发出变更事件 emitChange() { const value = this.listToString(this.fileList); - this.$emit('update:modelValue', value); + console.log('longwu---emitChange', value); + this.$emit('input', value); }, // 转换为字符串 @@ -464,6 +515,59 @@ export default { }); }, + // 提交文件 + handleSubmit() { + if (this.fileList.length === 0) { + uni.showToast({ + title: '请先选择文件', + icon: 'none', + }); + return; + } + + // 发出提交事件,传递文件列表 + this.$emit('submit', { + fileList: this.fileList, + ossIds: this.listToString(this.fileList), + }); + }, + + // 取消操作 + handleCancel() { + if (this.fileList.length > 0) { + this.showClearConfirm = true; + } else { + this.$emit('cancel'); + } + }, + + // 确认清空文件 + confirmClear() { + try { + this.fileList = []; + this.emitChange(); + this.$emit('cancel'); + + uni.showToast({ + title: '已清空文件列表', + icon: 'success', + }); + } catch (error) { + console.error('清空文件失败:', error); + uni.showToast({ + title: '清空文件失败', + icon: 'none', + }); + } finally { + this.showClearConfirm = false; + } + }, + + // 取消清空文件 + cancelClear() { + this.showClearConfirm = false; + }, + // 请求方法 async request(options) { const { request } = await import('@/common/request.js'); @@ -475,17 +579,24 @@ export default { diff --git a/mobile/src/components/Process/submitVerify.vue b/mobile/src/components/Process/submitVerify.vue index 95a47e24..395e201f 100644 --- a/mobile/src/components/Process/submitVerify.vue +++ b/mobile/src/components/Process/submitVerify.vue @@ -1,12 +1,13 @@ + + + +
+
+ + 关闭 + +
+
+
+ + + {{ form.leaveLocation }} + +
+
+
+ + +
+

位置解析:

+

+ 纬度:{{ parsedLocation.latitude }}
+ 经度:{{ parsedLocation.longitude }} +

+

+ 位置描述:{{ parsedLocation.address }} +

+
+ +
+
🗺️
+

地图显示区域

+ (可集成百度地图、高德地图等) +
+
+
+
+
+ +
@@ -233,7 +297,7 @@ import { ref, computed, onMounted, reactive, toRefs, nextTick, getCurrentInstanc import ElMessage from 'element-plus/es/components/message/index'; import ElMessageBox from 'element-plus/es/components/message-box/index'; import ElLoading from 'element-plus/es/components/loading/index'; -import { UploadFilled, Document, Loading, Download, Delete } from '@element-plus/icons-vue'; +import { UploadFilled, Document, Loading, Download, Delete, LocationFilled } from '@element-plus/icons-vue'; import { getToken } from '@/utils/auth'; import ImagePreview from '@/components/ImagePreview/index.vue'; import request from '@/utils/request'; @@ -362,6 +426,48 @@ const dialogVisible = reactive({ title: '流程定义' }); +// 地图弹窗状态 +const mapDialogVisible = ref(false); + +// 解析后的位置信息 +const parsedLocation = computed(() => { + if (!form.leaveLocation || form.leaveLocation.trim() === '') { + return null; + } + + const location = form.leaveLocation.trim(); + + // 尝试解析经纬度格式: "纬度: xxx, 经度: xxx" + const coordsRegex = /纬度:\s*([\d.-]+),\s*经度:\s*([\d.-]+)/; + const coordsMatch = location.match(coordsRegex); + + if (coordsMatch) { + return { + type: 'coordinates', + latitude: parseFloat(coordsMatch[1]), + longitude: parseFloat(coordsMatch[2]) + }; + } + + // 尝试解析简单经纬度格式: "lat,lng" 或 "lat, lng" + const simpleRegex = /^([\d.-]+),\s*([\d.-]+)$/; + const simpleMatch = location.match(simpleRegex); + + if (simpleMatch) { + return { + type: 'coordinates', + latitude: parseFloat(simpleMatch[1]), + longitude: parseFloat(simpleMatch[2]) + }; + } + + // 其他情况当作地址处理 + return { + type: 'address', + address: location + }; +}); + //提交组件 const submitVerifyRef = ref>(); //审批记录组件 @@ -988,6 +1094,51 @@ const cancel = () => { emit('cancel'); }; +// 打开地图定位弹窗 +const openLocationMap = () => { + if (!form.leaveLocation || form.leaveLocation.trim() === '') { + ElMessage.warning('暂无位置信息可显示'); + return; + } + mapDialogVisible.value = true; +}; + +// 关闭地图弹窗 +const closeMapDialog = () => { + mapDialogVisible.value = false; +}; + +// 在外部地图中查看 +const openExternalMap = () => { + if (!parsedLocation.value || parsedLocation.value.type !== 'coordinates') { + ElMessage.warning('无法获取有效的坐标信息'); + return; + } + + const { latitude, longitude } = parsedLocation.value; + + // 提供多种地图选择 + ElMessageBox.confirm('选择要使用的地图服务:', '打开外部地图', { + confirmButtonText: '百度地图', + cancelButtonText: '高德地图', + type: 'info', + distinguishCancelAndClose: true + }) + .then(() => { + // 百度地图 + const baiduUrl = `https://api.map.baidu.com/marker?location=${latitude},${longitude}&title=请假位置&content=${encodeURIComponent(form.leaveLocation)}&output=html&src=webapp`; + window.open(baiduUrl, '_blank'); + }) + .catch((action) => { + if (action === 'cancel') { + // 高德地图 + const gaodeUrl = `https://uri.amap.com/marker?position=${longitude},${latitude}&name=请假位置&src=webapp`; + window.open(gaodeUrl, '_blank'); + } + // 如果是 close,则不执行任何操作 + }); +}; + // 监听 isLeaveSchool 字段变化,更新销假状态 // watch( // () => form.isLeaveSchool, @@ -1640,4 +1791,93 @@ onMounted(() => { :deep(.el-card .el-card__header) { border-bottom: 0px !important; } + +/* 地图弹窗样式 */ +.map-container { + min-height: 500px; + max-height: 70vh; + overflow-y: auto; +} + +.no-location-tip { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; +} + +.location-info { + padding: 16px; +} + +.location-text { + margin-bottom: 16px; + text-align: center; +} + +.location-text .el-tag { + padding: 8px 16px; + font-size: 14px; + font-weight: 500; +} + +.location-text .el-icon { + margin-right: 8px; +} + +.map-placeholder { + margin-top: 16px; +} + +.map-tips { + margin-bottom: 16px; +} + +.coordinates-display { + margin: 16px 0; + padding: 12px; + background: #f8f9fa; + border-radius: 6px; + border-left: 4px solid #409eff; +} + +.coordinates-display p { + margin: 8px 0; + line-height: 1.5; +} + +.coordinates-display strong { + color: #303133; + font-weight: 600; +} + +.mock-map { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + border: 2px dashed #d0d7de; + border-radius: 8px; + color: #666; + text-align: center; + margin-top: 16px; +} + +.map-icon { + font-size: 48px; + margin-bottom: 8px; +} + +.mock-map p { + margin: 4px 0; + font-size: 16px; + font-weight: 500; +} + +.mock-map small { + color: #999; + font-size: 12px; +} diff --git a/web/src/views/xg/xsjcxx/xsqj/index.vue b/web/src/views/xg/xsjcxx/xsqj/index.vue index f082e9b0..785cf52a 100644 --- a/web/src/views/xg/xsjcxx/xsqj/index.vue +++ b/web/src/views/xg/xsjcxx/xsqj/index.vue @@ -238,20 +238,18 @@ --> - + From ed9826f92b2ed52a64de207996c827404809ca27 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 11 Aug 2025 07:15:51 +0800 Subject: [PATCH 12/23] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/common/workflow-api.js | 6 +- mobile/src/pages/task/myDocument.vue | 239 +++++++++++++++++---------- 2 files changed, 156 insertions(+), 89 deletions(-) diff --git a/mobile/src/common/workflow-api.js b/mobile/src/common/workflow-api.js index d682b5dd..c4fb5c17 100644 --- a/mobile/src/common/workflow-api.js +++ b/mobile/src/common/workflow-api.js @@ -12,15 +12,15 @@ export const workflowApi = { // 删除流程实例 deleteByInstanceIds: (instanceIds) => request({ - url: '/workflow/instance/deleteByInstanceIds', + url: `/workflow/instance/deleteByInstanceIds/${instanceIds}`, method: 'DELETE', - data: instanceIds, + // data: instanceIds, }), // 撤销流程申请 cancelProcessApply: (data) => request({ url: '/workflow/instance/cancelProcessApply', - method: 'POST', + method: 'PUT', data, }), // 获取流程分类树 diff --git a/mobile/src/pages/task/myDocument.vue b/mobile/src/pages/task/myDocument.vue index 217b2547..068c09e9 100644 --- a/mobile/src/pages/task/myDocument.vue +++ b/mobile/src/pages/task/myDocument.vue @@ -69,53 +69,46 @@ {{ formatTime(item.createTime) }}
- - - - - - + + + + + + 查看 + - - - - 查看 - + + + + 编辑 + - - - 编辑 - + + + + 撤销 + - - - 撤销 - - - - - 删除 - - + + + + 删除 @@ -384,13 +377,53 @@ export default { }, handleEdit(item) { - this.routerJump({ - businessId: item.businessId, - taskId: item.id, - type: 'update', - formCustom: item.formCustom, - formPath: item.formPath, - }); + // 动态跳转到formPath对应的路径,携带businessId参数 + if (item.formPath) { + // 确保路径以pages开头 + let fullPath = item.formPath; + + // 统一处理路径:去掉开头的/,然后添加pages前缀 + if (!fullPath.startsWith('pages/')) { + // 如果以/开头,先去掉/ + if (fullPath.startsWith('/')) { + fullPath = fullPath.substring(1); + } + // 添加pages前缀 + fullPath = `pages/${fullPath}`; + } + + const url = `/${fullPath}?id=${item.businessId}&type=update`; + + // 添加调试信息 + console.log('编辑页面 - 原始formPath:', item.formPath); + console.log('编辑页面 - 处理后的fullPath:', fullPath); + console.log('编辑页面 - 最终跳转URL:', url); + + // 使用navigateTo进行跳转 + uni.navigateTo({ + url: url, + fail: (err) => { + console.error('编辑页面跳转失败:', err); + // 如果跳转失败,使用原有的routerJump方法作为备选 + this.routerJump({ + businessId: item.businessId, + taskId: item.id, + type: 'update', + formCustom: item.formCustom, + formPath: item.formPath, + }); + }, + }); + } else { + // 如果没有formPath,使用原有的routerJump方法作为备选 + this.routerJump({ + businessId: item.businessId, + taskId: item.id, + type: 'update', + formCustom: item.formCustom, + formPath: item.formPath, + }); + } }, handleView(item) { @@ -409,7 +442,7 @@ export default { fullPath = `pages/${fullPath}`; } - const url = `/${fullPath}?id=${item.businessId}&type=${item.flowStatus}`; + const url = `/${fullPath}?id=${item.businessId}&type=update`; // 添加调试信息 console.log('原始formPath:', item.formPath); @@ -457,15 +490,19 @@ export default { const deleteRes = await workflowApi.deleteByInstanceIds([item.id]); if (deleteRes.code === 200) { uni.showToast({ title: '删除成功', icon: 'success' }); - this.refreshData(); + this.loading = false; + // 等待数据刷新完成 + await this.refreshData(); } else { uni.showToast({ title: deleteRes.msg || '删除失败', icon: 'error' }); + this.loading = false; } + } else { + this.loading = false; } } catch (e) { console.error('删除失败:', e); uni.showToast({ title: '删除失败', icon: 'error' }); - } finally { this.loading = false; } }, @@ -488,15 +525,19 @@ export default { const cancelRes = await workflowApi.cancelProcessApply(cancelData); if (cancelRes.code === 200) { uni.showToast({ title: '撤销成功', icon: 'success' }); - this.refreshData(); + this.loading = false; + // 等待数据刷新完成 + await this.refreshData(); } else { uni.showToast({ title: cancelRes.msg || '撤销失败', icon: 'error' }); + this.loading = false; } + } else { + this.loading = false; } } catch (e) { console.error('撤销失败:', e); uni.showToast({ title: '撤销失败', icon: 'error' }); - } finally { this.loading = false; } }, @@ -555,7 +596,11 @@ export default { }, canEdit(item) { - return item.flowStatus === 'draft' || item.flowStatus === 'back'; + return ( + item.flowStatus === 'draft' || + item.flowStatus === 'cancel' || + item.flowStatus === 'back' + ); }, canDelete(item) { @@ -707,47 +752,69 @@ export default { color: #999; } -/* 简洁卡片式样式 */ -.process-actions-card { - border-radius: 12px; -} - -.action-card-content { +/* 操作按钮样式 - 参考index.vue设计 */ +.card-actions { display: flex; - justify-content: space-between; - align-items: center; + justify-content: flex-end; + padding: 0 20rpx 0rpx 20rpx; + border-top: 1px solid #f0f0f0; + gap: 16rpx; } -.status-info { +.action-btn { display: flex; flex-direction: column; - gap: 4px; -} - -.quick-actions { - display: flex; - gap: 16px; - align-items: center; -} - -.quick-btn { - display: flex; align-items: center; justify-content: center; - transition: all 0.2s ease; - padding: 8px; - gap: 4px; + padding: 16rpx 20rpx; + border-radius: 12rpx; + background-color: #f8f9fa; + min-width: 120rpx; + transition: all 0.3s ease; +} + +.action-btn:active { + transform: scale(0.95); + opacity: 0.8; } .btn-text { - font-size: 12px; - color: #666; - margin-left: 2px; + font-size: 24rpx; + margin-top: 8rpx; + color: #303133; + font-weight: 500; } -.quick-btn:active { - transform: scale(0.9); - opacity: 0.7; +.info-btn { + background-color: rgba(60, 156, 255, 0); +} + +.info-btn .btn-text { + color: #3c9cff; +} + +.success-btn { + background-color: rgba(90, 199, 37, 0); +} + +.success-btn .btn-text { + color: #5ac725; +} + +.warning-btn { + background-color: rgba(255, 153, 0, 0); +} + +.warning-btn .btn-text { + color: #ff9900; +} + +.error-btn { + background-color: rgba(255, 77, 79, 0); +} + +.error-btn .btn-text { + color: #ff4d4f; } .pagination { From d634846222f58d5105e6d49e56659dcb339846cd Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 11 Aug 2025 10:53:47 +0800 Subject: [PATCH 13/23] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/components/FileUpload/index.vue | 4 +- .../src/components/Process/submitVerify.vue | 10 +- mobile/src/components/UserSelect/index.vue | 27 +- mobile/src/pages.json | 8 + mobile/src/pages/index/index.vue | 2 +- mobile/src/pages/task/taskWaiting.vue | 836 ++++++++++++++++++ 6 files changed, 867 insertions(+), 20 deletions(-) create mode 100644 mobile/src/pages/task/taskWaiting.vue diff --git a/mobile/src/components/FileUpload/index.vue b/mobile/src/components/FileUpload/index.vue index 165b8c9e..e13055f3 100644 --- a/mobile/src/components/FileUpload/index.vue +++ b/mobile/src/components/FileUpload/index.vue @@ -599,7 +599,7 @@ export default { From 866b510125e74a2aec3976490532e6312336b040 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 11 Aug 2025 14:58:49 +0800 Subject: [PATCH 14/23] =?UTF-8?q?=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/system/SysUserController.java | 9 ++ .../system/service/ISysUserService.java | 11 +- .../service/impl/SysUserServiceImpl.java | 106 +++++++++++++ .../src/components/Process/approvalRecord.vue | 5 +- .../src/components/Process/submitVerify.vue | 142 ++++++++++++++---- mobile/src/components/UserSelect/index.vue | 38 +++-- mobile/src/pages/index/index.vue | 3 +- mobile/src/pages/task/taskWaiting.vue | 120 +++++++++++++-- .../pages/xg/xsjcxx/xsqj/leaveEdit/index.vue | 16 +- 9 files changed, 392 insertions(+), 58 deletions(-) diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java index 87185c94..c52fcbff 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysUserController.java @@ -65,6 +65,15 @@ public class SysUserController extends BaseController { return userService.selectPageUserList(user, pageQuery); } + /** + * 获取用户列表 通过or条件查询 + */ + @SaCheckPermission("system:user:list") + @GetMapping("/listByOr") + public TableDataInfo listByOr(SysUserBo user, PageQuery pageQuery) { + return userService.selectPageUserListByOr(user, pageQuery); + } + /** * 导出用户列表 */ diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java index 1c1a2bc8..ca7d0d54 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysUserService.java @@ -20,11 +20,20 @@ public interface ISysUserService { * 根据条件分页查询用户列表 * * @param user 用户信息 - * @param pageQuery 发呢也 + * @param pageQuery 入参 * @return 用户信息 */ TableDataInfo selectPageUserList(SysUserBo user, PageQuery pageQuery); + /** + * 根据条件分页查询用户列表 通过or条件查询 + * + * @param user 用户信息 + * @param pageQuery 入参 + * @return 用户信息 + */ + TableDataInfo selectPageUserListByOr(SysUserBo user, PageQuery pageQuery); + /** * 导出用户列表 * diff --git a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java index 0195725b..a322e3d0 100644 --- a/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java +++ b/backend/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysUserServiceImpl.java @@ -68,6 +68,112 @@ public class SysUserServiceImpl implements ISysUserService, UserService { return TableDataInfo.build(page); } + @Override + public TableDataInfo selectPageUserListByOr(SysUserBo user, PageQuery pageQuery) { + Page page = baseMapper.selectPageUserList(pageQuery.build(), this.buildQueryWrapperByOr(user)); + return TableDataInfo.build(page); + } + + private Wrapper buildQueryWrapperByOr(SysUserBo user) { + Map params = user.getParams(); + QueryWrapper wrapper = Wrappers.query(); + + // 基础条件,必须保持AND + wrapper.eq("u.del_flag", SystemConstants.NORMAL); + wrapper.eq("u.status", '0'); + + // 检查是否有任何查询条件 + boolean hasAnyCondition = ObjectUtil.isNotNull(user.getUserId()) + || StringUtils.isNotBlank(user.getUserIds()) + || StringUtils.isNotBlank(user.getUserName()) + || StringUtils.isNotBlank(user.getNickName()) + // || StringUtils.isNotBlank(user.getStatus()) + || StringUtils.isNotBlank(user.getPhonenumber()) + || (params.get("beginTime") != null && params.get("endTime") != null) + || ObjectUtil.isNotNull(user.getDeptId()); + + // 只有当存在查询条件时,才构建OR条件组合 + if (hasAnyCondition) { + wrapper.and(w -> { + boolean hasCondition = false; + + // 用户ID条件 + if (ObjectUtil.isNotNull(user.getUserId())) { + w.eq("u.user_id", user.getUserId()); + hasCondition = true; + } + + // 用户ID串条件 + if (StringUtils.isNotBlank(user.getUserIds())) { + if (hasCondition) + w.or(); + w.in("u.user_id", StringUtils.splitTo(user.getUserIds(), Convert::toLong)); + hasCondition = true; + } + + // 用户名模糊查询条件 + if (StringUtils.isNotBlank(user.getUserName())) { + if (hasCondition) + w.or(); + w.like("u.user_name", user.getUserName()); + hasCondition = true; + } + + // 昵称模糊查询条件 + if (StringUtils.isNotBlank(user.getNickName())) { + if (hasCondition) + w.or(); + w.like("u.nick_name", user.getNickName()); + hasCondition = true; + } + + // 状态条件 + // if (StringUtils.isNotBlank(user.getStatus())) { + // if (hasCondition) + // w.or(); + // w.eq("u.status", user.getStatus()); + // hasCondition = true; + // } + + // 手机号模糊查询条件 + if (StringUtils.isNotBlank(user.getPhonenumber())) { + if (hasCondition) + w.or(); + w.like("u.phonenumber", user.getPhonenumber()); + hasCondition = true; + } + + // 创建时间范围条件 + if (params.get("beginTime") != null && params.get("endTime") != null) { + if (hasCondition) + w.or(); + w.between("u.create_time", params.get("beginTime"), params.get("endTime")); + hasCondition = true; + } + + // 部门ID条件(包括子部门) + if (ObjectUtil.isNotNull(user.getDeptId())) { + if (hasCondition) + w.or(); + List deptList = deptMapper.selectListByParentId(user.getDeptId()); + List ids = StreamUtils.toList(deptList, SysDept::getDeptId); + ids.add(user.getDeptId()); + w.in("u.dept_id", ids); + } + }); + } + + // 排除用户ID条件,保持AND + if (StringUtils.isNotBlank(user.getExcludeUserIds())) { + wrapper.notIn("u.user_id", StringUtils.splitTo(user.getExcludeUserIds(), Convert::toLong)); + } + + // 排序 + wrapper.orderByAsc("u.user_id"); + + return wrapper; + } + /** * 根据条件分页查询用户列表 * diff --git a/mobile/src/components/Process/approvalRecord.vue b/mobile/src/components/Process/approvalRecord.vue index 89373740..9f5074c4 100644 --- a/mobile/src/components/Process/approvalRecord.vue +++ b/mobile/src/components/Process/approvalRecord.vue @@ -6,8 +6,9 @@ :custom-style="{ width: '95vw', height: '85vh', - maxWidth: '800px', - maxHeight: '700px', + maxWidth: '95vw', + maxHeight: '85vh', + overflow: 'auto', }" @close="handleClose" > diff --git a/mobile/src/components/Process/submitVerify.vue b/mobile/src/components/Process/submitVerify.vue index 87374eda..c9a4e92b 100644 --- a/mobile/src/components/Process/submitVerify.vue +++ b/mobile/src/components/Process/submitVerify.vue @@ -7,7 +7,7 @@ height="90%" border-radius="12" @close="cancel" - :custom-style="{ width: '90vw' }" + :custom-style="{ width: '90vw', maxHeight: '90vh', overflow: 'auto' }" > @@ -248,33 +248,37 @@ width="80%" height="70%" border-radius="12" - @close="backVisible = false" + z-index="20080" + @close="closeBackDialog" :custom-style="{ width: '90vw' }" > 驳回 - + - - 驳回节点 - - - + + 驳回节点 + + + + + {{ selectedBackNodeName || '请选择驳回节点' }} + + + + @@ -342,7 +346,7 @@ 取消 @@ -359,6 +363,7 @@ width="90%" height="60%" border-radius="12" + z-index="20080" @close="deleteSignatureVisible = false" > @@ -425,6 +430,8 @@ export default { backButtonDisabled: true, taskNodeList: [], selectedBackNodeName: '', + selectedBackNodeIndex: 0, + showBackNodePicker: false, nickName: {}, nodeCode: '', buttonObj: { @@ -650,6 +657,7 @@ export default { this.backLoading = false; this.backButtonDisabled = false; if (this.taskNodeList.length > 0) { + this.selectedBackNodeIndex = 0; this.backForm.nodeCode = this.taskNodeList[0].nodeCode; this.selectedBackNodeName = this.taskNodeList[0].nodeName; } @@ -663,11 +671,26 @@ export default { } }, - // 退回节点变化 - handleBackNodeChange(e) { - const index = e.detail.value; - this.backForm.nodeCode = this.taskNodeList[index].nodeCode; - this.selectedBackNodeName = this.taskNodeList[index].nodeName; + // 确认选择驳回节点 + onBackNodeConfirm(e) { + const { indexs, value } = e; + this.selectedBackNodeIndex = indexs[0]; + this.backForm.nodeCode = value[0].nodeCode; + this.selectedBackNodeName = value[0].nodeName; + this.showBackNodePicker = false; + }, + + // 取消选择驳回节点 + onBackNodeClose() { + this.showBackNodePicker = false; + }, + + // 关闭退回弹窗 + closeBackDialog() { + this.backVisible = false; + this.showBackNodePicker = false; + this.selectedBackNodeIndex = 0; + this.selectedBackNodeName = ''; }, // 退回流程 @@ -689,7 +712,7 @@ export default { if (response.code === 200) { this.visible = false; - this.backVisible = false; + this.closeBackDialog(); this.$emit('submitCallback'); uni.showToast({ title: '操作成功', @@ -1055,21 +1078,30 @@ export default { // 显示确认对话框 showConfirm(content) { return new Promise((resolve) => { - // 临时隐藏主弹窗,避免层级冲突 + // 临时隐藏所有弹窗,避免层级冲突 const originalVisible = this.visible; + const originalBackVisible = this.backVisible; + const originalDeleteSignatureVisible = this.deleteSignatureVisible; + this.visible = false; + this.backVisible = false; + this.deleteSignatureVisible = false; uni.showModal({ title: '提示', content, success: (res) => { - // 恢复主弹窗显示状态 + // 恢复所有弹窗显示状态 this.visible = originalVisible; + this.backVisible = originalBackVisible; + this.deleteSignatureVisible = originalDeleteSignatureVisible; resolve(res); }, fail: () => { - // 恢复主弹窗显示状态 + // 恢复所有弹窗显示状态 this.visible = originalVisible; + this.backVisible = originalBackVisible; + this.deleteSignatureVisible = originalDeleteSignatureVisible; resolve({ confirm: false }); }, }); @@ -1146,6 +1178,12 @@ export default { display: flex; flex-wrap: wrap; gap: 16rpx; + max-height: 200rpx; + overflow-y: auto; + padding: 8rpx; + border: 1rpx solid #e6e6e6; + border-radius: 8rpx; + background-color: #fafafa; } .copy-tag { @@ -1300,4 +1338,50 @@ export default { padding: 0 24rpx; font-size: 24rpx; } + +.picker-input { + display: flex; + align-items: center; + justify-content: space-between; + height: 80rpx; + padding: 0 24rpx; + background-color: #f8f9fa; + border: 1rpx solid #e6e6e6; + border-radius: 8rpx; + cursor: pointer; +} + +.picker-input:active { + background-color: #f0f0f0; +} + +.picker-text { + flex: 1; + font-size: 28rpx; + color: #333; +} + +.picker-text:empty::before { + content: attr(placeholder); + color: #999; +} + +.horizontal-item { + display: flex; + align-items: center; + gap: 20rpx; +} + +.horizontal-label { + flex-shrink: 0; +} + +.horizontal-control { + flex: 1; + width: 100%; +} + +.horizontal-control .picker-input { + width: 80% !important; +} diff --git a/mobile/src/components/UserSelect/index.vue b/mobile/src/components/UserSelect/index.vue index 52eed807..0fbc95a7 100644 --- a/mobile/src/components/UserSelect/index.vue +++ b/mobile/src/components/UserSelect/index.vue @@ -6,7 +6,7 @@ height="85%" border-radius="12" @close="close" - :custom-style="{ width: '90vw' }" + :custom-style="{ width: '90vw', maxHeight: '90vh', overflow: 'auto' }" > @@ -34,7 +34,11 @@ @@ -171,7 +175,7 @@ export default { // 初始化数据 async initData() { try { - await this.getDeptList(); + // await this.getDeptList(); await this.getUserList(); await this.initSelectUser(); } catch (error) { @@ -223,6 +227,8 @@ export default { this.loadStatus = 'loading'; } + this.queryParams.status = 0; + try { // 设置查询参数 this.queryParams.userIds = this.userIds; @@ -232,22 +238,24 @@ export default { this.queryParams.userName = ''; } else { this.queryParams.userName = this.searchKeyword; + this.queryParams.nickName = this.searchKeyword; this.queryParams.phonenumber = ''; } } else { this.queryParams.userName = ''; this.queryParams.phonenumber = ''; + this.queryParams.nickName = ''; } // 设置部门ID - if (this.currentDeptIndex > 0) { - this.queryParams.deptId = this.deptList[this.currentDeptIndex].value; - } else { - this.queryParams.deptId = ''; - } + // if (this.currentDeptIndex > 0) { + // this.queryParams.deptId = this.deptList[this.currentDeptIndex].value; + // } else { + // this.queryParams.deptId = ''; + // } const response = await request({ - url: '/system/user/list', + url: '/system/user/listByOr', method: 'GET', data: this.queryParams, }); @@ -290,6 +298,7 @@ export default { // 搜索处理 handleSearch() { + console.log('执行到此-------------------'); this.getUserList(); }, @@ -364,14 +373,15 @@ export default { // 初始化已选用户 async initSelectUser() { + console.log('this.data:', this.data); if (this.data) { const userIds = this.computedIds(this.data); if (userIds.length > 0) { try { const response = await request({ - url: '/system/user/optionselect', - method: 'POST', - data: userIds, + url: '/system/user/optionselect?userIds=' + userIds, + method: 'GET', + // data: userIds, }); if (response.code === 200) { @@ -441,10 +451,16 @@ export default { border-bottom: 1rpx solid #f0f0f0; max-height: 200rpx; overflow-y: auto; + display: flex; + flex-wrap: wrap; + align-items: center; } .user-tag { margin: 8rpx 16rpx 8rpx 0; + display: inline-block; + width: auto; + flex-shrink: 0; } .search-section { diff --git a/mobile/src/pages/index/index.vue b/mobile/src/pages/index/index.vue index f350406b..74d8918d 100644 --- a/mobile/src/pages/index/index.vue +++ b/mobile/src/pages/index/index.vue @@ -76,6 +76,7 @@ + - + - + + + + @@ -224,6 +236,15 @@ export default { userList: [], // 用户列表 userSearchKeyword: '', // 用户搜索关键词 userSearchTimer: null, // 搜索防抖定时器 + + // 用户列表分页参数 + userListParams: { + pageNum: 1, + pageSize: 20, + total: 0, + }, + userListLoading: false, // 用户列表加载状态 + userLoadmoreStatus: 'loadmore', // 用户列表加载更多状态 }; }, @@ -459,6 +480,11 @@ export default { // 打开申请人选择 openUserSelect() { this.userSelectDialog.visible = true; + // 重置分页数据 + this.userListParams.pageNum = 1; + this.userListParams.total = 0; + this.userLoadmoreStatus = 'loadmore'; + this.userSearchKeyword = ''; this.loadUserList(); }, @@ -466,6 +492,10 @@ export default { cancelUserSelect() { this.userSelectDialog.visible = false; this.userSearchKeyword = ''; + // 重置用户列表分页数据 + this.userListParams.pageNum = 1; + this.userListParams.total = 0; + this.userLoadmoreStatus = 'loadmore'; }, // 确认申请人选择 @@ -505,40 +535,90 @@ export default { }, // 加载用户列表 - async loadUserList() { + async loadUserList(isLoadMore = false) { + if (!isLoadMore) { + this.userListLoading = true; + this.userListParams.pageNum = 1; + } else { + this.userLoadmoreStatus = 'loading'; + } + try { const res = await request({ - url: '/system/user/list', + url: '/system/user/listByOr', method: 'GET', params: { - pageNum: 1, - pageSize: 100, + pageNum: this.userListParams.pageNum, + pageSize: this.userListParams.pageSize, + status: '0', userName: this.userSearchKeyword || undefined, nickName: this.userSearchKeyword || undefined, }, }); if (res && res.code === 200) { - // 每次加载用户列表时,所有用户默认都不选中 - this.userList = (res.rows || []).map((user) => ({ + const newUsers = (res.rows || []).map((user) => ({ ...user, selected: false, })); - console.log('加载用户列表完成,用户数量:', this.userList.length); + + if (isLoadMore) { + // 加载更多,追加数据 + this.userList = [...this.userList, ...newUsers]; + // 判断是否还有更多数据 + if (newUsers.length < this.userListParams.pageSize) { + this.userLoadmoreStatus = 'nomore'; + } else { + this.userLoadmoreStatus = 'loadmore'; + } + } else { + // 首次加载或搜索,替换数据 + this.userList = newUsers; + this.userListParams.total = res.total || 0; + // 重置加载更多状态 + if (this.userList.length >= this.userListParams.total) { + this.userLoadmoreStatus = 'nomore'; + } else { + this.userLoadmoreStatus = 'loadmore'; + } + } + + console.log( + '加载用户列表完成,当前用户数量:', + this.userList.length, + '总数量:', + this.userListParams.total, + ); } else { - this.userList = []; + if (!isLoadMore) { + this.userList = []; + this.userListParams.total = 0; + } uni.showToast({ title: res?.msg || '获取用户列表失败', icon: 'none', }); + if (isLoadMore) { + this.userLoadmoreStatus = 'loadmore'; + } } } catch (error) { console.error('获取用户列表失败:', error); - this.userList = []; + if (!isLoadMore) { + this.userList = []; + this.userListParams.total = 0; + } uni.showToast({ title: '获取用户列表失败', icon: 'none', }); + if (isLoadMore) { + this.userLoadmoreStatus = 'loadmore'; + } + } finally { + if (!isLoadMore) { + this.userListLoading = false; + } } }, @@ -549,10 +629,24 @@ export default { } this.userSearchTimer = setTimeout(() => { + // 搜索时重置分页数据 + this.userListParams.pageNum = 1; + this.userListParams.total = 0; + this.userLoadmoreStatus = 'loadmore'; this.loadUserList(); }, 500); }, + // 加载更多用户 + loadMoreUsers() { + if (this.userLoadmoreStatus === 'nomore' || this.userLoadmoreStatus === 'loading') { + return; + } + + this.userListParams.pageNum++; + this.loadUserList(true); + }, + // 获取头像显示文字(取前两个字符) getAvatarText(nickName) { if (!nickName) return ''; @@ -769,6 +863,12 @@ export default { } } +// 用户列表的loadmore样式 +::v-deep .u-loadmore { + padding: 20rpx 0; + font-size: 24rpx; +} + .user-info { display: flex; align-items: center; diff --git a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue index 2fffc70a..d0f19e43 100644 --- a/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue +++ b/mobile/src/pages/xg/xsjcxx/xsqj/leaveEdit/index.vue @@ -3050,7 +3050,15 @@ export default { return false; } - // 1. 校验是否需要上传地理位置 + // 1. 校验请假时间 + const startTime = new Date(this.form.startTime); + const endTime = new Date(this.form.endTime); + if (endTime.getTime() <= startTime.getTime()) { + uni.showToast({ title: '请假结束时间必须大于开始时间', icon: 'none' }); + return false; + } + + // 2. 校验是否需要上传地理位置 if (leaveType.needLocationOnApply === '1') { if (!this.form.leaveLocation) { uni.showToast({ title: '该请假类型需要上传地理位置', icon: 'none' }); @@ -3058,7 +3066,7 @@ export default { } } - // 2. 校验和设置销假状态 + // 3. 校验和设置销假状态 if (leaveType.needCancelForNonLeave === '0' && this.form.isLeaveSchool === '0') { // 非离校请假且该类型不需要销假时,自动设置为不需要销假 this.form.needCancel = '0'; @@ -3067,7 +3075,7 @@ export default { this.form.needCancel = '1'; } - // 3. 校验是否可以提前请假 + // 4. 校验是否可以提前请假 if (leaveType.canApplyInAdvance === '0') { const now = new Date(); now.setHours(23, 59, 59, 999); @@ -3078,7 +3086,7 @@ export default { } } - // 4. 校验最大申请时长 + // 5. 校验最大申请时长 if (leaveType.maxApplyDuration && leaveType.maxApplyDuration > 0) { const start = new Date(this.form.startTime); const end = new Date(this.form.endTime); From ba5c521c9601b485bb358b726c9e638e94a366e1 Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 11 Aug 2025 15:14:52 +0800 Subject: [PATCH 15/23] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E9=BB=98=E8=AE=A4=E5=A4=B4=E5=83=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobile/src/pages/index/index.vue | 21 ++++++++++++++++----- mobile/src/utils/user.js | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/mobile/src/pages/index/index.vue b/mobile/src/pages/index/index.vue index 74d8918d..291c979f 100644 --- a/mobile/src/pages/index/index.vue +++ b/mobile/src/pages/index/index.vue @@ -12,11 +12,7 @@