IssueReportDialog.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. <template>
  2. <el-dialog
  3. v-model="dialogVisible"
  4. :title="dialogTitle"
  5. width="1000px"
  6. destroy-on-close
  7. @close="handleClose"
  8. >
  9. <ContentWrap>
  10. <!-- 设备清单 -->
  11. <div class="mb-4">
  12. <!-- 部分报告模式:只显示单个设备信息 -->
  13. <template v-if="props.isPartReport">
  14. <div class="flex justify-between items-center mb-2">
  15. <h4>设备信息</h4>
  16. </div>
  17. <el-table
  18. :data="partReportEquipmentList"
  19. size="small"
  20. border
  21. >
  22. <el-table-column label="设备注册代码" prop="equipCode" min-width="150px" />
  23. <el-table-column label="使用证编号" prop="useRegisterNo" min-width="120px" />
  24. <el-table-column label="设备名称" prop="equipName" min-width="120px" />
  25. <el-table-column label="出具报告状态" width="120px" align="center">
  26. <template #default="scope">
  27. {{ getReportStatusText(scope.row) }}
  28. </template>
  29. </el-table-column>
  30. </el-table>
  31. </template>
  32. <!-- 完整报告模式:显示所有设备和操作功能 -->
  33. <template v-else>
  34. <div class="flex justify-between items-center mb-2">
  35. <h4>设备清单</h4>
  36. <div class="flex items-center gap-4">
  37. <el-input
  38. v-model="searchKeyword"
  39. placeholder="搜索设备注册代码、使用证编号或设备名称"
  40. size="small"
  41. style="width: 300px"
  42. clearable
  43. prefix-icon="Search"
  44. />
  45. <div class="text-sm text-gray-600">
  46. {{ searchResultText }}
  47. </div>
  48. </div>
  49. </div>
  50. <!-- 批量操作按钮 -->
  51. <div class="flex items-center gap-2 mb-3">
  52. <el-button
  53. size="small"
  54. type="success"
  55. :disabled="tableSelectedRows.length === 0"
  56. @click="batchAddEquipments"
  57. >
  58. 批量添加 ({{ tableSelectedRows.length }})
  59. </el-button>
  60. <el-button
  61. size="small"
  62. type="warning"
  63. :disabled="tableSelectedRows.length === 0"
  64. @click="batchRemoveEquipments"
  65. >
  66. 批量移除 ({{ tableSelectedRows.length }})
  67. </el-button>
  68. <el-divider direction="vertical" />
  69. <el-button
  70. size="small"
  71. link
  72. @click="selectAllRows"
  73. >
  74. 全选
  75. </el-button>
  76. <el-button
  77. size="small"
  78. link
  79. @click="clearSelection"
  80. >
  81. 全不选
  82. </el-button>
  83. <el-button
  84. size="small"
  85. link
  86. @click="toggleSelection"
  87. >
  88. 反选
  89. </el-button>
  90. </div>
  91. <el-table
  92. ref="tableRef"
  93. :data="filteredEquipmentList"
  94. size="small"
  95. border
  96. :row-class-name="getRowClassName"
  97. v-loading="equipmentLoading"
  98. @selection-change="handleSelectionChange"
  99. >
  100. <el-table-column type="selection" width="50" />
  101. <el-table-column label="设备注册代码" prop="equipCode" min-width="150px" />
  102. <el-table-column label="使用证编号" prop="useRegisterNo" min-width="120px" />
  103. <el-table-column label="设备名称" prop="equipName" min-width="120px" />
  104. <el-table-column label="出具报告状态" width="120px" align="center">
  105. <template #default="scope">
  106. {{ getReportStatusText(scope.row) }}
  107. </template>
  108. </el-table-column>
  109. <el-table-column label="操作" width="120px" align="center">
  110. <template #default="scope">
  111. <el-button
  112. v-if="scope.row.selected"
  113. link
  114. type="danger"
  115. size="small"
  116. @click="toggleEquipmentSelection(scope.row, false)"
  117. >
  118. 移除
  119. </el-button>
  120. <el-button
  121. v-else
  122. link
  123. type="primary"
  124. size="small"
  125. @click="toggleEquipmentSelection(scope.row, true)"
  126. >
  127. 添加
  128. </el-button>
  129. </template>
  130. </el-table-column>
  131. </el-table>
  132. </template>
  133. </div>
  134. <!-- 动态表单区域 -->
  135. <div class="form-section">
  136. <!-- 实体报告表单 -->
  137. <EntityReportForm
  138. v-if="props.reportType === PressureIssueReportType.ENTITY"
  139. ref="entityFormRef"
  140. v-model="formData.entityForm"
  141. :is-part-report="props.isPartReport"
  142. @validate-change="handleFormValidateChange"
  143. />
  144. <!-- 电子报告表单 -->
  145. <ElectronicReportForm
  146. v-if="props.reportType === PressureIssueReportType.ELECTRONIC"
  147. ref="electronicFormRef"
  148. v-model="formData.electronicForm"
  149. :is-part-report="props.isPartReport"
  150. @validate-change="handleFormValidateChange"
  151. />
  152. </div>
  153. </ContentWrap>
  154. <template #footer>
  155. <div class="dialog-footer">
  156. <el-button @click="handleClose">取消</el-button>
  157. <el-button type="primary" @click="handleConfirm" :loading="submitting">
  158. 确定
  159. </el-button>
  160. </div>
  161. </template>
  162. </el-dialog>
  163. </template>
  164. <script setup lang="ts">
  165. import { ref, computed, watch, onMounted } from 'vue'
  166. import { ElMessage } from 'element-plus'
  167. import EntityReportForm from './EntityReportForm.vue'
  168. import ElectronicReportForm from './ElectronicReportForm.vue'
  169. import { BoilerTaskOrderApi } from '@/api/pressure2/boilertaskorder'
  170. import { PressureIssueReportType, PressureIssueReportChecklistType } from '@/utils/constants'
  171. export interface IssueReportDialogProps {
  172. visible: boolean
  173. taskOrderId: string
  174. reportType: number // 实体报告 | 电子报告
  175. defaultReportScope?: number // 默认报告清单类型
  176. defaultSelectedEquipments?: any[] // 默认选中的设备
  177. isPartReport?: boolean // 是否部分报告
  178. }
  179. const props = withDefaults(defineProps<IssueReportDialogProps>(), {
  180. visible: false,
  181. taskOrderId: '',
  182. reportType: PressureIssueReportType.ENTITY,
  183. defaultReportScope: PressureIssueReportChecklistType.ALL,
  184. defaultSelectedEquipments: () => [],
  185. isPartReport: false
  186. })
  187. const emit = defineEmits<{
  188. 'update:visible': [value: boolean]
  189. 'confirm': [data: any]
  190. }>()
  191. // 对话框状态
  192. const dialogVisible = ref(false)
  193. const submitting = ref(false)
  194. const equipmentLoading = ref(false)
  195. // 表单数据
  196. const formData = ref({
  197. entityForm: {},
  198. electronicForm: {}
  199. })
  200. // 设备清单数据
  201. const equipmentList = ref<any[]>([])
  202. const taskOrderDetail = ref<any>(null)
  203. // 表格选择状态
  204. const tableSelectedRows = ref<any[]>([])
  205. const tableRef = ref()
  206. // 搜索状态
  207. const searchKeyword = ref('')
  208. // 表单验证状态
  209. const isFormValid = ref(true)
  210. // 子组件引用
  211. const entityFormRef = ref()
  212. const electronicFormRef = ref()
  213. // 计算属性
  214. const dialogTitle = computed(() => {
  215. return props.reportType === PressureIssueReportType.ENTITY ? '出具实体报告' : '出具电子报告'
  216. })
  217. // 选中设备数量
  218. const selectedCount = computed(() => {
  219. if (props.isPartReport) {
  220. return partReportEquipmentList.value.length
  221. }
  222. return equipmentList.value.filter(item => item.selected).length
  223. })
  224. // 总设备数量
  225. const totalCount = computed(() => {
  226. if (props.isPartReport) {
  227. return partReportEquipmentList.value.length
  228. }
  229. return equipmentList.value.length
  230. })
  231. // 部分报告的设备列表
  232. const partReportEquipmentList = computed(() => {
  233. if (props.isPartReport && props.defaultSelectedEquipments?.length > 0) {
  234. return props.defaultSelectedEquipments.map(item => ({
  235. ...item,
  236. selected: true
  237. }))
  238. }
  239. return []
  240. })
  241. // 过滤后的设备列表
  242. const filteredEquipmentList = computed(() => {
  243. if (!searchKeyword.value.trim()) {
  244. return equipmentList.value
  245. }
  246. const keyword = searchKeyword.value.toLowerCase().trim()
  247. return equipmentList.value.filter(item => {
  248. const equipCode = (item.equipCode || '').toLowerCase()
  249. const useRegisterNo = (item.useRegisterNo || '').toLowerCase()
  250. const equipName = (item.equipName || '').toLowerCase()
  251. return equipCode.includes(keyword) ||
  252. useRegisterNo.includes(keyword) ||
  253. equipName.includes(keyword)
  254. })
  255. })
  256. // 搜索结果文本
  257. const searchResultText = computed(() => {
  258. const filteredCount = filteredEquipmentList.value.length
  259. const selectedInFilteredCount = filteredEquipmentList.value.filter(item => item.selected).length
  260. if (searchKeyword.value.trim()) {
  261. return `搜索到 ${filteredCount} 台设备,已选择 ${selectedInFilteredCount} 台`
  262. } else {
  263. return `已选择 ${selectedCount.value} / ${totalCount.value} 台设备`
  264. }
  265. })
  266. // 报告状态文本
  267. const getReportStatusText = (row: any) => {
  268. if (row.isIssueReport === 1) return '已出具'
  269. if (row.isIssueReport === 0) return '未出具'
  270. return '-'
  271. }
  272. // 监听visible变化
  273. watch(() => props.visible, (newVal) => {
  274. dialogVisible.value = newVal
  275. if (newVal) {
  276. resetForm()
  277. if (!props.isPartReport) {
  278. loadTaskOrderDetail()
  279. }
  280. }
  281. }, { immediate: true })
  282. // 监听dialogVisible变化
  283. watch(dialogVisible, (newVal) => {
  284. emit('update:visible', newVal)
  285. })
  286. // 加载任务单详情
  287. const loadTaskOrderDetail = async () => {
  288. if (!props.taskOrderId) return
  289. try {
  290. equipmentLoading.value = true
  291. const response = await TaskOrderApi.getTaskOrder(props.taskOrderId)
  292. taskOrderDetail.value = response
  293. // 设置设备清单,默认全部选中
  294. equipmentList.value = (response.orderItems || []).map(item => ({
  295. ...item,
  296. selected: true
  297. }))
  298. } catch (error) {
  299. console.error('加载任务单详情失败:', error)
  300. ElMessage.error('加载任务单详情失败')
  301. } finally {
  302. equipmentLoading.value = false
  303. }
  304. }
  305. // 切换设备选择状态
  306. const toggleEquipmentSelection = (equipment: any, selected: boolean) => {
  307. const index = equipmentList.value.findIndex(item => item.id === equipment.id)
  308. if (index > -1) {
  309. equipmentList.value[index].selected = selected
  310. }
  311. }
  312. // 表格选择变化
  313. const handleSelectionChange = (selection: any[]) => {
  314. tableSelectedRows.value = selection
  315. }
  316. // 批量添加设备
  317. const batchAddEquipments = () => {
  318. tableSelectedRows.value.forEach(equipment => {
  319. const index = equipmentList.value.findIndex(item => item.id === equipment.id)
  320. if (index > -1) {
  321. equipmentList.value[index].selected = true
  322. }
  323. })
  324. ElMessage.success(`已添加 ${tableSelectedRows.value.length} 台设备`)
  325. clearSelection()
  326. }
  327. // 批量移除设备
  328. const batchRemoveEquipments = () => {
  329. tableSelectedRows.value.forEach(equipment => {
  330. const index = equipmentList.value.findIndex(item => item.id === equipment.id)
  331. if (index > -1) {
  332. equipmentList.value[index].selected = false
  333. }
  334. })
  335. ElMessage.success(`已移除 ${tableSelectedRows.value.length} 台设备`)
  336. clearSelection()
  337. }
  338. // 全选
  339. const selectAllRows = () => {
  340. if (tableRef.value) {
  341. tableRef.value.toggleAllSelection()
  342. }
  343. }
  344. // 清除选择
  345. const clearSelection = () => {
  346. if (tableRef.value) {
  347. tableRef.value.clearSelection()
  348. }
  349. }
  350. // 反选
  351. const toggleSelection = () => {
  352. if (tableRef.value) {
  353. filteredEquipmentList.value.forEach(row => {
  354. tableRef.value.toggleRowSelection(row, !tableSelectedRows.value.includes(row))
  355. })
  356. }
  357. }
  358. // 表格行样式
  359. const getRowClassName = ({ row }: { row: any }) => {
  360. return row.selected ? '' : 'disabled-row'
  361. }
  362. // 表单验证状态变化
  363. const handleFormValidateChange = (valid: boolean) => {
  364. isFormValid.value = valid
  365. }
  366. // 重置表单
  367. const resetForm = () => {
  368. formData.value = {
  369. entityForm: {} as any,
  370. electronicForm: {} as any
  371. }
  372. equipmentList.value = []
  373. tableSelectedRows.value = []
  374. searchKeyword.value = ''
  375. isFormValid.value = true
  376. }
  377. // 确认提交
  378. const handleConfirm = async () => {
  379. // 验证基础表单
  380. const selectedEquipments = props.isPartReport
  381. ? partReportEquipmentList.value
  382. : equipmentList.value.filter(item => item.selected)
  383. if (selectedEquipments.length === 0) {
  384. ElMessage.warning('请选择需要出具报告的设备')
  385. return
  386. }
  387. // 验证子表单
  388. let subFormValid = false
  389. if (props.reportType === PressureIssueReportType.ENTITY && entityFormRef.value) {
  390. subFormValid = await entityFormRef.value.validate()
  391. } else if (props.reportType === PressureIssueReportType.ELECTRONIC && electronicFormRef.value) {
  392. subFormValid = await electronicFormRef.value.validate()
  393. }
  394. if (!subFormValid) {
  395. ElMessage.warning('请填写完整的表单信息')
  396. return
  397. }
  398. try {
  399. submitting.value = true
  400. const entityForm: any = formData.value.entityForm || {}
  401. const electronicForm: any = formData.value.electronicForm || {}
  402. // 判断报告类型:全部设备选中为全部报告,否则为部分报告
  403. const checklistType = selectedEquipments.length === totalCount.value ?
  404. PressureIssueReportChecklistType.ALL : PressureIssueReportChecklistType.PART
  405. // 构建提交数据,按照API文档格式
  406. const itemList = selectedEquipments.map(item => item.id)
  407. const submitData = {
  408. orderId: props.taskOrderId,
  409. checklistType: checklistType,
  410. reportType: props.reportType,
  411. // 实体报告字段
  412. issueMethod: props.reportType === PressureIssueReportType.ENTITY ? entityForm.deliveryType : null,
  413. recipient: entityForm.recipient || entityForm.pickupPerson || '',
  414. recipientPhone: entityForm.recipientPhone || entityForm.pickupPhone || '',
  415. recipientAddress: entityForm.recipientAddress || '',
  416. trackingNumber: entityForm.expressNo || '',
  417. trackingCompany: entityForm.expressCompany || '',
  418. remark: entityForm.remark || '',
  419. copyNumber: parseInt(entityForm.printCount) || 1,
  420. businessMan: entityForm.salesperson || '',
  421. otherMethod: entityForm.otherDescription || '',
  422. // 电子报告字段
  423. miniProgramPush: props.reportType === PressureIssueReportType.ELECTRONIC ? electronicForm.deliveryType?.includes('miniprogram') : false,
  424. emailPush: props.reportType === PressureIssueReportType.ELECTRONIC ? electronicForm.deliveryType?.includes('email') : false,
  425. email: electronicForm.emailAddress || '',
  426. miniProgramAdmin: props.reportType === PressureIssueReportType.ELECTRONIC ? electronicForm.miniprogramTarget?.includes('main') : false,
  427. miniProgramSubAccount: props.reportType === PressureIssueReportType.ELECTRONIC ? electronicForm.miniprogramTarget?.includes('sub') : false,
  428. // 设备列表
  429. itemList: itemList
  430. }
  431. // 调用API
  432. await TaskOrderApi.issueReport(submitData)
  433. emit('confirm', submitData)
  434. ElMessage.success('出具报告成功')
  435. handleClose()
  436. } catch (error) {
  437. console.error('出具报告失败:', error)
  438. ElMessage.error('出具报告失败')
  439. } finally {
  440. submitting.value = false
  441. }
  442. }
  443. // 关闭对话框
  444. const handleClose = () => {
  445. dialogVisible.value = false
  446. }
  447. // 暴露方法
  448. defineExpose({
  449. resetForm
  450. })
  451. </script>
  452. <style lang="scss" scoped>
  453. .form-section {
  454. margin-top: 16px;
  455. }
  456. .dialog-footer {
  457. text-align: right;
  458. }
  459. :deep(.el-badge__content) {
  460. background-color: var(--el-color-primary);
  461. }
  462. // 禁用行样式
  463. :deep(.disabled-row) {
  464. background-color: var(--el-fill-color-light);
  465. color: var(--el-text-color-disabled);
  466. td {
  467. background-color: var(--el-fill-color-light) !important;
  468. }
  469. }
  470. // 设备清单表格样式
  471. :deep(.el-table) {
  472. .el-table__row {
  473. transition: background-color 0.2s ease;
  474. }
  475. }
  476. </style>