123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734 |
- <template>
- <ContentWrap>
- <!-- 搜索工作栏 -->
- <el-form
- class="-mb-15px"
- :model="queryParams"
- ref="queryFormRef"
- :inline="true"
- label-width="68px"
- >
- <el-form-item label="客户名称" prop="manufactureName">
- <el-input
- v-model="queryParams.manufactureName"
- placeholder="请输入客户名称"
- clearable
- @keyup.enter="handleQuery"
- class="!w-240px"
- />
- </el-form-item>
- <el-form-item label="合同名称" prop="contractName">
- <el-input
- v-model="queryParams.contractName"
- placeholder="请输入合同名称"
- clearable
- @keyup.enter="handleQuery"
- class="!w-240px"
- />
- </el-form-item>
- <el-form-item label="合同编号" prop="contractCode">
- <el-input
- v-model="queryParams.contractCode"
- placeholder="请输入合同编号"
- clearable
- @keyup.enter="handleQuery"
- class="!w-240px"
- />
- </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="!w-220px"
- />
- </el-form-item>
- <el-form-item>
- <el-button @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
- @click="handleExport"
- :loading="exportLoading"
- v-hasPermi="['rq:iot-project-info:export']"
- >
- <Icon icon="ep:download" class="mr-5px" /> 导出
- </el-button>
- </el-form-item>
- </el-form>
- </ContentWrap>
- <!-- 列表 -->
- <ContentWrap>
- <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
- <el-table-column :label="t('iotDevice.serial')" width="70" align="center">
- <template #default="scope">
- {{ scope.$index + 1 }}
- </template>
- </el-table-column>
- <el-table-column label="客户名称" align="center" prop="manufactureName" />
- <el-table-column label="合同名称" align="center" prop="contractName" >
- <template #default="scope">
- <el-link type="primary" @click="showTaskList(scope.row)">
- {{ scope.row.contractName }}
- </el-link>
- </template>
- </el-table-column>
- <el-table-column label="合同编号" align="center" prop="contractCode" />
- <el-table-column label="工作量" align="center">
- <el-table-column label="总数" align="center" prop="workloadTotal" />
- <el-table-column label="完成" align="center" prop="workloadFinish" />
- </el-table-column>
- <el-table-column label="合同起止时间" align="center">
- <el-table-column
- label="开始时间"
- align="center"
- prop="startTime"
- :formatter="dateFormatter2"
- width="180px"
- />
- <el-table-column
- label="完成时间"
- align="center"
- prop="endTime"
- :formatter="dateFormatter2"
- width="180px"
- />
- </el-table-column>
- <!-- <el-table-column label="施工地点" align="center" prop="location" />
- <el-table-column label="施工工艺" align="center" prop="technique" />-->
- <el-table-column :label="t('project.payment')" align="center" prop="payment" min-width="90">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_SETTLEMENT" :value="scope.row.payment" />
- </template>
- </el-table-column>
- <el-table-column
- label="创建时间"
- align="center"
- prop="createTime"
- :formatter="dateFormatter"
- width="180px"
- />
- <el-table-column label="操作" align="center" min-width="180px">
- <template #default="scope">
- <el-button
- link
- type="primary"
- @click="openForm('update', scope.row.id)"
- v-hasPermi="['rq:iot-project-info:update']"
- >
- 编辑
- </el-button>
- <el-button
- link
- type="primary"
- @click="assignTask(scope.row)"
- v-hasPermi="['rq:iot-project-task:create']"
- >
- 分配任务
- </el-button>
- <el-button
- link
- type="danger"
- @click="handleDelete(scope.row.id)"
- v-hasPermi="['rq:iot-project-info:delete']"
- >
- 删除
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <!-- 分页 -->
- <Pagination
- :total="total"
- v-model:page="queryParams.pageNo"
- v-model:limit="queryParams.pageSize"
- @pagination="getList"
- />
- </ContentWrap>
- <!-- 任务列表区域 -->
- <ContentWrap v-if="selectedProject">
- <el-card class="box-card">
- <template #header>
- <div class="card-header">
- <span>任务列表 - {{ selectedProject.contractName }}</span>
- <el-button link @click="closeTaskList" class="close-btn">
- <Icon icon="ep:close" />
- </el-button>
- </div>
- </template>
- <el-table :data="taskList" v-loading="taskLoading">
- <el-table-column label="井号" align="center" prop="wellName" />
- <el-table-column :label="t('project.wellType')" align="center" prop="wellType" min-width="70">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_TYPE" :value="scope.row.wellType" />
- </template>
- </el-table-column>
- <el-table-column :label="t('project.wellCategory')" align="center" prop="wellCategory" min-width="70">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_CATEGORY" :value="scope.row.wellCategory" />
- </template>
- </el-table-column>
- <!--
- <el-table-column :label="t('project.status')" align="center" prop="status" min-width="70">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE" :value="scope.row.status" />
- </template>
- </el-table-column> -->
- <el-table-column :label="t('project.status')" align="center" prop="status" min-width="70">
- <template #default="scope">
- <el-link type="primary" @click="openTimelineDialog(scope.row)">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE" :value="scope.row.status" />
- </el-link>
- </template>
- </el-table-column>
- <el-table-column label="施工地点" align="center" prop="location" />
- <el-table-column :label="t('project.technology')" align="center" prop="technique" min-width="70">
- <template #default="scope">
- <dict-tag :type="DICT_TYPE.PMS_PROJECT_TECHNOLOGY" :value="scope.row.technique" />
- </template>
- </el-table-column>
- <el-table-column label="设计工作量" align="center" prop="workloadDesign" />
- <el-table-column label="施工队伍" align="center">
- <template #default="{ row }">
- <el-tooltip
- :content="getAllDeptNames(row.deptIds)"
- placement="top"
- >
- <span class="dept-names">
- {{ getBriefDeptNames(row.deptIds) }}
- </span>
- </el-tooltip>
- </template>
- </el-table-column>
- <el-table-column label="施工设备" align="center">
- <template #default="{ row }">
- <el-tooltip
- :content="getAllDeviceNames(row.deviceIds)"
- placement="top"
- >
- <span class="device-names">
- {{ getDeviceNames(row.deviceIds) }}
- </span>
- </el-tooltip>
- </template>
- </el-table-column>
- <el-table-column label="责任人" align="center">
- <template #default="{ row }">
- <el-tooltip
- :content="getAllResponsiblePersonNames(row.responsiblePerson)"
- placement="top"
- >
- <span class="responsible-names">
- {{ getResponsiblePersonNames(row.responsiblePerson) }}
- </span>
- </el-tooltip>
- </template>
- </el-table-column>
- <el-table-column label="备注" align="center" prop="remark" />
- </el-table>
- </el-card>
- </ContentWrap>
- <!-- Timeline 时间线 Dialog - 已修改为 el-steps -->
- <el-dialog v-model="timelineDialogVisible" :title="`任务进度 - ${currentTaskRow ? currentTaskRow.wellName : ''}`" width="700px">
- <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="100" />
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="timelineDialogVisible = false">关闭</el-button>
- </span>
- </template>
- </el-dialog>
- </template>
- <script setup lang="ts">
- 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 IotProjectInfoForm from './IotProjectInfoForm.vue'
- import {useUserStore} from "@/store/modules/user";
- import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
- import { IotDeviceApi } from '@/api/pms/device' // 新增:引入设备API
- import * as UserApi from "@/api/system/user"; // 新增:引入用户API
- import * as DeptApi from "@/api/system/dept"; // 新增:引入部门API
- import { handleTree } from "@/utils/tree"; // 新增:引入树形处理工具
- import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
- import dayjs from 'dayjs' // 引入 dayjs 用于时间格式化
- import { ref, reactive, onMounted, computed } from 'vue'
- /** 项目信息 列表 */
- defineOptions({ name: 'iotProjectInfo' })
- const message = useMessage() // 消息弹窗
- const { t } = useI18n() // 国际化
- // 新增:任务列表相关状态
- const taskLoading = ref(false) // 任务列表的加载中
- const taskList = ref([]) // 任务列表的数据
- const selectedProject = ref(null) // 当前选中的项目
- const deptList = ref([]) // 部门列表
- const deviceMap = ref({}) // 设备映射表
- const responsiblePersonList = ref([]) // 责任人列表
- const timelineData = ref<Array<{ timestamp: string; content: string }>>([]) // 时间线数据
- const taskScheduleDictOptions = ref<any[]>([]) // 任务进度字典选项
- const timelineDialogVisible = ref(false) // 控制时间线弹窗显示
- const currentTaskRow = ref<any>(null) // 当前选中的任务行数据
- const stepsData = ref<Array<{title: string, description?: string, status?: string}>>([]) // 步骤数据
- const currentStepIndex = ref(0) // 当前步骤索引
- const loading = ref(true) // 列表的加载中
- const list = ref<IotProjectInfoVO[]>([]) // 列表的数据
- const total = ref(0) // 列表的总页数
- const queryParams = reactive({
- pageNo: 1,
- pageSize: 10,
- deptId: 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 { push } = useRouter() // 路由跳转
- /** 查询列表 */
- 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
- } finally {
- loading.value = false
- }
- }
- /** 搜索按钮操作 */
- const handleQuery = () => {
- queryParams.pageNo = 1
- getList()
- }
- /** 重置按钮操作 */
- const resetQuery = () => {
- queryFormRef.value.resetFields()
- handleQuery()
- }
- // 显示任务列表
- 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();
- 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 (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;
- }
- } 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 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(', ') || '无有效责任人';
- };
- // 跳转到项目详情页面
- const goToDetail = (id: number) => {
- push({
- name: 'IotProjectInfoDetail',
- params: { id }
- })
- }
- /** 打开 Timeline 时间线 Dialog - 已修改为 el-steps */
- const openTimelineDialog = async (row: any) => {
- currentTaskRow.value = row
- timelineDialogVisible.value = true
- stepsData.value = [] // 清空旧数据
- currentStepIndex.value = 0 // 重置步骤索引
- try {
- // 获取任务进度字典
- if (taskScheduleDictOptions.value.length === 0) {
- await getTaskScheduleDictOptions()
- }
- // 获取时间线数据
- const params = { taskId: row.id } // 假设根据 taskId 获取
- const response = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules(params)
- if (response && response.length > 0) {
- // 处理数据:转换时间戳、匹配字典label
- const sortedSchedules = response.sort((a, b) => a.status - b.status);
- // 生成步骤数据
- stepsData.value = sortedSchedules.map((item: any) => {
- // 格式化时间戳 (假设 startTime 是毫秒时间戳)
- const formattedTimestamp = item.startTime ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm:ss') : '时间未设置'
- // 查找 status 对应的字典 label
- const dictItem = taskScheduleDictOptions.value.find(dict => dict.value === item.status)
- const statusLabel = dictItem ? dictItem.label : `未知状态 (${item.status})`
- // 核心修改:判断当前计划任务状态是否与当前行任务状态匹配
- const isCurrentStep = item.status === row.status;
- return {
- title: `${formattedTimestamp} ${statusLabel}`,
- // 如果匹配,description 设置为“当前进度”,否则设为空字符串
- description: isCurrentStep ? '当前进度' : '',
- // 可以根据需要设置 status,例如当前节点高亮
- status: isCurrentStep ? 'process' : undefined
- }
- })
- // 计算当前步骤索引
- const currentStatus = row.status;
- let activeIndex = -1;
- // 找到第一个状态大于当前任务状态的计划,当前步骤就是前一个
- for (let i = 0; i < sortedSchedules.length; i++) {
- if (currentStatus < sortedSchedules[i].status) {
- activeIndex = i - 0.5; // 中间状态
- break;
- } else if (currentStatus === sortedSchedules[i].status) {
- activeIndex = i; // 正好在这个节点上
- break;
- }
- }
- // 如果当前状态大于所有计划进度状态,则显示最后一步已完成
- if (activeIndex === -1 && sortedSchedules.length > 0) {
- if (currentStatus > sortedSchedules[sortedSchedules.length - 1].status) {
- activeIndex = sortedSchedules.length;
- } else {
- activeIndex = sortedSchedules.length - 1;
- }
- }
- currentStepIndex.value = Math.max(0, activeIndex);
- } else {
- stepsData.value = []
- }
- } catch (error) {
- console.error('获取任务进度时间线失败:', error)
- message.error('获取任务进度失败')
- stepsData.value = []
- }
- }
- /** 获取任务进度字典数据 */
- const getTaskScheduleDictOptions = async () => {
- try {
- taskScheduleDictOptions.value = getIntDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)
- } catch (error) {
- console.error('获取任务进度字典失败:', error)
- taskScheduleDictOptions.value = []
- }
- }
- /** 添加/修改操作 */
- const formRef = ref()
- 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',
- query: {
- projectId: row.id,
- contractName: row.contractName
- },
- params: {type: 'create'}
- })
- }
- /** 删除按钮操作 */
- 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 {
- // 导出的二次确认
- await message.exportConfirm()
- // 发起导出
- exportLoading.value = true
- const data = await IotProjectInfoApi.exportIotProjectInfo(queryParams)
- download.excel(data, '项目信息.xls')
- } catch {
- } finally {
- exportLoading.value = false
- }
- }
- /** 初始化 **/
- onMounted(async () => {
- deptList.value = handleTree(await DeptApi.companyLevelChildrenDepts());
- getList()
- // 预加载任务进度字典
- getTaskScheduleDictOptions()
- })
- </script>
- <style scoped>
- /* 新增:任务列表相关样式 */
- .card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
- .close-btn {
- padding: 0;
- min-height: auto;
- }
- .dept-names {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 120px;
- display: inline-block;
- }
- .device-names {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 120px;
- display: inline-block;
- }
- .responsible-names {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 120px;
- display: inline-block;
- }
- :deep(.el-step__description) {
- color: red !important;
- }
- </style>
|