index.vue 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <route lang="json5" type="page">
  2. {
  3. layout: 'default',
  4. style: {
  5. navigationBarTitleText: '安全检查记录详情',
  6. navigationStyle: 'custom',
  7. },
  8. }
  9. </route>
  10. <template>
  11. <view class="detail-container">
  12. <!-- 导航栏 -->
  13. <NavBar>
  14. <template #title>
  15. <HeadView v-if="renderCenter" title="操作指导书批准详情" :btn-array="btnArray" />
  16. <text v-else class="nav-title">安全检查记录详情</text>
  17. </template>
  18. </NavBar>
  19. <!-- PDF 预览区域 -->
  20. <view class="pdf-container">
  21. <view v-if="loading" class="loading-container">
  22. <text class="loading-text">加载中...</text>
  23. </view>
  24. <!-- #ifdef H5 -->
  25. <view v-else-if="pdfUrl" id="pdf-viewer-container" class="pdf-viewer-container"></view>
  26. <!-- #endif -->
  27. <!-- #ifndef H5 -->
  28. <web-view v-else-if="pdfUrl" :src="pdfUrl" />
  29. <!-- #endif -->
  30. <view v-else class="empty-container">
  31. <text class="empty-text">暂无PDF</text>
  32. </view>
  33. </view>
  34. </view>
  35. </template>
  36. <script lang="ts" setup>
  37. import { ref, computed, onMounted, watch, nextTick } from 'vue'
  38. import { onLoad } from '@dcloudio/uni-app'
  39. import { getSafetyCheckRecordTemplate, getServiceOrderPDF } from '@/api/task'
  40. import { useUserStore } from '@/store/user'
  41. import { isH5 } from '@/utils/platform'
  42. import dayjs from 'dayjs'
  43. import HeadView from '../components/HeadView.vue'
  44. import NavBar from '@/components/NavBar/NavBar.vue'
  45. // 路由参数
  46. const detailItem = ref<any>({})
  47. const orderId = ref('')
  48. const unitContact = ref('')
  49. const unitPhone = ref('')
  50. const receiverEmail = ref('')
  51. const templateId = ref('')
  52. onLoad((options) => {
  53. orderId.value = options.orderId || ''
  54. unitContact.value = options.unitContact || ''
  55. unitPhone.value = options.unitPhone || ''
  56. receiverEmail.value = options.receiverEmail || ''
  57. templateId.value = options.templateId || ''
  58. if (options.detailItem) {
  59. try {
  60. detailItem.value = JSON.parse(decodeURIComponent(options.detailItem))
  61. } catch (e) {
  62. console.error('解析 detailItem 失败:', e)
  63. }
  64. }
  65. })
  66. // 用户信息
  67. const userStore = useUserStore()
  68. // 状态
  69. const loading = ref(false)
  70. const pdfUrl = ref('')
  71. const templateParams = ref<any>({})
  72. // 是否显示操作按钮 (对应 PJ 中的 useMemo 计算)
  73. const renderCenter = computed(() => {
  74. const { id } = detailItem.value || {}
  75. if (!id) return false
  76. return true
  77. })
  78. // 操作按钮数组 (与 PJ 版本一致,空数组)
  79. const btnArray = computed(() => {
  80. const { signFilePdf, id } = detailItem.value || {}
  81. if (!id) return []
  82. return [
  83. // { value: signFilePdf ? '重新签名' : '签名', color: 'rgb(47,142,255)', click: handleToSign },
  84. ]
  85. })
  86. // Blob 转 Base64
  87. const blobToBase64 = (blob: Blob): Promise<string> => {
  88. return new Promise((resolve, reject) => {
  89. const reader = new FileReader()
  90. reader.onloadend = () => {
  91. const base64data = reader.result as string
  92. resolve(base64data.split(',')[1])
  93. }
  94. reader.onerror = reject
  95. reader.readAsDataURL(blob)
  96. })
  97. }
  98. // 获取模板
  99. const handleGetTemplate = async () => {
  100. const { nickname } = userStore.userInfo || {}
  101. const response = await getSafetyCheckRecordTemplate({ orderId: orderId.value })
  102. const result = response?.data || {}
  103. const dataStr = result.dataSource ? JSON.parse(result.dataSource) : {}
  104. const temPar = {
  105. ...result,
  106. templateUrl: result.recordTemplateUrl,
  107. dataSource: JSON.stringify({
  108. ...dataStr,
  109. signName: nickname,
  110. signDate: dayjs().format('YYYY年MM月DD日'),
  111. }),
  112. }
  113. templateParams.value = temPar
  114. return temPar
  115. }
  116. // 获取安全检查记录 PDF
  117. const getAppServiceOrderPDF = async (tplParams: any) => {
  118. const { signFilePdf, id } = detailItem.value || {}
  119. const { dataSource, templateUrl } = tplParams || {}
  120. if (signFilePdf) {
  121. pdfUrl.value = buildFileUrl(signFilePdf)
  122. return
  123. }
  124. const params = {
  125. dataStr: dataSource,
  126. templateUrl,
  127. orderId: orderId.value,
  128. securityCheckId: id,
  129. }
  130. const response: any = await getServiceOrderPDF(params)
  131. const base64Data = await blobToBase64(response)
  132. const filePath = `data:application/pdf;base64,${base64Data}`
  133. if (filePath) {
  134. pdfUrl.value = filePath
  135. }
  136. }
  137. // 构建文件 URL
  138. const buildFileUrl = (fileId: string) => {
  139. // const baseUrl = uni.getStorageSync('BASE_URL') || 'http://192.168.0.113:40080'
  140. const baseUrl = 'https://youdao.hofo.co/dexdev/'
  141. return `${baseUrl}${fileId}`
  142. }
  143. // 签名 (保留但不使用,与 PJ 版本一致)
  144. const handleToSign = async () => {
  145. const { signFilePdf, id } = detailItem.value || {}
  146. if (!id) return
  147. const url = signFilePdf ? '/pages/sign-detail/index' : '/pages/sign/index'
  148. uni.redirectTo({
  149. url: `${url}?orderId=${orderId.value}&type=AQJC&securityCheckId=${id}&unitContact=${unitContact.value}&unitPhone=${unitPhone.value}&receiverEmail=${receiverEmail.value}&templateId=${templateId.value}`,
  150. })
  151. }
  152. // 初始化
  153. const init = async () => {
  154. loading.value = true
  155. try {
  156. const tplParams = await handleGetTemplate()
  157. await getAppServiceOrderPDF(tplParams)
  158. } catch (error) {
  159. console.error('[Page] 初始化失败:', error)
  160. uni.showToast({ title: 'PDF 加载失败', icon: 'none' })
  161. } finally {
  162. loading.value = false
  163. }
  164. }
  165. // 返回
  166. const goBack = () => {
  167. uni.navigateBack()
  168. }
  169. // H5 端 PDF 渲染
  170. const initH5PdfViewer = async () => {
  171. if (!pdfUrl.value) return
  172. const container = document.getElementById('pdf-viewer-container')
  173. if (!container) return
  174. container.innerHTML = ''
  175. const isBase64 = pdfUrl.value.startsWith('data:application/pdf;base64,')
  176. if (!(window as any).pdfjsLib) {
  177. const script = document.createElement('script')
  178. script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'
  179. script.onerror = () => uni.showToast({ title: 'PDF.js 加载失败', icon: 'none' })
  180. document.head.appendChild(script)
  181. await new Promise<void>((resolve, reject) => {
  182. script.onload = () => resolve()
  183. script.onerror = reject
  184. })
  185. }
  186. try {
  187. const pdfjsLib = (window as any).pdfjsLib
  188. pdfjsLib.GlobalWorkerOptions.workerSrc =
  189. 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'
  190. let loadingTask
  191. if (isBase64) {
  192. const base64Data = pdfUrl.value.replace(/^data:application\/pdf;base64,/, '')
  193. const binaryString = atob(base64Data)
  194. const bytes = new Uint8Array(binaryString.length)
  195. for (let i = 0; i < binaryString.length; i++) {
  196. bytes[i] = binaryString.charCodeAt(i)
  197. }
  198. loadingTask = pdfjsLib.getDocument({ data: bytes })
  199. } else {
  200. loadingTask = pdfjsLib.getDocument(pdfUrl.value)
  201. }
  202. const pdf = await loadingTask.promise
  203. container.style.overflow = 'auto'
  204. for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) {
  205. const page = await pdf.getPage(pageNum)
  206. const scale = 1.5
  207. const viewport = page.getViewport({ scale })
  208. const canvas = document.createElement('canvas')
  209. canvas.id = `pdf-page-${pageNum}`
  210. canvas.style.display = 'block'
  211. canvas.style.marginBottom = '10px'
  212. const context = canvas.getContext('2d')
  213. canvas.height = viewport.height
  214. canvas.width = viewport.width
  215. container.appendChild(canvas)
  216. await page.render({ canvasContext: context, viewport }).promise
  217. }
  218. } catch (error) {
  219. console.error('[PDF] Error rendering PDF:', error)
  220. uni.showToast({ title: 'PDF 渲染失败', icon: 'none' })
  221. }
  222. }
  223. onMounted(async () => {
  224. await init()
  225. if (isH5) {
  226. await nextTick()
  227. if (pdfUrl.value) {
  228. setTimeout(() => initH5PdfViewer(), 100)
  229. } else {
  230. watch(pdfUrl, (val) => {
  231. if (val) {
  232. setTimeout(() => initH5PdfViewer(), 100)
  233. }
  234. })
  235. }
  236. }
  237. })
  238. </script>
  239. <style lang="scss" scoped>
  240. .detail-container {
  241. display: flex;
  242. flex-direction: column;
  243. height: 100vh;
  244. background-color: #f5f5f5;
  245. }
  246. .navigate-view {
  247. display: flex;
  248. flex-direction: row;
  249. justify-content: space-between;
  250. align-items: center;
  251. height: 44px;
  252. background-color: #fff;
  253. border-bottom: 1px solid #eee;
  254. padding: 0 15px;
  255. }
  256. .navigate-left {
  257. display: flex;
  258. align-items: center;
  259. width: 44px;
  260. }
  261. .back-icon {
  262. width: 20px;
  263. height: 20px;
  264. }
  265. .navigate-center {
  266. flex: 1;
  267. display: flex;
  268. justify-content: center;
  269. }
  270. .navigate-title {
  271. font-size: 17px;
  272. font-weight: 500;
  273. color: #333;
  274. }
  275. .navigate-right {
  276. width: 44px;
  277. display: flex;
  278. align-items: center;
  279. }
  280. .nav-title {
  281. font-size: 17px;
  282. font-weight: 500;
  283. color: #333;
  284. }
  285. .pdf-container {
  286. flex: 1;
  287. background-color: #f5f5f5;
  288. }
  289. /* #ifdef H5 */
  290. .pdf-viewer-container {
  291. width: 100%;
  292. height: 100%;
  293. background-color: #fff;
  294. text-align: center;
  295. padding: 10px;
  296. box-sizing: border-box;
  297. }
  298. /* #endif */
  299. .loading-container,
  300. .empty-container {
  301. display: flex;
  302. justify-content: center;
  303. align-items: center;
  304. height: 100%;
  305. }
  306. .loading-text,
  307. .empty-text {
  308. color: #999;
  309. font-size: 14px;
  310. }
  311. </style>