|
|
@@ -13,22 +13,90 @@
|
|
|
<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 } from 'vue'
|
|
|
+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 { buildFileUrl } from '@/utils/index'
|
|
|
-import { getStandardTemplate } from '@/api/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',
|
|
|
@@ -40,21 +108,47 @@ const businessConfig = ref({
|
|
|
saveButtonText: '保存',
|
|
|
cancelButtonText: '取消',
|
|
|
showAdditionalToolbar: true,
|
|
|
- customButtons: [],
|
|
|
+ customButtons: [
|
|
|
+ {
|
|
|
+ text: '方案上传',
|
|
|
+ class: 'upload-btn',
|
|
|
+ action: 'upload',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ text: '提交审核',
|
|
|
+ class: 'submit-btn',
|
|
|
+ action: 'submit',
|
|
|
+ },
|
|
|
+ ],
|
|
|
},
|
|
|
})
|
|
|
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()
|
|
|
@@ -121,9 +215,9 @@ const downloadFileAsBase64 = (fileUrl: string): Promise<string> => {
|
|
|
const handleSave = async (data: any) => {
|
|
|
const result = await saveDynamicTbVal({ params: data.dataJSON, instId: instId.value })
|
|
|
if (result?.code === 0 && result?.data) {
|
|
|
- uni.showToast({ title: '保存成功', icon: 'success' })
|
|
|
+ uni.showToast({ title: '保存报表成功', icon: 'success' })
|
|
|
} else {
|
|
|
- const msg = result?.msg || '保存失败'
|
|
|
+ const msg = result?.msg || '保存报表失败'
|
|
|
uni.showToast({ title: msg, icon: 'error' })
|
|
|
}
|
|
|
}
|
|
|
@@ -131,4 +225,344 @@ const handleSave = async (data: any) => {
|
|
|
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>
|