| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630 |
- <script lang="ts" setup>
- import {
- IotProjectTaskPlanApi,
- IotProjectTaskPlanUpdateData,
- type IotProjectTaskPlanCreateData,
- type IotProjectTaskPlanPageParams,
- type IotProjectTaskPlanPageVO,
- type IotProjectTaskPlanTeamProjectBiz
- } from '@/api/pms/iotprojecttaskplan'
- import * as DeptApi from '@/api/system/dept'
- import { useUserStore } from '@/store/modules/user'
- import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
- import { rangeShortcuts } from '@/utils/formatTime'
- import { defaultProps, handleTree } from '@/utils/tree'
- import { useDebounceFn } from '@vueuse/core'
- import dayjs from 'dayjs'
- import type { FormInstance, FormRules } from 'element-plus'
- defineOptions({ name: 'PlanWorkFill' })
- const TOP_DEPT_ID = 156
- const deptId = useUserStore().getUser.deptId
- const message = useMessage()
- const createDefaultQuery = (): IotProjectTaskPlanPageParams => ({
- pageNo: 1,
- pageSize: 10,
- deptId,
- startTime: [...rangeShortcuts[8].value().map((item) => dayjs(item).format('YYYY-MM-DD HH:mm:ss'))]
- })
- const query = ref<IotProjectTaskPlanPageParams>(createDefaultQuery())
- const loading = ref(false)
- const list = ref<IotProjectTaskPlanPageVO[]>([])
- const total = ref(0)
- type PlanField = 'planGasInjection' | 'planLayers' | 'planWellTrips' | 'planFootage'
- interface BusinessPlanConfig {
- field: PlanField
- unit: string
- }
- interface DeptOption {
- id: number
- name: string
- parentId?: number
- type?: string
- disabled?: boolean
- children?: DeptOption[]
- }
- interface CreatePlanForm {
- id?: number
- deptId?: number
- startTime?: number | string
- business: string
- workloadUnit: string
- planWorkload?: number
- }
- type FormMode = 'create' | 'edit' | 'view'
- const BUSINESS_PLAN_CONFIG: Record<string, BusinessPlanConfig> = {
- 注气: { field: 'planGasInjection', unit: '万方' },
- 压裂: { field: 'planLayers', unit: '层' },
- 连油: { field: 'planWellTrips', unit: '井' },
- 钻井: { field: 'planFootage', unit: '米' },
- 修井: { field: 'planWellTrips', unit: '井' },
- 井下: { field: 'planWellTrips', unit: '井' }
- }
- const businessDictOptions = getStrDictOptions(DICT_TYPE.RQ_IOT_WORKLOAD_PLAN_BIZ)
- const deptTreeProps = { ...defaultProps, disabled: 'disabled' }
- const teamProjectBizOptions = ref<IotProjectTaskPlanTeamProjectBiz[]>([])
- const getBusinessPlanConfig = (business: string) => {
- const businessValue = String(business || '').trim()
- const businessLabel =
- teamProjectBizOptions.value.find((option) => option.business === businessValue)?.businessName ||
- businessDictOptions.find((option) => option.value === businessValue)?.label
- return BUSINESS_PLAN_CONFIG[businessValue] || BUSINESS_PLAN_CONFIG[businessLabel || '']
- }
- const getPlanWorkload = (row: IotProjectTaskPlanPageVO) => {
- const config = getBusinessPlanConfig(row.business)
- if (!config) return '-'
- const value = row[config.field]
- return value === null || value === undefined || value === '' ? '-' : value
- }
- const createDialogVisible = ref(false)
- const createLoading = ref(false)
- const formMode = ref<FormMode>('create')
- const deptOptionsLoading = ref(false)
- const businessOptionsLoading = ref(false)
- const deptOptions = ref<DeptOption[]>([])
- const createFormRef = ref<FormInstance>()
- const isReadonly = computed(() => formMode.value === 'view')
- const dialogTitle = computed(() => {
- if (formMode.value === 'edit') return '编辑计划工作'
- if (formMode.value === 'view') return '查看计划工作'
- return '新增计划工作'
- })
- const dialogDescription = computed(() =>
- isReadonly.value ? '查看部门项目业务计划工作量详情' : '维护部门各月份的项目业务计划工作量'
- )
- const createInitialForm = (): CreatePlanForm => ({
- id: undefined,
- deptId: undefined,
- startTime: dayjs().startOf('month').valueOf(),
- business: '',
- workloadUnit: '',
- planWorkload: undefined
- })
- const createFormData = ref<CreatePlanForm>(createInitialForm())
- const createFormRules: FormRules = {
- deptId: [{ required: true, message: '请选择部门', trigger: 'change' }],
- startTime: [{ required: true, message: '请选择时间', trigger: 'change' }],
- business: [{ required: true, message: '请选择项目业务', trigger: 'change' }],
- planWorkload: [{ required: true, message: '请输入计划工作量', trigger: 'blur' }]
- }
- const loadDeptOptions = async () => {
- if (deptOptions.value.length) return
- deptOptionsLoading.value = true
- try {
- const data = (await DeptApi.specifiedSimpleDepts(deptId)) as DeptOption[]
- const selectableDeptOptions = data.map((dept) => ({
- ...dept,
- type: String(dept.type || ''),
- disabled: String(dept.type || '') !== '3'
- }))
- deptOptions.value = handleTree(selectableDeptOptions)
- } finally {
- deptOptionsLoading.value = false
- }
- }
- const loadTeamProjectBizOptions = async (selectedDeptId?: number) => {
- teamProjectBizOptions.value = []
- if (!selectedDeptId) return
- businessOptionsLoading.value = true
- try {
- const data = await IotProjectTaskPlanApi.getTeamProjectBiz(selectedDeptId)
- teamProjectBizOptions.value = Array.isArray(data) ? data : []
- } finally {
- businessOptionsLoading.value = false
- }
- }
- const handleDeptChange = async (selectedDeptId?: number) => {
- createFormData.value.business = ''
- createFormData.value.workloadUnit = ''
- createFormData.value.planWorkload = undefined
- await loadTeamProjectBizOptions(selectedDeptId)
- }
- const normalizeStartTime = (value: unknown) => {
- const timestamp = Number(value)
- if (Number.isFinite(timestamp) && timestamp > 0) return timestamp
- const parsedTimestamp = dayjs(value as string).valueOf()
- return Number.isFinite(parsedTimestamp) ? parsedTimestamp : undefined
- }
- const handleOpenForm = async (mode: FormMode, id?: number) => {
- formMode.value = mode
- createFormData.value = createInitialForm()
- createDialogVisible.value = true
- createLoading.value = true
- try {
- await loadDeptOptions()
- if (id) {
- const data = await IotProjectTaskPlanApi.get(id)
- const business = String(data.business || '')
- await loadTeamProjectBizOptions(data.deptId)
- const config = getBusinessPlanConfig(business)
- const businessOption = teamProjectBizOptions.value.find(
- (option) => option.business === business
- )
- const workloadValue = config ? data[config.field] : undefined
- createFormData.value = {
- id: data.id,
- deptId: data.deptId,
- startTime: normalizeStartTime(data.startTime),
- business,
- workloadUnit: data.workloadUnit || businessOption?.unit || config?.unit || '',
- planWorkload:
- workloadValue === null || workloadValue === undefined ? undefined : Number(workloadValue)
- }
- }
- await nextTick()
- createFormRef.value?.clearValidate()
- } finally {
- createLoading.value = false
- }
- }
- const handleOpenCreate = () => handleOpenForm('create')
- const handleBusinessChange = () => {
- const businessOption = teamProjectBizOptions.value.find(
- (option) => option.business === createFormData.value.business
- )
- const config = getBusinessPlanConfig(createFormData.value.business)
- createFormData.value.workloadUnit = businessOption?.unit || config?.unit || ''
- createFormData.value.planWorkload = undefined
- }
- const submitCreate = async () => {
- if (isReadonly.value) return
- await createFormRef.value?.validate()
- const {
- deptId: formDeptId,
- startTime,
- business,
- workloadUnit,
- planWorkload
- } = createFormData.value
- const config = getBusinessPlanConfig(business)
- if (!formDeptId || !startTime || !workloadUnit || planWorkload === undefined || !config) {
- message.error('表单数据不完整或项目业务未配置计划工作量字段')
- return
- }
- const data: IotProjectTaskPlanCreateData = {
- deptId: formDeptId,
- startTime: dayjs(Number(startTime)).startOf('month').valueOf(),
- business,
- workloadUnit
- }
- data[config.field] = planWorkload
- createLoading.value = true
- try {
- if (formMode.value === 'edit') {
- if (!createFormData.value.id) return
- await IotProjectTaskPlanApi.update({
- ...data,
- id: createFormData.value.id
- } as IotProjectTaskPlanUpdateData)
- message.success('编辑成功')
- } else {
- await IotProjectTaskPlanApi.create(data)
- message.success('新增成功')
- }
- createDialogVisible.value = false
- handleQuery()
- } finally {
- createLoading.value = false
- }
- }
- const loadPage = useDebounceFn(async () => {
- loading.value = true
- try {
- const data = await IotProjectTaskPlanApi.getPage(query.value)
- console.log('计划工作分页接口数据:', data)
- list.value = Array.isArray(data?.list) ? data.list : []
- total.value = Number(data?.total || 0)
- } finally {
- loading.value = false
- }
- }, 200)
- const handleQuery = () => {
- query.value.pageNo = 1
- loadPage()
- }
- const handleSizeChange = (pageSize: number) => {
- query.value.pageSize = pageSize
- handleQuery()
- }
- const handleCurrentChange = (pageNo: number) => {
- query.value.pageNo = pageNo
- loadPage()
- }
- const resetQuery = () => {
- query.value = createDefaultQuery()
- handleQuery()
- }
- watch(
- [() => query.value.deptId, () => query.value.startTime],
- () => {
- handleQuery()
- },
- { immediate: true }
- )
- </script>
- <template>
- <div
- 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))]">
- <DeptTreeSelect
- :top-id="TOP_DEPT_ID"
- :deptId="deptId"
- v-model="query.deptId"
- :show-title="false"
- class="row-span-2" />
- <el-form
- size="default"
- class="plan-query-form bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 flex items-center justify-between min-w-0 overflow-hidden">
- <el-form-item label="时间范围">
- <el-date-picker
- v-model="query.startTime"
- value-format="YYYY-MM-DD HH:mm:ss"
- type="daterange"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :shortcuts="rangeShortcuts"
- :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
- class="!w-260px" />
- </el-form-item>
- <el-form-item>
- <el-button type="primary" plain @click="handleOpenCreate">
- <Icon icon="ep:plus" class="mr-5px" />新增
- </el-button>
- <el-button type="primary" :loading="loading" @click="handleQuery">
- <Icon icon="ep:search" class="mr-5px" />搜索
- </el-button>
- <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
- </el-form-item>
- </el-form>
- <div
- 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">
- <div class="relative flex-1 min-h-0 min-w-0 overflow-hidden">
- <el-auto-resizer class="absolute inset-0">
- <template #default="{ width, height }">
- <ZmTable
- :data="list"
- :loading="loading"
- :width="width"
- :height="height"
- :max-height="height"
- empty-text="暂无数据"
- show-border>
- <ZmTableColumn prop="deptName" label="部门名称" fixed="left" show-overflow-tooltip />
- <ZmTableColumn prop="business" label="项目业务" />
- <ZmTableColumn prop="yearMonth" label="时间" />
- <ZmTableColumn label="计划工作量" cover-formatter :real-value="getPlanWorkload" />
- <ZmTableColumn prop="workloadUnit" label="工作量单位" />
- <ZmTableColumn label="操作" :width="140" fixed="right" action>
- <template #default="{ row }">
- <el-button
- size="default"
- type="success"
- link
- @click="handleOpenForm('view', row.id)">
- 查看
- </el-button>
- <el-button
- size="default"
- type="primary"
- link
- @click="handleOpenForm('edit', row.id)">
- 编辑
- </el-button>
- </template>
- </ZmTableColumn>
- </ZmTable>
- </template>
- </el-auto-resizer>
- </div>
- <div class="h-8 mt-2 flex items-center justify-end">
- <el-pagination
- size="default"
- v-show="total > 0"
- :current-page="query.pageNo"
- :page-size="query.pageSize"
- :page-sizes="[10, 20, 30, 50, 100]"
- :total="total"
- background
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange" />
- </div>
- </div>
- </div>
- <Dialog v-model="createDialogVisible" :title="dialogTitle" width="780px" class="plan-work-dialog">
- <template #title>
- <div class="dialog-title">
- <div class="dialog-title__icon">
- <Icon icon="ep:calendar" :size="22" />
- </div>
- <div>
- <div class="dialog-title__text">{{ dialogTitle }}</div>
- <div class="dialog-title__desc">{{ dialogDescription }}</div>
- </div>
- </div>
- </template>
- <div v-loading="createLoading" class="plan-form-body">
- <section class="plan-form-card">
- <div class="plan-form-card__header">
- <div class="plan-form-card__title">
- <span class="plan-form-card__marker"></span>
- <span>计划信息</span>
- </div>
- </div>
- <el-form
- ref="createFormRef"
- :model="createFormData"
- :rules="createFormRules"
- :disabled="isReadonly"
- size="default"
- label-position="top"
- class="plan-create-form">
- <el-row :gutter="20">
- <el-col :xs="24" :sm="12">
- <el-form-item label="选择部门" prop="deptId">
- <el-tree-select
- v-model="createFormData.deptId"
- :data="deptOptions"
- :props="deptTreeProps"
- :loading="deptOptionsLoading"
- :disabled="isReadonly"
- node-key="id"
- check-strictly
- default-expand-all
- filterable
- clearable
- placeholder="请选择部门"
- class="w-full"
- @change="handleDeptChange" />
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="选择时间" prop="startTime">
- <el-date-picker
- v-model="createFormData.startTime"
- type="month"
- value-format="x"
- format="YYYY年MM月"
- :disabled="isReadonly"
- clearable
- placeholder="请选择年月"
- class="!w-full" />
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="项目业务" prop="business">
- <el-select
- v-model="createFormData.business"
- :placeholder="createFormData.deptId ? '请选择项目业务' : '请先选择部门'"
- :loading="businessOptionsLoading"
- :disabled="isReadonly || !createFormData.deptId"
- clearable
- class="w-full"
- @change="handleBusinessChange">
- <el-option
- v-for="option in teamProjectBizOptions"
- :key="option.business"
- :label="option.businessName"
- :value="option.business" />
- </el-select>
- </el-form-item>
- </el-col>
- <el-col :xs="24" :sm="12">
- <el-form-item label="工作量单位">
- <el-input
- v-model="createFormData.workloadUnit"
- readonly
- placeholder="选择项目业务后自动填入" />
- </el-form-item>
- </el-col>
- <el-col :xs="24">
- <el-form-item label="计划工作量" prop="planWorkload">
- <el-input-number
- align="left"
- v-model="createFormData.planWorkload"
- :min="0"
- :controls="false"
- :disabled="isReadonly || !createFormData.business"
- :placeholder="createFormData.business ? '请输入计划工作量' : '请先选择项目业务'"
- class="!w-full" />
- </el-form-item>
- </el-col>
- </el-row>
- </el-form>
- </section>
- </div>
- <template #footer>
- <div class="dialog-footer">
- <el-button size="default" :disabled="createLoading" @click="createDialogVisible = false">
- {{ isReadonly ? '关 闭' : '取 消' }}
- </el-button>
- <el-button
- v-if="!isReadonly"
- size="default"
- type="primary"
- :loading="createLoading"
- @click="submitCreate">
- 确 定
- </el-button>
- </div>
- </template>
- </Dialog>
- </template>
- <style scoped>
- .plan-query-form :deep(.el-form-item) {
- margin-bottom: 0;
- }
- .dialog-title {
- display: flex;
- gap: 12px;
- align-items: center;
- }
- .dialog-title__icon {
- display: flex;
- width: 40px;
- height: 40px;
- color: var(--el-color-primary);
- background: rgb(64 158 255 / 12%);
- border: 1px solid rgb(64 158 255 / 18%);
- border-radius: 8px;
- align-items: center;
- justify-content: center;
- }
- .dialog-title__text {
- font-size: 17px;
- font-weight: 600;
- line-height: 22px;
- color: var(--el-text-color-primary);
- }
- .dialog-title__desc {
- margin-top: 3px;
- font-size: 13px;
- line-height: 18px;
- color: var(--el-text-color-secondary);
- }
- .plan-form-body {
- padding: 4px 0;
- }
- .plan-form-card {
- overflow: hidden;
- background: var(--el-bg-color);
- border: 1px solid var(--el-border-color-lighter);
- border-radius: 8px;
- box-shadow: 0 8px 24px rgb(31 45 61 / 5%);
- }
- .plan-form-card__header {
- display: flex;
- min-height: 52px;
- padding: 0 18px;
- background: linear-gradient(90deg, rgb(64 158 255 / 8%) 0%, rgb(255 255 255 / 0%) 72%);
- border-bottom: 1px solid var(--el-border-color-lighter);
- align-items: center;
- }
- .plan-form-card__title {
- display: flex;
- gap: 10px;
- font-size: 15px;
- font-weight: 600;
- color: var(--el-text-color-primary);
- align-items: center;
- }
- .plan-form-card__marker {
- width: 4px;
- height: 17px;
- background: var(--el-color-primary);
- border-radius: 2px;
- }
- .plan-create-form {
- padding: 20px 22px 4px;
- }
- .plan-create-form :deep(.el-form-item) {
- margin-bottom: 18px;
- }
- .plan-create-form :deep(.el-form-item__label) {
- margin-bottom: 6px;
- line-height: 18px;
- color: var(--el-text-color-secondary);
- }
- .dialog-footer {
- display: flex;
- gap: 12px;
- justify-content: flex-end;
- }
- @media (width <= 768px) {
- .dialog-title__desc {
- display: none;
- }
- .plan-create-form {
- padding: 16px 16px 0;
- }
- }
- </style>
|