自有人员考勤统计页面接口调试完成
This commit is contained in:
parent
dd58b8cb95
commit
654880e149
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@
|
|||
frameborder="0"
|
||||
@load="handleIframeLoad"
|
||||
@error="handleError"
|
||||
@loadstart="handleIframeLoadStart"
|
||||
></iframe>
|
||||
<!-- #endif -->
|
||||
|
||||
|
|
@ -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 = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,345 @@
|
|||
<template>
|
||||
<view class="pdf-preview-container">
|
||||
<!-- PDF预览区域 -->
|
||||
<view v-if="previewUrl" class="pdf-preview-wrapper">
|
||||
<!-- #ifdef H5 -->
|
||||
<!-- PC端使用iframe -->
|
||||
<iframe
|
||||
v-if="!isMobile"
|
||||
:src="previewUrl"
|
||||
class="pdf-iframe"
|
||||
frameborder="0"
|
||||
@load="handleLoad"
|
||||
@error="handleError"
|
||||
></iframe>
|
||||
<!-- 移动端使用web-view加载PDF.js viewer -->
|
||||
<web-view v-else :src="previewUrl" class="pdf-webview"></web-view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef H5 -->
|
||||
<web-view :src="previewUrl" class="pdf-webview"></web-view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<view class="loading-mask" v-if="loading">
|
||||
<view class="loading-content">
|
||||
<text class="loading-text">{{ loadingText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="error-mask" v-if="error">
|
||||
<view class="error-content">
|
||||
<text class="error-text">{{ error }}</text>
|
||||
<view class="error-btn" @click="retry">
|
||||
<text>重试</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
// PDF文件URL或base64
|
||||
src: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['load', 'error'])
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false)
|
||||
const loadingText = ref('加载中...')
|
||||
const error = ref('')
|
||||
const previewUrl = ref('')
|
||||
|
||||
// 检测是否为移动端
|
||||
const isMobile = computed(() => {
|
||||
// #ifdef H5
|
||||
if (typeof window !== 'undefined') {
|
||||
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
navigator.userAgent,
|
||||
)
|
||||
}
|
||||
// #endif
|
||||
return true // 非H5环境默认当作移动端
|
||||
})
|
||||
|
||||
// 生成唯一key
|
||||
const generateKey = () => {
|
||||
return `pdf_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||
}
|
||||
|
||||
// 清理过期的localStorage数据
|
||||
const cleanupExpiredStorage = (force = false) => {
|
||||
try {
|
||||
const now = Date.now()
|
||||
const keysToRemove = []
|
||||
|
||||
// 遍历所有localStorage项
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i)
|
||||
if (key && key.startsWith('pdf_') && key.endsWith('_expires')) {
|
||||
const expires = parseInt(localStorage.getItem(key) || '0')
|
||||
if (expires < now || force) {
|
||||
const dataKey = key.replace('_expires', '')
|
||||
keysToRemove.push(dataKey)
|
||||
keysToRemove.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除过期项
|
||||
keysToRemove.forEach((key) => {
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch (e) {
|
||||
console.warn('清理localStorage项失败:', key, e)
|
||||
}
|
||||
})
|
||||
|
||||
if (keysToRemove.length > 0) {
|
||||
console.log('清理了', keysToRemove.length / 2, '个过期的PDF缓存')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('清理localStorage失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理PDF URL - 移动端使用PDF.js viewer
|
||||
const processPdfUrl = async (url) => {
|
||||
if (!url) return ''
|
||||
|
||||
// #ifdef H5
|
||||
// PC端:直接使用原URL
|
||||
if (!isMobile.value) {
|
||||
if (url.startsWith('data:application/pdf')) {
|
||||
return url
|
||||
}
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url
|
||||
}
|
||||
return url
|
||||
}
|
||||
// #endif
|
||||
|
||||
// 移动端:使用PDF.js viewer HTML页面
|
||||
const baseUrl = window.location.origin
|
||||
const viewerUrl = `${baseUrl}/static/pdf-viewer.html`
|
||||
|
||||
if (url.startsWith('data:application/pdf')) {
|
||||
// base64数据:统一存储到localStorage,避免URL过长导致431错误
|
||||
const base64Data = url.split(',')[1]
|
||||
|
||||
if (!base64Data) {
|
||||
throw new Error('PDF base64数据为空')
|
||||
}
|
||||
|
||||
// 直接使用localStorage存储所有base64数据,避免URL长度限制
|
||||
const storageKey = generateKey()
|
||||
try {
|
||||
// 先清理旧的过期数据
|
||||
cleanupExpiredStorage()
|
||||
|
||||
// 存储到localStorage
|
||||
localStorage.setItem(storageKey, base64Data)
|
||||
// 设置过期时间(5分钟后自动清理)
|
||||
localStorage.setItem(`${storageKey}_expires`, String(Date.now() + 5 * 60 * 1000))
|
||||
|
||||
console.log('PDF数据已存储到localStorage, key:', storageKey, 'size:', base64Data.length)
|
||||
return `${viewerUrl}?key=${storageKey}`
|
||||
} catch (e) {
|
||||
console.error('localStorage存储失败:', e)
|
||||
|
||||
// 如果是存储空间不足,尝试清理一些旧数据
|
||||
if (e.name === 'QuotaExceededError' || e.code === 22) {
|
||||
console.warn('localStorage空间不足,尝试清理旧数据...')
|
||||
cleanupExpiredStorage(true) // 强制清理
|
||||
try {
|
||||
localStorage.setItem(storageKey, base64Data)
|
||||
localStorage.setItem(
|
||||
`${storageKey}_expires`,
|
||||
String(Date.now() + 5 * 60 * 1000),
|
||||
)
|
||||
console.log('清理后重新存储成功')
|
||||
return `${viewerUrl}?key=${storageKey}`
|
||||
} catch (e2) {
|
||||
console.error('清理后仍然存储失败:', e2)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果localStorage完全失败,抛出错误而不是使用URL(避免431错误)
|
||||
throw new Error('PDF文件过大,无法存储到本地。请使用HTTP URL或联系管理员。')
|
||||
}
|
||||
}
|
||||
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
// HTTP URL:直接传递
|
||||
return `${viewerUrl}?url=${encodeURIComponent(url)}`
|
||||
}
|
||||
|
||||
// 其他情况:可能是纯base64字符串,也存储到localStorage
|
||||
const storageKey = generateKey()
|
||||
try {
|
||||
localStorage.setItem(storageKey, url)
|
||||
localStorage.setItem(`${storageKey}_expires`, String(Date.now() + 5 * 60 * 1000))
|
||||
return `${viewerUrl}?key=${storageKey}`
|
||||
} catch (e) {
|
||||
console.error('localStorage存储失败:', e)
|
||||
// 如果存储失败,检查URL长度
|
||||
const testUrl = `${viewerUrl}?data=${encodeURIComponent(url)}`
|
||||
if (testUrl.length > 8000) {
|
||||
throw new Error('PDF数据过大且无法存储,请使用HTTP URL')
|
||||
}
|
||||
return testUrl
|
||||
}
|
||||
}
|
||||
|
||||
// 加载PDF
|
||||
const loadPDF = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
loadingText.value = '加载PDF文件...'
|
||||
|
||||
console.log('props.src', props.src)
|
||||
|
||||
const url = await processPdfUrl(props.src)
|
||||
|
||||
if (!url) {
|
||||
throw new Error('PDF URL不能为空')
|
||||
}
|
||||
|
||||
previewUrl.value = url
|
||||
loading.value = false
|
||||
emit('load', { url })
|
||||
} catch (err) {
|
||||
console.error('PDF加载失败:', err)
|
||||
error.value = err.message || 'PDF加载失败,请检查文件是否正确'
|
||||
loading.value = false
|
||||
emit('error', err)
|
||||
}
|
||||
}
|
||||
|
||||
// iframe/web-view加载完成
|
||||
const handleLoad = () => {
|
||||
loading.value = false
|
||||
console.log('PDF加载完成')
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
const handleError = (err) => {
|
||||
console.error('PDF预览错误:', err)
|
||||
error.value = 'PDF加载失败,请检查文件是否正确'
|
||||
loading.value = false
|
||||
emit('error', err)
|
||||
}
|
||||
|
||||
// 重试
|
||||
const retry = () => {
|
||||
error.value = ''
|
||||
previewUrl.value = ''
|
||||
loadPDF()
|
||||
}
|
||||
|
||||
// 监听src变化
|
||||
watch(
|
||||
() => props.src,
|
||||
() => {
|
||||
if (props.src) {
|
||||
console.log('src变化', props.src)
|
||||
loadPDF()
|
||||
}
|
||||
},
|
||||
{ immediate: false },
|
||||
)
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
if (props.src) {
|
||||
loadPDF()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pdf-preview-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #525252;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pdf-preview-wrapper {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pdf-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pdf-webview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.loading-mask,
|
||||
.error-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.loading-content,
|
||||
.error-content {
|
||||
background-color: #fff;
|
||||
padding: 30px 40px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.error-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 8px 20px;
|
||||
background-color: #007aff;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
|
||||
&:active {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,8 @@
|
|||
<view class="popup-actions">
|
||||
<up-button
|
||||
text="退出"
|
||||
type="default"
|
||||
type="primary"
|
||||
plain
|
||||
@tap="handleClosePieceworkModal"
|
||||
:customStyle="cancelBtnStyle"
|
||||
/>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ const getCurrentLocation = () => {
|
|||
gc.getLocation(
|
||||
point,
|
||||
function (rs) {
|
||||
console.log('逆地理编码结果', rs)
|
||||
if (!rs || !rs.point) {
|
||||
// 即使逆地理编码失败,也保存坐标信息
|
||||
circleCenter.value = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 导航栏 -->
|
||||
<NavBarModal navBarTitle="考勤统计">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
|
|
@ -9,11 +8,8 @@
|
|||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 日历区域 -->
|
||||
<view class="calendar-section">
|
||||
<!-- 月份导航 -->
|
||||
<view class="month-header">
|
||||
<view class="month-nav-btn" @tap="handlePrevMonth">
|
||||
<up-icon name="arrow-left" size="20" color="#333" />
|
||||
|
|
@ -24,14 +20,12 @@
|
|||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 星期标题 -->
|
||||
<view class="weekdays-header">
|
||||
<view v-for="(day, index) in weekdays" :key="index" class="weekday-cell">
|
||||
<text class="weekday-text">{{ day }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日历网格 -->
|
||||
<view class="calendar-grid">
|
||||
<view
|
||||
v-for="(date, index) in calendarDays"
|
||||
|
|
@ -45,17 +39,55 @@
|
|||
@tap="handleDateClick(date)"
|
||||
>
|
||||
<text class="date-number">{{ date.day }}</text>
|
||||
<text v-if="date.hasAbnormal" class="abnormal-label">异常</text>
|
||||
<text v-if="date.hasData && date.hasAbnormal" class="abnormal-label"
|
||||
>异常</text
|
||||
>
|
||||
<text
|
||||
v-else-if="date.hasData && !date.hasAbnormal"
|
||||
class="abnormal-label_normal"
|
||||
>正常</text
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期详情区域 -->
|
||||
<view class="detail-section">
|
||||
<text class="detail-date">{{ selectedDateText }}</text>
|
||||
<view class="detail-content">
|
||||
<view
|
||||
class="detail-content"
|
||||
:class="{
|
||||
'status-normal': isSelectedDateNormal,
|
||||
'status-empty': !isSelectedDateNormal && selectedDateStatus === '暂无数据',
|
||||
}"
|
||||
>
|
||||
<view class="detail-line"></view>
|
||||
<text class="detail-status">{{ selectedDateStatus }}</text>
|
||||
<view v-if="isSelectedDateNormal && currentDayInfo" class="detail-info">
|
||||
<view class="detail-time">{{ formatTime(currentDayInfo.addTime) }}</view>
|
||||
<view class="detail-type">{{
|
||||
getPunchTypeText(currentDayInfo.addType)
|
||||
}}</view>
|
||||
<view class="detail-address" v-if="currentDayInfo.address">
|
||||
<text class="detail-label">地址:</text>
|
||||
<text class="detail-value">{{ currentDayInfo.address }}</text>
|
||||
</view>
|
||||
<view class="detail-photo-row">
|
||||
<view class="detail-photo" v-if="currentDayInfo.photoPath">
|
||||
<image
|
||||
:src="getPhotoUrl(currentDayInfo.photoPath)"
|
||||
mode="aspectFill"
|
||||
class="photo-image"
|
||||
@tap="handlePreviewPhoto"
|
||||
/>
|
||||
</view>
|
||||
<view class="detail-remarks">
|
||||
<text class="detail-label">备注:</text>
|
||||
<text class="detail-value">{{
|
||||
currentDayInfo.remarks || '无'
|
||||
}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-else class="detail-status">{{ selectedDateStatus }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -64,20 +96,17 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import {
|
||||
getAttendanceStatisticsCalendarListAPI,
|
||||
getWorkerInfoByIdNumberAPI,
|
||||
} from '@/services/realName/own/attendance-statistics'
|
||||
import { useMemberStore } from '@/stores'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
/**
|
||||
* 自有考勤统计页面
|
||||
* 业务背景:用于查看自有人员的考勤统计数据
|
||||
* 设计决策:
|
||||
* 1. 使用日历视图展示考勤数据
|
||||
* 2. 支持月份切换
|
||||
* 3. 标记异常日期
|
||||
* 4. 显示选中日期的考勤详情
|
||||
*/
|
||||
|
||||
const memberStore = useMemberStore()
|
||||
const currentDayInfo = ref(null)
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
|
|
@ -86,156 +115,173 @@ const contentStyle = computed(() => {
|
|||
})
|
||||
})
|
||||
|
||||
// 星期标题
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||
|
||||
// 当前显示的月份
|
||||
const currentMonth = ref(dayjs())
|
||||
|
||||
// 选中的日期
|
||||
const selectedDate = ref(dayjs())
|
||||
const attendanceDataMap = ref(new Map())
|
||||
|
||||
// 异常日期列表(示例数据,后续从接口获取)
|
||||
const abnormalDates = ref([
|
||||
'2025-11-30',
|
||||
'2025-12-01',
|
||||
// 可以添加更多异常日期
|
||||
])
|
||||
|
||||
// 日期状态映射(示例数据,后续从接口获取)
|
||||
const dateStatusMap = ref({
|
||||
'2025-12-01': '上班未打卡',
|
||||
'2025-11-30': '下班未打卡',
|
||||
// 可以添加更多日期状态
|
||||
})
|
||||
|
||||
// 当前月份文本
|
||||
const currentMonthText = computed(() => {
|
||||
return currentMonth.value.format('YYYY年MM月')
|
||||
})
|
||||
|
||||
// 选中的日期文本
|
||||
const selectedDateText = computed(() => {
|
||||
return selectedDate.value.format('YYYY年MM月DD日')
|
||||
})
|
||||
|
||||
// 选中日期的状态
|
||||
const selectedDateStatus = computed(() => {
|
||||
const dateKey = selectedDate.value.format('YYYY-MM-DD')
|
||||
return dateStatusMap.value[dateKey] || '正常'
|
||||
if (!currentDayInfo.value) return '异常'
|
||||
return '正常'
|
||||
})
|
||||
|
||||
const isSelectedDateNormal = computed(() => {
|
||||
return !!currentDayInfo.value
|
||||
})
|
||||
|
||||
// 日历天数数组
|
||||
const calendarDays = computed(() => {
|
||||
const days = []
|
||||
const startOfMonth = currentMonth.value.startOf('month')
|
||||
const endOfMonth = currentMonth.value.endOf('month')
|
||||
const startDay = startOfMonth.day() // 0-6, 0是星期日
|
||||
const startDay = startOfMonth.day()
|
||||
|
||||
// 上个月的日期
|
||||
for (let i = startDay - 1; i >= 0; i--) {
|
||||
const date = dayjs(startOfMonth).subtract(i + 1, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
const data = attendanceDataMap.value.get(dateKey)
|
||||
const hasData = !!data
|
||||
const isAbnormal = hasData && (!data.addTimeOn || data.addTimeOn.trim() === '')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: false,
|
||||
isSelected: false,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
hasData,
|
||||
hasAbnormal: isAbnormal,
|
||||
})
|
||||
}
|
||||
|
||||
// 当前月的日期
|
||||
for (let i = 0; i < endOfMonth.date(); i++) {
|
||||
const date = dayjs(startOfMonth).add(i, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
const isSelected = date.isSame(selectedDate.value, 'day')
|
||||
const data = attendanceDataMap.value.get(dateKey)
|
||||
const hasData = !!data
|
||||
const isAbnormal = hasData && (!data.addTimeOn || data.addTimeOn.trim() === '')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: true,
|
||||
isSelected,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
hasData,
|
||||
hasAbnormal: isAbnormal,
|
||||
})
|
||||
}
|
||||
|
||||
// 下个月的日期(补齐到42个格子,6行x7列)
|
||||
const remainingDays = 42 - days.length
|
||||
for (let i = 1; i <= remainingDays; i++) {
|
||||
const date = dayjs(endOfMonth).add(i, 'day')
|
||||
const dateKey = date.format('YYYY-MM-DD')
|
||||
const data = attendanceDataMap.value.get(dateKey)
|
||||
const hasData = !!data
|
||||
const isAbnormal = hasData && (!data.addTimeOn || data.addTimeOn.trim() === '')
|
||||
days.push({
|
||||
date: date.toDate(),
|
||||
day: date.date(),
|
||||
isCurrentMonth: false,
|
||||
isSelected: false,
|
||||
hasAbnormal: abnormalDates.value.includes(dateKey),
|
||||
hasData,
|
||||
hasAbnormal: isAbnormal,
|
||||
})
|
||||
}
|
||||
|
||||
return days
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理日期点击
|
||||
* @param {Object} dateObj - 日期对象
|
||||
*/
|
||||
const handleDateClick = (dateObj) => {
|
||||
const handleDateClick = async (dateObj) => {
|
||||
selectedDate.value = dayjs(dateObj.date)
|
||||
// TODO: 加载该日期的详细考勤信息
|
||||
loadDateDetail(dateObj.date)
|
||||
getCurrentDayAttendanceData()
|
||||
}
|
||||
|
||||
const getCurrentDayAttendanceData = async () => {
|
||||
const res = await getWorkerInfoByIdNumberAPI({
|
||||
workerId: memberStore.realNameUserInfo.workerId,
|
||||
currentDay: selectedDate.value.format('YYYY-MM-DD'),
|
||||
})
|
||||
|
||||
if (res.obj === 'is null') {
|
||||
currentDayInfo.value = null
|
||||
} else {
|
||||
currentDayInfo.value = res.obj[0]
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (time) => {
|
||||
if (!time) return '--'
|
||||
return time
|
||||
}
|
||||
|
||||
const getPunchTypeText = (type) => {
|
||||
const typeMap = {
|
||||
0: '上班打卡',
|
||||
1: '下班打卡',
|
||||
}
|
||||
return typeMap[type] || '打卡'
|
||||
}
|
||||
|
||||
const getPhotoUrl = (photoPath) => {
|
||||
if (!photoPath) return ''
|
||||
if (photoPath.startsWith('http://') || photoPath.startsWith('https://')) {
|
||||
return photoPath
|
||||
}
|
||||
const baseURL = import.meta.env.VITE_API_REAL_NAME || ''
|
||||
return baseURL + (photoPath.startsWith('/') ? photoPath : '/' + photoPath)
|
||||
}
|
||||
|
||||
const handlePreviewPhoto = () => {
|
||||
if (!currentDayInfo.value?.photoPath) return
|
||||
const url = getPhotoUrl(currentDayInfo.value.photoPath)
|
||||
uni.previewImage({
|
||||
urls: [url],
|
||||
current: 0,
|
||||
fail: (err) => {
|
||||
console.error('预览图片失败:', err)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一个月
|
||||
*/
|
||||
const handlePrevMonth = () => {
|
||||
currentMonth.value = dayjs(currentMonth.value).subtract(1, 'month')
|
||||
// TODO: 重新加载该月的考勤数据
|
||||
loadMonthData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到下一个月
|
||||
*/
|
||||
const handleNextMonth = () => {
|
||||
currentMonth.value = dayjs(currentMonth.value).add(1, 'month')
|
||||
// TODO: 重新加载该月的考勤数据
|
||||
loadMonthData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载月份数据
|
||||
*/
|
||||
const loadMonthData = () => {
|
||||
// TODO: 调用接口获取该月的考勤数据
|
||||
// const res = await getAttendanceData(currentMonth.value.format('YYYY-MM'))
|
||||
// abnormalDates.value = res.abnormalDates
|
||||
// dateStatusMap.value = res.dateStatusMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载日期详情
|
||||
* @param {Date} date - 日期对象
|
||||
*/
|
||||
const loadDateDetail = (date) => {
|
||||
// TODO: 调用接口获取该日期的详细考勤信息
|
||||
// const res = await getDateDetail(dayjs(date).format('YYYY-MM-DD'))
|
||||
// 更新日期状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const loadAttendanceData = async () => {
|
||||
try {
|
||||
const res = await getAttendanceStatisticsCalendarListAPI({
|
||||
workerId: memberStore.realNameUserInfo.workerId,
|
||||
})
|
||||
if (res && res.obj && Array.isArray(res.obj)) {
|
||||
attendanceDataMap.value.clear()
|
||||
res.obj.forEach((item) => {
|
||||
if (item.currentDay) {
|
||||
attendanceDataMap.value.set(item.currentDay, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载考勤数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 默认选中今天
|
||||
selectedDate.value = dayjs()
|
||||
// 加载当前月数据
|
||||
loadMonthData()
|
||||
loadAttendanceData()
|
||||
getCurrentDayAttendanceData()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -337,9 +383,6 @@ onMounted(() => {
|
|||
|
||||
.date-cell.selected {
|
||||
background: #07c160;
|
||||
// border-radius: 50%;
|
||||
// width: 64rpx;
|
||||
// height: 64rpx;
|
||||
}
|
||||
|
||||
.date-cell.selected .date-number {
|
||||
|
|
@ -353,6 +396,12 @@ onMounted(() => {
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
.abnormal-label_normal {
|
||||
font-size: 20rpx;
|
||||
color: #07c160;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.date-cell.selected .abnormal-label {
|
||||
color: #fff;
|
||||
}
|
||||
|
|
@ -392,6 +441,14 @@ onMounted(() => {
|
|||
top: 0;
|
||||
}
|
||||
|
||||
.detail-content.status-normal .detail-line {
|
||||
background: #07c160;
|
||||
}
|
||||
|
||||
.detail-content.status-empty .detail-line {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
.detail-status {
|
||||
font-size: 28rpx;
|
||||
color: #e34d59;
|
||||
|
|
@ -399,6 +456,76 @@ onMounted(() => {
|
|||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-content.status-normal .detail-status {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.detail-content.status-empty .detail-status {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.detail-info {
|
||||
margin-left: 28rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.detail-time {
|
||||
font-size: 28rpx;
|
||||
color: #e34d59;
|
||||
margin-bottom: 16rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-content.status-normal .detail-time {
|
||||
color: #07c160;
|
||||
}
|
||||
|
||||
.detail-type {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-address {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.detail-photo-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.detail-photo {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.photo-image {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.detail-remarks {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
</view>
|
||||
|
||||
<view v-if="pdfUrl" class="document-wrapper">
|
||||
<DocumentPreview
|
||||
<!-- <DocumentPreview
|
||||
:file-url="pdfUrl"
|
||||
file-type="pdf"
|
||||
preview-service="pdfjs"
|
||||
|
|
@ -34,7 +34,9 @@
|
|||
:container-style="{ width: '100%', height: '100%' }"
|
||||
@load="handleDocumentLoad"
|
||||
@error="handleDocumentError"
|
||||
/>
|
||||
/> -->
|
||||
|
||||
<DocumentPreviewDemo :src="`data:application/pdf;base64,${pdfUrl}`" />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
|
@ -155,6 +157,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import DocumentPreview from '@/components/DocumentPreview/index.vue'
|
||||
import DocumentPreviewDemo from '@/components/DocumentPreviewDemo/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import { auditContractAPI } from '@/services/realName/contractReview'
|
||||
import { useMemberStore, useCommonStore } from '@/stores'
|
||||
|
|
@ -176,7 +179,7 @@ const contentStyle = computed(() => {
|
|||
// 合同数据
|
||||
const contractData = ref({})
|
||||
// PDF文档URL
|
||||
const pdfUrl = ref('https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf')
|
||||
const pdfUrl = ref('')
|
||||
// 视频URL
|
||||
const videoUrl = ref('')
|
||||
// 是否可审核(待审核状态)
|
||||
|
|
@ -227,7 +230,10 @@ const reviewButtonStyle = computed(() => {
|
|||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
// uni.navigateBack()
|
||||
uni.navigateTo({
|
||||
url: '/pages/work/contract-review/index',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -334,30 +340,11 @@ const loadContractPDF = async () => {
|
|||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
// TODO: 根据实际接口调整
|
||||
// const res = await realNameHttp({
|
||||
// url: `/contract/getPdf?${initParams({
|
||||
// id: contractData.value.id,
|
||||
// })}`,
|
||||
// method: 'POST',
|
||||
// })
|
||||
// if (res.res === 1 && res.obj) {
|
||||
// pdfUrl.value = res.obj.pdfUrl || res.obj.url
|
||||
// loading.value = false
|
||||
// } else {
|
||||
// error.value = res.resMsg || '获取PDF失败'
|
||||
// loading.value = false
|
||||
// }
|
||||
|
||||
// 临时测试:从合同数据中获取PDF URL
|
||||
// if (contractData.value.pdfUrl) {
|
||||
// pdfUrl.value = contractData.value.pdfUrl
|
||||
// loading.value = false
|
||||
// } else {
|
||||
// error.value = '暂无PDF文档'
|
||||
// loading.value = false
|
||||
// }
|
||||
console.log('contractData.value', contractData.value)
|
||||
pdfUrl.value = contractData.value.personPdfUrlPath
|
||||
// pdfUrl.value = contractData.value.personPdfUrl.startsWith('http')
|
||||
// ? contractData.value.personPdfUrl
|
||||
// : import.meta.env.VITE_API_FILE_ASE_URL + contractData.value.personPdfUrl
|
||||
} catch (err) {
|
||||
console.error('加载PDF失败:', err)
|
||||
error.value = '加载失败,请重试'
|
||||
|
|
@ -371,9 +358,10 @@ const loadContractPDF = async () => {
|
|||
const loadVideo = async () => {
|
||||
try {
|
||||
// 临时测试
|
||||
if (contractData.value.videoUrl) {
|
||||
videoUrl.value = import.meta.env.VITE_API_FILE_ASE_URL + contractData.value.videoUrl
|
||||
console.log(videoUrl.value, 'videoUrl.value')
|
||||
if (contractData.value.videoUrlPath) {
|
||||
videoUrl.value = contractData.value.videoUrlPath.startsWith('http')
|
||||
? contractData.value.videoUrlPath
|
||||
: import.meta.env.VITE_API_FILE_ASE_URL + contractData.value.videoUrlPath
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载视频失败:', err)
|
||||
|
|
@ -390,7 +378,7 @@ onLoad((options) => {
|
|||
|
||||
// 判断是否可以审核(待审核状态)
|
||||
canReview.value = contractData.value.isAudit == 0
|
||||
console.log(canReview.value, 'canReview.value')
|
||||
|
||||
// 加载PDF和视频
|
||||
loadContractPDF()
|
||||
loadVideo()
|
||||
|
|
@ -590,7 +578,7 @@ onLoad((options) => {
|
|||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 90%;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ const handleAudit = async (auditStatus) => {
|
|||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('审核过程中发生错误')
|
||||
// uni.$u.toast('审核过程中发生错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,20 +21,8 @@
|
|||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="error" class="error-wrapper">
|
||||
<up-icon name="close-circle" size="80" color="#e34d59" />
|
||||
<text class="error-text">{{ error }}</text>
|
||||
<up-button
|
||||
text="重试"
|
||||
type="primary"
|
||||
size="small"
|
||||
:customStyle="{ marginTop: '32rpx' }"
|
||||
@tap="loadDocument"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view v-if="pdfUrl" class="document-wrapper">
|
||||
<DocumentPreview
|
||||
<!-- <DocumentPreview
|
||||
:file-url="pdfUrl"
|
||||
file-type="pdf"
|
||||
preview-service="pdfjs"
|
||||
|
|
@ -44,7 +32,9 @@
|
|||
:container-style="{ width: '100%', height: '100%' }"
|
||||
@load="handleDocumentLoad"
|
||||
@error="handleDocumentError"
|
||||
/>
|
||||
/> -->
|
||||
|
||||
<DocumentPreviewDemo :src="`data:application/pdf;base64,${pdfUrl}`" />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
|
@ -157,6 +147,7 @@ import { ref, computed } from 'vue'
|
|||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import DocumentPreview from '@/components/DocumentPreview/index.vue'
|
||||
import DocumentPreviewDemo from '@/components/DocumentPreviewDemo/index.vue'
|
||||
import CommonPicker from '@/components/CommonPicker/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import { signContractAPI, getUserElectronicStampAPI } from '@/services/realName/contractSign'
|
||||
|
|
@ -259,9 +250,9 @@ const initDocumentOptions = () => {
|
|||
const handleDocumentChange = (option) => {
|
||||
const value = option.value || option
|
||||
if (value === 'contract') {
|
||||
loadDocument(contractData.value.personPdfUrl)
|
||||
loadDocument(contractData.value.personPdfUrlPath)
|
||||
} else if (value === 'safety') {
|
||||
loadDocument(contractData.value.aqxysPath)
|
||||
loadDocument(contractData.value.aqxysPathUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +260,10 @@ const handleDocumentChange = (option) => {
|
|||
* 返回上一页
|
||||
*/
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
// uni.navigateBack()
|
||||
uni.navigateTo({
|
||||
url: '/pages/work/contract-sign/index',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -332,17 +326,9 @@ const handleGetStamp = async () => {
|
|||
if (res.res === 1 && res.obj) {
|
||||
const imageUrl = res?.obj?.sealUrl
|
||||
stampImagePath.value = imageUrl
|
||||
if (imageUrl) {
|
||||
// 如果是相对路径,需要拼接完整URL
|
||||
if (imageUrl.startsWith('http')) {
|
||||
stampImageUrl.value = imageUrl
|
||||
} else {
|
||||
stampImageUrl.value = import.meta.env.VITE_API_FILE_ASE_URL + imageUrl
|
||||
}
|
||||
uni.$u.toast('获取公章成功')
|
||||
} else {
|
||||
uni.$u.toast('未找到公章信息')
|
||||
}
|
||||
stampImageUrl.value = res?.obj?.sealUrlPath.startsWith('http')
|
||||
? res?.obj?.sealUrlPath
|
||||
: import.meta.env.VITE_API_FILE_ASE_URL + res?.obj?.sealUrlPath
|
||||
} else {
|
||||
uni.$u.toast(res.resMsg || '获取公章失败')
|
||||
}
|
||||
|
|
@ -425,12 +411,14 @@ const loadDocument = (url) => {
|
|||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
pdfUrl.value = url
|
||||
|
||||
// 如果是相对路径,需要拼接完整URL
|
||||
if (url.startsWith('http')) {
|
||||
pdfUrl.value = url
|
||||
} else {
|
||||
pdfUrl.value = import.meta.env.VITE_API_FILE_ASE_URL + url
|
||||
}
|
||||
// if (url.startsWith('http')) {
|
||||
// pdfUrl.value = url
|
||||
// } else {
|
||||
// pdfUrl.value = import.meta.env.VITE_API_FILE_ASE_URL + url
|
||||
// }
|
||||
} catch (err) {
|
||||
console.error('加载文档失败:', err)
|
||||
error.value = '加载失败,请重试'
|
||||
|
|
@ -446,7 +434,7 @@ const loadDocument = (url) => {
|
|||
const loadVideo = () => {
|
||||
try {
|
||||
if (contractData.value.videoUrl) {
|
||||
const url = contractData.value.videoUrl
|
||||
const url = contractData.value.videoUrlPath
|
||||
if (url.startsWith('http')) {
|
||||
videoUrl.value = url
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -281,17 +281,11 @@ const handleGetStamp = async () => {
|
|||
if (res.res === 1 && res.obj) {
|
||||
const imageUrl = res?.obj?.sealUrl
|
||||
stampImagePath.value = imageUrl
|
||||
if (imageUrl) {
|
||||
// 如果是相对路径,需要拼接完整URL
|
||||
if (imageUrl.startsWith('http')) {
|
||||
stampImageUrl.value = imageUrl
|
||||
} else {
|
||||
stampImageUrl.value = import.meta.env.VITE_API_FILE_ASE_URL + imageUrl
|
||||
}
|
||||
uni.$u.toast('获取公章成功')
|
||||
} else {
|
||||
uni.$u.toast('未找到公章信息')
|
||||
}
|
||||
stampImageUrl.value = res?.obj?.sealUrlPath.startsWith('http')
|
||||
? res?.obj?.sealUrlPath
|
||||
: import.meta.env.VITE_API_FILE_ASE_URL + res?.obj?.sealUrlPath
|
||||
|
||||
console.log(stampImageUrl.value, '1--++')
|
||||
} else {
|
||||
uni.$u.toast(res.resMsg || '获取公章失败')
|
||||
}
|
||||
|
|
@ -435,7 +429,7 @@ const handleSign = async () => {
|
|||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('签署过程中发生错误')
|
||||
// uni.$u.toast('签署过程中发生错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,15 +16,15 @@
|
|||
<up-form :model="form" ref="formRef" label-position="top" label-width="auto">
|
||||
<!-- 人脸上传 -->
|
||||
<view class="section">
|
||||
<view class="section-title">人脸识别</view>
|
||||
<up-upload
|
||||
<view class="section-title">身份证识别</view>
|
||||
<!-- <up-upload
|
||||
:fileList="faceFile"
|
||||
:maxCount="1"
|
||||
accept="image"
|
||||
:capture="['camera']"
|
||||
@afterRead="handleFaceUpload"
|
||||
@delete="handleFaceDelete"
|
||||
/>
|
||||
/> -->
|
||||
</view>
|
||||
|
||||
<view
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
<up-input
|
||||
clearable
|
||||
v-model.trim="iptIdNumber"
|
||||
placeholder="请输入法定代表人"
|
||||
placeholder="请输入身份证查询基础信息"
|
||||
/>
|
||||
</view>
|
||||
<view>
|
||||
|
|
@ -65,6 +65,7 @@
|
|||
disabled
|
||||
v-model="form.partyA"
|
||||
placeholder="甲方公司名称"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
|
|
@ -73,7 +74,12 @@
|
|||
:borderBottom="true"
|
||||
required
|
||||
>
|
||||
<up-input disabled v-model="form.partAAdress" placeholder="地址" />
|
||||
<up-input
|
||||
disabled
|
||||
v-model="form.partAAdress"
|
||||
placeholder="地址"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
label="法定代表人"
|
||||
|
|
@ -85,6 +91,7 @@
|
|||
disabled
|
||||
v-model="form.legalPerson"
|
||||
placeholder="法定代表人"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item label="联系电话" :borderBottom="true">
|
||||
|
|
@ -92,6 +99,7 @@
|
|||
disabled
|
||||
v-model="form.partAPhone"
|
||||
placeholder="联系电话"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
|
|
@ -100,7 +108,12 @@
|
|||
:borderBottom="true"
|
||||
required
|
||||
>
|
||||
<up-input disabled v-model="form.partyB" placeholder="劳动者姓名" />
|
||||
<up-input
|
||||
disabled
|
||||
v-model="form.partyB"
|
||||
placeholder="劳动者姓名"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
label="身份证号"
|
||||
|
|
@ -112,6 +125,7 @@
|
|||
disabled
|
||||
v-model="form.partBIdCard"
|
||||
placeholder="身份证号"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
|
|
@ -124,6 +138,7 @@
|
|||
disabled
|
||||
v-model="form.partBPhone"
|
||||
placeholder="乙方电话"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
|
|
@ -136,6 +151,7 @@
|
|||
disabled
|
||||
v-model="form.partBAdress"
|
||||
placeholder="联系住址"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item
|
||||
|
|
@ -159,13 +175,19 @@
|
|||
prop="workTask"
|
||||
:borderBottom="true"
|
||||
>
|
||||
<up-input disabled v-model="form.workTask" placeholder="工作岗位" />
|
||||
<up-input
|
||||
disabled
|
||||
v-model="form.workTask"
|
||||
placeholder="工作岗位"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
<up-form-item label="工作地点" prop="workAdress" :borderBottom="true">
|
||||
<up-input
|
||||
disabled
|
||||
v-model="form.workAdress"
|
||||
placeholder="工作地点"
|
||||
disabledColor="#F5F5F5"
|
||||
/>
|
||||
</up-form-item>
|
||||
|
||||
|
|
@ -430,6 +452,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import { realNameHttp } from '@/utils/realNameHttp'
|
||||
import { useMemberStore } from '@/stores'
|
||||
import {
|
||||
getWorkerInfoByIdNumberAPI,
|
||||
getWorkerPhotoByIdNumberAPI,
|
||||
|
|
@ -443,6 +466,7 @@ import Signature from '@/components/Signature/index.vue'
|
|||
import CommonPicker from '@/components/CommonPicker/index.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
|
||||
const memberStore = useMemberStore()
|
||||
const iptIdNumber = ref('')
|
||||
const videoInfo = ref(null)
|
||||
const shortMessageImp = ref('')
|
||||
|
|
@ -552,36 +576,39 @@ const handleFaceDelete = () => {
|
|||
// 附件(全部为pdf)
|
||||
const attachments = ref([
|
||||
{
|
||||
url: 'http://192.168.0.14:1917/hnAma/gzRealName/contract/pdf/2025/12/22/1766388427803.pdf',
|
||||
url: '',
|
||||
isSign: false,
|
||||
fileType: '1',
|
||||
title: '施工人员健康承诺书',
|
||||
initPath: '',
|
||||
},
|
||||
{
|
||||
url: 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf',
|
||||
url: '',
|
||||
isSign: false,
|
||||
fileType: '2',
|
||||
title: '安全协议书',
|
||||
initPath: '',
|
||||
},
|
||||
{
|
||||
title: '施工人员安全告知书',
|
||||
fileType: '3',
|
||||
isSign: false,
|
||||
// url: 'http://192.168.0.14:1917/hnAma/gzRealName/contract/pdf/施工人员安全告知书.pdf',
|
||||
// url: 'http://192.168.0.14:1917/hnAma/gzRealName/contract/pdf/施工人员安全告知书.pdf',
|
||||
url: 'http://192.168.0.38:18080/bnscloud/realnameapp/gzRealName/contract/pdf/施工人员安全告知书.pdf',
|
||||
url: ``,
|
||||
initPath: '',
|
||||
},
|
||||
{
|
||||
url: 'http://bns-cloud.oss-cn-beijing.aliyuncs.com/upload/app/lsdPhoto/pdf/2025/12/24/01d319cb-a09f-4fe6-a16e-b34491913c94.pdf?Expires=1766631029&OSSAccessKeyId=TMP.3KnER1yK3SmuHqhpR7cyButgj9YipTKQazvma5ESj1VAHwJDZY83yvgyMFjra1SWYEFcbrmrQhRcoSD65kfTzuPQrkGPzY&Signature=qRCf3f4qPiWbmKDZu5%2BdVoL5ZCU%3D',
|
||||
url: '',
|
||||
isSign: false,
|
||||
fileType: '5',
|
||||
title: '签订用工协议承诺书',
|
||||
initPath: '',
|
||||
},
|
||||
{
|
||||
url: 'https://bns-cloud.oss-cn-beijing.aliyuncs.com/upload/app/lsdPhoto/pdf/2025/12/24/01d319cb-a09f-4fe6-a16e-b34491913c94.pdf?Expires=1766630782&OSSAccessKeyId=TMP.3KnER1yK3SmuHqhpR7cyButgj9YipTKQazvma5ESj1VAHwJDZY83yvgyMFjra1SWYEFcbrmrQhRcoSD65kfTzuPQrkGPzY&Signature=Lg%2FHxLITIMnaQnWfjGOb1h5rcxc%3D',
|
||||
url: '',
|
||||
isSign: false,
|
||||
fileType: '4',
|
||||
title: '安全承诺书',
|
||||
initPath: '',
|
||||
},
|
||||
])
|
||||
|
||||
|
|
@ -661,7 +688,7 @@ const handlePreview = (item) => {
|
|||
}
|
||||
|
||||
// 兜底:H5 等环境 eventChannel 不可用时,使用全局事件
|
||||
const handleSignedFallback = ({ fileType, newFileUrl, startDate, endDate }) => {
|
||||
const handleSignedFallback = ({ fileType, newFileUrl, startDate, endDate, initPath }) => {
|
||||
const target = attachments.value.find((attach) => attach.fileType === fileType)
|
||||
if (target) {
|
||||
target.isSign = true
|
||||
|
|
@ -670,6 +697,7 @@ const handleSignedFallback = ({ fileType, newFileUrl, startDate, endDate }) => {
|
|||
|
||||
if (fileType != 3) {
|
||||
target.url = newFileUrl
|
||||
target.initPath = initPath
|
||||
}
|
||||
|
||||
if (fileType == 2) {
|
||||
|
|
@ -679,6 +707,19 @@ const handleSignedFallback = ({ fileType, newFileUrl, startDate, endDate }) => {
|
|||
|
||||
onMounted(() => {
|
||||
uni.$on('contract-signed', handleSignedFallback)
|
||||
|
||||
uni.request({
|
||||
url: `${import.meta.env.VITE_API_FILE_ASE_URL}api/resource/getResourceFile?filePath=aqgzs.pdf&token=${memberStore.realNameUserInfo.token}`,
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
console.log(res)
|
||||
attachments.value[2].url = res?.data?.base64
|
||||
console.log(attachments.value[2].url)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
@ -780,7 +821,7 @@ const handlePreviewContract = async () => {
|
|||
partBSign: form.value.signatureUrl,
|
||||
message,
|
||||
signingDate: nowDate,
|
||||
faceUrl: videoInfo.value?.faceUrl || '',
|
||||
faceUrl: form.value?.faceUrl || '',
|
||||
messageTime: nowDateTime,
|
||||
shortMessage,
|
||||
proId: form.value?.proId,
|
||||
|
|
@ -823,7 +864,7 @@ const handlePreviewContract = async () => {
|
|||
}
|
||||
|
||||
uni.navigateTo({
|
||||
url: `/pages/work/contract/contractPreview/index?contractUrl=${result.obj}&fileType=pdf&personInfo=${encodeURIComponent(JSON.stringify(getFileAddressParams.value))}&otherInfo=${encodeURIComponent(JSON.stringify(otherInfo))}&isContract=true&attachments=${encodeURIComponent(JSON.stringify(attachments.value))}`,
|
||||
url: `/pages/work/contract/contractPreview/index?contractUrl=${result.resMsg}&contractNewUrl=${encodeURIComponent(result.obj)}&fileType=pdf&personInfo=${encodeURIComponent(JSON.stringify(getFileAddressParams.value))}&otherInfo=${encodeURIComponent(JSON.stringify(otherInfo))}&isContract=true&attachments=${encodeURIComponent(JSON.stringify(attachments.value))}`,
|
||||
})
|
||||
} catch (error) {
|
||||
uni.$u.toast(error?.data?.obj || '预览失败')
|
||||
|
|
@ -902,19 +943,20 @@ const handleGetWorkerInfo = async () => {
|
|||
getFileAddressParams.value.partBPhone = phone
|
||||
getFileAddressParams.value.effectDate = form.value.effectDate
|
||||
|
||||
// const file1 = await getFileAddressFun('1') // 施工人员健康承诺书
|
||||
// const file2 = await getFileAddressFun('2') // 安全协议书
|
||||
// const file4 = await getFileAddressFun('5') // 签订用工协议承诺书
|
||||
// const file5 = await getFileAddressFun('4') // 安全承诺书
|
||||
const file1 = await getFileAddressFun('1') // 施工人员健康承诺书
|
||||
const file2 = await getFileAddressFun('2') // 安全协议书
|
||||
const file4 = await getFileAddressFun('5') // 签订用工协议承诺书
|
||||
const file5 = await getFileAddressFun('4') // 安全承诺书
|
||||
|
||||
// const fileList = [file1, file2, file4, file5]
|
||||
const fileList = [file1, file2, file4, file5]
|
||||
|
||||
// attachments.value.forEach((e) => {
|
||||
// const file = fileList.find((f) => f.type === e.fileType)
|
||||
// if (file) {
|
||||
// e.url = file.url
|
||||
// }
|
||||
// })
|
||||
attachments.value.forEach((e) => {
|
||||
const file = fileList.find((f) => f.type === e.fileType)
|
||||
if (file) {
|
||||
e.url = file.url
|
||||
e.initPath = file.initPath
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const { obj: photoRes } = await getWorkerPhotoByIdNumberAPI({
|
||||
|
|
@ -922,10 +964,10 @@ const handleGetWorkerInfo = async () => {
|
|||
})
|
||||
|
||||
if (photoRes !== 'is null') {
|
||||
const { signaturePhoto, facePhoto } = photoRes
|
||||
form.value.signaturePath = signaturePhoto.startsWith('http')
|
||||
? signaturePhoto
|
||||
: import.meta.env.VITE_API_FILE_ASE_URL + signaturePhoto
|
||||
const { signaturePhoto, facePhoto, signaturePhotoUrl } = photoRes
|
||||
form.value.signaturePath = signaturePhotoUrl.startsWith('http')
|
||||
? signaturePhotoUrl
|
||||
: import.meta.env.VITE_API_FILE_ASE_URL + signaturePhotoUrl
|
||||
form.value.signatureUrl = signaturePhoto
|
||||
form.value.faceUrl = facePhoto
|
||||
}
|
||||
|
|
@ -933,9 +975,10 @@ const handleGetWorkerInfo = async () => {
|
|||
|
||||
// 获取附件信息
|
||||
const getFileAddressFun = async (type) => {
|
||||
const { resMsg: res } = await getFileAddressAPI({ ...getFileAddressParams.value, type })
|
||||
const res = await getFileAddressAPI({ ...getFileAddressParams.value, type })
|
||||
return {
|
||||
url: res,
|
||||
url: res?.obj.bast64,
|
||||
initPath: res?.obj.url,
|
||||
type,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
|
||||
<!-- 文档预览 -->
|
||||
<view v-else-if="documentUrl" class="document-wrapper">
|
||||
<DocumentPreview
|
||||
<!-- <DocumentPreview
|
||||
:file-url="documentUrl"
|
||||
file-type="pdf"
|
||||
preview-service="pdfjs"
|
||||
|
|
@ -42,7 +42,9 @@
|
|||
:container-style="{ width: '100%', height: '100%' }"
|
||||
@load="handleDocumentLoad"
|
||||
@error="handleDocumentError"
|
||||
/>
|
||||
/> -->
|
||||
|
||||
<DocumentPreviewDemo :src="`data:application/pdf;base64,${documentUrl}`" />
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
|
|
@ -80,10 +82,14 @@ import { ref, computed } from 'vue'
|
|||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import DocumentPreview from '@/components/DocumentPreview/index.vue'
|
||||
import DocumentPreviewDemo from '@/components/DocumentPreviewDemo/index.vue'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import { realNameHttp } from '@/utils/realNameHttp'
|
||||
import { initParams } from '@/utils/initParams'
|
||||
import { useMemberStore } from '@/stores'
|
||||
import { getPreviewContractAddressAPI } from '@/services/realName/contract.js'
|
||||
|
||||
const memberStore = useMemberStore()
|
||||
const contentStyle = computed(() => {
|
||||
return getContentStyle({
|
||||
includeNavBar: true,
|
||||
|
|
@ -132,44 +138,19 @@ const loadContractDocument = async () => {
|
|||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
// TODO: 等后台接口准备好后,取消注释以下代码
|
||||
// if (!contractData.value?.templateValue) {
|
||||
// error.value = '缺少合同模板信息'
|
||||
// loading.value = false
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 调用接口获取合同文档
|
||||
// const res = await realNameHttp({
|
||||
// url: `/contract/getDocument?${initParams({
|
||||
// templateId: contractData.value.templateValue,
|
||||
// })}`,
|
||||
// method: 'POST',
|
||||
// })
|
||||
|
||||
// if (res.res === 1 && res.obj) {
|
||||
// const fileUrl = res.obj.documentUrl || res.obj.fileUrl || res.obj.url
|
||||
// if (fileUrl) {
|
||||
// pdfUrl.value = fileUrl
|
||||
// } else {
|
||||
// error.value = '未获取到文档地址'
|
||||
// }
|
||||
// } else {
|
||||
// error.value = res.resMsg || '获取合同文档失败'
|
||||
// }
|
||||
|
||||
// 临时使用公开的测试文档链接
|
||||
// 使用一个公开可访问的Word文档示例
|
||||
const testDocumentUrl =
|
||||
'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'
|
||||
// 'https://calibre-ebook.com/downloads/demos/demo.docx'
|
||||
// 模拟接口延迟
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
|
||||
documentUrl.value = testDocumentUrl
|
||||
uni.request({
|
||||
url: `${import.meta.env.VITE_API_FILE_ASE_URL}api/resource/getResourceFile?filePath=ygxy.pdf&token=${memberStore.realNameUserInfo.token}`,
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
console.log(res)
|
||||
documentUrl.value = res?.data?.base64
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(err)
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('加载合同文档失败:', err)
|
||||
error.value = '加载失败,请重试'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,34 @@
|
|||
<template>
|
||||
<view class="page-container">
|
||||
<NavBarModal :navBarTitle="navTitle">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
<view>
|
||||
<view class="page-container">
|
||||
<NavBarModal :navBarTitle="navTitle">
|
||||
<template #left>
|
||||
<view class="back-btn" @tap="handleBack">
|
||||
<up-icon name="arrow-left" size="20" color="#fff" />
|
||||
</view>
|
||||
</template>
|
||||
</NavBarModal>
|
||||
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<view class="file-header">
|
||||
<text class="file-title">{{ title }}</text>
|
||||
<view v-if="attachmentOptions.length > 1" class="file-picker">
|
||||
<view class="content-wrapper" :style="contentStyle">
|
||||
<!-- 单独文件预览模式:显示标题 -->
|
||||
<view v-if="!isContractMode" class="file-header">
|
||||
<text class="file-title">{{ title }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 合同预览模式:显示选择框(始终显示,即使只有一个附件) -->
|
||||
<view
|
||||
v-if="isContractMode && attachmentOptions.length > 0"
|
||||
class="attachment-selector"
|
||||
>
|
||||
<CommonPicker
|
||||
placeholder="选择附件"
|
||||
:options="attachmentOptions"
|
||||
v-model="currentAttachmentValue"
|
||||
:placeholder="currentAttachmentLabel"
|
||||
@change="handleAttachmentChange"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<DocumentPreview
|
||||
|
||||
<!-- <DocumentPreview
|
||||
:file-url="fileUrl"
|
||||
file-type="pdf"
|
||||
:auto-load="true"
|
||||
|
|
@ -30,32 +38,35 @@
|
|||
@load="handleDocumentLoad"
|
||||
@error="handleDocumentError"
|
||||
:container-style="{ width: '100%', height: '100%' }"
|
||||
/>
|
||||
</view>
|
||||
/> -->
|
||||
|
||||
<view class="bottom-bar">
|
||||
<up-button
|
||||
v-if="isContractMode"
|
||||
text="确定提交"
|
||||
type="primary"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleConfirmContract"
|
||||
/>
|
||||
<up-button
|
||||
v-else-if="isSafeNotice"
|
||||
:text="readButtonText"
|
||||
type="primary"
|
||||
:disabled="readCountdown > 0 || !!previewError"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleReadConfirm"
|
||||
/>
|
||||
<up-button
|
||||
v-else
|
||||
text="去签订"
|
||||
type="primary"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleOpenSignatureModal"
|
||||
/>
|
||||
<DocumentPreviewDemo :src="`data:application/pdf;base64,${fileUrl}`" />
|
||||
</view>
|
||||
|
||||
<view class="bottom-bar">
|
||||
<up-button
|
||||
v-if="isContractMode"
|
||||
text="确定提交"
|
||||
type="primary"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleConfirmContract"
|
||||
/>
|
||||
<up-button
|
||||
v-else-if="isSafeNotice"
|
||||
:text="readButtonText"
|
||||
type="primary"
|
||||
:disabled="readCountdown > 0 || !!previewError"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleReadConfirm"
|
||||
/>
|
||||
<up-button
|
||||
v-else
|
||||
:text="isSign ? '重新签订' : '去签订'"
|
||||
type="primary"
|
||||
:customStyle="primaryButtonStyle"
|
||||
@tap="handleOpenSignatureModal"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<up-popup
|
||||
|
|
@ -110,7 +121,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getContentStyle } from '@/utils/safeArea'
|
||||
import { realNameHttp } from '@/utils/realNameHttp'
|
||||
|
|
@ -119,6 +130,7 @@ import { signProtocolAPI, signContractAPI } from '@/services/realName/contract'
|
|||
import dayjs from 'dayjs'
|
||||
import NavBarModal from '@/components/NavBarModal/index.vue'
|
||||
import DocumentPreview from '@/components/DocumentPreview/index.vue'
|
||||
import DocumentPreviewDemo from '@/components/DocumentPreviewDemo/index.vue'
|
||||
import Signature from '@/components/Signature/index.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
import CommonPicker from '@/components/CommonPicker/index.vue'
|
||||
|
|
@ -135,6 +147,7 @@ const fileUrl = ref('')
|
|||
const signatureRef = ref(null)
|
||||
const attachmentType = ref('')
|
||||
const contractUrl = ref('')
|
||||
const contractNewUrl = ref('')
|
||||
const isContract = ref(false)
|
||||
const attachments = ref([])
|
||||
const title = ref('')
|
||||
|
|
@ -178,6 +191,12 @@ const attachmentOptions = computed(() =>
|
|||
value: index,
|
||||
})),
|
||||
)
|
||||
const currentAttachmentLabel = computed(() => {
|
||||
const option = attachmentOptions.value.find(
|
||||
(item) => item.value === currentAttachmentValue.value,
|
||||
)
|
||||
return option?.label || '选择附件'
|
||||
})
|
||||
|
||||
const handleBack = () => {
|
||||
uni.navigateBack()
|
||||
|
|
@ -188,7 +207,9 @@ onLoad((options) => {
|
|||
|
||||
// 合同预览模式(整合同预览)
|
||||
if (isContract.value && options.contractUrl) {
|
||||
// console.log(options, '1--++997')
|
||||
contractUrl.value = decodeURIComponent(options.contractUrl)
|
||||
contractNewUrl.value = decodeURIComponent(options.contractNewUrl)
|
||||
otherInfo.value = options.otherInfo ? JSON.parse(decodeURIComponent(options.otherInfo)) : {}
|
||||
personInfo.value = options.personInfo
|
||||
? JSON.parse(decodeURIComponent(options.personInfo))
|
||||
|
|
@ -212,20 +233,13 @@ onLoad((options) => {
|
|||
title: item.title || '附件',
|
||||
url: item.url,
|
||||
fileType: item.fileType || '',
|
||||
initPath: item.initPath || '',
|
||||
}))
|
||||
|
||||
// // 在最前面插入合同信息
|
||||
// if (contractUrl.value) {
|
||||
// attachmentList.unshift({
|
||||
// title: '合同信息',
|
||||
// url: contractUrl.value,
|
||||
// fileType: 'contract',
|
||||
// })
|
||||
// }
|
||||
|
||||
attachments.value = attachmentList
|
||||
|
||||
attachments.value.unshift({
|
||||
url: contractUrl.value,
|
||||
url: contractNewUrl.value,
|
||||
title: '合同信息',
|
||||
fileType: 'contract',
|
||||
})
|
||||
|
|
@ -237,7 +251,7 @@ onLoad((options) => {
|
|||
title.value = first.title
|
||||
attachmentType.value = first.fileType || ''
|
||||
} else {
|
||||
fileUrl.value = contractUrl.value
|
||||
fileUrl.value = contractNewUrl.value
|
||||
title.value = '合同信息'
|
||||
}
|
||||
} else if (options.fileUrl) {
|
||||
|
|
@ -245,7 +259,7 @@ onLoad((options) => {
|
|||
fileUrl.value = decodeURIComponent(options?.fileUrl)
|
||||
attachmentType.value = options.attachmentType || ''
|
||||
title.value = options.title ? decodeURIComponent(options.title) : ''
|
||||
isSign.value = options?.isSign === 'true'
|
||||
isSign.value = options?.isSign
|
||||
personInfo.value = options.personInfo
|
||||
? JSON.parse(decodeURIComponent(options.personInfo))
|
||||
: {}
|
||||
|
|
@ -285,7 +299,8 @@ const handleDocumentError = (err) => {
|
|||
// 切换附件
|
||||
const getAttachmentUrlByType = (fileType) => {
|
||||
const target = attachments.value.find((item) => String(item.fileType) === String(fileType))
|
||||
return target?.url || ''
|
||||
// console.log(target, '996666')
|
||||
return target?.initPath || ''
|
||||
}
|
||||
|
||||
const handleAttachmentChange = (option) => {
|
||||
|
|
@ -319,28 +334,23 @@ const stopCountdown = () => {
|
|||
}
|
||||
}
|
||||
|
||||
const emitSigned = (url, type, startDate = '', endDate = '') => {
|
||||
const emitSigned = (newFileUrl = '', type, startDate = '', endDate = '', initPath) => {
|
||||
const payload = {
|
||||
fileType: attachmentType.value,
|
||||
title: title.value,
|
||||
newFileUrl: url,
|
||||
newFileUrl: newFileUrl,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
initPath,
|
||||
}
|
||||
|
||||
if (eventChannel.value?.emit) {
|
||||
eventChannel.value.emit('signed', payload)
|
||||
console.log('已发送签订事件(eventChannel):', payload)
|
||||
} else {
|
||||
console.warn('eventChannel 不可用,使用全局事件兜底通知父页面')
|
||||
uni.$emit('contract-signed', payload)
|
||||
}
|
||||
uni.$emit('contract-signed', payload)
|
||||
}
|
||||
|
||||
const handleReadConfirm = () => {
|
||||
if (readCountdown.value > 0) return
|
||||
isSign.value = true
|
||||
emitSigned()
|
||||
emitSigned('', attachmentType.value)
|
||||
uni.$u.toast('已阅读并确认')
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
|
@ -387,7 +397,7 @@ const handleConfirmContract = async () => {
|
|||
shortMessage: o.shortMessage || '',
|
||||
proId: o.proId,
|
||||
// 新生成的个人合同 pdf 地址
|
||||
personPdfUrl: p.personPdfUrl || finalUrl,
|
||||
personPdfUrl: finalUrl,
|
||||
contractTemplateType: o.contractTemplateType,
|
||||
agreedMethod: o.agreedMethod || '',
|
||||
secondContent: o.secondContent || '',
|
||||
|
|
@ -400,8 +410,6 @@ const handleConfirmContract = async () => {
|
|||
isXbg: o.isXbg ?? '',
|
||||
}
|
||||
|
||||
console.log('params组装的参数', params)
|
||||
|
||||
try {
|
||||
const res = await signContractAPI(params)
|
||||
if (res.res === 1) {
|
||||
|
|
@ -451,7 +459,7 @@ const handleConfirmSignature = async () => {
|
|||
if (signatureRef.value?.exportSignature) {
|
||||
const tempPath = await signatureRef.value.exportSignature()
|
||||
signaturePath.value = tempPath || ''
|
||||
console.log('signaturePath', signaturePath.value)
|
||||
// console.log('signaturePath', signaturePath.value)
|
||||
|
||||
try {
|
||||
const uploadRes = await realNameHttp.uploadFile({
|
||||
|
|
@ -465,8 +473,6 @@ const handleConfirmSignature = async () => {
|
|||
|
||||
if (uploadRes.res === 1 && uploadRes.obj) {
|
||||
handleSignProtocol(uploadRes.obj, attachmentType.value)
|
||||
} else {
|
||||
uni.$u.toast(uploadRes.resMsg || '上传失败')
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
|
@ -497,9 +503,9 @@ const handleSignProtocol = async (signaturePath, type) => {
|
|||
}
|
||||
const result = await signProtocolAPI(params)
|
||||
if (result.res === 1) {
|
||||
console.log(result, 'result签订协议成功')
|
||||
// console.log(result, 'result签订协议成功')
|
||||
// 通知父页面更新附件签订状态
|
||||
emitSigned(result.resMsg, type, params.startDate, params.endDate, result.obj)
|
||||
emitSigned(result.obj.bast64, type, params.startDate, params.endDate, result.obj.url)
|
||||
uni.$u.toast('签订协议成功')
|
||||
// 延迟返回,确保事件已发送
|
||||
setTimeout(() => {
|
||||
|
|
@ -517,6 +523,12 @@ const handleSignProtocol = async (signaturePath, type) => {
|
|||
onUnmounted(() => {
|
||||
stopCountdown()
|
||||
})
|
||||
|
||||
watch(isSafeNotice, (newVal) => {
|
||||
if (newVal) {
|
||||
handleDocumentLoad()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -528,21 +540,32 @@ onUnmounted(() => {
|
|||
}
|
||||
|
||||
.content-wrapper {
|
||||
// flex: 1;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// overflow: hidden;
|
||||
}
|
||||
|
||||
.file-header {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.file-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
padding: 12rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.attachment-selector {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #e5e5e5;
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@
|
|||
mode="scaleToFill"
|
||||
class="current-photo"
|
||||
/>
|
||||
|
||||
<!-- <up-image
|
||||
v-if="currentFacePhoto"
|
||||
:src="currentFacePhoto"
|
||||
mode="scaleToFill"
|
||||
class="current-photo"
|
||||
/> -->
|
||||
<view v-else class="photo-placeholder">
|
||||
<up-icon name="image" size="48" color="#ccc" />
|
||||
<text class="placeholder-text">暂无照片</text>
|
||||
|
|
|
|||
|
|
@ -513,25 +513,25 @@ const jobTypeColumns = computed(() => {
|
|||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
name: '伞兵一号-卢本伟',
|
||||
idNumber: '36042419920927085X',
|
||||
birthday: '1992-09-27',
|
||||
age: '26',
|
||||
ethnic: '汉族',
|
||||
gender: '男',
|
||||
address: '江西省抚州市临川区',
|
||||
signDate: '2025-12-11',
|
||||
expiryDate: '2025-12-11',
|
||||
issueAuthority: '江西省抚州市临川区公安局',
|
||||
name: '',
|
||||
idNumber: '',
|
||||
birthday: '',
|
||||
age: '',
|
||||
ethnic: '',
|
||||
gender: '',
|
||||
address: '',
|
||||
signDate: '',
|
||||
expiryDate: '',
|
||||
issueAuthority: '',
|
||||
projectName: '',
|
||||
projectId: '',
|
||||
subcontractor: '',
|
||||
subcontractorId: '',
|
||||
team: '',
|
||||
teamId: '',
|
||||
jobType: '技工',
|
||||
jobType: '',
|
||||
jobTypeId: '',
|
||||
contact: '13656235623',
|
||||
contact: '',
|
||||
facePhoto: [],
|
||||
facePhotoPath: '',
|
||||
frontPhoto: [],
|
||||
|
|
|
|||
|
|
@ -333,17 +333,29 @@ const handleAudit = async (examineStatus) => {
|
|||
// 获取所有选中的人员
|
||||
const checkedList = list.value.filter((item) => item.isChecked)
|
||||
|
||||
if (checkedList.length === 0) {
|
||||
// 判断是否为单个审核场景
|
||||
let singleAuditItem = null
|
||||
|
||||
// 场景1:直接点击卡片打开弹框(没有勾选复选框)
|
||||
if (checkedList.length === 0 && !isViewDetail.value && detail.value?.idNumber) {
|
||||
singleAuditItem = detail.value
|
||||
}
|
||||
// 场景2:勾选了单个复选框
|
||||
else if (checkedList.length === 1) {
|
||||
singleAuditItem = checkedList[0]
|
||||
}
|
||||
// 场景3:查看详情模式但没有选择人员
|
||||
else if (checkedList.length === 0 && isViewDetail.value) {
|
||||
uni.$u.toast('请选择要审核的人员')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是单个审核,直接调用
|
||||
if (checkedList.length === 1) {
|
||||
if (singleAuditItem) {
|
||||
try {
|
||||
const res = await auditPersonAPI({
|
||||
examineStatus,
|
||||
idNumber: checkedList[0].idNumber,
|
||||
idNumber: singleAuditItem.idNumber,
|
||||
proId: commonStore.activeProjectId,
|
||||
examineUser: memberStore.realNameUserInfo.id,
|
||||
examineRemark: reviewRemark.value,
|
||||
|
|
@ -354,6 +366,7 @@ const handleAudit = async (examineStatus) => {
|
|||
uni.$u.toast('审核成功')
|
||||
loadList()
|
||||
showDetail.value = false
|
||||
reviewRemark.value = '' // 清空备注
|
||||
} else {
|
||||
uni.$u.toast(res.resMsg || '审核失败')
|
||||
}
|
||||
|
|
@ -430,7 +443,7 @@ const handleAudit = async (examineStatus) => {
|
|||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading()
|
||||
uni.$u.toast('审核过程中发生错误')
|
||||
// uni.$u.toast('审核过程中发生错误')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,19 @@ export const attendancePunchConfirmAPI = (data) => {
|
|||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// 判断是否是小包干班组
|
||||
export const isSmallPackageGroupAPI = (data) => {
|
||||
return realNameHttp({
|
||||
url: `/personAtt/getTeamType?${initParams(data)}`,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// 查询桩位
|
||||
export const getProcessListAPI = (data) => {
|
||||
return realNameHttp({
|
||||
url: `/personAtt/getLumpSumContractAndProcess?${initParams(data)}`,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,3 +56,11 @@ export const isSmallPackageGroupAPI = (data) => {
|
|||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// 获取预览合同地址
|
||||
export const getPreviewContractAddressAPI = (data) => {
|
||||
return realNameHttp({
|
||||
url: `/workPerson/downloadWorkPerson?${initParams(data)}`,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
// 自有人员 考勤统计
|
||||
import { realNameHttp } from '@/utils/realNameHttp'
|
||||
import { initParams } from '@/utils/initParams'
|
||||
|
||||
// 根据日期查询当天考勤统计数据
|
||||
export const getWorkerInfoByIdNumberAPI = (data) => {
|
||||
return realNameHttp({
|
||||
url: `/statistics/getDataByDay?${initParams(data)}`,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
// 查询考勤统计日历列表数据
|
||||
export const getAttendanceStatisticsCalendarListAPI = (data) => {
|
||||
return realNameHttp({
|
||||
url: `/statistics/getStatisticsDataByworkerId?${initParams(data)}`,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<title>PDF预览</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #525252;
|
||||
}
|
||||
|
||||
#pdf-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.pdf-page {
|
||||
margin: 10px auto;
|
||||
display: block;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="loading" class="loading">加载中...</div>
|
||||
<div id="error" class="error" style="display: none;"></div>
|
||||
<div id="pdf-container"></div>
|
||||
|
||||
<script src="https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.min.js"></script>
|
||||
<script>
|
||||
// 配置PDF.js worker
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@3.11.174/build/pdf.worker.min.js';
|
||||
|
||||
// 从URL参数获取PDF数据
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const pdfData = urlParams.get('data');
|
||||
const pdfUrl = urlParams.get('url');
|
||||
const storageKey = urlParams.get('key'); // 从localStorage读取的key
|
||||
|
||||
const container = document.getElementById('pdf-container');
|
||||
const loadingEl = document.getElementById('loading');
|
||||
const errorEl = document.getElementById('error');
|
||||
|
||||
// 从localStorage读取数据并清理
|
||||
function getDataFromStorage(key) {
|
||||
try {
|
||||
const data = localStorage.getItem(key);
|
||||
const expires = localStorage.getItem(`${key}_expires`);
|
||||
|
||||
// 检查是否过期
|
||||
if (expires && Date.now() > parseInt(expires)) {
|
||||
localStorage.removeItem(key);
|
||||
localStorage.removeItem(`${key}_expires`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 清理数据
|
||||
if (data) {
|
||||
localStorage.removeItem(key);
|
||||
localStorage.removeItem(`${key}_expires`);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (e) {
|
||||
console.error('读取localStorage失败:', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载PDF
|
||||
async function loadPDF() {
|
||||
try {
|
||||
let pdfDocumentSource = null;
|
||||
let base64Data = null;
|
||||
|
||||
// 优先使用url参数(HTTP URL)
|
||||
if (pdfUrl) {
|
||||
pdfDocumentSource = {
|
||||
url: decodeURIComponent(pdfUrl),
|
||||
};
|
||||
} else if (storageKey) {
|
||||
// 从localStorage读取base64数据
|
||||
base64Data = getDataFromStorage(storageKey);
|
||||
if (!base64Data) {
|
||||
throw new Error('PDF数据已过期或不存在');
|
||||
}
|
||||
} else if (pdfData) {
|
||||
// 使用data参数(base64,小文件)
|
||||
base64Data = pdfData;
|
||||
} else {
|
||||
throw new Error('未提供PDF数据');
|
||||
}
|
||||
|
||||
// 如果有base64数据,转换为Uint8Array
|
||||
if (base64Data) {
|
||||
const binaryString = atob(base64Data);
|
||||
const bytes = new Uint8Array(binaryString.length);
|
||||
for (let i = 0; i < binaryString.length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
pdfDocumentSource = { data: bytes };
|
||||
}
|
||||
|
||||
// 加载PDF文档
|
||||
const loadingTask = pdfjsLib.getDocument({
|
||||
...pdfDocumentSource,
|
||||
cMapUrl: 'https://unpkg.com/pdfjs-dist@3.11.174/cmaps/',
|
||||
cMapPacked: true,
|
||||
});
|
||||
|
||||
const pdf = await loadingTask.promise;
|
||||
const numPages = pdf.numPages;
|
||||
|
||||
// 获取设备像素比(高DPI屏幕通常是2或3)
|
||||
const devicePixelRatio = window.devicePixelRatio || 1;
|
||||
// 使用更高的像素比来提升清晰度(但不超过3,避免内存问题)
|
||||
const outputScale = Math.min(devicePixelRatio, 3);
|
||||
|
||||
// 计算缩放比例(适应屏幕宽度)
|
||||
const firstPage = await pdf.getPage(1);
|
||||
const viewport = firstPage.getViewport({ scale: 1 });
|
||||
const screenWidth = window.innerWidth - 20; // 留出边距
|
||||
// 提高基础缩放比例,确保清晰度
|
||||
const baseScale = Math.min(screenWidth / viewport.width, 2.0);
|
||||
|
||||
// 渲染所有页面
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
// 用于显示的viewport(用于CSS尺寸和渲染)
|
||||
const displayViewport = page.getViewport({ scale: baseScale });
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
// 设置canvas的实际分辨率(高分辨率,考虑设备像素比)
|
||||
canvas.width = Math.floor(displayViewport.width * outputScale);
|
||||
canvas.height = Math.floor(displayViewport.height * outputScale);
|
||||
|
||||
// 设置canvas的显示尺寸(低分辨率,用于CSS)
|
||||
canvas.style.width = displayViewport.width + 'px';
|
||||
canvas.style.height = displayViewport.height + 'px';
|
||||
|
||||
canvas.className = 'pdf-page';
|
||||
|
||||
// 优化canvas渲染质量
|
||||
context.imageSmoothingEnabled = true;
|
||||
context.imageSmoothingQuality = 'high';
|
||||
|
||||
// 缩放context以匹配高分辨率canvas
|
||||
context.scale(outputScale, outputScale);
|
||||
|
||||
// 使用显示viewport进行渲染(context已经缩放,所以实际渲染是高分辨率)
|
||||
await page.render({
|
||||
canvasContext: context,
|
||||
viewport: displayViewport,
|
||||
}).promise;
|
||||
|
||||
container.appendChild(canvas);
|
||||
}
|
||||
|
||||
loadingEl.style.display = 'none';
|
||||
} catch (error) {
|
||||
console.error('PDF加载失败:', error);
|
||||
loadingEl.style.display = 'none';
|
||||
errorEl.style.display = 'block';
|
||||
errorEl.textContent = 'PDF加载失败: ' + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后开始加载PDF
|
||||
window.addEventListener('DOMContentLoaded', loadPDF);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Loading…
Reference in New Issue