index.vue 27 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051
  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="sign-container">
  12. <!-- 导航栏 -->
  13. <NavBar>
  14. <template #title>
  15. <text class="nav-title">{{ title }}</text>
  16. </template>
  17. </NavBar>
  18. <!-- 内容区域 -->
  19. <scroll-view class="scroll-content" scroll-y>
  20. <!-- PDF 图片预览 -->
  21. <SpreadPDFViewer
  22. ref="spreadPdfViewerRef"
  23. :businessConfig="businessConfig"
  24. :templateData="templateData"
  25. :templateBlob="templateBlob"
  26. viewer-height="50vh"
  27. />
  28. <!-- 签名区域分割线 -->
  29. <view class="sign-divider"></view>
  30. <!-- 签名区域(三种状态:空/签名中/已签名) -->
  31. <view class="sign-section">
  32. <!-- 状态1:空状态 - 点击签名 -->
  33. <template v-if="signStatus === 'empty'">
  34. <view class="sign-header">
  35. <text class="sign-title-text">手写签名</text>
  36. </view>
  37. <view class="sign-view" @click="handleToSign">
  38. <text class="sign-view-text">点击签名</text>
  39. </view>
  40. </template>
  41. <!-- 状态2:签名中 - 显示画布 -->
  42. <template v-if="signStatus === 'signing'">
  43. <view class="sign-header">
  44. <text class="sign-title-text">手写签名</text>
  45. <view class="sign-header-actions">
  46. <button class="header-action-btn header-reset-btn" @click="clearCanvas">清除</button>
  47. <button class="header-action-btn header-cancel-btn" @click="handleCancelSign">
  48. 取消
  49. </button>
  50. <button class="header-action-btn header-confirm-btn" @click="confirmSign">
  51. 确认
  52. </button>
  53. </view>
  54. </view>
  55. <view class="sign-canvas-wrapper">
  56. <SignatureCanvas
  57. ref="signatureRef"
  58. canvas-id="signCanvas"
  59. :width="canvasWidth"
  60. :height="178"
  61. @signed="onCanvasSigned"
  62. />
  63. </view>
  64. </template>
  65. <!-- 状态3:已签名 - 显示签名图片 -->
  66. <template v-if="signStatus === 'signed'">
  67. <view class="sign-header">
  68. <text class="sign-title-text">签名时间:</text>
  69. <text class="sign-title-text">{{ signTime }}</text>
  70. </view>
  71. <view class="sign-img">
  72. <view class="sign-img-del" @click="handleDelSign">X</view>
  73. <image :src="showSignImg" mode="aspectFit" class="sign-image" :style="signImgStyle" />
  74. </view>
  75. </template>
  76. </view>
  77. <!-- 联系信息表单(服务单/受理单类型) -->
  78. <view v-if="routeType === 'FWD'" class="form-section">
  79. <view class="form-item">
  80. <text class="form-label">接收人手机号:</text>
  81. <input
  82. v-model="fwdInputPhone"
  83. class="form-input"
  84. type="number"
  85. maxlength="11"
  86. placeholder="请输入接收人手机号"
  87. />
  88. </view>
  89. </view>
  90. </scroll-view>
  91. <!-- 底部按钮 -->
  92. <view class="footer-bar">
  93. <button class="footer-btn more-btn" @click="handlePushOrder">更多操作</button>
  94. <button class="footer-btn confirm-btn" @click="signSubmit">
  95. {{ signButtonText }}
  96. </button>
  97. <!-- <button v-show="isSigned == '1'" class="footer-btn confirm-btn">重新签名</button> -->
  98. </view>
  99. <!-- 底部操作面板 -->
  100. <wd-action-sheet
  101. v-model="showActionSheet"
  102. :actions="actionSheetActions"
  103. @select="handleActionSelect"
  104. />
  105. <!-- 服务单接收人确认弹窗 -->
  106. <view v-if="showFwdPopup" class="popup-overlay" @click="closeFwdPopup">
  107. <view class="popup-content" @click.stop>
  108. <text class="popup-title">确认提交</text>
  109. <text class="popup-message">是否确认签约代表签名?</text>
  110. <view class="popup-actions">
  111. <button class="action-btn cancel-btn" @click="closeFwdPopup">取消</button>
  112. <button class="action-btn confirm-btn" @click="confirmFwdSubmit">确定</button>
  113. </view>
  114. </view>
  115. </view>
  116. <!-- 推送任务单弹窗 -->
  117. <view v-if="showInputPopup" class="popup-overlay" @click="closeInputPopup">
  118. <view class="popup-content input-popup" @click.stop>
  119. <text class="popup-title">推送任务单</text>
  120. <view class="input-row">
  121. <text class="row-label">接收人名称:</text>
  122. <input class="row-input" v-model="inputName" placeholder="请输入接收人名称" />
  123. </view>
  124. <view class="input-row">
  125. <text class="row-label">接收人手机号:</text>
  126. <input
  127. class="row-input"
  128. v-model="inputPhone"
  129. type="number"
  130. maxlength="11"
  131. placeholder="请输入接收人手机号"
  132. />
  133. </view>
  134. <view v-if="routeType !== 'ZXXX'" class="input-row">
  135. <text class="row-label">电子邮箱:</text>
  136. <input
  137. class="row-input"
  138. v-model="inputEmail"
  139. type="text"
  140. placeholder="请输入电子邮箱(选填)"
  141. />
  142. </view>
  143. <view class="popup-actions">
  144. <button class="action-btn cancel-btn" @click="closeInputPopup">取消</button>
  145. <button class="action-btn confirm-btn" @click="handlePushOrderSubmit">确定</button>
  146. </view>
  147. </view>
  148. </view>
  149. </view>
  150. </template>
  151. <script lang="ts" setup>
  152. import { ref, computed, onMounted, nextTick } from 'vue'
  153. import { onLoad } from '@dcloudio/uni-app'
  154. import SignatureCanvas from '@/components/Signature/SignatureCanvas.vue'
  155. import SpreadPDFViewer from '@/components/SpreadDesigner/SpreadPDFViewer.vue'
  156. import NavBar from '@/components/NavBar/NavBar.vue'
  157. import { useUserStore } from '@/store/user'
  158. import { useConfigStore } from '@/store/config'
  159. import { getGcConfig, getTaskOrderImg, pushPressure2TaskOrder, uploadSignImage } from '@/api/sign'
  160. import { getDynamicTbVal } from '@/api/task'
  161. import { getStandardTemplate } from '@/api/index'
  162. import { getTaskOrderReport } from '@/api/orderReport'
  163. import { buildFileUrl } from '@/utils/index'
  164. import { SignFuncName, requestFunc } from '@/api/ApiRouter/sign'
  165. import { TaskOrderFuncName, requestFunc as taskOrderRequestFunc } from '@/api/ApiRouter/taskOrder'
  166. import {
  167. requestFunc as SecurityRequestFunc,
  168. SecurityCheckFuncName,
  169. } from '@/api/ApiRouter/taskOrderSecurityCheck'
  170. import { EquipmentType } from '@/utils/dictMap'
  171. const equipType = useConfigStore().getEquipType()
  172. const title = ref('')
  173. const routeType = ref<'FWD' | 'JYRS' | 'AQJC' | 'ZXXX'>()
  174. const orderId = ref('')
  175. const orderItemId = ref('')
  176. const securityCheckId = ref('')
  177. const reportId = ref('')
  178. const unitContact = ref('')
  179. const unitPhone = ref('')
  180. const receiverEmail = ref('')
  181. const templateId = ref('')
  182. const refId = ref('')
  183. const showSignImg = ref('')
  184. const signImg = ref('')
  185. const uploadedSignUrl = ref('')
  186. const signImgStyle = ref<any>({})
  187. const signTime = ref('')
  188. const fwdInputPhone = ref('')
  189. const showFwdPopup = ref(false)
  190. const showInputPopup = ref(false)
  191. const showActionSheet = ref(false)
  192. const actionSheetActions = ref<any[]>([])
  193. const canvasWidth = ref(300)
  194. const inputName = ref('')
  195. const inputPhone = ref('')
  196. const inputEmail = ref('')
  197. const gcConfig = ref<any>(null)
  198. const spreadPdfViewerRef = ref<any>(null)
  199. const templateBlob = ref<any>(null)
  200. const businessConfig = ref<any>({
  201. businessType: 'AQJC',
  202. title: '安全检查记录编辑',
  203. disableNavigate: true, // 是否禁用跳转,默认不禁用
  204. ui: {
  205. title: '安全检查记录',
  206. saveButtonText: '保存',
  207. cancelButtonText: '取消',
  208. showAdditionalToolbar: true,
  209. customButtons: [],
  210. },
  211. })
  212. const templateData = ref<any>({})
  213. // 签名区域状态:empty | signing | signed
  214. const signStatus = ref<'empty' | 'signing' | 'signed'>('empty')
  215. const signatureRef = ref<any>(null)
  216. const userStore = useUserStore()
  217. const userInfo = computed(() => userStore.userInfo)
  218. const titleTextMap: Record<string, string> = {
  219. FWD: '服务单/受理单',
  220. JYRS: '检验结果告知',
  221. AQJC: '安全检查记录',
  222. ZXXX: '重大问题线索告知',
  223. }
  224. const businessTypeMap: Record<string, number> = {
  225. FWD: 100,
  226. JYRS: 200,
  227. AQJC: 300,
  228. ZXXX: 400,
  229. }
  230. const signButtonTextMap: Record<string, string> = {
  231. FWD: '签约代表签名',
  232. JYRS: '客户代表签名',
  233. AQJC: '受检单位签名',
  234. ZXXX: '受检单位签名',
  235. }
  236. const confirmTextMap: Record<string, string> = {
  237. FWD: '是否确认签约代表签名?',
  238. JYRS: '是否确认客户代表签名?',
  239. AQJC: '是否确认受检单位签名?',
  240. ZXXX: '是否提交审核?',
  241. }
  242. const signButtonText = computed(() => {
  243. return routeType.value ? signButtonTextMap[routeType.value] || '签名' : '签名'
  244. })
  245. onLoad((options) => {
  246. const type = options?.type as string
  247. routeType.value = type as any
  248. title.value = titleTextMap[type] || '签字'
  249. orderId.value = options?.orderId || ''
  250. orderItemId.value = options?.orderItemId || ''
  251. securityCheckId.value = options?.securityCheckId || ''
  252. reportId.value = options?.reportId || ''
  253. unitContact.value = options?.unitContact || ''
  254. unitPhone.value = options?.unitPhone || ''
  255. receiverEmail.value = options?.receiverEmail || ''
  256. templateId.value = options?.templateId || ''
  257. fwdInputPhone.value = unitPhone.value
  258. inputName.value = unitContact.value
  259. inputPhone.value = unitPhone.value
  260. inputEmail.value = receiverEmail.value
  261. // 初始化画布宽度
  262. const sysInfo = uni.getSystemInfoSync()
  263. canvasWidth.value = sysInfo.windowWidth - 32
  264. })
  265. const orderReportId = ref('')
  266. const isSigned = ref('0')
  267. const getPreviewData = async () => {
  268. // orderId --> templateId + refId --> templateBlob 和 templateData
  269. /*
  270. orderId: 1fd8970b25933aee9486431bfbc10751
  271. templateId: dce2478ef6a153fbd2874b1f2ea33389
  272. */
  273. // orderId 查询服务单,获取 templateId + refId
  274. if (!routeType.value) {
  275. uni.showToast({ title: '必须选择签字文件类型', icon: 'error' })
  276. return
  277. }
  278. const orderDetail = await taskOrderRequestFunc(TaskOrderFuncName.TaskOrderDetail, equipType, {
  279. id: orderId.value,
  280. })
  281. const signFileList = orderDetail.data.signFileList || []
  282. const targetBusinessType = businessTypeMap[routeType.value]
  283. const signFile = signFileList.find((row: any) => row.businessType === targetBusinessType)
  284. if (signFile == null || signFile.isSignature == '0') {
  285. isSigned.value = '0'
  286. } else {
  287. isSigned.value = '1'
  288. }
  289. switch (routeType.value) {
  290. case 'FWD':
  291. const orderReportResp = await getTaskOrderReport({ taskOrderId: orderId.value })
  292. const orderReportRespList = orderReportResp.data?.list
  293. let orderReport = null
  294. if (orderReportRespList?.length) {
  295. orderReport = orderReportRespList[0]
  296. }
  297. templateId.value = orderReport?.templateId
  298. refId.value = orderReport?.acceptOrderId
  299. orderReportId.value = orderReport?.id || ''
  300. break
  301. case 'JYRS':
  302. const notificationformReport = orderDetail.data?.notificationformReport
  303. if (!notificationformReport) {
  304. const orderFormRes = await taskOrderRequestFunc(TaskOrderFuncName.GetOrderForm, equipType, {
  305. orderId: orderId.value,
  306. businessType: 1000,
  307. })
  308. const JYRSTemplateId = orderFormRes.data?.templateId || ''
  309. const addMajorIssuesRes = await taskOrderRequestFunc(
  310. TaskOrderFuncName.AddMajorIssues,
  311. equipType,
  312. {
  313. orderId: orderId.value,
  314. businessType: 1000,
  315. templateId: JYRSTemplateId,
  316. },
  317. )
  318. const JYRSReportId = addMajorIssuesRes.data
  319. templateId.value = JYRSTemplateId
  320. refId.value = (JYRSReportId as string) || ''
  321. } else {
  322. templateId.value = notificationformReport?.templateId || ''
  323. refId.value = notificationformReport?.id || ''
  324. }
  325. break
  326. case 'AQJC':
  327. const defaultTemplateResp = await SecurityRequestFunc(
  328. SecurityCheckFuncName.getTemplate,
  329. equipType,
  330. { orderId: orderId.value },
  331. )
  332. templateId.value = defaultTemplateResp.data?.templateId
  333. refId.value = securityCheckId.value
  334. break
  335. default:
  336. uni.showToast({ title: '请选择正确的签字文件类型', icon: 'error' })
  337. break
  338. }
  339. // 获取templateSchema
  340. const res = await getStandardTemplate({ id: templateId.value })
  341. const resData = (res as any).data
  342. // 加载报表数据
  343. const dataMap: any = {}
  344. const dynamicTbResp = await getDynamicTbVal({
  345. refId: refId.value,
  346. })
  347. const dynamicTb: any = dynamicTbResp.data
  348. for (let i = 0; i < dynamicTb.dynamicTbValRespVOList.length; i++) {
  349. const item = dynamicTb.dynamicTbValRespVOList[i]
  350. dataMap[item.colCode] = item.valValue
  351. }
  352. // 组装templateData
  353. templateData.value = {
  354. schema: resData.bindingPathSchema ? JSON.parse(resData.bindingPathSchema) : {},
  355. data: {
  356. ...dataMap,
  357. templateId: templateId.value,
  358. templateUrl: resData.fileUrl,
  359. },
  360. pathNameMapping: JSON.parse(resData.bindingPathNameJson),
  361. template: templateId.value,
  362. templateUrl: resData.fileUrl,
  363. }
  364. // 获取 template 文件
  365. const fileUri = resData.fileUrl
  366. const fileUrl = buildFileUrl(fileUri)
  367. const fileBase64 = await spreadPdfViewerRef.value.downloadFileAsBase64(fileUrl)
  368. templateBlob.value = fileBase64
  369. }
  370. const handleToSign = () => {
  371. signStatus.value = 'signing'
  372. // 重新计算画布宽度
  373. const sysInfo = uni.getSystemInfoSync()
  374. canvasWidth.value = sysInfo.windowWidth - 32
  375. }
  376. // 画布签名完成回调
  377. const onCanvasSigned = (hasContent: boolean) => {
  378. console.log('画布签名状态:', hasContent)
  379. }
  380. // 清除画布
  381. const clearCanvas = () => {
  382. if (signatureRef.value) {
  383. signatureRef.value.clear()
  384. }
  385. }
  386. // 取消签名 - 返回空状态
  387. const handleCancelSign = () => {
  388. signStatus.value = 'empty'
  389. }
  390. const base64ToFile = (base64Data: string, fileName: string = 'signature.png'): File | null => {
  391. // #ifdef H5
  392. try {
  393. const arr = base64Data.split(',')
  394. const mime = arr[0].match(/:(.*?);/)?.[1] || 'image/png'
  395. const bstr = atob(arr[1])
  396. let n = bstr.length
  397. const u8arr = new Uint8Array(n)
  398. while (n--) {
  399. u8arr[n] = bstr.charCodeAt(n)
  400. }
  401. return new File([u8arr], fileName, { type: mime })
  402. } catch (e) {
  403. console.error('base64 转 File 失败:', e)
  404. return null
  405. }
  406. // #endif
  407. }
  408. const base64ToTempFilePath = (base64Data: string): Promise<string> => {
  409. return new Promise((resolve, reject) => {
  410. // #ifdef H5
  411. const file = base64ToFile(base64Data)
  412. if (file) {
  413. resolve('') // H5 使用 File 对象
  414. } else {
  415. reject(new Error('转换失败'))
  416. }
  417. // #endif
  418. // #ifndef H5
  419. const fs = uni.getFileSystemManager()
  420. const filePath = `${uni.env.USER_DATA_PATH}/signature_${Date.now()}.png`
  421. // 移除 data:image/png;base64, 前缀
  422. const base64 = base64Data.replace(/^data:image\/\w+;base64,/, '')
  423. fs.writeFile({
  424. filePath,
  425. data: base64,
  426. encoding: 'base64',
  427. success: () => {
  428. resolve(filePath)
  429. },
  430. fail: (err) => {
  431. reject(err)
  432. },
  433. })
  434. // #endif
  435. })
  436. }
  437. const uploadSignature = async (base64Data: string): Promise<string> => {
  438. try {
  439. // #ifdef H5
  440. const file = base64ToFile(base64Data)
  441. if (!file) {
  442. throw new Error('签名图片转换失败')
  443. }
  444. const fd = new FormData()
  445. fd.append('file', file)
  446. const resp: any = await uploadSignImage(fd)
  447. if (resp?.code === 0 && resp?.data) {
  448. return resp.data
  449. }
  450. throw new Error(resp?.msg || '上传失败')
  451. // #endif
  452. } catch (error) {
  453. console.error('签名图片上传失败:', error)
  454. throw error
  455. }
  456. }
  457. // 确认签名
  458. const confirmSign = async () => {
  459. if (!signatureRef.value) return
  460. if (signatureRef.value.isEmpty()) {
  461. uni.showToast({ title: '请先签字再保存', icon: 'none' })
  462. return
  463. }
  464. try {
  465. uni.showLoading({ title: '正在保存...' })
  466. const path = await signatureRef.value.getImage()
  467. signImg.value = path
  468. showSignImg.value = path
  469. // console.log('imagePath....', path)
  470. uni.getImageInfo({
  471. src: path,
  472. success: (imageInfo) => {
  473. const screenWidth = uni.getSystemInfoSync().windowWidth - 32
  474. signImgStyle.value = {
  475. width: `${screenWidth}px`,
  476. height: `${(screenWidth * imageInfo.height) / imageInfo.width}px`,
  477. }
  478. },
  479. })
  480. // 上传签名图片
  481. try {
  482. const uploadedUrl = await uploadSignature(path)
  483. uploadedSignUrl.value = uploadedUrl
  484. console.log('签名图片上传成功:', uploadedUrl)
  485. } catch (uploadError) {
  486. console.error('签名图片上传失败,继续使用原图片:', uploadError)
  487. uni.showToast({ title: '签名保存成功,上传失败', icon: 'none' })
  488. }
  489. const now = new Date()
  490. signTime.value = `${now.getFullYear()}年${now.getMonth() + 1}月${now.getDate()}日`
  491. signStatus.value = 'signed'
  492. uni.hideLoading()
  493. } catch (err) {
  494. uni.hideLoading()
  495. console.error('转换失败:', err)
  496. uni.showToast({ title: '签名失败', icon: 'error' })
  497. }
  498. }
  499. // 删除签名
  500. const handleDelSign = () => {
  501. signImg.value = ''
  502. showSignImg.value = ''
  503. uploadedSignUrl.value = ''
  504. signTime.value = ''
  505. signStatus.value = 'empty'
  506. }
  507. // 提交签名
  508. const signSubmit = () => {
  509. if (!signImg.value) {
  510. uni.showToast({ title: '请先签名', icon: 'none' })
  511. return
  512. }
  513. if (routeType.value === 'FWD') {
  514. if (!fwdInputPhone.value) {
  515. uni.showToast({ title: '请输入接收人手机号', icon: 'none' })
  516. return
  517. }
  518. if (!/^1[3456789]\d{9}$/.test(fwdInputPhone.value)) {
  519. uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
  520. return
  521. }
  522. showFwdPopup.value = true
  523. return
  524. }
  525. submitConfirm()
  526. }
  527. // 确认提交
  528. const submitConfirm = async () => {
  529. try {
  530. const params: any = {
  531. id: orderId.value,
  532. signUrl: uploadedSignUrl.value || signImg.value,
  533. // signTime: ,
  534. businessType: routeType.value ? businessTypeMap[routeType.value] : '',
  535. // orderItemId: orderItemId.value || undefined,
  536. // securityCheckId: ,
  537. }
  538. if (routeType.value === 'FWD') {
  539. params.receiverPhone = fwdInputPhone.value
  540. params.orderReportId = orderReportId.value
  541. }
  542. const result: any = await requestFunc(SignFuncName.SubmitSign, equipType, params)
  543. if (result?.code === 0) {
  544. uni.showToast({
  545. title: routeType.value === 'ZXXX' ? '已自动提交审核' : '签名成功',
  546. icon: 'success',
  547. })
  548. // setTimeout(() => {
  549. // uni.redirectTo({
  550. // url: `/pages/orderDetail/detail?orderId=${orderId.value}&type=${routeType.value}`,
  551. // })
  552. // }, 1500)
  553. } else {
  554. uni.showToast({ title: result?.msg || '签名失败', icon: 'error' })
  555. }
  556. } catch (error: any) {
  557. console.error('签名失败:', error)
  558. uni.showToast({ title: error?.msg || '签名失败', icon: 'error' })
  559. } finally {
  560. showFwdPopup.value = false
  561. }
  562. }
  563. // 服务单提交确认
  564. const confirmFwdSubmit = () => {
  565. submitConfirm()
  566. }
  567. // 关闭服务单弹窗
  568. const closeFwdPopup = () => {
  569. showFwdPopup.value = false
  570. }
  571. // 更多操作
  572. const handlePushOrder = () => {
  573. if (routeType.value === 'ZXXX') {
  574. actionSheetActions.value = [{ name: '小程序推送签名' }, { name: '更新' }]
  575. } else {
  576. actionSheetActions.value = [{ name: '推送' }, { name: '更新' }]
  577. }
  578. showActionSheet.value = true
  579. }
  580. // 处理操作面板选择
  581. const handleActionSelect = (item: any) => {
  582. const name = item.item.name
  583. if (name === '推送' || name === '小程序推送签名') {
  584. showInputPopup.value = true
  585. } else if (name === '更新') {
  586. if (!templateId.value || !refId.value) {
  587. return uni.showToast({ title: '配置信息不完整', icon: 'none' })
  588. }
  589. let url = ''
  590. if (routeType.value === 'FWD') {
  591. url = `/pages/serviceOrderDetail/serviceOrderEditor?templateId=${templateId.value}&refId=${refId.value}`
  592. } else if (routeType.value === 'JYRS') {
  593. uni.showToast({ title: '更新检验结果告知表单暂未实现', icon: 'none' })
  594. } else {
  595. uni.showToast({ title: '系统错误,不能更新', icon: 'none' })
  596. }
  597. uni.navigateTo({ url })
  598. }
  599. }
  600. // 关闭推送弹窗
  601. const closeInputPopup = () => {
  602. showInputPopup.value = false
  603. inputEmail.value = ''
  604. }
  605. // 推送任务单提交
  606. const handlePushOrderSubmit = () => {
  607. if (!inputName.value || !inputPhone.value) {
  608. return uni.showToast({ title: '请输入接收人姓名和手机号', icon: 'none' })
  609. }
  610. if (!/^1[3456789]\d{9}$/.test(inputPhone.value)) {
  611. return uni.showToast({ title: '请输入正确的手机号', icon: 'none' })
  612. }
  613. const params: any = {
  614. orderId: orderId.value,
  615. receiver: inputName.value,
  616. receiverPhone: inputPhone.value,
  617. receiverEmail: routeType.value !== 'ZXXX' ? inputEmail.value || '' : '',
  618. businessType: routeType.value ? businessTypeMap[routeType.value] : '',
  619. signUrl: uploadedSignUrl.value || signImg.value,
  620. orderItemId: orderItemId.value || undefined,
  621. securityCheckId: securityCheckId.value,
  622. equipMainType: equipTypeCode(equipType),
  623. }
  624. pushPressure2TaskOrder(params)
  625. .then((res: any) => {
  626. if (res?.code === 0) {
  627. uni.showToast({ title: '推送成功', icon: 'success' })
  628. showInputPopup.value = false
  629. } else {
  630. uni.showToast({ title: res?.msg || '推送失败', icon: 'none' })
  631. }
  632. })
  633. .catch((error) => {
  634. console.error('推送失败:', error)
  635. uni.showToast({ title: '推送失败', icon: 'none' })
  636. })
  637. }
  638. const equipTypeCode = (type: EquipmentType) => {
  639. switch (type) {
  640. case EquipmentType.BOILER:
  641. return '200'
  642. case EquipmentType.PIPE:
  643. return '300'
  644. case EquipmentType.CONTAINER:
  645. return '100'
  646. default:
  647. return ''
  648. }
  649. }
  650. onMounted(() => {
  651. if (orderId.value) {
  652. getPreviewData()
  653. }
  654. })
  655. </script>
  656. <style lang="scss" scoped>
  657. .sign-container {
  658. display: flex;
  659. flex-direction: column;
  660. height: 100vh;
  661. background-color: #fff;
  662. }
  663. .navigate-view {
  664. display: flex;
  665. flex-direction: row;
  666. align-items: center;
  667. justify-content: space-between;
  668. height: 44px;
  669. padding: 0 15px;
  670. background-color: #fff;
  671. border-bottom: 1px solid #eee;
  672. .navigate-left {
  673. display: flex;
  674. align-items: center;
  675. }
  676. .back-icon {
  677. width: 20px;
  678. height: 20px;
  679. }
  680. .navigate-title {
  681. font-size: 17px;
  682. font-weight: 500;
  683. color: #333;
  684. }
  685. .navigate-right {
  686. width: 20px;
  687. }
  688. }
  689. .nav-title {
  690. font-size: 17px;
  691. font-weight: 500;
  692. color: #333;
  693. }
  694. .scroll-content {
  695. flex: 1;
  696. padding: 16px;
  697. }
  698. // 分割线
  699. .sign-divider {
  700. width: 100%;
  701. height: 1px;
  702. margin: 0 0 12px;
  703. background-color: #e0e0e0;
  704. }
  705. // 签名区域
  706. .sign-section {
  707. width: 100%;
  708. padding-bottom: 12px;
  709. background-color: #fff;
  710. .sign-header {
  711. display: flex;
  712. flex-direction: row;
  713. align-items: center;
  714. justify-content: space-between;
  715. margin-bottom: 8px;
  716. .sign-title-text {
  717. font-size: 20px;
  718. line-height: 28px;
  719. color: #333;
  720. }
  721. .sign-header-actions {
  722. display: flex;
  723. flex-direction: row;
  724. gap: 8px;
  725. .header-action-btn {
  726. display: flex;
  727. align-items: center;
  728. justify-content: center;
  729. height: 30px;
  730. padding: 0 12px;
  731. font-size: 14px;
  732. border: none;
  733. border-radius: 6px;
  734. }
  735. .header-reset-btn {
  736. color: rgb(16, 16, 16);
  737. background-color: rgb(230, 238, 245);
  738. border: 1px solid rgb(187, 187, 187);
  739. }
  740. .header-cancel-btn {
  741. color: rgb(16, 16, 16);
  742. background-color: #f5f5f5;
  743. border: 1px solid rgb(187, 187, 187);
  744. }
  745. .header-confirm-btn {
  746. color: #fff;
  747. background-color: rgb(7, 31, 80);
  748. }
  749. }
  750. }
  751. // 空状态 - 点击签名
  752. .sign-view {
  753. display: flex;
  754. align-items: center;
  755. justify-content: center;
  756. width: 100%;
  757. height: 178px;
  758. background-color: #d8d8d8;
  759. .sign-view-text {
  760. font-size: 42px;
  761. font-weight: bold;
  762. color: #999;
  763. }
  764. }
  765. // 签名画布区域
  766. .sign-canvas-wrapper {
  767. width: 100%;
  768. overflow: hidden;
  769. background-color: #ffffff;
  770. border: 2px dashed rgb(187, 187, 187);
  771. border-radius: 8px;
  772. }
  773. // 已签名 - 图片展示
  774. .sign-img {
  775. position: relative;
  776. display: flex;
  777. align-items: center;
  778. justify-content: center;
  779. padding: 10px;
  780. border: 1px solid #ccc;
  781. border-radius: 10px;
  782. .sign-img-del {
  783. position: absolute;
  784. top: 0;
  785. right: 0;
  786. z-index: 2;
  787. display: flex;
  788. align-items: center;
  789. justify-content: center;
  790. width: 20px;
  791. height: 20px;
  792. font-size: 12px;
  793. color: #333;
  794. background-color: #e0e0e0;
  795. border-radius: 10px;
  796. }
  797. .sign-image {
  798. display: block;
  799. width: 100%;
  800. }
  801. }
  802. }
  803. // 表单区域
  804. .form-section {
  805. padding: 15px 0;
  806. margin-bottom: 10px;
  807. background-color: #fff;
  808. border-top: 1px solid #eee;
  809. .form-item {
  810. display: flex;
  811. flex-direction: row;
  812. align-items: center;
  813. .form-label {
  814. width: 120px;
  815. font-size: 14px;
  816. color: #666;
  817. }
  818. .form-input {
  819. flex: 1;
  820. height: 40px;
  821. padding: 0 10px;
  822. font-size: 14px;
  823. border: 1px solid #ddd;
  824. border-radius: 5px;
  825. }
  826. }
  827. }
  828. // 底部按钮
  829. .footer-bar {
  830. display: flex;
  831. flex-direction: row;
  832. gap: 12px;
  833. padding: 12px 16px 16px;
  834. background-color: #ffffff;
  835. border-top: 1px solid #e0e0e0;
  836. .footer-btn {
  837. display: flex;
  838. flex: 1;
  839. align-items: center;
  840. justify-content: center;
  841. height: 44px;
  842. font-size: 16px;
  843. color: #fff;
  844. border: none;
  845. border-radius: 6px;
  846. }
  847. .more-btn {
  848. background-color: #e6a23c;
  849. }
  850. .confirm-btn {
  851. background-color: #00a811;
  852. }
  853. .resign-btn {
  854. background-color: #ffffff;
  855. }
  856. }
  857. // 弹窗
  858. .popup-overlay {
  859. position: fixed;
  860. top: 0;
  861. right: 0;
  862. bottom: 0;
  863. left: 0;
  864. z-index: 999;
  865. display: flex;
  866. align-items: center;
  867. justify-content: center;
  868. background-color: rgba(0, 0, 0, 0.5);
  869. .popup-content {
  870. display: flex;
  871. flex-direction: column;
  872. align-items: center;
  873. width: 66.67%;
  874. padding: 20px;
  875. background-color: #fff;
  876. border-radius: 10px;
  877. &.input-popup {
  878. width: 80%;
  879. max-width: 360px;
  880. }
  881. .popup-title {
  882. margin-bottom: 15px;
  883. font-size: 18px;
  884. font-weight: 500;
  885. color: #333;
  886. }
  887. .popup-message {
  888. margin-bottom: 20px;
  889. font-size: 15px;
  890. color: #666;
  891. }
  892. .popup-actions {
  893. display: flex;
  894. flex-direction: row;
  895. gap: 10px;
  896. width: 100%;
  897. .action-btn {
  898. display: flex;
  899. flex: 1;
  900. align-items: center;
  901. justify-content: center;
  902. height: 40px;
  903. font-size: 15px;
  904. border: none;
  905. border-radius: 5px;
  906. }
  907. .cancel-btn {
  908. color: #fff;
  909. background-color: #94bddf;
  910. }
  911. .confirm-btn {
  912. color: #fff;
  913. background-color: #071f50;
  914. }
  915. }
  916. }
  917. }
  918. .input-row {
  919. display: flex;
  920. flex-direction: row;
  921. align-items: center;
  922. width: 100%;
  923. margin-bottom: 12px;
  924. .row-label {
  925. flex-shrink: 0;
  926. width: 90px;
  927. font-size: 14px;
  928. color: #666;
  929. }
  930. .row-input {
  931. flex: 1;
  932. height: 36px;
  933. padding: 0 10px;
  934. font-size: 14px;
  935. border: 1px solid #ddd;
  936. border-radius: 5px;
  937. }
  938. }
  939. </style>