自有人员考勤统计页面接口调试完成

This commit is contained in:
BianLzhaoMin 2026-01-04 15:14:41 +08:00
parent dd58b8cb95
commit 654880e149
22 changed files with 1357 additions and 375 deletions

View File

@ -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

View File

@ -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 URLX-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
// URLURL
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: {
// tokenURL
},
})
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)
}
// OSSURLURL
// new URL()URL
if (hasOssSignature(fullUrl)) {
// console.log('OSSURL使URL')
console.log('原始URL:', fullUrl)
return fullUrl
}
// #ifdef H5
// APIX-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('/')
//
// OSSURL使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)
}
// OSSURL
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()
// iframeload
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 = '文档加载失败'
// OSS403
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) {
// // 访iframe403
// 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>

View File

@ -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({
// PDFURLbase64
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')) {
// base64localStorageURL431
const base64Data = url.split(',')[1]
if (!base64Data) {
throw new Error('PDF base64数据为空')
}
// 使localStoragebase64URL
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使URL431
throw new Error('PDF文件过大无法存储到本地。请使用HTTP URL或联系管理员。')
}
}
if (url.startsWith('http://') || url.startsWith('https://')) {
// HTTP URL
return `${viewerUrl}?url=${encodeURIComponent(url)}`
}
// base64localStorage
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>

View File

@ -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()

View File

@ -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
}

View File

@ -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)

View File

@ -249,6 +249,7 @@ const getCurrentLocation = () => {
gc.getLocation(
point,
function (rs) {
console.log('逆地理编码结果', rs)
if (!rs || !rs.point) {
// 使
circleCenter.value = {

View File

@ -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,
})
}
// 426x7
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;

View File

@ -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({})
// PDFURL
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;
}

View File

@ -397,7 +397,7 @@ const handleAudit = async (auditStatus) => {
}
} catch (err) {
uni.hideLoading()
uni.$u.toast('审核过程中发生错误')
// uni.$u.toast('')
}
}

View File

@ -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 {

View File

@ -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('')
}
}

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -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>

View File

@ -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: [],

View File

@ -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('')
}
}

View File

@ -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',
})
}

View File

@ -56,3 +56,11 @@ export const isSmallPackageGroupAPI = (data) => {
method: 'POST',
})
}
// 获取预览合同地址
export const getPreviewContractAddressAPI = (data) => {
return realNameHttp({
url: `/workPerson/downloadWorkPerson?${initParams(data)}`,
method: 'POST',
})
}

View File

@ -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',
})
}

209
src/static/pdf-viewer.html Normal file
View File

@ -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>