index.vue 28 KB

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