PipePlanScheduleDialog.vue 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. <template>
  2. <el-dialog
  3. :title="title"
  4. v-model="dialogVisible"
  5. :width="'1000px'"
  6. append-to-body
  7. destroy-on-close
  8. >
  9. <div class="flex flex-col">
  10. <div class="w-full">
  11. <el-form
  12. ref="formRef"
  13. :model="formData"
  14. :rules="formRules"
  15. label-width="120px"
  16. >
  17. <div v-if="props.selectedRows.length > 1" class="mb-4 text-gray-500">
  18. 已选择 {{ props.selectedRows.length }} 条记录进行批量排期
  19. </div>
  20. <el-form-item label="检验性质" prop="checkType" labelWidth="80px">
  21. <el-radio-group v-model="formData.checkType">
  22. <el-radio
  23. v-for="(label, value) in PressurePipeCheckTypeMap"
  24. :key="value"
  25. :value="value"
  26. >
  27. {{ label }}
  28. </el-radio>
  29. </el-radio-group>
  30. </el-form-item>
  31. <!-- 定期检验配置 -->
  32. <div class="plan-section" v-if="formData.checkType == PressurePipeCheckType.REGULAR">
  33. <div class="section-title">
  34. <span>定期检验</span>
  35. <span class="section-info">
  36. 待检设备数量: {{ legalEquipCount }} &nbsp;&nbsp; 最近临期时间: {{ legalLatestTime }}
  37. </span>
  38. </div>
  39. <el-form-item label="检验日期" prop="legalDate">
  40. <el-date-picker
  41. v-model="formData.legalDate"
  42. type="date"
  43. placeholder="选择日期"
  44. value-format="YYYY-MM-DD"
  45. :disabled-date="(time) => time.getTime() < Date.now() - 8.64e7"
  46. class="!w-240px"
  47. />
  48. <el-checkbox
  49. v-if="!isFrontDesk"
  50. v-model="formData.legalIsOrderConfirm"
  51. class="ml-4"
  52. :disabled="legalEquipCount === 0"
  53. >
  54. 由前台约检确认
  55. </el-checkbox>
  56. </el-form-item>
  57. <el-form-item label="检验员">
  58. <CheckerSelect
  59. ref="checkerLegalRef"
  60. v-model="legalSelectedCheckers"
  61. @change="(v: CheckerItem[]) => handleCheckerSelectChange('legal', v)"
  62. />
  63. </el-form-item>
  64. <el-form-item label="收费形式" prop="legalChargeType">
  65. <div class="flex items-center gap-4">
  66. <el-select v-model="formData.legalChargeType" placeholder="请选择收费形式"
  67. class="!w-120px">
  68. <el-option label="非合同收费" value="1"/>
  69. </el-select>
  70. <span class="text-gray-600">是否免征:</span>
  71. <el-select v-model="formData.legalIsExempt" placeholder="请选择是否免征"
  72. class="!w-60px" @change="handleLegalExemptChange">
  73. <el-option label="否" value="0"/>
  74. <el-option label="是" value="1"/>
  75. </el-select>
  76. <span class="text-gray-600">应收法定金额: {{ formData.legalShouldAmount }}</span>
  77. <span class="text-gray-600">服务收费金额: {{ formData.legalServiceAmount }}</span>
  78. <span class="text-gray-600">免征费用: {{ formData.legalReduceFee }}</span>
  79. </div>
  80. </el-form-item>
  81. </div>
  82. <!-- 年度检查配置 -->
  83. <div class="plan-section" v-else-if="formData.checkType == PressurePipeCheckType.ANNUAL">
  84. <div class="section-title">
  85. <span>年度检查</span>
  86. <span class="section-info">
  87. 待检设备数量: {{ yearEquipCount }} &nbsp;&nbsp; 最近临期时间: {{ yearLatestTime }}
  88. </span>
  89. </div>
  90. <el-form-item label="检验日期" prop="yearDate">
  91. <el-date-picker
  92. v-model="formData.yearDate"
  93. type="date"
  94. placeholder="选择日期"
  95. value-format="YYYY-MM-DD"
  96. :disabled-date="(time) => time.getTime() < Date.now() - 8.64e7"
  97. class="!w-240px"
  98. />
  99. <el-checkbox
  100. v-if="!isFrontDesk"
  101. v-model="formData.yearIsOrderConfirm"
  102. class="ml-4"
  103. :disabled="yearEquipCount === 0"
  104. >
  105. 由前台约检确认
  106. </el-checkbox>
  107. </el-form-item>
  108. <el-form-item label="检验员">
  109. <CheckerSelect
  110. ref="checkerYearRef"
  111. v-model="yearSelectedCheckers"
  112. @change="(v: CheckerItem[]) => handleCheckerSelectChange('year', v)"
  113. />
  114. </el-form-item>
  115. <el-form-item label="收费形式" prop="yearChargeType">
  116. <div class="flex items-center gap-4">
  117. <el-select v-model="formData.yearChargeType" placeholder="请选择收费形式"
  118. class="!w-120px">
  119. <el-option label="非合同收费" value="1"/>
  120. </el-select>
  121. <span class="text-gray-600">应收法定金额: {{ formData.yearShouldAmount }}</span>
  122. <span class="text-gray-600">服务收费金额: {{ formData.yearServiceAmount }}</span>
  123. </div>
  124. </el-form-item>
  125. </div>
  126. <!-- 检验项目 -->
  127. <div
  128. class="checkItemContent"
  129. v-for="checkItem in checkItemList"
  130. v-show="showCheck(checkItem)"
  131. :key="checkItem.inspectionNature + '-' + checkItem.type"
  132. >
  133. <div v-if="checkItem.inspectionNature == formData.checkType && getEquipCountByType(checkItem.inspectionNature) != 0 && (checkItem.inspectionNature == '100' ? checkItem.type == '1' : checkItem.type == '2')">
  134. <div class="content-title" @click="toggleCheckItemCollapse(checkItem)">
  135. <el-icon class="collapse-icon" :class="{ 'is-collapsed': !checkItem.isExpanded }">
  136. <ArrowDown />
  137. </el-icon>
  138. {{ checkItem.inspectionNatureName }}
  139. </div>
  140. <div class="inspection-grid" v-show="checkItem.isExpanded">
  141. <template v-if="checkItem.itemList.length > 0">
  142. <!-- Grid 容器 -->
  143. <div
  144. class="grid-container"
  145. :style="{
  146. 'grid-template-columns':
  147. checkItem.itemList.length < 3
  148. ? `repeat(${checkItem.itemList.length}, 1fr)`
  149. : `repeat(3, 1fr)`
  150. }"
  151. >
  152. <!-- 循环渲染子项(模拟数据) -->
  153. <div class="grid-item" v-for="(item, index) in checkItem.itemList" :key="index">
  154. <el-checkbox
  155. :disabled="item.isMainProject === '1'|| item.name === '一览表'"
  156. v-model="item.use"
  157. />
  158. <!-- @change="(val) => handleCheckItemSelectedChange(item, val)"-->
  159. <div style="display: flex; align-items: center; gap: 4px;">
  160. <span v-if="item.isMainProject === '1'"
  161. style="display: inline-flex; align-items: center; justify-content: center; width: 20px; height: 20px; background-color: #6cbcf5; color: #fff; font-size: 12px; border-radius: 2px; font-weight: bold;">主</span>
  162. <span>{{ item.name }}</span>
  163. </div>
  164. <span v-if="item.isFee === '1'">
  165. (
  166. <el-button link type="primary" @click="() => {
  167. openFeeDialog(item)
  168. }"
  169. >费用:{{ item.fee || '无' }}</el-button
  170. >
  171. )
  172. </span>
  173. </div>
  174. </div>
  175. </template>
  176. <el-empty style="height: 200px;" v-else
  177. :description="`暂无【${checkItem.inspectionNatureName}】检验项目`"/>
  178. </div>
  179. </div>
  180. </div>
  181. </el-form>
  182. </div>
  183. </div>
  184. <template #footer>
  185. <div class="flex justify-end">
  186. <el-button @click="handleCancel">取消</el-button>
  187. <el-button type="primary" :loading="submitLoading" @click="handleConfirm">确定</el-button>
  188. </div>
  189. </template>
  190. <!-- 费用计算弹窗 -->
  191. <CalcProjectFee
  192. v-if="currentFeeItem"
  193. v-model="showCalcCheckItemFeeDialog"
  194. :project-id="currentFeeItem.connectId"
  195. :old-fee="currentFeeItem.fee || 0"
  196. :fee-calculate-json-str="currentFeeItem.feeCalculateJson || ''"
  197. @save="handleFeeSave"
  198. />
  199. </el-dialog>
  200. </template>
  201. <script setup lang="ts">
  202. import {FormInstance} from 'element-plus'
  203. import {ArrowDown} from '@element-plus/icons-vue'
  204. import {useMessage} from '@/hooks/web/useMessage'
  205. import dayjs from 'dayjs';
  206. import {EquipPipeSchedulingApi, EquipPipeSchedulingVO} from "@/api/pressure2/pipescheduling";
  207. import {
  208. PressurePipeCheckType,
  209. PressurePipeCheckTypeMap
  210. } from "@/utils/constants";
  211. import {PipeTaskOrderApi} from "@/api/pressure2/pipetaskorder";
  212. import {InspectionNatureTypeApi} from "@/api/pressure2/inspectionnaturetype";
  213. import CheckerSelect from '@/views/pressure2/components/CheckerSelect/index.vue'
  214. import type { CheckerItem } from '@/views/pressure2/components/CheckerSelect'
  215. import {ClientUnitApi} from "../../../../api/system/clientunit";
  216. import CalcProjectFee from '@/views/pressure2/equipboilerscheduling/components/calcProjectFee.vue'
  217. const message = useMessage()
  218. const {queryCheckItemList, querySchedulingCheckItemList} = PipeTaskOrderApi
  219. const props = defineProps({
  220. selectedRows: {
  221. type: Array as PropType<EquipPipeSchedulingVO[]>,
  222. default: () => []
  223. },
  224. selectedPipeRows: {
  225. type: Array as PropType<EquipPipeSchedulingVO[]>,
  226. default: () => []
  227. },
  228. selectedLegalList: {
  229. type: Array as PropType<EquipPipeSchedulingVO[]>,
  230. default: () => []
  231. },
  232. selectedYearList: {
  233. type: Array as PropType<EquipPipeSchedulingVO[]>,
  234. default: () => []
  235. },
  236. source: {
  237. type: String as PropType<string>,
  238. default: 'pressure'
  239. }
  240. })
  241. // 是否前台约检来源:前台约检跳过计划表提交+约检确认,直接生成受理单
  242. const isFrontDesk = computed(() => props.source === 'plan')
  243. const emit = defineEmits(['success', 'close'])
  244. const dialogVisible = ref(false)
  245. const formRef = ref<FormInstance>()
  246. const submitLoading = ref(false) // 提交按钮 loading
  247. const checkerLegalRef = ref()
  248. const checkerYearRef = ref()
  249. const title = computed(() => props.selectedPipeRows.length > 1 ? '批量计划排期' : '计划排期')
  250. // 检验项目
  251. const checkItemList = ref<any[]>([])
  252. const showCalcCheckItemFeeDialog = ref(false)
  253. const currentFeeItem = ref<any>(null)
  254. const equipList = ref<any[]>([])
  255. // 定期检验设备数量
  256. const legalEquipCount = computed(() => {
  257. return props.selectedLegalList.length
  258. })
  259. // 年度检查设备数量
  260. const yearEquipCount = computed(() => {
  261. return props.selectedYearList.length
  262. })
  263. // 定期检验最近临期时间
  264. const legalLatestTime = computed(() => {
  265. const latestTime = props.selectedPipeRows.reduce((nextLegalCheckDate, row) => {
  266. if (!row.nextLegalCheckDate) return nextLegalCheckDate
  267. if (!nextLegalCheckDate || row.nextLegalCheckDate < nextLegalCheckDate) {
  268. return row.nextLegalCheckDate
  269. }
  270. return nextLegalCheckDate
  271. }, '')
  272. return latestTime ? dayjs(latestTime).format('YYYY-MM-DD') : '无'
  273. })
  274. // 年度检查最近临期时间
  275. const yearLatestTime = computed(() => {
  276. const latestTime = props.selectedPipeRows.reduce((nextYearCheckDate, row) => {
  277. if (!row.nextYearCheckDate) return nextYearCheckDate
  278. if (!nextYearCheckDate || row.nextYearCheckDate < nextYearCheckDate) {
  279. return row.nextYearCheckDate
  280. }
  281. return nextYearCheckDate
  282. }, '')
  283. return latestTime ? dayjs(latestTime).format('YYYY-MM-DD') : '无'
  284. })
  285. const formData = ref({
  286. checkType: '100',
  287. // 定期检验
  288. legalDate: '',
  289. legalTeamList: [],
  290. legalTaskList: [],
  291. legalIsOrderConfirm: true,
  292. legalChargeType: '1',
  293. legalIsExempt: '0',
  294. legalShouldAmount: 0,
  295. legalServiceAmount: 0,
  296. legalReduceFee: 0,
  297. // 年度检查
  298. yearDate: '',
  299. yearTeamList: [],
  300. yearTaskList: [],
  301. yearIsOrderConfirm: true,
  302. yearChargeType: '1',
  303. yearIsExempt: '0',
  304. yearShouldAmount: 0,
  305. yearServiceAmount: 0,
  306. yearReduceFee: 0,
  307. })
  308. // 各类型检验员选择状态
  309. const legalSelectedCheckers = ref<CheckerItem[]>([])
  310. const yearSelectedCheckers = ref<CheckerItem[]>([])
  311. // 表单验证规则
  312. const formRules = {
  313. legalDate: [{
  314. validator: (rule, value, callback) => {
  315. if (!value && formData.value.legalTaskList.length > 0) {
  316. callback(new Error('请选择定检日期'))
  317. } else {
  318. callback()
  319. }
  320. },
  321. trigger: 'change'
  322. }],
  323. yearDate: [{
  324. validator: (rule, value, callback) => {
  325. if (!value && formData.value.yearTaskList.length > 0) {
  326. callback(new Error('请选择年检日期'))
  327. } else {
  328. callback()
  329. }
  330. },
  331. trigger: 'change'
  332. }],
  333. }
  334. /** 处理统一 CheckerSelect 组件的选择变化 */
  335. const handleCheckerSelectChange = (type: 'legal' | 'year', checkers: CheckerItem[]) => {
  336. updateTeamList(type)
  337. }
  338. /** 监听检验员变化 */
  339. const syncCheckers = (sourceType: string, checkers: CheckerItem[]) => {
  340. const typeMap = {
  341. legal: ['year'],
  342. year: ['legal']
  343. }
  344. typeMap[sourceType].forEach(type => {
  345. const checkerMap = {
  346. legal: legalSelectedCheckers,
  347. year: yearSelectedCheckers,
  348. }
  349. // 只有在未选择检验员的情况下才自动同步
  350. if (checkerMap[type].value.length === 0) {
  351. checkerMap[type].value = [...checkers]
  352. updateTeamList(type)
  353. }
  354. })
  355. }
  356. const openFeeDialog = (item) => {
  357. showCalcCheckItemFeeDialog.value = true
  358. currentFeeItem.value = item
  359. }
  360. const handleFeeSave = (result: { fee: number; feeCalculateJson: string }) => {
  361. if (currentFeeItem.value) {
  362. currentFeeItem.value.fee = result.fee
  363. currentFeeItem.value.feeCalculateJson = result.feeCalculateJson
  364. }
  365. currentFeeItem.value = null
  366. handleCheckItemFeeConfirm()
  367. showCalcCheckItemFeeDialog.value = false
  368. }
  369. /** 处理定期检验免征变化 */
  370. const handleLegalExemptChange = (val: string) => {
  371. calculateLegalAmounts()
  372. }
  373. /** 处理年度检查免征变化 */
  374. const handleYearExemptChange = (val: string) => {
  375. calculateYearAmounts()
  376. }
  377. /** 计算定期检验金额 */
  378. const calculateLegalAmounts = () => {
  379. const legalItems = checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.REGULAR)
  380. if (!legalItems) return
  381. // 分离法定收费项目和服务收费项目
  382. const find = legalItems?.find(item => item.type === '1');
  383. if (!find) return;
  384. const statutoryItems = find.itemList.filter(item => item.use)
  385. const find1 = legalItems?.find(item => item.type === '2');
  386. if (!find1) return;
  387. const serviceItems = find1.itemList.filter(item => item.use)
  388. // 计算法定金额总和
  389. const totalStatutoryAmount = statutoryItems.reduce((sum, item) => sum + (item.fee || 0), 0)
  390. // 计算服务收费金额总和
  391. const totalServiceAmount = serviceItems.reduce((sum, item) => sum + (item.fee || 0), 0)
  392. if (formData.value.legalIsExempt === '1') {
  393. // 免征:免征费用=应收法定金额,应收法定金额=0
  394. formData.value.legalReduceFee = totalStatutoryAmount
  395. formData.value.legalShouldAmount = 0
  396. } else {
  397. // 不免征:免征费用=0,应收法定金额=总金额
  398. formData.value.legalReduceFee = 0
  399. formData.value.legalShouldAmount = totalStatutoryAmount
  400. }
  401. formData.value.legalServiceAmount = totalServiceAmount
  402. }
  403. /** 计算年度检查金额 */
  404. const calculateYearAmounts = () => {
  405. const yearItems = checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.ANNUAL)
  406. if (!yearItems) return
  407. // 分离法定收费项目和服务收费项目
  408. const find = yearItems?.find(item => item.type === '1');
  409. if (!find) return;
  410. const statutoryItems = find.itemList.filter(item => item.use)
  411. const find1 = yearItems?.find(item => item.type === '2');
  412. if (!find1) return;
  413. const serviceItems = find1.itemList.filter(item => item.use)
  414. // 计算法定金额总和
  415. const totalStatutoryAmount = statutoryItems.reduce((sum, item) => sum + (item.fee || 0), 0)
  416. // 计算服务收费金额总和
  417. const totalServiceAmount = serviceItems.reduce((sum, item) => sum + (item.fee || 0), 0)
  418. if (formData.value.yearIsExempt === '1') {
  419. // 免征:免征费用=应收法定金额,应收法定金额=0
  420. formData.value.yearReduceFee = totalStatutoryAmount
  421. formData.value.yearShouldAmount = 0
  422. } else {
  423. // 不免征:免征费用=0,应收法定金额=总金额
  424. formData.value.yearReduceFee = 0
  425. formData.value.yearShouldAmount = totalStatutoryAmount
  426. }
  427. formData.value.yearServiceAmount = totalServiceAmount
  428. }
  429. const handleCheckItemFeeConfirm = () => {
  430. // 费用修改后重新计算金额
  431. calculateLegalAmounts()
  432. calculateYearAmounts()
  433. }
  434. /**
  435. * 根据任务列表长度展示项目
  436. * @param checkItem
  437. */
  438. const showCheck = (checkItem) => {
  439. if (checkItem.inspectionNature == PressurePipeCheckType.REGULAR) {
  440. return formData.value.legalTaskList.length > 0
  441. }
  442. if (checkItem.inspectionNature == PressurePipeCheckType.ANNUAL) {
  443. return formData.value.yearTaskList.length > 0
  444. }
  445. return true
  446. }
  447. // 监听检验性质切换,按需查询项目
  448. watch(() => formData.value.checkType, (newType, oldType) => {
  449. if (newType && newType !== oldType && !isOpening.value) {
  450. handleQueryCheckItemList(newType)
  451. }
  452. })
  453. // 监听检验项目使用状态变化,自动重新计算金额
  454. watch(() => checkItemList.value, () => {
  455. calculateLegalAmounts()
  456. calculateYearAmounts()
  457. }, {deep: true})
  458. // 根据检验类型获取设备数量
  459. const getEquipCountByType = (type: string) => {
  460. if (type == 100) {
  461. return legalEquipCount.value
  462. } else if (type == 200) {
  463. return yearEquipCount.value
  464. }
  465. return 0
  466. }
  467. /** 更新 teamList */
  468. const updateTeamList = (type) => {
  469. const checkerMap = {
  470. legal: legalSelectedCheckers,
  471. year: yearSelectedCheckers
  472. }
  473. const teamListMap = {
  474. legal: 'legalTeamList',
  475. year: 'yearTeamList',
  476. }
  477. // 按 groupTeamId 分组
  478. const groupedTeams = checkerMap[type].value.reduce((acc, checker) => {
  479. const {groupTeamId} = checker
  480. if (!acc[groupTeamId]) {
  481. acc[groupTeamId] = {
  482. groupTeamId,
  483. leaderId: '',
  484. memberIdList: []
  485. }
  486. }
  487. if (checker.isLeader) {
  488. acc[groupTeamId].leaderId = checker.memberId
  489. } else {
  490. acc[groupTeamId].memberIdList.push(checker.memberId)
  491. }
  492. return acc
  493. }, {})
  494. formData.value[teamListMap[type]] = Object.values(groupedTeams)
  495. }
  496. // 查询项目列表(按当前检验性质查询,并缓存结果)
  497. const checkItemListCache = ref<Record<string, any[]>>({})
  498. const isOpening = ref(false) // 标记弹窗是否正在打开,防止watch在数据未就绪时触发查询
  499. const handleQueryCheckItemList = async (checkType?: string) => {
  500. const targetType = checkType || formData.value.checkType
  501. // 如果已缓存,直接使用缓存数据
  502. if (checkItemListCache.value[targetType]) {
  503. checkItemList.value = [
  504. ...checkItemList.value.filter(i => i.inspectionNature != targetType),
  505. ...checkItemListCache.value[targetType]
  506. ]
  507. return
  508. }
  509. // 校验所有选中的任务列表中的设备类型是否一致
  510. const allTasks = [
  511. ...formData.value.legalTaskList,
  512. ...formData.value.yearTaskList
  513. ]
  514. if (allTasks.length > 0) {
  515. const firstType = allTasks[0].pipeCategory
  516. const firstUnit = allTasks[0].unitId
  517. const hasDifferentType = allTasks.some(task => task.pipeCategory !== firstType)
  518. const hasDifferentUnit = allTasks.some(task => task.unitId !== firstUnit)
  519. if (hasDifferentType) {
  520. ElMessage.error('请选择同一类型设备')
  521. dialogVisible.value = false
  522. return
  523. }
  524. if (hasDifferentUnit) {
  525. ElMessage.error('请选择同一单位设备')
  526. dialogVisible.value = false
  527. return
  528. }
  529. if (firstUnit){
  530. ClientUnitApi.getClientUnit(firstUnit).then(res => {
  531. formData.value.yearIsExempt = res.isExempt
  532. formData.value.legalIsExempt = res.isExempt
  533. })
  534. }
  535. }
  536. // 获取统一的设备类型,如果列表为空则设为 undefined
  537. const equipType = allTasks.length > 0 ? allTasks[0].pipeCategory : undefined
  538. const type = Number(targetType) || targetType
  539. const params = {
  540. orderId: "0",
  541. itemIds: equipList.value.map(item => item.id),
  542. equipmentCategory: 300,
  543. inspectionNature: [type],
  544. equipType:equipType
  545. };
  546. const queryResult = await querySchedulingCheckItemList(params);
  547. const inspectionNatureType = await InspectionNatureTypeApi.getInspectionNatureTypePage({
  548. pageNo: 1,
  549. pageSize: 1,
  550. equip: '300',
  551. nature: type
  552. })
  553. const inType = inspectionNatureType.list[0].type
  554. const newItems: any[] = []
  555. newItems.push({
  556. inspectionNatureName: PressurePipeCheckTypeMap[type] + " 法定收费项目",
  557. inspectionNature: type,
  558. type: '1',
  559. isExpanded: inType == 1,
  560. itemList: (queryResult || []).map((item) => (
  561. {
  562. ...item,
  563. isAutoAmount: '0',
  564. inspectionNature: type,
  565. type: '1',
  566. use: inType == 1 ? item.name === "一览表" ? true : item.use : false
  567. }
  568. ))
  569. });
  570. newItems.push({
  571. inspectionNatureName: PressurePipeCheckTypeMap[type] + " 服务收费项目",
  572. inspectionNature: type,
  573. type: '2',
  574. isExpanded: inType == 2,
  575. itemList: (queryResult || []).map((item) => (
  576. {
  577. ...item,
  578. isAutoAmount: '0',
  579. inspectionNature: type,
  580. type: '2',
  581. use: inType == 2 ? item.name === "一览表" ? true : item.use : false
  582. }
  583. ))
  584. });
  585. // 缓存当前类型的结果
  586. checkItemListCache.value[type] = newItems
  587. // 合并到 checkItemList
  588. checkItemList.value = [
  589. ...checkItemList.value.filter(i => i.inspectionNature != type),
  590. ...newItems
  591. ]
  592. }
  593. /** 切换检验项目展开/收缩状态 */
  594. const toggleCheckItemCollapse = (checkItem: any) => {
  595. checkItem.isExpanded = !checkItem.isExpanded
  596. }
  597. /** 打开弹窗 */
  598. const open = async (selectedLegalList?: EquipPipeSchedulingVO[], selectedYearList?: EquipPipeSchedulingVO[],checkType?) => {
  599. //console.log('open', props.selectedRows)
  600. isOpening.value = true
  601. dialogVisible.value = true
  602. // 重置所有检验员选择
  603. legalSelectedCheckers.value = []
  604. yearSelectedCheckers.value = []
  605. nextTick(() => {
  606. checkerLegalRef.value?.resetValidation()
  607. checkerYearRef.value?.resetValidation()
  608. })
  609. // 重置表单数据
  610. formData.value = {
  611. checkType: checkType || '100',
  612. // 定期检验
  613. legalDate: '',
  614. legalTeamList: [],
  615. legalTaskList: [],
  616. legalIsOrderConfirm: true,
  617. legalChargeType: '1',
  618. legalIsExempt: '0',
  619. legalShouldAmount: 0,
  620. legalServiceAmount: 0,
  621. legalReduceFee: 0,
  622. // 年度检查
  623. yearDate: '',
  624. yearTeamList: [],
  625. yearTaskList: [],
  626. yearIsOrderConfirm: true,
  627. yearChargeType: '1',
  628. yearIsExempt: '0',
  629. yearShouldAmount: 0,
  630. yearServiceAmount: 0,
  631. yearReduceFee: 0,
  632. }
  633. // 初始化 taskList
  634. formData.value.legalTaskList = selectedLegalList
  635. formData.value.yearTaskList = selectedYearList
  636. // 清空缓存和项目列表,只查询当前检验性质
  637. checkItemList.value = []
  638. checkItemListCache.value = {}
  639. await handleQueryCheckItemList(formData.value.checkType)
  640. isOpening.value = false
  641. // 初始化金额计算(等待checkItemList加载完成)
  642. setTimeout(() => {
  643. calculateLegalAmounts()
  644. calculateYearAmounts()
  645. }, 100)
  646. }
  647. /** 取消操作 */
  648. const handleCancel = () => {
  649. dialogVisible.value = false
  650. checkItemList.value = []
  651. checkItemListCache.value = {}
  652. emit('close')
  653. }
  654. /** 确认操作 */
  655. const handleConfirm = async () => {
  656. const form = unref(formRef)
  657. if (!form) return
  658. submitLoading.value = true
  659. try {
  660. //验证(检验员改为非必选,任务单阶段才必选,故移除 CheckerSelect 的 validate 校验)
  661. await form.validate()
  662. // 组装新的数据结构
  663. const submitData: { taskList: any[]; source: number } = {
  664. taskList: [],
  665. source: props.source === 'pressure' ? 100 : 200
  666. }
  667. console.log("formData.value.legalTaskList", formData.value.yearTaskList)
  668. const legalUnitList = formData.value.legalTaskList.reduce((acc, item) => {
  669. const key = item.unitId;
  670. if (!acc[key]) {
  671. acc[key] = [];
  672. }
  673. acc[key].push(item)
  674. return acc;
  675. }, {})
  676. const yearUnitList = formData.value.yearTaskList.reduce((acc, item) => {
  677. const key = item.unitId;
  678. if (!acc[key]) {
  679. acc[key] = [];
  680. }
  681. acc[key].push(item)
  682. return acc;
  683. }, {})
  684. //console.log(Object.values(legalUnitList))
  685. console.log(formData.value.checkType);
  686. if (formData.value.checkType == '100') {
  687. Object.values(legalUnitList).forEach(item => {
  688. // 处理定期检验
  689. if (formData.value.legalDate && formData.value.legalTaskList.length > 0) {
  690. let actualAmount = 0
  691. checkItemList.value.find(i => i.inspectionNature == PressurePipeCheckType.REGULAR).itemList.filter(i => i.use).forEach(i => {
  692. actualAmount += i.fee
  693. })
  694. submitData.taskList.push({
  695. equipIds: item.map(item => item.equipPipeId).join(','),
  696. type: '100',
  697. hasOrderConfirm: isFrontDesk.value ? false : formData.value.legalIsOrderConfirm,
  698. date: formData.value.legalDate,
  699. teamList: formData.value.legalTeamList,
  700. pipeDetailList: item,
  701. chargeType: formData.value.legalChargeType,
  702. isExempt: formData.value.legalIsExempt,
  703. shouldAmount: formData.value.legalShouldAmount,
  704. serviceAmount: formData.value.legalServiceAmount,
  705. reduceFee: formData.value.legalReduceFee,
  706. actualAmount: actualAmount,
  707. checkItemList: [...checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.REGULAR)[0].itemList.filter(i => i.use).map(
  708. i => {
  709. return {
  710. //reportId: i.templateId,
  711. reportId: i.connectId,
  712. newAmount: i.fee,
  713. type: i.type,
  714. feeCalculateJson: i.feeCalculateJson || '',
  715. defaultCount: i.defaultCount || 1,
  716. sort: i.sort || 0,
  717. }
  718. }
  719. ), ...checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.REGULAR)[1].itemList.filter(i => i.use).map(
  720. i => {
  721. return {
  722. //reportId: i.templateId,
  723. reportId: i.connectId,
  724. newAmount: i.fee,
  725. type: i.type,
  726. feeCalculateJson: i.feeCalculateJson || '',
  727. defaultCount: i.defaultCount || 1,
  728. sort: i.sort || 0,
  729. }
  730. }
  731. )]
  732. })
  733. }
  734. })
  735. }
  736. if (formData.value.checkType == '200') {
  737. Object.values(yearUnitList).forEach(item => {
  738. // 处理年度检查
  739. if (formData.value.yearDate && formData.value.yearTaskList.length > 0) {
  740. let actualAmount = 0
  741. checkItemList.value.find(i => i.inspectionNature == PressurePipeCheckType.ANNUAL).itemList.filter(i => i.use).forEach(i => {
  742. actualAmount += i.fee
  743. })
  744. submitData.taskList.push({
  745. equipIds: item.map(item => item.equipPipeId).join(','),
  746. type: '200',
  747. hasOrderConfirm: isFrontDesk.value ? false : formData.value.yearIsOrderConfirm,
  748. date: formData.value.yearDate,
  749. teamList: formData.value.yearTeamList,
  750. pipeDetailList: item,
  751. chargeType: formData.value.yearChargeType,
  752. isExempt: formData.value.yearIsExempt,
  753. shouldAmount: formData.value.yearShouldAmount,
  754. serviceAmount: formData.value.yearServiceAmount,
  755. reduceFee: formData.value.yearReduceFee,
  756. actualAmount: actualAmount,
  757. checkItemList: [...checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.ANNUAL)[0].itemList.filter(i => i.use).map(
  758. i => {
  759. return {
  760. //reportId: i.templateId,
  761. reportId: i.connectId,
  762. newAmount: i.fee,
  763. type: i.type,
  764. feeCalculateJson: i.feeCalculateJson || '',
  765. defaultCount: i.defaultCount || 1,
  766. sort: i.sort || 0,
  767. }
  768. }
  769. ), ...checkItemList.value.filter(i => i.inspectionNature == PressurePipeCheckType.ANNUAL)[1].itemList.filter(i => i.use).map(
  770. i => {
  771. return {
  772. //reportId: i.templateId,
  773. reportId: i.connectId,
  774. newAmount: i.fee,
  775. type: i.type,
  776. feeCalculateJson: i.feeCalculateJson || '',
  777. defaultCount: i.defaultCount || 1,
  778. sort: i.sort || 0,
  779. }
  780. }
  781. )]
  782. })
  783. }
  784. })
  785. }
  786. console.log('submitData', submitData)
  787. const res = await EquipPipeSchedulingApi.planSchedulingAssign(submitData)
  788. message.success('保存成功')
  789. dialogVisible.value = false
  790. emit('success')
  791. } catch (error) {
  792. console.log(error)
  793. message.error('保存失败')
  794. } finally {
  795. submitLoading.value = false
  796. }
  797. }
  798. /**
  799. * 禁用内部日期
  800. * @param time
  801. * @return true 禁用 false 不禁用
  802. */
  803. const disabledDatePlan = (time: Date) => {
  804. return time.getTime() > dayjs(legalLatestTime.value).valueOf();
  805. }
  806. // 向父组件暴露方法
  807. defineExpose({
  808. open
  809. })
  810. </script>
  811. <style lang="scss" scoped>
  812. .plan-section {
  813. margin-bottom: 24px;
  814. padding: 16px;
  815. border: 1px solid #EBEEF5;
  816. border-radius: 4px;
  817. .section-title {
  818. margin: -16px -16px 16px -16px;
  819. padding: 8px 16px;
  820. background-color: #F5F7FA;
  821. border-bottom: 1px solid #EBEEF5;
  822. font-weight: bold;
  823. display: flex;
  824. align-items: center;
  825. .section-info {
  826. font-weight: normal;
  827. font-size: 13px;
  828. color: #606266;
  829. margin-left: 26px;
  830. }
  831. }
  832. }
  833. .content-title {
  834. display: flex;
  835. justify-content: flex-start;
  836. align-items: center;
  837. width: 100%;
  838. height: 36px;
  839. line-height: 36px;
  840. background-color: var(--el-color-primary-light-9);
  841. cursor: pointer;
  842. user-select: none;
  843. transition: background-color 0.3s;
  844. &:hover {
  845. background-color: var(--el-color-primary-light-8);
  846. }
  847. .collapse-icon {
  848. margin-right: 8px;
  849. transition: transform 0.3s;
  850. font-size: 14px;
  851. &.is-collapsed {
  852. transform: rotate(-90deg);
  853. }
  854. }
  855. &::before {
  856. content: '';
  857. height: 70%;
  858. width: 4px;
  859. margin-right: 12px;
  860. background-color: var(--el-color-primary);
  861. }
  862. }
  863. /* 外层容器 */
  864. .inspection-grid {
  865. width: 100%;
  866. padding: 16px;
  867. }
  868. /* 标题行 */
  869. .grid-header {
  870. font-size: 16px;
  871. font-weight: bold;
  872. text-align: center;
  873. height: 38px;
  874. line-height: 38px;
  875. border: 1px solid var(--el-border-color-light);
  876. border-bottom: 0;
  877. background-color: var(--el-color-primary-light-9);
  878. }
  879. /* Grid 容器核心配置 */
  880. .grid-container {
  881. display: grid;
  882. /* 5列等宽,列间距 12px,行间距 16px */
  883. grid-template-columns: repeat(5, 1fr);
  884. align-items: center; /* 垂直居中 */
  885. grid-gap: 1px;
  886. background-color: var(--el-border-color-light);
  887. border: 1px solid var(--el-border-color-light);
  888. }
  889. /* 子项布局:复选框 + 文本 + 图标 */
  890. .grid-item {
  891. display: flex;
  892. align-items: center;
  893. padding-left: 20px;
  894. padding-right: 12px;
  895. height: 52px;
  896. background-color: #fff;
  897. .el-checkbox {
  898. margin-right: 6px;
  899. }
  900. }
  901. .form-row {
  902. display: flex;
  903. align-items: center;
  904. .info-label {
  905. font-weight: bold;
  906. text-align: right;
  907. margin-right: 10px;
  908. }
  909. .info-value {
  910. flex: 1;
  911. }
  912. }
  913. .fee-amount {
  914. font-weight: bold;
  915. color: #e6a23c;
  916. font-size: 16px;
  917. }
  918. .fee-form-item {
  919. margin-top: 20px;
  920. :deep(.el-form-item__label) {
  921. font-weight: bold;
  922. }
  923. }
  924. </style>