fill.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. <script lang="ts" setup>
  2. import {
  3. IotProjectTaskPlanApi,
  4. IotProjectTaskPlanUpdateData,
  5. type IotProjectTaskPlanCreateData,
  6. type IotProjectTaskPlanPageParams,
  7. type IotProjectTaskPlanPageVO,
  8. type IotProjectTaskPlanTeamProjectBiz
  9. } from '@/api/pms/iotprojecttaskplan'
  10. import * as DeptApi from '@/api/system/dept'
  11. import { useUserStore } from '@/store/modules/user'
  12. import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
  13. import { rangeShortcuts } from '@/utils/formatTime'
  14. import { defaultProps, handleTree } from '@/utils/tree'
  15. import { useDebounceFn } from '@vueuse/core'
  16. import dayjs from 'dayjs'
  17. import type { FormInstance, FormRules } from 'element-plus'
  18. defineOptions({ name: 'PlanWorkFill' })
  19. const TOP_DEPT_ID = 156
  20. const deptId = useUserStore().getUser.deptId
  21. const message = useMessage()
  22. const createDefaultQuery = (): IotProjectTaskPlanPageParams => ({
  23. pageNo: 1,
  24. pageSize: 10,
  25. deptId,
  26. startTime: [...rangeShortcuts[8].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
  27. })
  28. const query = ref<IotProjectTaskPlanPageParams>(createDefaultQuery())
  29. const loading = ref(false)
  30. const list = ref<IotProjectTaskPlanPageVO[]>([])
  31. const total = ref(0)
  32. type PlanField = 'planGasInjection' | 'planLayers' | 'planWellTrips' | 'planFootage'
  33. interface BusinessPlanConfig {
  34. field: PlanField
  35. unit: string
  36. }
  37. interface DeptOption {
  38. id: number
  39. name: string
  40. parentId?: number
  41. type?: string
  42. disabled?: boolean
  43. children?: DeptOption[]
  44. }
  45. interface CreatePlanForm {
  46. id?: number
  47. deptId?: number
  48. startTime?: number | string
  49. business: string
  50. workloadUnit: string
  51. planWorkload?: number
  52. }
  53. type FormMode = 'create' | 'edit' | 'view'
  54. const BUSINESS_PLAN_CONFIG: Record<string, BusinessPlanConfig> = {
  55. 注气: { field: 'planGasInjection', unit: '万方' },
  56. 压裂: { field: 'planLayers', unit: '层' },
  57. 连油: { field: 'planWellTrips', unit: '井' },
  58. 钻井: { field: 'planFootage', unit: '米' },
  59. 修井: { field: 'planWellTrips', unit: '井' },
  60. 井下: { field: 'planWellTrips', unit: '井' }
  61. }
  62. const businessDictOptions = getStrDictOptions(DICT_TYPE.RQ_IOT_WORKLOAD_PLAN_BIZ)
  63. const deptTreeProps = { ...defaultProps, disabled: 'disabled' }
  64. const teamProjectBizOptions = ref<IotProjectTaskPlanTeamProjectBiz[]>([])
  65. const getBusinessPlanConfig = (business: string) => {
  66. const businessValue = String(business || '').trim()
  67. const businessLabel =
  68. teamProjectBizOptions.value.find((option) => option.business === businessValue)?.businessName ||
  69. businessDictOptions.find((option) => option.value === businessValue)?.label
  70. return BUSINESS_PLAN_CONFIG[businessValue] || BUSINESS_PLAN_CONFIG[businessLabel || '']
  71. }
  72. const getPlanWorkload = (row: IotProjectTaskPlanPageVO) => {
  73. const config = getBusinessPlanConfig(row.business)
  74. if (!config) return '-'
  75. const value = row[config.field]
  76. return value === null || value === undefined || value === '' ? '-' : value
  77. }
  78. const createDialogVisible = ref(false)
  79. const createLoading = ref(false)
  80. const formMode = ref<FormMode>('create')
  81. const deptOptionsLoading = ref(false)
  82. const businessOptionsLoading = ref(false)
  83. const deptOptions = ref<DeptOption[]>([])
  84. const createFormRef = ref<FormInstance>()
  85. const isReadonly = computed(() => formMode.value === 'view')
  86. const dialogTitle = computed(() => {
  87. if (formMode.value === 'edit') return '编辑计划工作'
  88. if (formMode.value === 'view') return '查看计划工作'
  89. return '新增计划工作'
  90. })
  91. const dialogDescription = computed(() =>
  92. isReadonly.value ? '查看部门项目业务计划工作量详情' : '维护部门各月份的项目业务计划工作量'
  93. )
  94. const createInitialForm = (): CreatePlanForm => ({
  95. id: undefined,
  96. deptId: undefined,
  97. startTime: dayjs().startOf('month').valueOf(),
  98. business: '',
  99. workloadUnit: '',
  100. planWorkload: undefined
  101. })
  102. const createFormData = ref<CreatePlanForm>(createInitialForm())
  103. const createFormRules: FormRules = {
  104. deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
  105. startTime: [{ required: true, message: '请选择时间', trigger: 'change' }],
  106. business: [{ required: true, message: '请选择项目业务', trigger: 'change' }],
  107. planWorkload: [{ required: true, message: '请输入计划工作量', trigger: 'blur' }]
  108. }
  109. const loadDeptOptions = async () => {
  110. if (deptOptions.value.length) return
  111. deptOptionsLoading.value = true
  112. try {
  113. const data = (await DeptApi.specifiedSimpleDepts(deptId)) as DeptOption[]
  114. const selectableDeptOptions = data.map((dept) => ({
  115. ...dept,
  116. type: String(dept.type || ''),
  117. disabled: String(dept.type || '') !== '3'
  118. }))
  119. deptOptions.value = handleTree(selectableDeptOptions)
  120. } finally {
  121. deptOptionsLoading.value = false
  122. }
  123. }
  124. const loadTeamProjectBizOptions = async (selectedDeptId?: number) => {
  125. teamProjectBizOptions.value = []
  126. if (!selectedDeptId) return
  127. businessOptionsLoading.value = true
  128. try {
  129. const data = await IotProjectTaskPlanApi.getTeamProjectBiz(selectedDeptId)
  130. teamProjectBizOptions.value = Array.isArray(data) ? data : []
  131. } finally {
  132. businessOptionsLoading.value = false
  133. }
  134. }
  135. const handleDeptChange = async (selectedDeptId?: number) => {
  136. createFormData.value.business = ''
  137. createFormData.value.workloadUnit = ''
  138. createFormData.value.planWorkload = undefined
  139. await loadTeamProjectBizOptions(selectedDeptId)
  140. }
  141. const normalizeStartTime = (value: unknown) => {
  142. const timestamp = Number(value)
  143. if (Number.isFinite(timestamp) && timestamp > 0) return timestamp
  144. const parsedTimestamp = dayjs(value as string).valueOf()
  145. return Number.isFinite(parsedTimestamp) ? parsedTimestamp : undefined
  146. }
  147. const handleOpenForm = async (mode: FormMode, id?: number) => {
  148. formMode.value = mode
  149. createFormData.value = createInitialForm()
  150. createDialogVisible.value = true
  151. createLoading.value = true
  152. try {
  153. await loadDeptOptions()
  154. if (id) {
  155. const data = await IotProjectTaskPlanApi.get(id)
  156. const business = String(data.business || '')
  157. await loadTeamProjectBizOptions(data.deptId)
  158. const config = getBusinessPlanConfig(business)
  159. const businessOption = teamProjectBizOptions.value.find(
  160. (option) => option.business === business
  161. )
  162. const workloadValue = config ? data[config.field] : undefined
  163. createFormData.value = {
  164. id: data.id,
  165. deptId: data.deptId,
  166. startTime: normalizeStartTime(data.startTime),
  167. business,
  168. workloadUnit: data.workloadUnit || businessOption?.unit || config?.unit || '',
  169. planWorkload:
  170. workloadValue === null || workloadValue === undefined ? undefined : Number(workloadValue)
  171. }
  172. }
  173. await nextTick()
  174. createFormRef.value?.clearValidate()
  175. } finally {
  176. createLoading.value = false
  177. }
  178. }
  179. const handleOpenCreate = () => handleOpenForm('create')
  180. const handleBusinessChange = () => {
  181. const businessOption = teamProjectBizOptions.value.find(
  182. (option) => option.business === createFormData.value.business
  183. )
  184. const config = getBusinessPlanConfig(createFormData.value.business)
  185. createFormData.value.workloadUnit = businessOption?.unit || config?.unit || ''
  186. createFormData.value.planWorkload = undefined
  187. }
  188. const submitCreate = async () => {
  189. if (isReadonly.value) return
  190. await createFormRef.value?.validate()
  191. const {
  192. deptId: formDeptId,
  193. startTime,
  194. business,
  195. workloadUnit,
  196. planWorkload
  197. } = createFormData.value
  198. const config = getBusinessPlanConfig(business)
  199. if (!formDeptId || !startTime || !workloadUnit || planWorkload === undefined || !config) {
  200. message.error('表单数据不完整或项目业务未配置计划工作量字段')
  201. return
  202. }
  203. const data: IotProjectTaskPlanCreateData = {
  204. deptId: formDeptId,
  205. startTime: dayjs(Number(startTime)).startOf('month').valueOf(),
  206. business,
  207. workloadUnit
  208. }
  209. data[config.field] = planWorkload
  210. createLoading.value = true
  211. try {
  212. if (formMode.value === 'edit') {
  213. if (!createFormData.value.id) return
  214. await IotProjectTaskPlanApi.update({
  215. ...data,
  216. id: createFormData.value.id
  217. } as IotProjectTaskPlanUpdateData)
  218. message.success('编辑成功')
  219. } else {
  220. await IotProjectTaskPlanApi.create(data)
  221. message.success('新增成功')
  222. }
  223. createDialogVisible.value = false
  224. handleQuery()
  225. } finally {
  226. createLoading.value = false
  227. }
  228. }
  229. const loadPage = useDebounceFn(async () => {
  230. loading.value = true
  231. try {
  232. const data = await IotProjectTaskPlanApi.getPage(query.value)
  233. console.log('计划工作分页接口数据:', data)
  234. list.value = Array.isArray(data?.list) ? data.list : []
  235. total.value = Number(data?.total || 0)
  236. } finally {
  237. loading.value = false
  238. }
  239. }, 200)
  240. const handleQuery = () => {
  241. query.value.pageNo = 1
  242. loadPage()
  243. }
  244. const handleSizeChange = (pageSize: number) => {
  245. query.value.pageSize = pageSize
  246. handleQuery()
  247. }
  248. const handleCurrentChange = (pageNo: number) => {
  249. query.value.pageNo = pageNo
  250. loadPage()
  251. }
  252. const resetQuery = () => {
  253. query.value = createDefaultQuery()
  254. handleQuery()
  255. }
  256. watch(
  257. [() => query.value.deptId, () => query.value.startTime],
  258. () => {
  259. handleQuery()
  260. },
  261. { immediate: true }
  262. )
  263. </script>
  264. <template>
  265. <div
  266. class="grid grid-cols-[auto_minmax(0,1fr)] grid-rows-[62px_minmax(0,1fr)] gap-4 w-full min-w-0 overflow-hidden h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
  267. <DeptTreeSelect
  268. :top-id="TOP_DEPT_ID"
  269. :deptId="deptId"
  270. v-model="query.deptId"
  271. :show-title="false"
  272. class="row-span-2" />
  273. <el-form
  274. size="default"
  275. class="plan-query-form bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 flex items-center justify-between min-w-0 overflow-hidden">
  276. <el-form-item label="时间范围">
  277. <el-date-picker
  278. v-model="query.startTime"
  279. value-format="YYYY-MM-DD HH:mm:ss"
  280. type="daterange"
  281. start-placeholder="开始日期"
  282. end-placeholder="结束日期"
  283. :shortcuts="rangeShortcuts"
  284. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  285. class="!w-260px" />
  286. </el-form-item>
  287. <el-form-item>
  288. <el-button type="primary" plain @click="handleOpenCreate">
  289. <Icon icon="ep:plus" class="mr-5px" />新增
  290. </el-button>
  291. <el-button type="primary" :loading="loading" @click="handleQuery">
  292. <Icon icon="ep:search" class="mr-5px" />搜索
  293. </el-button>
  294. <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
  295. </el-form-item>
  296. </el-form>
  297. <div
  298. class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow min-h-0 min-w-0 w-full max-w-full p-4 overflow-hidden flex flex-col">
  299. <div class="relative flex-1 min-h-0 min-w-0 overflow-hidden">
  300. <el-auto-resizer class="absolute inset-0">
  301. <template #default="{ width, height }">
  302. <ZmTable
  303. :data="list"
  304. :loading="loading"
  305. :width="width"
  306. :height="height"
  307. :max-height="height"
  308. empty-text="暂无数据"
  309. show-border>
  310. <ZmTableColumn prop="deptName" label="部门名称" fixed="left" show-overflow-tooltip />
  311. <ZmTableColumn prop="business" label="项目业务" />
  312. <ZmTableColumn prop="yearMonth" label="时间" />
  313. <ZmTableColumn label="计划工作量" cover-formatter :real-value="getPlanWorkload" />
  314. <ZmTableColumn prop="workloadUnit" label="工作量单位" />
  315. <ZmTableColumn label="操作" :width="140" fixed="right" action>
  316. <template #default="{ row }">
  317. <el-button
  318. size="default"
  319. type="success"
  320. link
  321. @click="handleOpenForm('view', row.id)">
  322. 查看
  323. </el-button>
  324. <el-button
  325. size="default"
  326. type="primary"
  327. link
  328. @click="handleOpenForm('edit', row.id)">
  329. 编辑
  330. </el-button>
  331. </template>
  332. </ZmTableColumn>
  333. </ZmTable>
  334. </template>
  335. </el-auto-resizer>
  336. </div>
  337. <div class="h-8 mt-2 flex items-center justify-end">
  338. <el-pagination
  339. size="default"
  340. v-show="total > 0"
  341. :current-page="query.pageNo"
  342. :page-size="query.pageSize"
  343. :page-sizes="[10, 20, 30, 50, 100]"
  344. :total="total"
  345. background
  346. layout="total, sizes, prev, pager, next, jumper"
  347. @size-change="handleSizeChange"
  348. @current-change="handleCurrentChange" />
  349. </div>
  350. </div>
  351. </div>
  352. <Dialog v-model="createDialogVisible" :title="dialogTitle" width="780px" class="plan-work-dialog">
  353. <template #title>
  354. <div class="dialog-title">
  355. <div class="dialog-title__icon">
  356. <Icon icon="ep:calendar" :size="22" />
  357. </div>
  358. <div>
  359. <div class="dialog-title__text">{{ dialogTitle }}</div>
  360. <div class="dialog-title__desc">{{ dialogDescription }}</div>
  361. </div>
  362. </div>
  363. </template>
  364. <div v-loading="createLoading" class="plan-form-body">
  365. <section class="plan-form-card">
  366. <div class="plan-form-card__header">
  367. <div class="plan-form-card__title">
  368. <span class="plan-form-card__marker"></span>
  369. <span>计划信息</span>
  370. </div>
  371. </div>
  372. <el-form
  373. ref="createFormRef"
  374. :model="createFormData"
  375. :rules="createFormRules"
  376. :disabled="isReadonly"
  377. size="default"
  378. label-position="top"
  379. class="plan-create-form">
  380. <el-row :gutter="20">
  381. <el-col :xs="24" :sm="12">
  382. <el-form-item label="选择部门" prop="deptId">
  383. <el-tree-select
  384. v-model="createFormData.deptId"
  385. :data="deptOptions"
  386. :props="deptTreeProps"
  387. :loading="deptOptionsLoading"
  388. :disabled="isReadonly"
  389. node-key="id"
  390. check-strictly
  391. default-expand-all
  392. filterable
  393. clearable
  394. placeholder="请选择部门"
  395. class="w-full"
  396. @change="handleDeptChange" />
  397. </el-form-item>
  398. </el-col>
  399. <el-col :xs="24" :sm="12">
  400. <el-form-item label="选择时间" prop="startTime">
  401. <el-date-picker
  402. v-model="createFormData.startTime"
  403. type="month"
  404. value-format="x"
  405. format="YYYY年MM月"
  406. :disabled="isReadonly"
  407. clearable
  408. placeholder="请选择年月"
  409. class="!w-full" />
  410. </el-form-item>
  411. </el-col>
  412. <el-col :xs="24" :sm="12">
  413. <el-form-item label="项目业务" prop="business">
  414. <el-select
  415. v-model="createFormData.business"
  416. :placeholder="createFormData.deptId ? '请选择项目业务' : '请先选择部门'"
  417. :loading="businessOptionsLoading"
  418. :disabled="isReadonly || !createFormData.deptId"
  419. clearable
  420. class="w-full"
  421. @change="handleBusinessChange">
  422. <el-option
  423. v-for="option in teamProjectBizOptions"
  424. :key="option.business"
  425. :label="option.businessName"
  426. :value="option.business" />
  427. </el-select>
  428. </el-form-item>
  429. </el-col>
  430. <el-col :xs="24" :sm="12">
  431. <el-form-item label="工作量单位">
  432. <el-input
  433. v-model="createFormData.workloadUnit"
  434. readonly
  435. placeholder="选择项目业务后自动填入" />
  436. </el-form-item>
  437. </el-col>
  438. <el-col :xs="24">
  439. <el-form-item label="计划工作量" prop="planWorkload">
  440. <el-input-number
  441. align="left"
  442. v-model="createFormData.planWorkload"
  443. :min="0"
  444. :controls="false"
  445. :disabled="isReadonly || !createFormData.business"
  446. :placeholder="createFormData.business ? '请输入计划工作量' : '请先选择项目业务'"
  447. class="!w-full" />
  448. </el-form-item>
  449. </el-col>
  450. </el-row>
  451. </el-form>
  452. </section>
  453. </div>
  454. <template #footer>
  455. <div class="dialog-footer">
  456. <el-button size="default" :disabled="createLoading" @click="createDialogVisible = false">
  457. {{ isReadonly ? '关 闭' : '取 消' }}
  458. </el-button>
  459. <el-button
  460. v-if="!isReadonly"
  461. size="default"
  462. type="primary"
  463. :loading="createLoading"
  464. @click="submitCreate">
  465. 确 定
  466. </el-button>
  467. </div>
  468. </template>
  469. </Dialog>
  470. </template>
  471. <style scoped>
  472. .plan-query-form :deep(.el-form-item) {
  473. margin-bottom: 0;
  474. }
  475. .dialog-title {
  476. display: flex;
  477. gap: 12px;
  478. align-items: center;
  479. }
  480. .dialog-title__icon {
  481. display: flex;
  482. width: 40px;
  483. height: 40px;
  484. color: var(--el-color-primary);
  485. background: rgb(64 158 255 / 12%);
  486. border: 1px solid rgb(64 158 255 / 18%);
  487. border-radius: 8px;
  488. align-items: center;
  489. justify-content: center;
  490. }
  491. .dialog-title__text {
  492. font-size: 17px;
  493. font-weight: 600;
  494. line-height: 22px;
  495. color: var(--el-text-color-primary);
  496. }
  497. .dialog-title__desc {
  498. margin-top: 3px;
  499. font-size: 13px;
  500. line-height: 18px;
  501. color: var(--el-text-color-secondary);
  502. }
  503. .plan-form-body {
  504. padding: 4px 0;
  505. }
  506. .plan-form-card {
  507. overflow: hidden;
  508. background: var(--el-bg-color);
  509. border: 1px solid var(--el-border-color-lighter);
  510. border-radius: 8px;
  511. box-shadow: 0 8px 24px rgb(31 45 61 / 5%);
  512. }
  513. .plan-form-card__header {
  514. display: flex;
  515. min-height: 52px;
  516. padding: 0 18px;
  517. background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
  518. border-bottom: 1px solid var(--el-border-color-lighter);
  519. align-items: center;
  520. }
  521. .plan-form-card__title {
  522. display: flex;
  523. gap: 10px;
  524. font-size: 15px;
  525. font-weight: 600;
  526. color: var(--el-text-color-primary);
  527. align-items: center;
  528. }
  529. .plan-form-card__marker {
  530. width: 4px;
  531. height: 17px;
  532. background: var(--el-color-primary);
  533. border-radius: 2px;
  534. }
  535. .plan-create-form {
  536. padding: 20px 22px 4px;
  537. }
  538. .plan-create-form :deep(.el-form-item) {
  539. margin-bottom: 18px;
  540. }
  541. .plan-create-form :deep(.el-form-item__label) {
  542. margin-bottom: 6px;
  543. line-height: 18px;
  544. color: var(--el-text-color-secondary);
  545. }
  546. .dialog-footer {
  547. display: flex;
  548. gap: 12px;
  549. justify-content: flex-end;
  550. }
  551. @media (width <= 768px) {
  552. .dialog-title__desc {
  553. display: none;
  554. }
  555. .plan-create-form {
  556. padding: 16px 16px 0;
  557. }
  558. }
  559. </style>