| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106 |
- <template>
- <div
- class="iot-project-info-page grid grid-rows-[auto_minmax(360px,1fr)_auto] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]">
- <el-form
- ref="queryFormRef"
- :model="queryParams"
- size="default"
- label-width="72px"
- class="iot-project-info-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
- <div class="query-row">
- <el-form-item label="公司" prop="companyDeptId">
- <el-select
- v-model="queryParams.companyDeptId"
- placeholder="请选择公司"
- clearable
- filterable
- class="query-control">
- <el-option
- v-for="item in companyDeptList"
- :key="item.id"
- :label="item.name"
- :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="项目部" prop="projectDeptId">
- <el-select
- v-model="queryParams.projectDeptId"
- :placeholder="queryParams.companyDeptId ? '请选择项目部' : '请先选择公司'"
- :disabled="!queryParams.companyDeptId"
- :loading="projectDeptLoading"
- clearable
- filterable
- class="query-control">
- <el-option
- v-for="item in projectDeptOptions"
- :key="item.value"
- :label="item.label"
- :value="item.value" />
- </el-select>
- </el-form-item>
- <el-form-item label="合同名称" prop="contractName">
- <el-input
- v-model="queryParams.contractName"
- placeholder="请输入合同名称"
- clearable
- class="query-control"
- @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item label="合同编号" prop="contractCode">
- <el-input
- v-model="queryParams.contractCode"
- placeholder="请输入合同编号"
- clearable
- class="query-control"
- @keyup.enter="handleQuery" />
- </el-form-item>
- <el-form-item label="起止日期" prop="startTime">
- <el-date-picker
- v-model="queryParams.startTime"
- value-format="YYYY-MM-DD HH:mm:ss"
- type="daterange"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
- class="query-control query-control--date" />
- </el-form-item>
- </div>
- <el-form-item class="query-actions">
- <el-button type="primary" @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-button
- type="primary"
- plain
- @click="openForm('create', undefined)"
- v-hasPermi="['rq:iot-project-info:create']">
- <Icon icon="ep:plus" class="mr-5px" />新增
- </el-button>
- <el-button type="success" plain :loading="exportLoading" @click="handleExport">
- <Icon icon="ep:download" class="mr-5px" />导出
- </el-button>
- </el-form-item>
- </el-form>
- <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
- <div class="flex-1 relative min-h-0">
- <el-auto-resizer class="absolute">
- <template #default="{ width, height }">
- <ZmTable
- :data="list"
- :loading="loading"
- :width="width"
- :height="height"
- :max-height="height"
- show-border>
- <ZmTableColumn
- type="index"
- :label="t('monitor.serial')"
- :width="70"
- fixed="left"
- hide-in-column-settings />
- <ZmTableColumn
- prop="manufactureName"
- label="客户名称"
- min-width="180"
- fixed="left"
- align="left" />
- <ZmTableColumn prop="contractName" label="合同名称" min-width="220" fixed="left">
- <template #default="{ row }">
- <el-link type="primary" :underline="false" @click="showTaskList(row)">
- {{ row.contractName }}
- </el-link>
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="contractCode" label="合同编号" min-width="150" />
- <ZmTableColumn prop="startTime" label="合同开始时间" min-width="140">
- <template #default="{ row }">
- {{ dateFormatter2(row, null, row.startTime) }}
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="endTime" label="合同完成时间" min-width="140">
- <template #default="{ row }">
- {{ dateFormatter2(row, null, row.endTime) }}
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="payment" :label="t('project.payment')" min-width="110">
- <template #default="{ row }">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_SETTLEMENT" :value="row.payment" />
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="createTime" label="创建时间" min-width="180">
- <template #default="{ row }">
- {{ dateFormatter(row, null, row.createTime) }}
- </template>
- </ZmTableColumn>
- <ZmTableColumn label="操作" width="190" fixed="right" action>
- <template #default="{ row }">
- <el-button
- link
- type="primary"
- @click="openForm('update', row.id)"
- v-hasPermi="['rq:iot-project-info:update']">
- 编辑
- </el-button>
- <el-button
- link
- type="primary"
- @click="assignTask(row)"
- v-hasPermi="['rq:iot-project-task:create']">
- 分配任务
- </el-button>
- <el-button
- link
- type="danger"
- @click="handleDelete(row.id)"
- v-hasPermi="['rq:iot-project-info:delete']">
- 删除
- </el-button>
- </template>
- </ZmTableColumn>
- </ZmTable>
- </template>
- </el-auto-resizer>
- </div>
- <div class="h-8 mt-2 flex items-center justify-end">
- <el-pagination
- v-show="total > 0"
- size="default"
- :current-page="queryParams.pageNo"
- :page-size="queryParams.pageSize"
- :background="true"
- :page-sizes="[10, 20, 30, 50, 100]"
- :total="total"
- layout="total, sizes, prev, pager, next, jumper"
- @size-change="handleSizeChange"
- @current-change="handleCurrentChange" />
- </div>
- </div>
- <div
- v-if="selectedProject"
- :style="{ height: taskListPanelHeight }"
- class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0 task-list-panel">
- <div class="card-header mb-3">
- <span class="font-600">任务列表 - {{ selectedProject.contractName }}</span>
- <el-button link @click="closeTaskList" class="close-btn">
- <Icon icon="ep:close" />
- </el-button>
- </div>
- <div class="task-table-body relative min-h-0">
- <el-auto-resizer class="absolute">
- <template #default="{ width, height }">
- <ZmTable
- :data="taskList"
- :loading="taskLoading"
- :width="width"
- :height="height"
- :max-height="height"
- show-border>
- <ZmTableColumn prop="wellName" label="井号" min-width="100" />
- <ZmTableColumn prop="status" :label="t('project.status')" min-width="120">
- <template #default="{ row }">
- <el-link type="primary" :underline="false" @click="openTimelineDialog(row)">
- <template
- v-if="row.status !== null && row.status !== undefined && row.status !== ''">
- <dict-tag :type="getStatusDictType(row.deptId)" :value="row.status" />
- </template>
- <template v-else>未更新状态</template>
- </el-link>
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="location" label="施工地点" min-width="140" />
- <ZmTableColumn prop="technique" :label="t('project.technology')" min-width="120">
- <template #default="{ row }">
- {{ getTechniqueLabel(row) }}
- </template>
- </ZmTableColumn>
- <ZmTableColumn prop="workloadDesign" label="设计工作量" min-width="120" />
- <ZmTableColumn prop="workloadUnit" label="工作量单位" min-width="120">
- <template #default="{ row }">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_WORKLOAD_UNIT" :value="row.workloadUnit" />
- </template>
- </ZmTableColumn>
- <ZmTableColumn label="施工队伍" min-width="150">
- <template #default="{ row }">
- <el-tooltip :content="getAllDeptNames(row.deptIds)" placement="top">
- <span class="dept-names">
- {{ getBriefDeptNames(row.deptIds) }}
- </span>
- </el-tooltip>
- </template>
- </ZmTableColumn>
- <ZmTableColumn label="施工设备" min-width="180">
- <template #default="{ row }">
- <el-tooltip :content="getAllDeviceNames(row.deviceIds)" placement="top">
- <span class="device-names">
- {{ getDeviceNames(row.deviceIds) }}
- </span>
- </el-tooltip>
- </template>
- </ZmTableColumn>
- <ZmTableColumn label="责任人" min-width="120">
- <template #default="{ row }">
- <el-tooltip
- :content="getAllResponsiblePersonNames(row.responsiblePerson)"
- placement="top">
- <span class="responsible-names">
- {{ getResponsiblePersonNames(row.responsiblePerson) }}
- </span>
- </el-tooltip>
- </template>
- </ZmTableColumn>
- </ZmTable>
- </template>
- </el-auto-resizer>
- </div>
- </div>
- </div>
- <!-- Timeline 时间线 Dialog - 已修改为 el-steps -->
- <el-dialog
- v-model="timelineDialogVisible"
- :title="`任务进度 - ${currentTaskRow ? currentTaskRow.wellName : ''}`"
- :width="dialogWidth">
- <div class="progress-container">
- <!-- 计划进度 -->
- <!-- <div class="progress-section">
- <h3 class="progress-title">计划进度</h3>
- <div v-if="stepsData.length > 0">
- <el-steps direction="horizontal" :active="currentStepIndex" finish-status="success">
- <el-step
- v-for="(step, index) in stepsData"
- :key="index"
- :title="step.title"
- :description="step.description"
- :status="step.status"
- />
- </el-steps>
- </div>
- <el-empty v-else description="暂无计划进度数据" :image-size="80" />
- </div> -->
- <!-- 实际进度 -->
- <div class="progress-section">
- <h3 class="progress-title">实际进度</h3>
- <div v-if="actualStepsData.length > 0">
- <el-steps
- direction="horizontal"
- :active="actualStepsData.length - 1"
- finish-status="success">
- <el-step
- v-for="(step, index) in actualStepsData"
- :key="index"
- :title="step.title"
- :description="step.description"
- :status="step.status" />
- </el-steps>
- </div>
- <el-empty v-else description="暂无实际进度数据" :image-size="80" />
- </div>
- </div>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="timelineDialogVisible = false">关闭</el-button>
- </span>
- </template>
- </el-dialog>
- </template>
- <script setup lang="ts">
- import { useTableComponents } from '@/components/ZmTable/useTableComponents'
- import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
- import download from '@/utils/download'
- import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
- import { IotProjectTaskApi } from '@/api/pms/iotprojecttask'
- import { useUserStore } from '@/store/modules/user'
- import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
- import { IotDeviceApi } from '@/api/pms/device'
- import * as UserApi from '@/api/system/user'
- import * as DeptApi from '@/api/system/dept'
- import { handleTree } from '@/utils/tree'
- import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
- import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
- import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
- import dayjs from 'dayjs'
- import { ref, reactive, onMounted, computed, watch } from 'vue'
- /** 项目信息 列表 */
- // eslint-disable-next-line vue/component-definition-name-casing
- defineOptions({ name: 'iotProjectInfo' })
- const message = useMessage() // 消息弹窗
- const { t } = useI18n() // 国际化
- const { ZmTable, ZmTableColumn } = useTableComponents<any>()
- // 任务列表相关状态
- const taskLoading = ref(false) // 任务列表的加载中
- const taskList = ref([]) // 任务列表的数据
- const selectedProject = ref(null) // 当前选中的项目
- const deptList = ref([]) // 部门列表
- const companyDeptList = ref<any[]>([]) // 在公司级部门列表
- const projectDeptOptions = ref<any[]>([]) // 项目部选项
- const deviceMap = ref({}) // 设备映射表
- const responsiblePersonList = ref([]) // 责任人列表
- const taskScheduleDictOptions = ref<any[]>([]) // 任务进度字典选项
- const timelineDialogVisible = ref(false) // 控制时间线弹窗显示
- const currentTaskRow = ref<any>(null) // 当前选中的任务行数据
- const stepsData = ref<Array<{ title: string; description?: string; status?: string }>>([]) // 步骤数据
- const actualStepsData = ref<Array<{ title: string; description?: string; status?: string }>>([]) // 实际步骤数据
- const currentStepIndex = ref(0) // 当前步骤索引
- // 施工工艺字典选项映射表
- const technologyDictOptionsMap = ref<Record<number, any[]>>({})
- // 正在加载的deptId集合
- const loadingDeptIds = new Set<number>()
- const loading = ref(true) // 列表的加载中
- const list = ref<IotProjectInfoVO[]>([]) // 列表的数据
- const total = ref(0) // 列表的总页数
- const queryParams = reactive({
- pageNo: 1,
- pageSize: 10,
- deptId: undefined,
- companyDeptId: undefined,
- projectDeptId: undefined,
- deptName: undefined,
- contractName: undefined,
- contractCode: undefined,
- workloadTotal: undefined,
- workloadFinish: undefined,
- startTime: [],
- endTime: [],
- location: undefined,
- technique: undefined,
- payment: undefined,
- createTime: [],
- userName: undefined,
- userId: undefined
- })
- const queryFormRef = ref() // 搜索的表单
- const exportLoading = ref(false) // 导出的加载中
- const projectDeptLoading = ref(false)
- const { push } = useRouter() // 路由跳转
- const taskListPanelHeight = computed(() => {
- const headerHeight = 40
- const tableHeaderHeight = 40
- const rowHeight = 42
- const panelPadding = 32
- const maxHeight = 360
- const minRows = Math.max(taskList.value.length, 1)
- const height = panelPadding + headerHeight + tableHeaderHeight + minRows * rowHeight
- return `${Math.min(height, maxHeight)}px`
- })
- /** 查询列表 */
- const getList = async () => {
- loading.value = true
- try {
- queryParams.deptId = useUserStore().getUser.deptId
- const data = await IotProjectInfoApi.getIotProjectInfoPage(queryParams)
- list.value = data.list
- total.value = data.total
- // 列表加载完成后,如果列表有数据,则默认选中第一行
- if (list.value.length > 0) {
- showTaskList(list.value[0])
- } else {
- // 如果没有数据,确保任务列表区域关闭
- closeTaskList()
- }
- } finally {
- loading.value = false
- }
- }
- /** 搜索按钮操作 */
- const handleQuery = () => {
- queryParams.pageNo = 1
- getList()
- }
- /** 重置按钮操作 */
- const resetQuery = () => {
- queryFormRef.value.resetFields()
- handleQuery()
- }
- const handleSizeChange = (val: number) => {
- queryParams.pageSize = val
- handleQuery()
- }
- const handleCurrentChange = (val: number) => {
- queryParams.pageNo = val
- getList()
- }
- const loadProjectDeptOptions = async (companyDeptId?: number) => {
- if (!companyDeptId) {
- projectDeptOptions.value = []
- return
- }
- projectDeptLoading.value = true
- try {
- const res = await DeptApi.specifiedSimpleDepts(companyDeptId)
- projectDeptOptions.value = res
- .map((item: any) => ({
- label: item.name,
- value: item.id,
- raw: item
- }))
- .filter((item) => item.raw.type === '2')
- } catch (error) {
- projectDeptOptions.value = []
- } finally {
- projectDeptLoading.value = false
- }
- }
- // 加载施工工艺数据字典选项
- const loadTechnologyDictOptions = async (deptId: number) => {
- // 如果正在加载或已加载,直接返回
- if (loadingDeptIds.has(deptId)) return
- loadingDeptIds.add(deptId)
- try {
- // 获取数据字典标识符
- const dictLabel = await getDictLabel(DICT_TYPE.PMS_PROJECT_TECHNOLOGY, deptId)
- if (dictLabel) {
- // 获取真正的数据字典选项
- const dictOptions = getStrDictOptions(dictLabel)
- technologyDictOptionsMap.value[deptId] = dictOptions
- } else {
- // 如果没有获取到,设置为空数组
- technologyDictOptionsMap.value[deptId] = []
- }
- } catch (error) {
- console.error(`加载部门${deptId}的施工工艺数据字典失败:`, error)
- technologyDictOptionsMap.value[deptId] = []
- } finally {
- loadingDeptIds.delete(deptId)
- }
- }
- // 获取施工工艺标签
- const getTechniqueLabel = (row: any) => {
- const deptId = row.deptId
- const techniqueValue = row.technique
- if (!deptId || !technologyDictOptionsMap.value[deptId]) {
- return techniqueValue // 如果还没有加载到数据字典选项,显示原始值
- }
- const options = technologyDictOptionsMap.value[deptId]
- const option = options.find((opt) => opt.value === techniqueValue)
- return option ? option.label : techniqueValue
- }
- // 显示任务列表
- const showTaskList = async (project) => {
- selectedProject.value = project
- taskLoading.value = true
- try {
- // 获取任务列表
- const queryParams = {
- projectId: project.id
- }
- const taskData = await IotProjectTaskApi.getIotProjectTaskPage(queryParams)
- taskList.value = taskData.list
- // 收集所有设备ID和责任人ID
- const allDeviceIds = new Set()
- const allResponsiblePersonIds = new Set()
- // 收集所有部门ID用于加载施工工艺字典
- const allDeptIds = new Set()
- taskList.value.forEach((item) => {
- if (item.deviceIds?.length) {
- item.deviceIds.forEach((id) => allDeviceIds.add(id))
- }
- if (item.responsiblePerson?.length) {
- item.responsiblePerson.forEach((id) => allResponsiblePersonIds.add(id))
- }
- if (item.deptId) {
- allDeptIds.add(item.deptId)
- }
- })
- // 批量获取设备信息
- if (allDeviceIds.size > 0) {
- const deviceIdsArray = Array.from(allDeviceIds)
- const devices = await IotDeviceApi.getDevicesByDepts({
- deviceIds: deviceIdsArray
- })
- // 更新设备映射表
- devices.forEach((device) => {
- deviceMap.value[device.id] = device
- })
- }
- // 批量获取责任人信息
- if (allResponsiblePersonIds.size > 0) {
- const personIdsArray = Array.from(allResponsiblePersonIds)
- const persons = await UserApi.companyDeptsEmployee({
- userIds: personIdsArray
- })
- responsiblePersonList.value = persons
- }
- // 加载所有部门对应的施工工艺字典选项
- allDeptIds.forEach((deptId) => {
- if (deptId && !technologyDictOptionsMap.value[deptId]) {
- loadTechnologyDictOptions(deptId)
- }
- })
- } catch (error) {
- console.error('加载任务列表失败:', error)
- message.error('加载任务列表失败')
- } finally {
- taskLoading.value = false
- }
- }
- // 关闭任务列表
- const closeTaskList = () => {
- selectedProject.value = null
- taskList.value = []
- }
- // 获取部门名称
- const getDeptNames = (deptIds) => {
- if (!deptIds || deptIds.length === 0) return ''
- const names = []
- const findDept = (list, id) => {
- for (const item of list) {
- if (item.id === id) {
- names.push(item.name)
- return true
- }
- if (item.children && findDept(item.children, id)) {
- return true
- }
- }
- return false
- }
- deptIds.forEach((id) => findDept(deptList.value, id))
- return names.join(', ')
- }
- // 添加计算属性计算对话框宽度
- const dialogWidth = computed(() => {
- // if (stepsData.value.length === 0) return '700px';
- // 根据步骤数量计算宽度,每个步骤大约需要 200px
- const baseWidth = Math.max(stepsData.value.length * 200, actualStepsData.value.length * 200)
- // 限制最小和最大宽度
- const minWidth = 900
- const maxWidth = window.innerWidth * 0.9 // 最大为视口宽度的90%
- // 应用限制
- let calculatedWidth = Math.max(minWidth, baseWidth)
- calculatedWidth = Math.min(calculatedWidth, maxWidth)
- return `${calculatedWidth}px`
- })
- // 获取设备名称
- const getDeviceNames = (deviceIds) => {
- if (!deviceIds || deviceIds.length === 0) return ''
- const deviceNames = deviceIds.map((id) => deviceMap.value[id]?.deviceCode).filter((name) => name)
- if (deviceNames.length === 0) return ''
- if (deviceNames.length > 2) {
- return `${deviceNames[0]}, ${deviceNames[1]}...`
- }
- return deviceNames.join(', ')
- }
- // 获取所有设备名称
- const getAllDeviceNames = (deviceIds) => {
- if (!deviceIds || deviceIds.length === 0) return '无设备'
- const deviceNames = deviceIds
- .map((id) => deviceMap.value[id]?.deviceCode || '未知设备')
- .filter((name) => name !== '未知设备')
- return deviceNames.join(', ') || '无有效设备'
- }
- // 获取简略部门名称(用于表格显示)
- const getBriefDeptNames = (deptIds) => {
- if (!deptIds || deptIds.length === 0) return ''
- const names = []
- const findDept = (list, id) => {
- for (const item of list) {
- if (item.id === id) {
- names.push(item.name)
- return true
- }
- if (item.children && findDept(item.children, id)) {
- return true
- }
- }
- return false
- }
- deptIds.forEach((id) => findDept(deptList.value, id))
- if (names.length === 0) return ''
- if (names.length > 2) {
- return `${names[0]}, ${names[1]}...`
- }
- return names.join(', ')
- }
- // 获取所有部门名称(用于tooltip显示)
- const getAllDeptNames = (deptIds) => {
- if (!deptIds || deptIds.length === 0) return '无施工队伍'
- return getDeptNames(deptIds)
- }
- // 获取责任人名称
- const getResponsiblePersonNames = (responsiblePersonIds) => {
- if (!responsiblePersonIds || responsiblePersonIds.length === 0) return ''
- const personNames = responsiblePersonIds
- .map((id) => {
- const person = responsiblePersonList.value.find((p) => p.id === id)
- return person ? person.nickname : ''
- })
- .filter((name) => name)
- if (personNames.length === 0) return ''
- if (personNames.length > 2) {
- return `${personNames[0]}, ${personNames[1]}...`
- }
- return personNames.join(', ')
- }
- // 获取所有责任人名称
- const getAllResponsiblePersonNames = (responsiblePersonIds) => {
- if (!responsiblePersonIds || responsiblePersonIds.length === 0) return '无责任人'
- const personNames = responsiblePersonIds
- .map((id) => {
- const person = responsiblePersonList.value.find((p) => p.id === id)
- return person ? person.nickname : '未知人员'
- })
- .filter((name) => name !== '未知人员')
- return personNames.join(', ') || '无有效责任人'
- }
- /** 打开 Timeline 时间线 Dialog - 已修改为 el-steps */
- const openTimelineDialog = async (row: any) => {
- currentTaskRow.value = row
- timelineDialogVisible.value = true
- stepsData.value = [] // 清空旧数据
- actualStepsData.value = [] // 清空实际进度数据
- currentStepIndex.value = -1 // 初始化为-1,不选中任何步骤
- try {
- // 获取任务进度字典
- if (taskScheduleDictOptions.value.length === 0) {
- await getTaskScheduleDictOptions(row.deptId)
- }
- // 获取计划进度数据
- const params = { taskId: row.id }
- const response = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules(params)
- // 根据 deptId 决定调用哪个接口获取实际进度
- let actualProgress = null
- if (row.deptId === 157) {
- // 瑞恒 157 - 调用瑞恒日报API
- actualProgress = await IotRhDailyReportApi.taskActualProgress(params)
- } else if (row.deptId === 163) {
- // 瑞都 163 - 调用瑞都日报API
- actualProgress = await IotRdDailyReportApi.taskActualProgress(params)
- } else {
- // 其他情况,可以根据需要添加更多条件或保持为空
- console.warn(`未知的部门ID: ${row.deptId},无法获取实际进度`)
- actualProgress = []
- }
- // 处理计划进度数据
- if (response && response.length > 0) {
- const sortedSchedules = response.sort((a, b) => a.status - b.status)
- stepsData.value = sortedSchedules.map((item: any) => {
- const formattedTimestamp = item.startTime
- ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm')
- : '时间未设置'
- const dictItem = taskScheduleDictOptions.value.find((dict) => dict.value === item.status)
- const statusLabel = dictItem ? dictItem.label : `未知状态 (${item.status})`
- return {
- title: `${formattedTimestamp} ${statusLabel}`,
- description: '',
- status: undefined
- }
- })
- } else {
- stepsData.value = []
- }
- // 处理实际进度数据
- if (actualProgress && actualProgress.length > 0) {
- const sortedActualProgress = actualProgress.sort(
- (a, b) => (a.constructionStartDate || 0) - (b.constructionStartDate || 0)
- )
- // 根据 deptId 决定使用哪个状态字段
- const statusField = row.deptId === 163 ? 'rdStatus' : 'constructionStatus'
- actualStepsData.value = sortedActualProgress.map((item: any) => {
- const formattedTimestamp = item.constructionStartDate
- ? dayjs(item.constructionStartDate).format('YYYY-MM-DD HH:mm')
- : '时间未设置'
- // 使用动态的状态字段
- const statusValue = item[statusField]
- const dictItem = taskScheduleDictOptions.value.find((dict) => dict.value === statusValue)
- const statusLabel = dictItem ? dictItem.label : `未知状态 (${statusValue})`
- return {
- title: `${formattedTimestamp} ${statusLabel}`,
- description: '',
- status: undefined
- }
- })
- } else {
- actualStepsData.value = []
- }
- } catch (error) {
- console.error('获取任务进度时间线失败:', error)
- message.error('获取任务进度失败')
- stepsData.value = []
- actualStepsData.value = []
- }
- }
- /** 获取任务进度字典数据 */
- const getTaskScheduleDictOptions = async (deptId: number) => {
- try {
- const dictType = getStatusDictType(deptId)
- taskScheduleDictOptions.value = getStrDictOptions(dictType)
- } catch (error) {
- console.error('获取任务进度字典失败:', error)
- taskScheduleDictOptions.value = []
- }
- }
- const openForm = (type: string, id?: number) => {
- if (id === undefined) {
- push({ name: 'IotProjectInfo', params: { type } })
- } else {
- push({ name: 'IotProjectInfo', params: { type, id } })
- }
- }
- /** 分配任务操作 */
- const assignTask = (row: IotProjectInfoVO) => {
- push({
- name: 'IotProjectTaskInfo',
- params: {
- type: 'create',
- projectId: row.id,
- deptId: row.deptId
- },
- query: {
- contractName: row.contractName
- }
- })
- }
- /** 删除按钮操作 */
- const handleDelete = async (id: number) => {
- try {
- queryParams.deptId = useUserStore().getUser.deptId
- const data = await IotProjectTaskApi.getIotProjectTaskList(queryParams)
- if (data.length === 0) {
- // 删除的二次确认
- await message.delConfirm()
- // 发起删除
- await IotProjectInfoApi.deleteIotProjectInfo(id)
- message.success(t('common.delSuccess'))
- } else {
- message.error(t('form.relatedProject'))
- }
- // 刷新列表
- await getList()
- } catch {}
- }
- /** 导出按钮操作 */
- const handleExport = async () => {
- try {
- // 发起导出
- exportLoading.value = true
- const data = await IotProjectInfoApi.exportIotProjectInfo(queryParams)
- download.excel(data, '项目信息.xls')
- } catch {
- } finally {
- exportLoading.value = false
- }
- }
- /** 根据部门ID获取状态字典类型 */
- const getStatusDictType = (deptId: number) => {
- if (deptId === 163) {
- return DICT_TYPE.PMS_PROJECT_RD_STATUS // 瑞都
- } else if (deptId === 157) {
- return DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE // 瑞恒
- }
- return DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE // 默认瑞恒
- }
- /** 初始化 **/
- onMounted(async () => {
- // 获取公司级的部门 用于 公司 筛选 管理员使用
- companyDeptList.value = await DeptApi.companyLevelDepts()
- deptList.value = handleTree(await DeptApi.companyLevelChildrenDepts())
- if (queryParams.companyDeptId) {
- await loadProjectDeptOptions(queryParams.companyDeptId)
- }
- getList()
- // 预加载任务进度字典
- // getTaskScheduleDictOptions()
- })
- watch(
- () => queryParams.companyDeptId,
- async (newVal, oldVal) => {
- if (newVal === oldVal) return
- queryParams.projectDeptId = undefined
- await loadProjectDeptOptions(newVal)
- }
- )
- </script>
- <style scoped>
- @media (width <= 1600px) {
- .iot-project-info-query .query-row {
- grid-template-columns: repeat(3, minmax(240px, 1fr));
- }
- }
- @media (width <= 1200px) {
- .iot-project-info-page {
- grid-template-rows: auto minmax(480px, 1fr) auto;
- height: auto;
- min-height: calc(
- 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
- );
- }
- .iot-project-info-query .query-row {
- grid-template-columns: repeat(2, minmax(240px, 1fr));
- }
- }
- @media (width <= 768px) {
- .iot-project-info-query {
- padding: 12px;
- }
- .iot-project-info-query .query-row {
- grid-template-columns: minmax(0, 1fr);
- }
- .iot-project-info-query .query-actions :deep(.el-form-item__content) {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 8px;
- width: 100%;
- }
- .iot-project-info-query .query-actions :deep(.el-button) {
- width: 100%;
- margin-left: 0;
- }
- }
- .iot-project-info-query {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
- .query-row {
- display: grid;
- grid-template-columns: repeat(4, minmax(220px, 1fr)) minmax(320px, 1.4fr);
- gap: 12px 20px;
- min-width: 0;
- }
- .query-actions {
- width: 100%;
- }
- .query-actions :deep(.el-form-item__content) {
- display: flex;
- flex-wrap: wrap;
- justify-content: flex-start;
- gap: 8px 10px;
- }
- .query-actions :deep(.el-button) {
- margin-left: 0;
- }
- .query-control {
- width: 100%;
- }
- .query-control--date {
- width: 100%;
- }
- :deep(.el-form-item) {
- margin-bottom: 0;
- }
- /* 任务列表相关样式 */
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .close-btn {
- min-height: auto;
- padding: 0;
- }
- .task-list-panel {
- max-height: 360px;
- transition: height 0.2s ease;
- }
- .task-table-body {
- flex: 1;
- }
- .dept-names {
- display: inline-block;
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .device-names {
- display: inline-block;
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .responsible-names {
- display: inline-block;
- max-width: 120px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- :deep(.el-step__description) {
- color: red !important;
- }
- :deep(.timeline-dialog) {
- max-width: 90vw;
- }
- /* 确保步骤标题完全显示 */
- :deep(.timeline-dialog .el-step__title) {
- max-width: none;
- overflow: visible;
- text-overflow: clip;
- white-space: nowrap;
- }
- /* 调整步骤容器的布局 */
- :deep(.timeline-dialog .el-steps--horizontal) {
- flex-wrap: nowrap;
- overflow-x: auto;
- justify-content: flex-start;
- }
- /* 调整对话框内容区域的滚动 */
- :deep(.timeline-dialog .el-dialog__body) {
- overflow-x: auto;
- }
- /* 进度展示容器样式 */
- .progress-container {
- display: flex;
- flex-direction: column;
- gap: 24px;
- }
- .progress-section {
- padding: 16px;
- background-color: #fafafa;
- border: 1px solid #e6e6e6;
- border-radius: 8px;
- }
- .progress-section:first-child {
- background-color: #f0f7ff;
- border-color: #409eff;
- }
- .progress-section:last-child {
- background-color: #f0f9eb;
- border-color: #67c23a;
- }
- .progress-title {
- margin: 0 0 16px;
- font-size: 16px;
- font-weight: bold;
- color: #606266;
- }
- .progress-section:first-child .progress-title {
- color: #409eff;
- }
- .progress-section:last-child .progress-title {
- color: #67c23a;
- }
- /* 调整空状态显示 */
- :deep(.progress-section .el-empty) {
- padding: 20px 0;
- }
- :deep(.progress-section .el-empty__description) {
- margin-top: 8px;
- font-size: 14px;
- color: #909399;
- }
- </style>
|