index.vue 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981
  1. <template>
  2. <ContentWrap>
  3. <!-- 搜索工作栏 -->
  4. <el-form
  5. class="-mb-15px"
  6. :model="queryParams"
  7. ref="queryFormRef"
  8. :inline="true"
  9. label-width="68px"
  10. >
  11. <el-form-item label="客户名称" prop="manufactureName">
  12. <el-input
  13. v-model="queryParams.manufactureName"
  14. placeholder="请输入客户名称"
  15. clearable
  16. @keyup.enter="handleQuery"
  17. class="!w-240px"
  18. />
  19. </el-form-item>
  20. <el-form-item label="合同名称" prop="contractName">
  21. <el-input
  22. v-model="queryParams.contractName"
  23. placeholder="请输入合同名称"
  24. clearable
  25. @keyup.enter="handleQuery"
  26. class="!w-240px"
  27. />
  28. </el-form-item>
  29. <el-form-item label="合同编号" prop="contractCode">
  30. <el-input
  31. v-model="queryParams.contractCode"
  32. placeholder="请输入合同编号"
  33. clearable
  34. @keyup.enter="handleQuery"
  35. class="!w-240px"
  36. />
  37. </el-form-item>
  38. <el-form-item label="起止日期" prop="startTime">
  39. <el-date-picker
  40. v-model="queryParams.startTime"
  41. value-format="YYYY-MM-DD HH:mm:ss"
  42. type="daterange"
  43. start-placeholder="开始日期"
  44. end-placeholder="结束日期"
  45. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  46. class="!w-220px"
  47. />
  48. </el-form-item>
  49. <el-form-item>
  50. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
  51. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
  52. <el-button
  53. type="primary"
  54. plain
  55. @click="openForm('create',undefined)"
  56. v-hasPermi="['rq:iot-project-info:create']"
  57. >
  58. <Icon icon="ep:plus" class="mr-5px" /> 新增
  59. </el-button>
  60. <el-button
  61. type="success"
  62. plain
  63. @click="handleExport"
  64. :loading="exportLoading"
  65. v-hasPermi="['rq:iot-project-info:export']"
  66. >
  67. <Icon icon="ep:download" class="mr-5px" /> 导出
  68. </el-button>
  69. </el-form-item>
  70. </el-form>
  71. </ContentWrap>
  72. <!-- 列表 -->
  73. <ContentWrap ref="tableContainerRef">
  74. <div class="table-container">
  75. <el-table ref="tableRef" v-loading="loading" :data="list" :stripe="true" style="width: 100%" :cell-style="{padding: '5px'}">
  76. <el-table-column :label="t('iotDevice.serial')" :width="columnWidths.serial" align="center" v-if="false">
  77. <template #default="scope">
  78. {{ scope.$index + 1 }}
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="客户名称" align="left" prop="manufactureName" :width="columnWidths.manufactureName" />
  82. <el-table-column label="合同名称" align="center" prop="contractName" :width="columnWidths.contractName" >
  83. <template #default="scope">
  84. <el-link type="primary" @click="showTaskList(scope.row)">
  85. {{ scope.row.contractName }}
  86. </el-link>
  87. </template>
  88. </el-table-column>
  89. <el-table-column label="合同编号" align="center" prop="contractCode" :width="columnWidths.contractCode" />
  90. <el-table-column label="总工作量" align="center" prop="workloadTotal" :width="columnWidths.workloadTotal" />
  91. <el-table-column label="已完成工作量" align="center" prop="workloadFinish" :width="columnWidths.workloadFinish" />
  92. <el-table-column
  93. label="合同开始时间"
  94. align="center"
  95. prop="startTime"
  96. :formatter="dateFormatter2"
  97. :width="columnWidths.startTime"
  98. />
  99. <el-table-column
  100. label="合同完成时间"
  101. align="center"
  102. prop="endTime"
  103. :formatter="dateFormatter2"
  104. :width="columnWidths.endTime"
  105. />
  106. <el-table-column :label="t('project.payment')" align="center" prop="payment" :width="columnWidths.payment">
  107. <template #default="scope">
  108. <dict-tag :type="DICT_TYPE.PMS_PROJECT_SETTLEMENT" :value="scope.row.payment" />
  109. </template>
  110. </el-table-column>
  111. <el-table-column
  112. label="创建时间"
  113. align="center"
  114. prop="createTime"
  115. :formatter="dateFormatter"
  116. :width="columnWidths.createTime"
  117. />
  118. <el-table-column label="操作" align="center" :width="columnWidths.operation" fixed="right">
  119. <template #default="scope">
  120. <el-button
  121. link
  122. type="primary"
  123. @click="openForm('update', scope.row.id)"
  124. v-hasPermi="['rq:iot-project-info:update']"
  125. >
  126. 编辑
  127. </el-button>
  128. <el-button
  129. link
  130. type="primary"
  131. @click="assignTask(scope.row)"
  132. v-hasPermi="['rq:iot-project-task:create']"
  133. >
  134. 分配任务
  135. </el-button>
  136. <el-button
  137. link
  138. type="danger"
  139. @click="handleDelete(scope.row.id)"
  140. v-hasPermi="['rq:iot-project-info:delete']"
  141. >
  142. 删除
  143. </el-button>
  144. </template>
  145. </el-table-column>
  146. </el-table>
  147. </div>
  148. <!-- 分页 -->
  149. <Pagination
  150. :total="total"
  151. v-model:page="queryParams.pageNo"
  152. v-model:limit="queryParams.pageSize"
  153. @pagination="getList"
  154. />
  155. </ContentWrap>
  156. <!-- 任务列表区域 -->
  157. <ContentWrap v-if="selectedProject">
  158. <el-card class="box-card">
  159. <template #header>
  160. <div class="card-header">
  161. <span>任务列表 - {{ selectedProject.contractName }}</span>
  162. <el-button link @click="closeTaskList" class="close-btn">
  163. <Icon icon="ep:close" />
  164. </el-button>
  165. </div>
  166. </template>
  167. <el-table :data="taskList" v-loading="taskLoading">
  168. <el-table-column label="井号" align="center" prop="wellName" />
  169. <!--
  170. <el-table-column :label="t('project.wellType')" align="center" prop="wellType" min-width="70">
  171. <template #default="scope">
  172. <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_TYPE" :value="scope.row.wellType" />
  173. </template>
  174. </el-table-column>
  175. <el-table-column :label="t('project.wellCategory')" align="center" prop="wellCategory" min-width="70">
  176. <template #default="scope">
  177. <dict-tag :type="DICT_TYPE.PMS_PROJECT_WELL_CATEGORY" :value="scope.row.wellCategory" />
  178. </template>
  179. </el-table-column> -->
  180. <el-table-column :label="t('project.status')" align="center" prop="status" min-width="70">
  181. <template #default="scope">
  182. <el-link type="primary" @click="openTimelineDialog(scope.row)">
  183. <!-- 修改这里:添加状态判断逻辑 -->
  184. <template v-if="scope.row.status !== null && scope.row.status !== undefined && scope.row.status !== ''">
  185. <dict-tag :type="DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE" :value="scope.row.status" />
  186. </template>
  187. <template v-else>
  188. 未更新状态
  189. </template>
  190. </el-link>
  191. </template>
  192. </el-table-column>
  193. <el-table-column label="施工地点" align="center" prop="location" />
  194. <el-table-column :label="t('project.technology')" align="center" prop="technique" min-width="70">
  195. <template #default="scope">
  196. <dict-tag :type="DICT_TYPE.PMS_PROJECT_TECHNOLOGY" :value="scope.row.technique" />
  197. </template>
  198. </el-table-column>
  199. <el-table-column label="设计工作量" align="center" prop="workloadDesign" />
  200. <el-table-column label="施工队伍" align="center">
  201. <template #default="{ row }">
  202. <el-tooltip
  203. :content="getAllDeptNames(row.deptIds)"
  204. placement="top"
  205. >
  206. <span class="dept-names">
  207. {{ getBriefDeptNames(row.deptIds) }}
  208. </span>
  209. </el-tooltip>
  210. </template>
  211. </el-table-column>
  212. <el-table-column label="施工设备" align="center">
  213. <template #default="{ row }">
  214. <el-tooltip
  215. :content="getAllDeviceNames(row.deviceIds)"
  216. placement="top"
  217. >
  218. <span class="device-names">
  219. {{ getDeviceNames(row.deviceIds) }}
  220. </span>
  221. </el-tooltip>
  222. </template>
  223. </el-table-column>
  224. <el-table-column label="责任人" align="center">
  225. <template #default="{ row }">
  226. <el-tooltip
  227. :content="getAllResponsiblePersonNames(row.responsiblePerson)"
  228. placement="top"
  229. >
  230. <span class="responsible-names">
  231. {{ getResponsiblePersonNames(row.responsiblePerson) }}
  232. </span>
  233. </el-tooltip>
  234. </template>
  235. </el-table-column>
  236. <!-- <el-table-column label="备注" align="center" prop="remark" /> -->
  237. </el-table>
  238. </el-card>
  239. </ContentWrap>
  240. <!-- Timeline 时间线 Dialog - 已修改为 el-steps -->
  241. <el-dialog v-model="timelineDialogVisible" :title="`任务进度 - ${currentTaskRow ? currentTaskRow.wellName : ''}`" :width="dialogWidth">
  242. <div v-if="stepsData.length > 0">
  243. <el-steps direction="horizontal" :active="currentStepIndex" finish-status="success">
  244. <el-step
  245. v-for="(step, index) in stepsData"
  246. :key="index"
  247. :title="step.title"
  248. :description="step.description"
  249. :status="step.status"
  250. />
  251. </el-steps>
  252. </div>
  253. <el-empty v-else description="暂无进度数据" :image-size="100" />
  254. <template #footer>
  255. <span class="dialog-footer">
  256. <el-button @click="timelineDialogVisible = false">关闭</el-button>
  257. </span>
  258. </template>
  259. </el-dialog>
  260. </template>
  261. <script setup lang="ts">
  262. import { dateFormatter,dateFormatter2 } from '@/utils/formatTime'
  263. import download from '@/utils/download'
  264. import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
  265. import {IotProjectTaskApi} from '@/api/pms/iotprojecttask'
  266. import IotProjectInfoForm from './IotProjectInfoForm.vue'
  267. import {useUserStore} from "@/store/modules/user";
  268. import {DICT_TYPE, getIntDictOptions} from "@/utils/dict";
  269. import { IotDeviceApi } from '@/api/pms/device' // 引入设备API
  270. import * as UserApi from "@/api/system/user"; // 引入用户API
  271. import * as DeptApi from "@/api/system/dept"; // 引入部门API
  272. import { handleTree } from "@/utils/tree"; // 引入树形处理工具
  273. import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
  274. import dayjs from 'dayjs' // 引入 dayjs 用于时间格式化
  275. import { ref, reactive, onMounted, computed, nextTick, watch } from 'vue'
  276. /** 项目信息 列表 */
  277. defineOptions({ name: 'iotProjectInfo' })
  278. const message = useMessage() // 消息弹窗
  279. const { t } = useI18n() // 国际化
  280. // 任务列表相关状态
  281. const taskLoading = ref(false) // 任务列表的加载中
  282. const taskList = ref([]) // 任务列表的数据
  283. const selectedProject = ref(null) // 当前选中的项目
  284. const deptList = ref([]) // 部门列表
  285. const deviceMap = ref({}) // 设备映射表
  286. const responsiblePersonList = ref([]) // 责任人列表
  287. const timelineData = ref<Array<{ timestamp: string; content: string }>>([]) // 时间线数据
  288. const taskScheduleDictOptions = ref<any[]>([]) // 任务进度字典选项
  289. const timelineDialogVisible = ref(false) // 控制时间线弹窗显示
  290. const currentTaskRow = ref<any>(null) // 当前选中的任务行数据
  291. const stepsData = ref<Array<{title: string, description?: string, status?: string}>>([]) // 步骤数据
  292. const currentStepIndex = ref(0) // 当前步骤索引
  293. // 表格引用
  294. const tableRef = ref()
  295. // 表格容器引用
  296. const tableContainerRef = ref()
  297. const loading = ref(true) // 列表的加载中
  298. const list = ref<IotProjectInfoVO[]>([]) // 列表的数据
  299. const total = ref(0) // 列表的总页数
  300. const queryParams = reactive({
  301. pageNo: 1,
  302. pageSize: 10,
  303. deptId: undefined,
  304. deptName: undefined,
  305. contractName: undefined,
  306. contractCode: undefined,
  307. workloadTotal: undefined,
  308. workloadFinish: undefined,
  309. startTime: [],
  310. endTime: [],
  311. location: undefined,
  312. technique: undefined,
  313. payment: undefined,
  314. createTime: [],
  315. userName: undefined,
  316. userId: undefined,
  317. })
  318. const queryFormRef = ref() // 搜索的表单
  319. const exportLoading = ref(false) // 导出的加载中
  320. const { push } = useRouter() // 路由跳转
  321. // 列宽度配置
  322. const columnWidths = ref({
  323. serial: '50px',
  324. manufactureName: '120px',
  325. contractName: '150px',
  326. contractCode: '120px',
  327. workloadTotal: '100px',
  328. workloadFinish: '120px',
  329. startTime: '150px',
  330. endTime: '150px',
  331. payment: '100px',
  332. createTime: '150px',
  333. operation: '200px'
  334. })
  335. // 计算文本宽度
  336. const getTextWidth = (text: string, fontSize = 14) => {
  337. const span = document.createElement('span');
  338. span.style.visibility = 'hidden';
  339. span.style.position = 'absolute';
  340. span.style.whiteSpace = 'nowrap';
  341. span.style.fontSize = `${fontSize}px`;
  342. span.style.fontFamily = 'inherit';
  343. span.innerText = text;
  344. document.body.appendChild(span);
  345. const width = span.offsetWidth;
  346. document.body.removeChild(span);
  347. return width;
  348. };
  349. /** 查询列表 */
  350. const getList = async () => {
  351. loading.value = true
  352. try {
  353. queryParams.deptId = useUserStore().getUser.deptId;
  354. const data = await IotProjectInfoApi.getIotProjectInfoPage(queryParams)
  355. list.value = data.list
  356. total.value = data.total
  357. // 列表加载完成后,如果列表有数据,则默认选中第一行
  358. if (list.value.length > 0) {
  359. // 使用 nextTick 确保 DOM 更新后再执行选中操作
  360. nextTick(() => {
  361. showTaskList(list.value[0]);
  362. // 计算列宽
  363. calculateColumnWidths();
  364. });
  365. } else {
  366. // 如果没有数据,确保任务列表区域关闭
  367. closeTaskList();
  368. }
  369. } finally {
  370. loading.value = false
  371. }
  372. }
  373. // 计算列宽度
  374. const calculateColumnWidths = () => {
  375. const MIN_WIDTH = 80; // 最小列宽
  376. const PADDING = 25; // 列内边距
  377. const FLEXIBLE_COLUMNS = ['manufactureName', 'contractName', 'contractCode']; // 可伸缩列
  378. // 确保表格容器存在
  379. if (!tableContainerRef.value?.$el) return;
  380. const container = tableContainerRef.value.$el;
  381. const containerWidth = container.clientWidth;
  382. // 1. 计算所有列的最小宽度
  383. const minWidths: Record<string, number> = {};
  384. let totalMinWidth = 0;
  385. // 计算列最小宽度的函数
  386. const calculateColumnMinWidth = (key: string, label: string, getValue: Function) => {
  387. const headerWidth = getTextWidth(label) * 1.2;
  388. let contentMaxWidth = 0;
  389. // 计算内容最大宽度
  390. list.value.forEach((row, index) => {
  391. const text = String(getValue ? getValue(row, index) : (row[key] || ''));
  392. const textWidth = getTextWidth(text);
  393. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
  394. });
  395. const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
  396. minWidths[key] = minWidth;
  397. totalMinWidth += minWidth;
  398. return minWidth;
  399. };
  400. // 计算各列最小宽度
  401. calculateColumnMinWidth('serial', t('iotDevice.serial'), (row: any, index: number) => `${index + 1}`);
  402. calculateColumnMinWidth('manufactureName', '客户名称', (row: any) => row.manufactureName);
  403. calculateColumnMinWidth('contractName', '合同名称', (row: any) => row.contractName);
  404. calculateColumnMinWidth('contractCode', '合同编号', (row: any) => row.contractCode);
  405. calculateColumnMinWidth('workloadTotal', '总工作量', (row: any) => row.workloadTotal);
  406. calculateColumnMinWidth('workloadFinish', '已完成工作量', (row: any) => row.workloadFinish);
  407. calculateColumnMinWidth('startTime', '合同开始时间', (row: any) => dateFormatter2(null, null, row.startTime));
  408. calculateColumnMinWidth('endTime', '合同完成时间', (row: any) => dateFormatter2(null, null, row.endTime));
  409. calculateColumnMinWidth('payment', t('project.payment'), (row: any) => {
  410. const dict = getIntDictOptions(DICT_TYPE.PMS_PROJECT_SETTLEMENT)
  411. .find(d => d.value === row.payment);
  412. return dict ? dict.label : '';
  413. });
  414. calculateColumnMinWidth('createTime', '创建时间', (row: any) => dateFormatter(null, null, row.createTime));
  415. // 操作列固定宽度
  416. minWidths.operation = 200;
  417. totalMinWidth += 200;
  418. // 2. 计算可伸缩列最终宽度
  419. const newWidths: Record<string, string> = {};
  420. const availableWidth = containerWidth - 17; // 减去滚动条宽度
  421. // 应用最小宽度到所有列
  422. Object.keys(minWidths).forEach(key => {
  423. newWidths[key] = `${minWidths[key]}px`;
  424. });
  425. // 计算可伸缩列需要的宽度
  426. if (totalMinWidth < availableWidth) {
  427. // 有剩余空间:按比例分配给可伸缩列
  428. const extraSpace = availableWidth - totalMinWidth;
  429. const flexibleColumnCount = FLEXIBLE_COLUMNS.length;
  430. const spacePerColumn = Math.floor(extraSpace / flexibleColumnCount);
  431. FLEXIBLE_COLUMNS.forEach(key => {
  432. newWidths[key] = `${minWidths[key] + spacePerColumn}px`;
  433. });
  434. }
  435. // 3. 更新列宽配置
  436. columnWidths.value = newWidths;
  437. // 4. 触发表格重新布局
  438. nextTick(() => {
  439. tableRef.value?.doLayout();
  440. });
  441. };
  442. /** 搜索按钮操作 */
  443. const handleQuery = () => {
  444. queryParams.pageNo = 1
  445. getList()
  446. }
  447. /** 重置按钮操作 */
  448. const resetQuery = () => {
  449. queryFormRef.value.resetFields()
  450. handleQuery()
  451. }
  452. // 显示任务列表
  453. const showTaskList = async (project) => {
  454. selectedProject.value = project
  455. taskLoading.value = true
  456. try {
  457. // 获取任务列表
  458. const queryParams = {
  459. projectId: project.id
  460. };
  461. const taskData = await IotProjectTaskApi.getIotProjectTaskPage(queryParams);
  462. taskList.value = taskData.list;
  463. // 收集所有设备ID和责任人ID
  464. const allDeviceIds = new Set();
  465. const allResponsiblePersonIds = new Set();
  466. taskList.value.forEach(item => {
  467. if (item.deviceIds?.length) {
  468. item.deviceIds.forEach(id => allDeviceIds.add(id));
  469. }
  470. if (item.responsiblePerson?.length) {
  471. item.responsiblePerson.forEach(id => allResponsiblePersonIds.add(id));
  472. }
  473. });
  474. // 批量获取设备信息
  475. if (allDeviceIds.size > 0) {
  476. const deviceIdsArray = Array.from(allDeviceIds);
  477. const devices = await IotDeviceApi.getDevicesByDepts({
  478. deviceIds: deviceIdsArray
  479. });
  480. // 更新设备映射表
  481. devices.forEach(device => {
  482. deviceMap.value[device.id] = device;
  483. });
  484. }
  485. // 批量获取责任人信息
  486. if (allResponsiblePersonIds.size > 0) {
  487. const personIdsArray = Array.from(allResponsiblePersonIds);
  488. const persons = await UserApi.companyDeptsEmployee({
  489. userIds: personIdsArray
  490. });
  491. responsiblePersonList.value = persons;
  492. }
  493. } catch (error) {
  494. console.error('加载任务列表失败:', error);
  495. message.error('加载任务列表失败');
  496. } finally {
  497. taskLoading.value = false;
  498. }
  499. };
  500. // 关闭任务列表
  501. const closeTaskList = () => {
  502. selectedProject.value = null;
  503. taskList.value = [];
  504. };
  505. // 获取部门名称
  506. const getDeptNames = (deptIds) => {
  507. if (!deptIds || deptIds.length === 0) return '';
  508. const names = [];
  509. const findDept = (list, id) => {
  510. for (const item of list) {
  511. if (item.id === id) {
  512. names.push(item.name);
  513. return true;
  514. }
  515. if (item.children && findDept(item.children, id)) {
  516. return true;
  517. }
  518. }
  519. return false;
  520. };
  521. deptIds.forEach(id => findDept(deptList.value, id));
  522. return names.join(', ');
  523. };
  524. // 添加计算属性计算对话框宽度
  525. const dialogWidth = computed(() => {
  526. if (stepsData.value.length === 0) return '700px';
  527. // 根据步骤数量计算宽度,每个步骤大约需要 200px
  528. const baseWidth = stepsData.value.length * 200;
  529. // 限制最小和最大宽度
  530. const minWidth = 400;
  531. const maxWidth = window.innerWidth * 0.9; // 最大为视口宽度的90%
  532. // 应用限制
  533. let calculatedWidth = Math.max(minWidth, baseWidth);
  534. calculatedWidth = Math.min(calculatedWidth, maxWidth);
  535. return `${calculatedWidth}px`;
  536. });
  537. // 获取设备名称
  538. const getDeviceNames = (deviceIds) => {
  539. if (!deviceIds || deviceIds.length === 0) return '';
  540. const deviceNames = deviceIds
  541. .map(id => deviceMap.value[id]?.deviceCode)
  542. .filter(name => name);
  543. if (deviceNames.length === 0) return '';
  544. if (deviceNames.length > 2) {
  545. return `${deviceNames[0]}, ${deviceNames[1]}...`;
  546. }
  547. return deviceNames.join(', ');
  548. };
  549. // 获取所有设备名称
  550. const getAllDeviceNames = (deviceIds) => {
  551. if (!deviceIds || deviceIds.length === 0) return '无设备';
  552. const deviceNames = deviceIds
  553. .map(id => deviceMap.value[id]?.deviceCode || '未知设备')
  554. .filter(name => name !== '未知设备');
  555. return deviceNames.join(', ') || '无有效设备';
  556. };
  557. // 获取简略部门名称(用于表格显示)
  558. const getBriefDeptNames = (deptIds) => {
  559. if (!deptIds || deptIds.length === 0) return '';
  560. const names = [];
  561. const findDept = (list, id) => {
  562. for (const item of list) {
  563. if (item.id === id) {
  564. names.push(item.name);
  565. return true;
  566. }
  567. if (item.children && findDept(item.children, id)) {
  568. return true;
  569. }
  570. }
  571. return false;
  572. };
  573. deptIds.forEach(id => findDept(deptList.value, id));
  574. if (names.length === 0) return '';
  575. if (names.length > 2) {
  576. return `${names[0]}, ${names[1]}...`;
  577. }
  578. return names.join(', ');
  579. };
  580. // 获取所有部门名称(用于tooltip显示)
  581. const getAllDeptNames = (deptIds) => {
  582. if (!deptIds || deptIds.length === 0) return '无施工队伍';
  583. return getDeptNames(deptIds);
  584. };
  585. // 获取责任人名称
  586. const getResponsiblePersonNames = (responsiblePersonIds) => {
  587. if (!responsiblePersonIds || responsiblePersonIds.length === 0) return '';
  588. const personNames = responsiblePersonIds
  589. .map(id => {
  590. const person = responsiblePersonList.value.find(p => p.id === id);
  591. return person ? person.nickname : '';
  592. })
  593. .filter(name => name);
  594. if (personNames.length === 0) return '';
  595. if (personNames.length > 2) {
  596. return `${personNames[0]}, ${personNames[1]}...`;
  597. }
  598. return personNames.join(', ');
  599. };
  600. // 获取所有责任人名称
  601. const getAllResponsiblePersonNames = (responsiblePersonIds) => {
  602. if (!responsiblePersonIds || responsiblePersonIds.length === 0) return '无责任人';
  603. const personNames = responsiblePersonIds
  604. .map(id => {
  605. const person = responsiblePersonList.value.find(p => p.id === id);
  606. return person ? person.nickname : '未知人员';
  607. })
  608. .filter(name => name !== '未知人员');
  609. return personNames.join(', ') || '无有效责任人';
  610. };
  611. // 跳转到项目详情页面
  612. const goToDetail = (id: number) => {
  613. push({
  614. name: 'IotProjectInfoDetail',
  615. params: { id }
  616. })
  617. }
  618. /** 打开 Timeline 时间线 Dialog - 已修改为 el-steps */
  619. const openTimelineDialog = async (row: any) => {
  620. currentTaskRow.value = row
  621. timelineDialogVisible.value = true
  622. stepsData.value = [] // 清空旧数据
  623. currentStepIndex.value = -1 // 初始化为-1,不选中任何步骤
  624. try {
  625. // 获取任务进度字典
  626. if (taskScheduleDictOptions.value.length === 0) {
  627. await getTaskScheduleDictOptions()
  628. }
  629. // 获取时间线数据
  630. const params = { taskId: row.id } // 假设根据 taskId 获取
  631. const response = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules(params)
  632. if (response && response.length > 0) {
  633. // 处理数据:转换时间戳、匹配字典label
  634. const sortedSchedules = response.sort((a, b) => a.status - b.status);
  635. // 生成步骤数据
  636. stepsData.value = sortedSchedules.map((item: any) => {
  637. // 格式化时间戳 (假设 startTime 是毫秒时间戳)
  638. const formattedTimestamp = item.startTime ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm') : '时间未设置'
  639. // 查找 status 对应的字典 label
  640. const dictItem = taskScheduleDictOptions.value.find(dict => dict.value === item.status)
  641. const statusLabel = dictItem ? dictItem.label : `未知状态 (${item.status})`
  642. return {
  643. title: `${formattedTimestamp} ${statusLabel}`,
  644. description: '', // 初始为空
  645. status: undefined // 初始状态为任何特殊状态
  646. }
  647. })
  648. // 只有当任务有明确状态时才计算当前步骤
  649. if (row.status !== null && row.status !== undefined && row.status !== '') {
  650. const currentStatus = row.status;
  651. let activeIndex = -1;
  652. // 找到第一个状态大于当前任务状态的计划,当前步骤就是前一个
  653. for (let i = 0; i < sortedSchedules.length; i++) {
  654. if (currentStatus < sortedSchedules[i].status) {
  655. activeIndex = i - 0.5; // 中间状态
  656. break;
  657. } else if (currentStatus === sortedSchedules[i].status) {
  658. activeIndex = i; // 正好在这个节点上
  659. break;
  660. }
  661. }
  662. // 如果当前状态大于所有计划进度状态,则显示最后一步已完成
  663. if (activeIndex === -1 && sortedSchedules.length > 0) {
  664. if (currentStatus > sortedSchedules[sortedSchedules.length - 1].status) {
  665. activeIndex = sortedSchedules.length;
  666. } else {
  667. activeIndex = sortedSchedules.length - 1;
  668. }
  669. }
  670. currentStepIndex.value = Math.max(0, activeIndex);
  671. // 更新步骤数据,设置当前步骤的描述和状态
  672. if (currentStepIndex.value >= 0 && currentStepIndex.value < stepsData.value.length) {
  673. stepsData.value = stepsData.value.map((step, index) => ({
  674. ...step,
  675. description: index === currentStepIndex.value ? '当前进度' : '',
  676. status: index === currentStepIndex.value ? 'process' : undefined
  677. }))
  678. }
  679. }
  680. } else {
  681. stepsData.value = []
  682. }
  683. } catch (error) {
  684. console.error('获取任务进度时间线失败:', error)
  685. message.error('获取任务进度失败')
  686. stepsData.value = []
  687. }
  688. }
  689. /** 获取任务进度字典数据 */
  690. const getTaskScheduleDictOptions = async () => {
  691. try {
  692. taskScheduleDictOptions.value = getIntDictOptions(DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE)
  693. } catch (error) {
  694. console.error('获取任务进度字典失败:', error)
  695. taskScheduleDictOptions.value = []
  696. }
  697. }
  698. /** 添加/修改操作 */
  699. const formRef = ref()
  700. const openForm = (type: string, id?: number) => {
  701. if(id===undefined){
  702. push({ name: 'IotProjectInfo', params: { type} })
  703. }else{
  704. push({ name: 'IotProjectInfo', params: { type,id} })
  705. }
  706. }
  707. /** 分配任务操作 */
  708. const assignTask = (row: IotProjectInfoVO) => {
  709. push({
  710. name: 'IotProjectTaskInfo',
  711. query: {
  712. projectId: row.id,
  713. contractName: row.contractName
  714. },
  715. params: {type: 'create'}
  716. })
  717. }
  718. /** 删除按钮操作 */
  719. const handleDelete = async (id: number) => {
  720. try {
  721. queryParams.deptId = useUserStore().getUser.deptId;
  722. const data = await IotProjectTaskApi.getIotProjectTaskList(queryParams)
  723. if(data.length===0){
  724. // 删除的二次确认
  725. await message.delConfirm()
  726. // 发起删除
  727. await IotProjectInfoApi.deleteIotProjectInfo(id)
  728. message.success(t('common.delSuccess'))
  729. }else {
  730. message.error(t('form.relatedProject'))
  731. }
  732. // 刷新列表
  733. await getList()
  734. } catch {}
  735. }
  736. /** 导出按钮操作 */
  737. const handleExport = async () => {
  738. try {
  739. // 导出的二次确认
  740. await message.exportConfirm()
  741. // 发起导出
  742. exportLoading.value = true
  743. const data = await IotProjectInfoApi.exportIotProjectInfo(queryParams)
  744. download.excel(data, '项目信息.xls')
  745. } catch {
  746. } finally {
  747. exportLoading.value = false
  748. }
  749. }
  750. // 声明 ResizeObserver 实例
  751. let resizeObserver: ResizeObserver | null = null;
  752. /** 初始化 **/
  753. onMounted(async () => {
  754. deptList.value = handleTree(await DeptApi.companyLevelChildrenDepts());
  755. getList()
  756. // 预加载任务进度字典
  757. getTaskScheduleDictOptions()
  758. // 创建 ResizeObserver 监听表格容器尺寸变化
  759. if (tableContainerRef.value?.$el) {
  760. resizeObserver = new ResizeObserver(() => {
  761. // 使用防抖避免频繁触发
  762. clearTimeout((window as any).resizeTimer);
  763. (window as any).resizeTimer = setTimeout(() => {
  764. calculateColumnWidths();
  765. }, 100);
  766. });
  767. resizeObserver.observe(tableContainerRef.value.$el);
  768. }
  769. })
  770. onUnmounted(() => {
  771. // 清除 ResizeObserver
  772. if (resizeObserver && tableContainerRef.value?.$el) {
  773. resizeObserver.unobserve(tableContainerRef.value.$el);
  774. resizeObserver = null;
  775. }
  776. // 清除定时器
  777. if ((window as any).resizeTimer) {
  778. clearTimeout((window as any).resizeTimer);
  779. }
  780. })
  781. // 监听列表数据变化重新计算列宽
  782. watch(list, () => {
  783. nextTick(calculateColumnWidths)
  784. }, { deep: true })
  785. </script>
  786. <style scoped>
  787. /* 任务列表相关样式 */
  788. .card-header {
  789. display: flex;
  790. justify-content: space-between;
  791. align-items: center;
  792. }
  793. .close-btn {
  794. padding: 0;
  795. min-height: auto;
  796. }
  797. .dept-names {
  798. white-space: nowrap;
  799. overflow: hidden;
  800. text-overflow: ellipsis;
  801. max-width: 120px;
  802. display: inline-block;
  803. }
  804. .device-names {
  805. white-space: nowrap;
  806. overflow: hidden;
  807. text-overflow: ellipsis;
  808. max-width: 120px;
  809. display: inline-block;
  810. }
  811. .responsible-names {
  812. white-space: nowrap;
  813. overflow: hidden;
  814. text-overflow: ellipsis;
  815. max-width: 120px;
  816. display: inline-block;
  817. }
  818. :deep(.el-step__description) {
  819. color: red !important;
  820. }
  821. :deep(.timeline-dialog) {
  822. max-width: 90vw;
  823. }
  824. /* 确保步骤标题完全显示 */
  825. :deep(.timeline-dialog .el-step__title) {
  826. white-space: nowrap;
  827. overflow: visible;
  828. text-overflow: clip;
  829. max-width: none;
  830. }
  831. /* 调整步骤容器的布局 */
  832. :deep(.timeline-dialog .el-steps--horizontal) {
  833. flex-wrap: nowrap;
  834. overflow-x: auto;
  835. justify-content: flex-start;
  836. }
  837. /* 调整对话框内容区域的滚动 */
  838. :deep(.timeline-dialog .el-dialog__body) {
  839. overflow-x: auto;
  840. }
  841. /* 表格容器样式,确保水平滚动 */
  842. .table-container {
  843. width: 100%;
  844. overflow-x: auto;
  845. }
  846. /* 确保表格单元格内容不换行 */
  847. :deep(.el-table .cell) {
  848. white-space: nowrap;
  849. }
  850. /* 确保表格列标题不换行 */
  851. :deep(.el-table th > .cell) {
  852. white-space: nowrap;
  853. }
  854. /* 调整表格最小宽度,确保内容完全显示 */
  855. :deep(.el-table) {
  856. min-width: 100%;
  857. }
  858. /* 强制显示所有内容,防止省略号 */
  859. :deep(.el-table td.el-table__cell),
  860. :deep(.el-table th.el-table__cell) {
  861. overflow: visible !important;
  862. }
  863. :deep(.el-table .cell) {
  864. overflow: visible !important;
  865. text-overflow: clip !important;
  866. }
  867. </style>