| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596 |
- <route lang="json5" type="page">
- {
- layout: 'default',
- style: {
- navigationBarTitleText: '',
- navigationStyle: 'custom',
- disableScroll: true,
- },
- }
- </route>
- <!-- 检验方案编辑 -->
- <template>
- <div>
- <SpreadDesignerGeneric
- ref="designerRef"
- :businessConfig="businessConfig"
- :templateData="templateData"
- :templateBlob="templateBlob"
- @save="handleSave"
- @cancel="handleCancel"
- @customAction="handleCustomAction"
- >
- <template #navBar>
- <ReportNavBar>
- <template #right>
- <view class="nav-btn-group">
- <view
- class="nav-btn"
- @click="designerRef?.handleNavButtonClick({ action: 'cancel' })"
- >
- 取消
- </view>
- <view
- class="nav-btn primary"
- @click="designerRef?.handleNavButtonClick({ action: 'save' })"
- >
- 保存
- </view>
- <view
- class="nav-btn upload-btn"
- @click="designerRef?.handleNavButtonClick({ action: 'upload' })"
- >
- 方案上传
- </view>
- <view
- class="nav-btn submit-btn"
- @click="designerRef?.handleNavButtonClick({ action: 'submit' })"
- >
- 提交审核
- </view>
- </view>
- </template>
- </ReportNavBar>
- </template>
- </SpreadDesignerGeneric>
- <!-- 审核人选择弹窗 -->
- <view v-if="auditorVisible" class="auditor-overlay" @click="auditorVisible = false">
- <view class="auditor-dialog" @click.stop>
- <view class="auditor-title">选择审核人</view>
- <scroll-view scroll-y class="auditor-list">
- <view
- v-for="item in auditorList"
- :key="item.id"
- :class="['auditor-item', { active: selectedAuditorId === item.id }]"
- @click="selectedAuditorId = item.id"
- >
- <text class="auditor-name">{{ item.nickname || item.userName }}</text>
- <view v-if="selectedAuditorId === item.id" class="auditor-check">✓</view>
- </view>
- <view v-if="auditorLoading" class="auditor-loading">加载中...</view>
- <view v-if="!auditorLoading && auditorList.length === 0" class="auditor-empty">
- 暂无可选审核人
- </view>
- </scroll-view>
- <view class="auditor-footer">
- <view class="auditor-btn cancel" @click="auditorVisible = false">取消</view>
- <view class="auditor-btn confirm" @click="confirmSubmit">确定</view>
- </view>
- </view>
- </view>
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onUnmounted } from 'vue'
- import SpreadDesignerGeneric from '@/components/SpreadDesigner/spreadDesignerGeneric.vue'
- import ReportNavBar from '@/components/NavBar/ReportNavBar.vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { getEnvBaseUrl } from '@/utils/index'
- import { getStandardTemplate, uploadFile } from '@/api/index'
- import { getAuditList } from '@/api/system/user'
- import { getDynamicTbVal, saveDynamicTbVal } from '@/api/task'
- import { useConfigStore } from '@/store/config'
- import { useUserStore } from '@/store'
- import { EquipmentType, PressureReportType } from '@/utils/dictMap'
- import { requestFunc, TaskOrderFuncName } from '@/api/ApiRouter/taskOrder'
- import dayjs from 'dayjs'
- const businessConfig = ref({
- businessType: 'JYFA',
- title: '检验方案编辑',
- disableNavigate: true,
- ui: {
- title: '检验方案',
- saveButtonText: '保存',
- cancelButtonText: '取消',
- showAdditionalToolbar: true,
- customButtons: [
- {
- text: '方案上传',
- class: 'upload-btn',
- action: 'upload',
- },
- {
- text: '提交审核',
- class: 'submit-btn',
- action: 'submit',
- },
- ],
- },
- })
- const skipEntryKeys = ref<string[]>([])
- const templateBlob = ref<string>('')
- const templateData = ref<any>({})
- const equipType = useConfigStore().getEquipType()
- const userStore = useUserStore()
- const userInfo = userStore.userInfo
- let templateId: string = ''
- let refId: string = ''
- let manualUrl: string = ''
- onLoad((options: any) => {
- templateId = options.templateId
- refId = options.refId
- manualUrl = options.manualUrl || ''
- // 注册RN文件选择回调
- if (isRNEnv()) {
- registerRNFileCallback()
- }
- init()
- })
- onUnmounted(() => {
- if ((window as any).onFileSelectedFromRN) {
- delete (window as any).onFileSelectedFromRN
- }
- })
- const instId = ref<string>('')
- const init = async () => {
- uni.hideLoading()
- skipEntryKeys.value = []
- if (!templateId || !refId) {
- return
- }
- const res = await getStandardTemplate({ id: templateId })
- const resData = (res as any).data
- const dataMap: any = {}
- const dynamicTbValResp = await getDynamicTbVal({ refId, templateId })
- const dynamicTb: any = dynamicTbValResp.data
- instId.value = dynamicTb?.dynamicTbInsRespVO?.id || ''
- for (let i = 0; i < dynamicTb.dynamicTbValRespVOList.length; i++) {
- const item = dynamicTb.dynamicTbValRespVOList[i]
- const val = item.valValue
- if (typeof val === 'string' && /\.(jpg|png)$/i.test(val)) {
- skipEntryKeys.value.push(item.colCode)
- continue
- }
- dataMap[item.colCode] = val
- }
- templateData.value = {
- schema: resData.bindingPathSchema ? JSON.parse(resData.bindingPathSchema) : {},
- data: {
- ...dataMap,
- templateId,
- templateUrl: resData.fileUrl,
- },
- pathNameMapping: JSON.parse(resData.bindingPathNameJson),
- template: templateId,
- templateUrl: resData.fileUrl,
- }
- const excelBlob = await fetchExcel(refId, templateId)
- const fileBase64 = await blobToBase64(excelBlob)
- templateBlob.value = fileBase64
- }
- const fetchExcel = async (refId: string | number, templateId: string | number): Promise<Blob> => {
- const apiPath = '/pressure2/excel/excel'
- let requestUrl = apiPath
- if (JSON.parse(import.meta.env.VITE_APP_PROXY) && import.meta.env.MODE === 'development') {
- requestUrl = import.meta.env.VITE_APP_PROXY_PREFIX + apiPath
- } else {
- requestUrl = getEnvBaseUrl() + apiPath
- }
- const token = uni.getStorageSync('APP_ACCESS_TOKEN')
- const headers: Record<string, string> = { 'Content-Type': 'application/json' }
- if (token) {
- headers.Authorization = `Bearer ${token}`
- }
- const response = await fetch(requestUrl, {
- method: 'POST',
- headers,
- body: JSON.stringify({ refId, templateId }),
- })
- if (!response.ok) {
- throw new Error(`获取Excel失败, status ${response.status}`)
- }
- return await response.blob()
- }
- const blobToBase64 = (blob: Blob): Promise<string> => {
- return new Promise((resolve, reject) => {
- const reader = new FileReader()
- reader.onload = () => {
- const base64 = (reader.result as string).split(',')[1]
- resolve(base64)
- }
- reader.onerror = reject
- reader.readAsDataURL(blob)
- })
- }
- const handleSave = async (data: any) => {
- const reqData = {}
- for (const key in data.dataJSON) {
- if (skipEntryKeys.value.includes(key)) {
- continue
- }
- const element = data.dataJSON[key];
- reqData[key] = element
- }
- const result = await saveDynamicTbVal({ params: reqData, instId: instId.value })
- if (result?.code === 0 && result?.data) {
- uni.showToast({ title: '保存报表成功', icon: 'success' })
- } else {
- const msg = result?.msg || '保存报表失败'
- uni.showToast({ title: msg, icon: 'error' })
- }
- }
- const handleCancel = () => {
- uni.navigateBack()
- }
- const designerRef = ref<any>(null)
- // 检测RN WebView环境
- const isRNEnv = () => {
- return !!(window as any).ReactNativeWebView
- }
- // 注册RN文件选择回调
- const registerRNFileCallback = () => {
- ;(window as any).onFileSelectedFromRN = handleFileFromRN
- }
- // 暂存待上传的文件
- const pendingFile = ref<{ base64: string; name: string; type: string; size: number } | null>(null)
- // 审核人选择相关
- const auditorVisible = ref(false)
- const auditorList = ref<any[]>([])
- const auditorLoading = ref(false)
- const selectedAuditorId = ref<string>('')
- // 处理RN回传的文件数据
- const handleFileFromRN = async (fileData: any) => {
- if (fileData.canceled) return
- if (fileData.error) {
- uni.showToast({ title: fileData.error, icon: 'none' })
- return
- }
- storeFile(fileData.base64, fileData.name, fileData.type, fileData.size)
- }
- // 暂存文件到内存,已存在则弹窗确认覆盖
- const storeFile = (base64: string, name: string, mimeType: string, size: number) => {
- if (pendingFile.value) {
- uni.showModal({
- title: '提示',
- content: `当前已选择文件「${pendingFile.value.name}」,是否覆盖为「${name}」?`,
- confirmText: '覆盖',
- cancelText: '取消',
- success: (res) => {
- if (res.confirm) {
- pendingFile.value = { base64, name, type: mimeType, size }
- uni.showToast({ title: '文件已更新', icon: 'success' })
- }
- },
- })
- } else {
- pendingFile.value = { base64, name, type: mimeType, size }
- uni.showToast({ title: '文件已选择', icon: 'success' })
- }
- }
- // 执行文件上传,返回文件URL
- const doUpload = async (): Promise<string | null> => {
- if (!pendingFile.value) return null
- const { base64, name, type: mimeType } = pendingFile.value
- try {
- uni.showLoading({ title: '正在上传...' })
- const byteString = atob(base64)
- const ab = new ArrayBuffer(byteString.length)
- const ia = new Uint8Array(ab)
- for (let i = 0; i < byteString.length; i++) {
- ia[i] = byteString.charCodeAt(i)
- }
- const blob = new Blob([ab], { type: mimeType || 'application/octet-stream' })
- const file = new File([blob], name, { type: mimeType || 'application/octet-stream' })
- const formData = new FormData()
- formData.append('file', file)
- const result = await uploadFile(formData)
- uni.hideLoading()
- if (result?.code === 0 && result.data) {
- return result.data as string
- } else {
- uni.showToast({ title: result?.msg || '上传失败', icon: 'none' })
- return null
- }
- } catch (error) {
- uni.hideLoading()
- console.error('上传文件异常:', error)
- uni.showToast({ title: '上传失败', icon: 'none' })
- return null
- }
- }
- // 选择文件(RN环境)
- const pickFileRN = () => {
- ;(window as any).ReactNativeWebView.postMessage(
- JSON.stringify({
- type: 'selectFile',
- allowedTypes: [
- 'application/pdf',
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
- ],
- multiple: false,
- }),
- )
- }
- // 选择文件(非RN环境)
- const pickFileH5 = () => {
- const input = document.createElement('input')
- input.type = 'file'
- input.accept = '.pdf,.docx'
- input.style.display = 'none'
- input.onchange = async (e: Event) => {
- const file = (e.target as HTMLInputElement).files?.[0]
- if (!file) return
- const reader = new FileReader()
- reader.onload = async () => {
- const base64 = (reader.result as string).split(',')[1]
- storeFile(base64, file.name, file.type, file.size)
- }
- reader.readAsDataURL(file)
- document.body.removeChild(input)
- }
- document.body.appendChild(input)
- input.click()
- }
- // 打开审核人选择弹窗
- const openAuditorDialog = async () => {
- auditorVisible.value = true
- auditorLoading.value = true
- auditorList.value = []
- selectedAuditorId.value = ''
- try {
- const roleCode = equipType === EquipmentType.BOILER ? 'Boiler Director' : 'Pipeline Director'
- const res = await getAuditList({ roleCode })
- auditorList.value = (res as any)?.data?.list || []
- } catch (e) {
- console.error('获取审核人列表失败:', e)
- uni.showToast({ title: '获取审核人列表失败', icon: 'none' })
- } finally {
- auditorLoading.value = false
- }
- }
- // 确认提交:上传文件后调用提交接口
- const confirmSubmit = async () => {
- if (!selectedAuditorId.value) {
- uni.showToast({ title: '请选择审核人', icon: 'none' })
- return
- }
- auditorVisible.value = false
- const fileUrl = await doUpload()
- if (!fileUrl) {
- uni.showToast({ title: '请先上传检验方案文件', icon: 'none' })
- return
- }
- try {
- const dataSource = designerRef.value?.getSpreadDataSource()
- if (dataSource) {
- const saveResult = await saveDynamicTbVal({ params: dataSource, instId: instId.value })
- if (saveResult?.code !== 0) {
- throw new Error(saveResult?.msg || '保存报表失败,请重试')
- }
- }
- const reqData = {
- id: refId,
- reportType: PressureReportType.INSPECTIONPLAN,
- prepareJson: JSON.stringify({
- prepareName: uni.getStorageSync('userInfo')?.name || '',
- prepareDate: dayjs().format('YYYY年MM月DD日'),
- }),
- auditUserIds: [selectedAuditorId.value],
- approveUserIds: [],
- manualUrl: fileUrl,
- }
- uni.showLoading({ title: '提交审核中...', mask: true })
- // 调用提交审核接口
- const res = await requestFunc(TaskOrderFuncName.SubmitOpinionNoticeApproval, equipType, reqData)
- if (res?.code === 0) {
- uni.showToast({ title: '提交审核成功', icon: 'success' })
- } else {
- throw new Error(res?.msg || '提交审核失败,请重试')
- }
- } catch (error) {
- console.error('提交审核异常:', error)
- uni.showToast({ title: error.message || '提交审核失败', icon: 'none' })
- } finally {
- uni.navigateBack()
- uni.hideLoading()
- }
- }
- const handleCustomAction = ({ action, data }) => {
- switch (action) {
- case 'upload':
- isRNEnv() ? pickFileRN() : pickFileH5()
- break
- case 'submit':
- if (!pendingFile.value) {
- uni.showToast({ title: '请先上传检验方案文件', icon: 'none' })
- break
- }
- uni.showModal({
- title: '提交审核',
- content: `检验方案文件目前为「${pendingFile.value.name}」,确认提交审核?`,
- confirmText: '确定',
- cancelText: '取消',
- success: async (res) => {
- if (!res.confirm) return
- await openAuditorDialog()
- },
- })
- break
- default:
- break
- }
- }
- </script>
- <style scoped>
- .nav-btn-group {
- display: flex;
- gap: 8px;
- align-items: center;
- }
- .nav-btn {
- padding: 0 12px;
- font-size: 12px;
- line-height: 28px;
- color: #495057;
- white-space: nowrap;
- background-color: #fff;
- border: 1px solid #dee2e6;
- border-radius: 4px;
- }
- .nav-btn.primary {
- color: white;
- background-color: #007bff;
- border-color: #007bff;
- }
- .nav-btn.upload-btn {
- color: white;
- background-color: #e6a23c;
- border-color: #e6a23c;
- }
- .nav-btn.submit-btn {
- color: white;
- background-color: #67c23a;
- border-color: #67c23a;
- }
- /* 审核人弹窗 */
- .auditor-overlay {
- position: fixed;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- z-index: 10001;
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: rgba(0, 0, 0, 0.5);
- }
- .auditor-dialog {
- width: 80%;
- max-height: 70vh;
- overflow: hidden;
- background: #fff;
- border-radius: 12px;
- }
- .auditor-title {
- padding: 16px;
- font-size: 16px;
- font-weight: 600;
- color: #333;
- text-align: center;
- border-bottom: 1px solid #eee;
- }
- .auditor-list {
- max-height: 50vh;
- padding: 8px 0;
- }
- .auditor-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 12px 16px;
- cursor: pointer;
- }
- .auditor-item.active {
- background-color: #ecf5ff;
- }
- .auditor-name {
- font-size: 14px;
- color: #333;
- }
- .auditor-check {
- font-size: 16px;
- color: #007bff;
- }
- .auditor-loading,
- .auditor-empty {
- padding: 20px;
- font-size: 14px;
- color: #999;
- text-align: center;
- }
- .auditor-footer {
- display: flex;
- border-top: 1px solid #eee;
- }
- .auditor-btn {
- flex: 1;
- padding: 14px 0;
- font-size: 15px;
- text-align: center;
- cursor: pointer;
- }
- .auditor-btn.cancel {
- color: #666;
- border-right: 1px solid #eee;
- }
- .auditor-btn.confirm {
- font-weight: 600;
- color: #007bff;
- }
- </style>
|