diff --git a/.env.development b/.env.development index e4d0676..0554304 100644 --- a/.env.development +++ b/.env.development @@ -2,7 +2,7 @@ VITE_API_BASE_URL = /api VITE_API_REAL_NAME = /bmw VITE_API_LEADER = /leader -VITE_API_FILE_ASE_URL = http://192.168.0.14:1917/hnAma/ +VITE_API_FILE_ASE_URL = http://192.168.0.38:18080/bnscloud/realnameapp/ # VITE_API_BASE_URL = http://192.168.0.14:1999/hd-real-name # VITE_API_BASE_URL = http://192.168.0.234:38080/hd-real-name # VITE_API_BASE_URL = http://192.168.0.234:38080 diff --git a/src/components/DocumentPreview/index.vue b/src/components/DocumentPreview/index.vue index bc83ed3..d7def40 100644 --- a/src/components/DocumentPreview/index.vue +++ b/src/components/DocumentPreview/index.vue @@ -39,6 +39,7 @@ frameborder="0" @load="handleIframeLoad" @error="handleError" + @loadstart="handleIframeLoadStart" > @@ -154,6 +155,8 @@ const emit = defineEmits(['load', 'error', 'retry']) // 预览URL const previewUrl = ref('') +// Blob URL(用于处理X-Frame-Options问题) +const blobUrl = ref('') // 加载状态 const loading = ref(false) // 错误信息 @@ -183,25 +186,121 @@ const detectFileType = (url) => { /** * 获取完整的文件URL + * 注意:对于包含OSS签名的URL,必须保持原始编码,不做任何处理 */ const getFullUrl = (url) => { if (!url) return '' - // 如果已经是完整URL,直接返回 + // 如果已经是完整URL,直接返回(保持原始编码) if (url.startsWith('http://') || url.startsWith('https://')) { return url } - // 拼接基础URL + // 拼接基础URL(仅对非完整URL进行拼接) const base = props.baseURL || import.meta.env.VITE_API_BASE_URL || '' return base + (url.startsWith('/') ? url : '/' + url) } +/** + * 检查URL是否包含OSS签名参数 + */ +const hasOssSignature = (url) => { + return url.includes('Expires=') && url.includes('OSSAccessKeyId=') && url.includes('Signature=') +} + +/** + * 检查OSS签名URL是否过期 + */ +const isOssUrlExpired = (url) => { + if (!hasOssSignature(url)) { + return false + } + + try { + const expiresMatch = url.match(/Expires=(\d+)/) + if (expiresMatch) { + const expiresTimestamp = parseInt(expiresMatch[1], 10) + const currentTimestamp = Math.floor(Date.now() / 1000) + // 如果过期时间小于当前时间,或者剩余时间少于5分钟,认为已过期 + return expiresTimestamp < currentTimestamp || expiresTimestamp - currentTimestamp < 300 + } + } catch (e) { + console.error('检查OSS URL过期时间失败:', e) + } + + return false +} + +/** + * 检查URL是否是API接口(可能设置了X-Frame-Options) + */ +const isApiEndpoint = (url) => { + if (!url) return false + // 检查是否包含常见的API路径标识 + return url.includes('/api/') || url.includes('/resource/') || url.includes('/getResourceFile') +} + +/** + * 通过fetch下载文件并创建blob URL + */ +const createBlobUrl = async (url) => { + try { + const response = await fetch(url, { + method: 'GET', + headers: { + // 如果需要token,可以从URL中提取或通过其他方式传递 + }, + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const blob = await response.blob() + const blobUrlValue = URL.createObjectURL(blob) + blobUrl.value = blobUrlValue + return blobUrlValue + } catch (err) { + console.error('创建blob URL失败:', err) + throw err + } +} + /** * 获取PDF预览URL */ -const getPdfPreviewUrl = (url) => { - const fullUrl = getFullUrl(url) +const getPdfPreviewUrl = async (url) => { + // 先获取完整URL,但保持原始编码 + // 对于已经是完整URL的情况,直接使用,不进行任何处理 + let fullUrl = url + if (!url.startsWith('http://') && !url.startsWith('https://')) { + // 只有非完整URL才进行拼接 + const base = props.baseURL || import.meta.env.VITE_API_BASE_URL || '' + fullUrl = base + (url.startsWith('/') ? url : '/' + url) + } + + // 重要:如果是OSS签名URL,必须保持原始编码,不进行任何URL解析操作 + // 提前检测并直接返回,避免new URL()等操作导致URL被解码 + if (hasOssSignature(fullUrl)) { + // console.log('检测到OSS签名URL,使用直接预览模式避免签名过期和URL解码问题') + console.log('原始URL:', fullUrl) + return fullUrl + } + + // #ifdef H5 + // 如果是API接口,可能设置了X-Frame-Options,需要使用blob URL + if (isApiEndpoint(fullUrl)) { + try { + const blobUrlValue = await createBlobUrl(fullUrl) + console.log('检测到API接口,使用blob URL避免X-Frame-Options问题') + return blobUrlValue + } catch (err) { + console.error('创建blob URL失败,尝试直接使用原URL:', err) + // 如果创建blob URL失败,回退到直接使用原URL + return fullUrl + } + } + // #endif // 使用 pdf.js viewer 方案(适用于微信内置浏览器无法直接预览的场景) if (props.previewService === 'pdfjs') { @@ -210,7 +309,7 @@ const getPdfPreviewUrl = (url) => { // 只有https的外部地址才使用pdfjs viewer const isLocalOrHttp = fullUrl.startsWith('http://') || fullUrl.startsWith('/') - // 检查是否是同源地址 + // 检查是否是同源地址(注意:这里已经排除了OSS签名URL,所以可以安全使用new URL) let isSameOrigin = false try { if (typeof window !== 'undefined' && window.location) { @@ -241,7 +340,17 @@ const getPdfPreviewUrl = (url) => { * 获取Word预览URL */ const getWordPreviewUrl = (url) => { - const fullUrl = getFullUrl(url) + // 先获取完整URL,但保持原始编码 + let fullUrl = url + if (!url.startsWith('http://') && !url.startsWith('https://')) { + const base = props.baseURL || import.meta.env.VITE_API_BASE_URL || '' + fullUrl = base + (url.startsWith('/') ? url : '/' + url) + } + + // 重要:如果是OSS签名URL,必须保持原始编码,直接返回 + if (hasOssSignature(fullUrl)) { + return fullUrl + } // 方案1: 直接返回URL(可能触发下载) if (props.previewService === 'direct') { @@ -264,7 +373,7 @@ const getWordPreviewUrl = (url) => { /** * 获取预览URL */ -const getPreviewUrl = () => { +const getPreviewUrl = async () => { if (!props.fileUrl) { return '' } @@ -272,7 +381,7 @@ const getPreviewUrl = () => { const fileType = props.fileType === 'auto' ? detectFileType(props.fileUrl) : props.fileType if (fileType === 'pdf') { - return getPdfPreviewUrl(props.fileUrl) + return await getPdfPreviewUrl(props.fileUrl) } else if (fileType === 'word') { return getWordPreviewUrl(props.fileUrl) } @@ -283,7 +392,7 @@ const getPreviewUrl = () => { /** * 加载文档 */ -const loadDocument = () => { +const loadDocument = async () => { if (!props.fileUrl) { error.value = '文件地址不能为空' loading.value = false @@ -304,8 +413,8 @@ const loadDocument = () => { return } - // 获取预览URL - previewUrl.value = getPreviewUrl() + // 获取预览URL(可能是异步的,如果使用了blob URL) + previewUrl.value = await getPreviewUrl() if (!previewUrl.value) { error.value = '无法生成预览地址' @@ -341,17 +450,57 @@ const loadDocument = () => { } } catch (err) { console.error('加载文档失败:', err) - error.value = '加载失败,请重试' + error.value = err.message || '加载失败,请重试' loading.value = false } } +/** + * 处理iframe开始加载 + */ +const handleIframeLoadStart = (event) => { + console.log('iframe开始加载', event) + // 可以在这里添加加载开始的处理逻辑 +} + /** * 处理iframe加载完成 */ const handleIframeLoad = (event) => { console.log('iframe加载完成', event) - handleLoad() + + // 延迟检查iframe内容,因为load事件可能在内容完全加载前触发 + setTimeout(() => { + try { + const iframe = event.target + if (iframe && iframe.contentWindow) { + // 尝试检查iframe是否加载成功 + // 如果iframe内容加载失败,可能会抛出错误 + const iframeDoc = iframe.contentDocument || iframe.contentWindow.document + if (iframeDoc) { + // 检查是否有错误信息 + const body = iframeDoc.body + if (body && body.textContent) { + const text = body.textContent.toLowerCase() + if ( + text.includes('403') || + text.includes('forbidden') || + text.includes('expired') + ) { + handleError(event) + return + } + } + } + } + } catch (e) { + // 跨域访问iframe内容会报错,这是正常的,说明可能加载成功 + // 或者确实是跨域问题,但这不是403错误 + console.log('无法访问iframe内容(可能是跨域):', e.message) + } + + handleLoad() + }, 500) } /** @@ -378,7 +527,7 @@ const handleLoad = () => { /** * 处理加载错误 */ -const handleError = () => { +const handleError = (event) => { // 清除超时定时器 if (loadTimeout) { clearTimeout(loadTimeout) @@ -386,11 +535,33 @@ const handleError = () => { } loading.value = false - error.value = '文档加载失败' + + // 检查是否是OSS签名过期导致的403错误 + const fullUrl = getFullUrl(props.fileUrl) + let errorMessage = '文档加载失败' + + // if (hasOssSignature(fullUrl) && isOssUrlExpired(fullUrl)) { + // errorMessage = '文档链接已过期,请刷新页面重试' + // } else if (event?.target?.src) { + // // 检查iframe的错误 + // try { + // const iframe = event.target + // if (iframe.contentWindow) { + // // 尝试访问iframe内容来判断是否是403错误 + // console.error('PDF加载失败,可能是签名过期或权限问题') + // errorMessage = '文档加载失败,可能是链接已过期' + // } + // } catch (e) { + // // 跨域访问iframe内容会报错,这是正常的 + // console.error('PDF加载失败:', e) + // } + // } + + error.value = errorMessage emit('error', { fileUrl: props.fileUrl, fileType: currentFileType.value, - error: '文档加载失败', + error: errorMessage, }) } @@ -424,7 +595,8 @@ const handleRetry = () => { * 下载文档 */ const handleDownload = () => { - const fullUrl = getFullUrl(props.fileUrl) + // const fullUrl = getFullUrl(props.fileUrl) + const fullUrl = props.fileUrl // #ifdef H5 // H5环境:直接打开新窗口下载 @@ -478,12 +650,17 @@ watch( }, ) -// 组件卸载时清除定时器 +// 组件卸载时清除定时器和blob URL onUnmounted(() => { if (loadTimeout) { clearTimeout(loadTimeout) loadTimeout = null } + // 释放blob URL,避免内存泄漏 + if (blobUrl.value) { + URL.revokeObjectURL(blobUrl.value) + blobUrl.value = '' + } }) diff --git a/src/components/DocumentPreviewDemo/index.vue b/src/components/DocumentPreviewDemo/index.vue new file mode 100644 index 0000000..8be5ec9 --- /dev/null +++ b/src/components/DocumentPreviewDemo/index.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index 20fcd12..98667c7 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -273,6 +273,12 @@ const showModule = (label) => { .find((item) => item.name === label) } +const handlePdfDebug = () => { + uni.navigateTo({ + url: '/pages/home/pdf-debug/index', + }) +} + onLoad(async () => { checkFromRedirect() diff --git a/src/pages/own/attendance-punch/confirm/index.vue b/src/pages/own/attendance-punch/confirm/index.vue index ee0a8c3..31aef19 100644 --- a/src/pages/own/attendance-punch/confirm/index.vue +++ b/src/pages/own/attendance-punch/confirm/index.vue @@ -116,6 +116,7 @@ const commonStore = useCommonStore() const punchType = ref(1) // 打卡类型 1:计日 2:计件 3:窝工 const contractId = ref('') // 合同ID(仅计件打卡) const gxId = ref('') // 工序桩位ID(仅计件打卡) +const gxName = ref('') // 工序桩位名称(仅计件打卡) const longitude = ref('') // 经度 const latitude = ref('') // 纬度 const address = ref('') // 地址 @@ -337,7 +338,7 @@ const handleSubmit = async () => { address: address.value || '', // 计件相关 - gxName: '', // 后端暂未提供名称,先留空 + gxName: '', gxId: gxId.value || '', // 其他字段 @@ -380,6 +381,7 @@ const handleBack = () => { } onLoad((options) => { + console.log(options, 'options参数') // 获取传递的参数 if (options.punchType) { punchType.value = parseInt(options.punchType) @@ -390,6 +392,9 @@ onLoad((options) => { if (options.gxId) { gxId.value = options.gxId } + if (options.gxName) { + gxName.value = options.gxName + } if (options.longitude) { longitude.value = options.longitude } diff --git a/src/pages/own/attendance-punch/index.vue b/src/pages/own/attendance-punch/index.vue index 75d83ee..cbea04c 100644 --- a/src/pages/own/attendance-punch/index.vue +++ b/src/pages/own/attendance-punch/index.vue @@ -105,7 +105,8 @@ @@ -145,7 +146,13 @@ import { ref, computed } from 'vue' import NavBarModal from '@/components/NavBarModal/index.vue' import { getContentStyle, getSafeAreaInfo } from '@/utils/safeArea' +import { useMemberStore } from '@/stores' +import { + isSmallPackageGroupAPI, + getProcessListAPI, +} from '@/services/realName/attendance/attendance-punch' +const memberStore = useMemberStore() const contentStyle = computed(() => { return getContentStyle({ includeNavBar: true, @@ -191,18 +198,10 @@ const showContractPicker = ref(false) const showProcessPicker = ref(false) // 合同列表(示例数据,需要从API获取) -const contractList = ref([ - { id: '1', name: '612-小袁-001' }, - { id: '2', name: '612-小袁-002' }, - // TODO: 从API获取合同列表 -]) +const contractList = ref([]) // 工序桩位列表(示例数据,需要从API获取) -const processList = ref([ - { id: '1', name: '工序:包干工序1 => 桩位:桩位3' }, - { id: '2', name: '工序:包干工序2 => 桩位:桩位5' }, - // TODO: 从API获取工序桩位列表 -]) +const processList = ref([]) // 合同选择器列 const contractColumns = computed(() => { @@ -342,6 +341,7 @@ const getCurrentLocation = () => { gc.getLocation( point, function (rs) { + console.log('逆地理编码结果', rs) if (!rs || !rs.point) { // 即使逆地理编码失败,也保存坐标信息 const lng = Number(point.lng) || 0 @@ -460,7 +460,7 @@ const handleRefresh = () => { * 处理打卡按钮点击 * @param {Number} type - 打卡类型 1:计日 2:计件 3:窝工 */ -const handlePunchClick = (type) => { +const handlePunchClick = async (type) => { // if (!hasLocation.value) { // uni.$u.toast('请先获取位置信息') // return @@ -470,7 +470,38 @@ const handlePunchClick = (type) => { if (type === 2) { // 计件打卡,需要先选择合同和工序桩位 - showPieceworkModal.value = true + // 判断是否是小包干班组 + const res = await isSmallPackageGroupAPI({ + idNumber: memberStore.realNameUserInfo.idNumber, + }) + + if (res.res === 1 && res.resMsg === '小包干班组') { + // 获取合同 + const contractRes = await getProcessListAPI({ + idNumber: memberStore.realNameUserInfo.idNumber, + }) + + console.log('contractRes', contractRes) + + if (contractRes?.obj?.contract && contractRes?.obj?.process) { + const { contract, process } = contractRes.obj + + contractList.value = contract.map((item) => ({ + id: item.key, + name: item.value1 || '', + })) + + processList.value = process[0].processList.map((item) => ({ + id: item.key, + name: `工序:${item.value1 || ''} -- 桩位:${item.value2 || ''}`, + })) + } + + // // 显示弹框 + showPieceworkModal.value = true + } else { + uni.$u.toast('您所属班组非小包干班组,不能进行计件打卡') + } } else { // 计日和窝工,直接跳转到确认页面 navigateToConfirm() @@ -558,6 +589,7 @@ const navigateToConfirm = () => { if (punchType.value === 2) { params.contractId = selectedContractId.value params.gxId = selectedProcessId.value + params.gxName = selectedProcess.value } const queryString = Object.keys(params) diff --git a/src/pages/own/attendance-punch/location/index.vue b/src/pages/own/attendance-punch/location/index.vue index 8020284..4d03da9 100644 --- a/src/pages/own/attendance-punch/location/index.vue +++ b/src/pages/own/attendance-punch/location/index.vue @@ -249,6 +249,7 @@ const getCurrentLocation = () => { gc.getLocation( point, function (rs) { + console.log('逆地理编码结果', rs) if (!rs || !rs.point) { // 即使逆地理编码失败,也保存坐标信息 circleCenter.value = { diff --git a/src/pages/own/attendance-statistics/index.vue b/src/pages/own/attendance-statistics/index.vue index 3b68123..16de790 100644 --- a/src/pages/own/attendance-statistics/index.vue +++ b/src/pages/own/attendance-statistics/index.vue @@ -1,6 +1,5 @@