index.vue 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106
  1. <template>
  2. <div
  3. 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))]">
  4. <el-form
  5. ref="queryFormRef"
  6. :model="queryParams"
  7. size="default"
  8. label-width="72px"
  9. class="iot-project-info-query bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-6 py-3 min-w-0">
  10. <div class="query-row">
  11. <el-form-item label="公司" prop="companyDeptId">
  12. <el-select
  13. v-model="queryParams.companyDeptId"
  14. placeholder="请选择公司"
  15. clearable
  16. filterable
  17. class="query-control">
  18. <el-option
  19. v-for="item in companyDeptList"
  20. :key="item.id"
  21. :label="item.name"
  22. :value="item.id" />
  23. </el-select>
  24. </el-form-item>
  25. <el-form-item label="项目部" prop="projectDeptId">
  26. <el-select
  27. v-model="queryParams.projectDeptId"
  28. :placeholder="queryParams.companyDeptId ? '请选择项目部' : '请先选择公司'"
  29. :disabled="!queryParams.companyDeptId"
  30. :loading="projectDeptLoading"
  31. clearable
  32. filterable
  33. class="query-control">
  34. <el-option
  35. v-for="item in projectDeptOptions"
  36. :key="item.value"
  37. :label="item.label"
  38. :value="item.value" />
  39. </el-select>
  40. </el-form-item>
  41. <el-form-item label="合同名称" prop="contractName">
  42. <el-input
  43. v-model="queryParams.contractName"
  44. placeholder="请输入合同名称"
  45. clearable
  46. class="query-control"
  47. @keyup.enter="handleQuery" />
  48. </el-form-item>
  49. <el-form-item label="合同编号" prop="contractCode">
  50. <el-input
  51. v-model="queryParams.contractCode"
  52. placeholder="请输入合同编号"
  53. clearable
  54. class="query-control"
  55. @keyup.enter="handleQuery" />
  56. </el-form-item>
  57. <el-form-item label="起止日期" prop="startTime">
  58. <el-date-picker
  59. v-model="queryParams.startTime"
  60. value-format="YYYY-MM-DD HH:mm:ss"
  61. type="daterange"
  62. start-placeholder="开始日期"
  63. end-placeholder="结束日期"
  64. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  65. class="query-control query-control--date" />
  66. </el-form-item>
  67. </div>
  68. <el-form-item class="query-actions">
  69. <el-button type="primary" @click="handleQuery">
  70. <Icon icon="ep:search" class="mr-5px" />搜索
  71. </el-button>
  72. <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
  73. <el-button
  74. type="primary"
  75. plain
  76. @click="openForm('create', undefined)"
  77. v-hasPermi="['rq:iot-project-info:create']">
  78. <Icon icon="ep:plus" class="mr-5px" />新增
  79. </el-button>
  80. <el-button type="success" plain :loading="exportLoading" @click="handleExport">
  81. <Icon icon="ep:download" class="mr-5px" />导出
  82. </el-button>
  83. </el-form-item>
  84. </el-form>
  85. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0">
  86. <div class="flex-1 relative min-h-0">
  87. <el-auto-resizer class="absolute">
  88. <template #default="{ width, height }">
  89. <ZmTable
  90. :data="list"
  91. :loading="loading"
  92. :width="width"
  93. :height="height"
  94. :max-height="height"
  95. show-border>
  96. <ZmTableColumn
  97. type="index"
  98. :label="t('monitor.serial')"
  99. :width="70"
  100. fixed="left"
  101. hide-in-column-settings />
  102. <ZmTableColumn
  103. prop="manufactureName"
  104. label="客户名称"
  105. min-width="180"
  106. fixed="left"
  107. align="left" />
  108. <ZmTableColumn prop="contractName" label="合同名称" min-width="220" fixed="left">
  109. <template #default="{ row }">
  110. <el-link type="primary" :underline="false" @click="showTaskList(row)">
  111. {{ row.contractName }}
  112. </el-link>
  113. </template>
  114. </ZmTableColumn>
  115. <ZmTableColumn prop="contractCode" label="合同编号" min-width="150" />
  116. <ZmTableColumn prop="startTime" label="合同开始时间" min-width="140">
  117. <template #default="{ row }">
  118. {{ dateFormatter2(row, null, row.startTime) }}
  119. </template>
  120. </ZmTableColumn>
  121. <ZmTableColumn prop="endTime" label="合同完成时间" min-width="140">
  122. <template #default="{ row }">
  123. {{ dateFormatter2(row, null, row.endTime) }}
  124. </template>
  125. </ZmTableColumn>
  126. <ZmTableColumn prop="payment" :label="t('project.payment')" min-width="110">
  127. <template #default="{ row }">
  128. <dict-tag :type="DICT_TYPE.PMS_PROJECT_SETTLEMENT" :value="row.payment" />
  129. </template>
  130. </ZmTableColumn>
  131. <ZmTableColumn prop="createTime" label="创建时间" min-width="180">
  132. <template #default="{ row }">
  133. {{ dateFormatter(row, null, row.createTime) }}
  134. </template>
  135. </ZmTableColumn>
  136. <ZmTableColumn label="操作" width="190" fixed="right" action>
  137. <template #default="{ row }">
  138. <el-button
  139. link
  140. type="primary"
  141. @click="openForm('update', row.id)"
  142. v-hasPermi="['rq:iot-project-info:update']">
  143. 编辑
  144. </el-button>
  145. <el-button
  146. link
  147. type="primary"
  148. @click="assignTask(row)"
  149. v-hasPermi="['rq:iot-project-task:create']">
  150. 分配任务
  151. </el-button>
  152. <el-button
  153. link
  154. type="danger"
  155. @click="handleDelete(row.id)"
  156. v-hasPermi="['rq:iot-project-info:delete']">
  157. 删除
  158. </el-button>
  159. </template>
  160. </ZmTableColumn>
  161. </ZmTable>
  162. </template>
  163. </el-auto-resizer>
  164. </div>
  165. <div class="h-8 mt-2 flex items-center justify-end">
  166. <el-pagination
  167. v-show="total > 0"
  168. size="default"
  169. :current-page="queryParams.pageNo"
  170. :page-size="queryParams.pageSize"
  171. :background="true"
  172. :page-sizes="[10, 20, 30, 50, 100]"
  173. :total="total"
  174. layout="total, sizes, prev, pager, next, jumper"
  175. @size-change="handleSizeChange"
  176. @current-change="handleCurrentChange" />
  177. </div>
  178. </div>
  179. <div
  180. v-if="selectedProject"
  181. :style="{ height: taskListPanelHeight }"
  182. class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-4 min-w-0 min-h-0 task-list-panel">
  183. <div class="card-header mb-3">
  184. <span class="font-600">任务列表 - {{ selectedProject.contractName }}</span>
  185. <el-button link @click="closeTaskList" class="close-btn">
  186. <Icon icon="ep:close" />
  187. </el-button>
  188. </div>
  189. <div class="task-table-body relative min-h-0">
  190. <el-auto-resizer class="absolute">
  191. <template #default="{ width, height }">
  192. <ZmTable
  193. :data="taskList"
  194. :loading="taskLoading"
  195. :width="width"
  196. :height="height"
  197. :max-height="height"
  198. show-border>
  199. <ZmTableColumn prop="wellName" label="井号" min-width="100" />
  200. <ZmTableColumn prop="status" :label="t('project.status')" min-width="120">
  201. <template #default="{ row }">
  202. <el-link type="primary" :underline="false" @click="openTimelineDialog(row)">
  203. <template
  204. v-if="row.status !== null && row.status !== undefined && row.status !== ''">
  205. <dict-tag :type="getStatusDictType(row.deptId)" :value="row.status" />
  206. </template>
  207. <template v-else>未更新状态</template>
  208. </el-link>
  209. </template>
  210. </ZmTableColumn>
  211. <ZmTableColumn prop="location" label="施工地点" min-width="140" />
  212. <ZmTableColumn prop="technique" :label="t('project.technology')" min-width="120">
  213. <template #default="{ row }">
  214. {{ getTechniqueLabel(row) }}
  215. </template>
  216. </ZmTableColumn>
  217. <ZmTableColumn prop="workloadDesign" label="设计工作量" min-width="120" />
  218. <ZmTableColumn prop="workloadUnit" label="工作量单位" min-width="120">
  219. <template #default="{ row }">
  220. <dict-tag :type="DICT_TYPE.PMS_PROJECT_WORKLOAD_UNIT" :value="row.workloadUnit" />
  221. </template>
  222. </ZmTableColumn>
  223. <ZmTableColumn label="施工队伍" min-width="150">
  224. <template #default="{ row }">
  225. <el-tooltip :content="getAllDeptNames(row.deptIds)" placement="top">
  226. <span class="dept-names">
  227. {{ getBriefDeptNames(row.deptIds) }}
  228. </span>
  229. </el-tooltip>
  230. </template>
  231. </ZmTableColumn>
  232. <ZmTableColumn label="施工设备" min-width="180">
  233. <template #default="{ row }">
  234. <el-tooltip :content="getAllDeviceNames(row.deviceIds)" placement="top">
  235. <span class="device-names">
  236. {{ getDeviceNames(row.deviceIds) }}
  237. </span>
  238. </el-tooltip>
  239. </template>
  240. </ZmTableColumn>
  241. <ZmTableColumn label="责任人" min-width="120">
  242. <template #default="{ row }">
  243. <el-tooltip
  244. :content="getAllResponsiblePersonNames(row.responsiblePerson)"
  245. placement="top">
  246. <span class="responsible-names">
  247. {{ getResponsiblePersonNames(row.responsiblePerson) }}
  248. </span>
  249. </el-tooltip>
  250. </template>
  251. </ZmTableColumn>
  252. </ZmTable>
  253. </template>
  254. </el-auto-resizer>
  255. </div>
  256. </div>
  257. </div>
  258. <!-- Timeline 时间线 Dialog - 已修改为 el-steps -->
  259. <el-dialog
  260. v-model="timelineDialogVisible"
  261. :title="`任务进度 - ${currentTaskRow ? currentTaskRow.wellName : ''}`"
  262. :width="dialogWidth">
  263. <div class="progress-container">
  264. <!-- 计划进度 -->
  265. <!-- <div class="progress-section">
  266. <h3 class="progress-title">计划进度</h3>
  267. <div v-if="stepsData.length > 0">
  268. <el-steps direction="horizontal" :active="currentStepIndex" finish-status="success">
  269. <el-step
  270. v-for="(step, index) in stepsData"
  271. :key="index"
  272. :title="step.title"
  273. :description="step.description"
  274. :status="step.status"
  275. />
  276. </el-steps>
  277. </div>
  278. <el-empty v-else description="暂无计划进度数据" :image-size="80" />
  279. </div> -->
  280. <!-- 实际进度 -->
  281. <div class="progress-section">
  282. <h3 class="progress-title">实际进度</h3>
  283. <div v-if="actualStepsData.length > 0">
  284. <el-steps
  285. direction="horizontal"
  286. :active="actualStepsData.length - 1"
  287. finish-status="success">
  288. <el-step
  289. v-for="(step, index) in actualStepsData"
  290. :key="index"
  291. :title="step.title"
  292. :description="step.description"
  293. :status="step.status" />
  294. </el-steps>
  295. </div>
  296. <el-empty v-else description="暂无实际进度数据" :image-size="80" />
  297. </div>
  298. </div>
  299. <template #footer>
  300. <span class="dialog-footer">
  301. <el-button @click="timelineDialogVisible = false">关闭</el-button>
  302. </span>
  303. </template>
  304. </el-dialog>
  305. </template>
  306. <script setup lang="ts">
  307. import { useTableComponents } from '@/components/ZmTable/useTableComponents'
  308. import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
  309. import download from '@/utils/download'
  310. import { IotProjectInfoApi, IotProjectInfoVO } from '@/api/pms/iotprojectinfo'
  311. import { IotProjectTaskApi } from '@/api/pms/iotprojecttask'
  312. import { useUserStore } from '@/store/modules/user'
  313. import { DICT_TYPE, getDictLabel, getStrDictOptions } from '@/utils/dict'
  314. import { IotDeviceApi } from '@/api/pms/device'
  315. import * as UserApi from '@/api/system/user'
  316. import * as DeptApi from '@/api/system/dept'
  317. import { handleTree } from '@/utils/tree'
  318. import { IotProjectTaskScheduleApi } from '@/api/pms/iotprojecttaskschedule'
  319. import { IotRhDailyReportApi } from '@/api/pms/iotrhdailyreport'
  320. import { IotRdDailyReportApi } from '@/api/pms/iotrddailyreport'
  321. import dayjs from 'dayjs'
  322. import { ref, reactive, onMounted, computed, watch } from 'vue'
  323. /** 项目信息 列表 */
  324. // eslint-disable-next-line vue/component-definition-name-casing
  325. defineOptions({ name: 'iotProjectInfo' })
  326. const message = useMessage() // 消息弹窗
  327. const { t } = useI18n() // 国际化
  328. const { ZmTable, ZmTableColumn } = useTableComponents<any>()
  329. // 任务列表相关状态
  330. const taskLoading = ref(false) // 任务列表的加载中
  331. const taskList = ref([]) // 任务列表的数据
  332. const selectedProject = ref(null) // 当前选中的项目
  333. const deptList = ref([]) // 部门列表
  334. const companyDeptList = ref<any[]>([]) // 在公司级部门列表
  335. const projectDeptOptions = ref<any[]>([]) // 项目部选项
  336. const deviceMap = ref({}) // 设备映射表
  337. const responsiblePersonList = ref([]) // 责任人列表
  338. const taskScheduleDictOptions = ref<any[]>([]) // 任务进度字典选项
  339. const timelineDialogVisible = ref(false) // 控制时间线弹窗显示
  340. const currentTaskRow = ref<any>(null) // 当前选中的任务行数据
  341. const stepsData = ref<Array<{ title: string; description?: string; status?: string }>>([]) // 步骤数据
  342. const actualStepsData = ref<Array<{ title: string; description?: string; status?: string }>>([]) // 实际步骤数据
  343. const currentStepIndex = ref(0) // 当前步骤索引
  344. // 施工工艺字典选项映射表
  345. const technologyDictOptionsMap = ref<Record<number, any[]>>({})
  346. // 正在加载的deptId集合
  347. const loadingDeptIds = new Set<number>()
  348. const loading = ref(true) // 列表的加载中
  349. const list = ref<IotProjectInfoVO[]>([]) // 列表的数据
  350. const total = ref(0) // 列表的总页数
  351. const queryParams = reactive({
  352. pageNo: 1,
  353. pageSize: 10,
  354. deptId: undefined,
  355. companyDeptId: undefined,
  356. projectDeptId: undefined,
  357. deptName: undefined,
  358. contractName: undefined,
  359. contractCode: undefined,
  360. workloadTotal: undefined,
  361. workloadFinish: undefined,
  362. startTime: [],
  363. endTime: [],
  364. location: undefined,
  365. technique: undefined,
  366. payment: undefined,
  367. createTime: [],
  368. userName: undefined,
  369. userId: undefined
  370. })
  371. const queryFormRef = ref() // 搜索的表单
  372. const exportLoading = ref(false) // 导出的加载中
  373. const projectDeptLoading = ref(false)
  374. const { push } = useRouter() // 路由跳转
  375. const taskListPanelHeight = computed(() => {
  376. const headerHeight = 40
  377. const tableHeaderHeight = 40
  378. const rowHeight = 42
  379. const panelPadding = 32
  380. const maxHeight = 360
  381. const minRows = Math.max(taskList.value.length, 1)
  382. const height = panelPadding + headerHeight + tableHeaderHeight + minRows * rowHeight
  383. return `${Math.min(height, maxHeight)}px`
  384. })
  385. /** 查询列表 */
  386. const getList = async () => {
  387. loading.value = true
  388. try {
  389. queryParams.deptId = useUserStore().getUser.deptId
  390. const data = await IotProjectInfoApi.getIotProjectInfoPage(queryParams)
  391. list.value = data.list
  392. total.value = data.total
  393. // 列表加载完成后,如果列表有数据,则默认选中第一行
  394. if (list.value.length > 0) {
  395. showTaskList(list.value[0])
  396. } else {
  397. // 如果没有数据,确保任务列表区域关闭
  398. closeTaskList()
  399. }
  400. } finally {
  401. loading.value = false
  402. }
  403. }
  404. /** 搜索按钮操作 */
  405. const handleQuery = () => {
  406. queryParams.pageNo = 1
  407. getList()
  408. }
  409. /** 重置按钮操作 */
  410. const resetQuery = () => {
  411. queryFormRef.value.resetFields()
  412. handleQuery()
  413. }
  414. const handleSizeChange = (val: number) => {
  415. queryParams.pageSize = val
  416. handleQuery()
  417. }
  418. const handleCurrentChange = (val: number) => {
  419. queryParams.pageNo = val
  420. getList()
  421. }
  422. const loadProjectDeptOptions = async (companyDeptId?: number) => {
  423. if (!companyDeptId) {
  424. projectDeptOptions.value = []
  425. return
  426. }
  427. projectDeptLoading.value = true
  428. try {
  429. const res = await DeptApi.specifiedSimpleDepts(companyDeptId)
  430. projectDeptOptions.value = res
  431. .map((item: any) => ({
  432. label: item.name,
  433. value: item.id,
  434. raw: item
  435. }))
  436. .filter((item) => item.raw.type === '2')
  437. } catch (error) {
  438. projectDeptOptions.value = []
  439. } finally {
  440. projectDeptLoading.value = false
  441. }
  442. }
  443. // 加载施工工艺数据字典选项
  444. const loadTechnologyDictOptions = async (deptId: number) => {
  445. // 如果正在加载或已加载,直接返回
  446. if (loadingDeptIds.has(deptId)) return
  447. loadingDeptIds.add(deptId)
  448. try {
  449. // 获取数据字典标识符
  450. const dictLabel = await getDictLabel(DICT_TYPE.PMS_PROJECT_TECHNOLOGY, deptId)
  451. if (dictLabel) {
  452. // 获取真正的数据字典选项
  453. const dictOptions = getStrDictOptions(dictLabel)
  454. technologyDictOptionsMap.value[deptId] = dictOptions
  455. } else {
  456. // 如果没有获取到,设置为空数组
  457. technologyDictOptionsMap.value[deptId] = []
  458. }
  459. } catch (error) {
  460. console.error(`加载部门${deptId}的施工工艺数据字典失败:`, error)
  461. technologyDictOptionsMap.value[deptId] = []
  462. } finally {
  463. loadingDeptIds.delete(deptId)
  464. }
  465. }
  466. // 获取施工工艺标签
  467. const getTechniqueLabel = (row: any) => {
  468. const deptId = row.deptId
  469. const techniqueValue = row.technique
  470. if (!deptId || !technologyDictOptionsMap.value[deptId]) {
  471. return techniqueValue // 如果还没有加载到数据字典选项,显示原始值
  472. }
  473. const options = technologyDictOptionsMap.value[deptId]
  474. const option = options.find((opt) => opt.value === techniqueValue)
  475. return option ? option.label : techniqueValue
  476. }
  477. // 显示任务列表
  478. const showTaskList = async (project) => {
  479. selectedProject.value = project
  480. taskLoading.value = true
  481. try {
  482. // 获取任务列表
  483. const queryParams = {
  484. projectId: project.id
  485. }
  486. const taskData = await IotProjectTaskApi.getIotProjectTaskPage(queryParams)
  487. taskList.value = taskData.list
  488. // 收集所有设备ID和责任人ID
  489. const allDeviceIds = new Set()
  490. const allResponsiblePersonIds = new Set()
  491. // 收集所有部门ID用于加载施工工艺字典
  492. const allDeptIds = new Set()
  493. taskList.value.forEach((item) => {
  494. if (item.deviceIds?.length) {
  495. item.deviceIds.forEach((id) => allDeviceIds.add(id))
  496. }
  497. if (item.responsiblePerson?.length) {
  498. item.responsiblePerson.forEach((id) => allResponsiblePersonIds.add(id))
  499. }
  500. if (item.deptId) {
  501. allDeptIds.add(item.deptId)
  502. }
  503. })
  504. // 批量获取设备信息
  505. if (allDeviceIds.size > 0) {
  506. const deviceIdsArray = Array.from(allDeviceIds)
  507. const devices = await IotDeviceApi.getDevicesByDepts({
  508. deviceIds: deviceIdsArray
  509. })
  510. // 更新设备映射表
  511. devices.forEach((device) => {
  512. deviceMap.value[device.id] = device
  513. })
  514. }
  515. // 批量获取责任人信息
  516. if (allResponsiblePersonIds.size > 0) {
  517. const personIdsArray = Array.from(allResponsiblePersonIds)
  518. const persons = await UserApi.companyDeptsEmployee({
  519. userIds: personIdsArray
  520. })
  521. responsiblePersonList.value = persons
  522. }
  523. // 加载所有部门对应的施工工艺字典选项
  524. allDeptIds.forEach((deptId) => {
  525. if (deptId && !technologyDictOptionsMap.value[deptId]) {
  526. loadTechnologyDictOptions(deptId)
  527. }
  528. })
  529. } catch (error) {
  530. console.error('加载任务列表失败:', error)
  531. message.error('加载任务列表失败')
  532. } finally {
  533. taskLoading.value = false
  534. }
  535. }
  536. // 关闭任务列表
  537. const closeTaskList = () => {
  538. selectedProject.value = null
  539. taskList.value = []
  540. }
  541. // 获取部门名称
  542. const getDeptNames = (deptIds) => {
  543. if (!deptIds || deptIds.length === 0) return ''
  544. const names = []
  545. const findDept = (list, id) => {
  546. for (const item of list) {
  547. if (item.id === id) {
  548. names.push(item.name)
  549. return true
  550. }
  551. if (item.children && findDept(item.children, id)) {
  552. return true
  553. }
  554. }
  555. return false
  556. }
  557. deptIds.forEach((id) => findDept(deptList.value, id))
  558. return names.join(', ')
  559. }
  560. // 添加计算属性计算对话框宽度
  561. const dialogWidth = computed(() => {
  562. // if (stepsData.value.length === 0) return '700px';
  563. // 根据步骤数量计算宽度,每个步骤大约需要 200px
  564. const baseWidth = Math.max(stepsData.value.length * 200, actualStepsData.value.length * 200)
  565. // 限制最小和最大宽度
  566. const minWidth = 900
  567. const maxWidth = window.innerWidth * 0.9 // 最大为视口宽度的90%
  568. // 应用限制
  569. let calculatedWidth = Math.max(minWidth, baseWidth)
  570. calculatedWidth = Math.min(calculatedWidth, maxWidth)
  571. return `${calculatedWidth}px`
  572. })
  573. // 获取设备名称
  574. const getDeviceNames = (deviceIds) => {
  575. if (!deviceIds || deviceIds.length === 0) return ''
  576. const deviceNames = deviceIds.map((id) => deviceMap.value[id]?.deviceCode).filter((name) => name)
  577. if (deviceNames.length === 0) return ''
  578. if (deviceNames.length > 2) {
  579. return `${deviceNames[0]}, ${deviceNames[1]}...`
  580. }
  581. return deviceNames.join(', ')
  582. }
  583. // 获取所有设备名称
  584. const getAllDeviceNames = (deviceIds) => {
  585. if (!deviceIds || deviceIds.length === 0) return '无设备'
  586. const deviceNames = deviceIds
  587. .map((id) => deviceMap.value[id]?.deviceCode || '未知设备')
  588. .filter((name) => name !== '未知设备')
  589. return deviceNames.join(', ') || '无有效设备'
  590. }
  591. // 获取简略部门名称(用于表格显示)
  592. const getBriefDeptNames = (deptIds) => {
  593. if (!deptIds || deptIds.length === 0) return ''
  594. const names = []
  595. const findDept = (list, id) => {
  596. for (const item of list) {
  597. if (item.id === id) {
  598. names.push(item.name)
  599. return true
  600. }
  601. if (item.children && findDept(item.children, id)) {
  602. return true
  603. }
  604. }
  605. return false
  606. }
  607. deptIds.forEach((id) => findDept(deptList.value, id))
  608. if (names.length === 0) return ''
  609. if (names.length > 2) {
  610. return `${names[0]}, ${names[1]}...`
  611. }
  612. return names.join(', ')
  613. }
  614. // 获取所有部门名称(用于tooltip显示)
  615. const getAllDeptNames = (deptIds) => {
  616. if (!deptIds || deptIds.length === 0) return '无施工队伍'
  617. return getDeptNames(deptIds)
  618. }
  619. // 获取责任人名称
  620. const getResponsiblePersonNames = (responsiblePersonIds) => {
  621. if (!responsiblePersonIds || responsiblePersonIds.length === 0) return ''
  622. const personNames = responsiblePersonIds
  623. .map((id) => {
  624. const person = responsiblePersonList.value.find((p) => p.id === id)
  625. return person ? person.nickname : ''
  626. })
  627. .filter((name) => name)
  628. if (personNames.length === 0) return ''
  629. if (personNames.length > 2) {
  630. return `${personNames[0]}, ${personNames[1]}...`
  631. }
  632. return personNames.join(', ')
  633. }
  634. // 获取所有责任人名称
  635. const getAllResponsiblePersonNames = (responsiblePersonIds) => {
  636. if (!responsiblePersonIds || responsiblePersonIds.length === 0) return '无责任人'
  637. const personNames = responsiblePersonIds
  638. .map((id) => {
  639. const person = responsiblePersonList.value.find((p) => p.id === id)
  640. return person ? person.nickname : '未知人员'
  641. })
  642. .filter((name) => name !== '未知人员')
  643. return personNames.join(', ') || '无有效责任人'
  644. }
  645. /** 打开 Timeline 时间线 Dialog - 已修改为 el-steps */
  646. const openTimelineDialog = async (row: any) => {
  647. currentTaskRow.value = row
  648. timelineDialogVisible.value = true
  649. stepsData.value = [] // 清空旧数据
  650. actualStepsData.value = [] // 清空实际进度数据
  651. currentStepIndex.value = -1 // 初始化为-1,不选中任何步骤
  652. try {
  653. // 获取任务进度字典
  654. if (taskScheduleDictOptions.value.length === 0) {
  655. await getTaskScheduleDictOptions(row.deptId)
  656. }
  657. // 获取计划进度数据
  658. const params = { taskId: row.id }
  659. const response = await IotProjectTaskScheduleApi.getIotProjectTaskSchedules(params)
  660. // 根据 deptId 决定调用哪个接口获取实际进度
  661. let actualProgress = null
  662. if (row.deptId === 157) {
  663. // 瑞恒 157 - 调用瑞恒日报API
  664. actualProgress = await IotRhDailyReportApi.taskActualProgress(params)
  665. } else if (row.deptId === 163) {
  666. // 瑞都 163 - 调用瑞都日报API
  667. actualProgress = await IotRdDailyReportApi.taskActualProgress(params)
  668. } else {
  669. // 其他情况,可以根据需要添加更多条件或保持为空
  670. console.warn(`未知的部门ID: ${row.deptId},无法获取实际进度`)
  671. actualProgress = []
  672. }
  673. // 处理计划进度数据
  674. if (response && response.length > 0) {
  675. const sortedSchedules = response.sort((a, b) => a.status - b.status)
  676. stepsData.value = sortedSchedules.map((item: any) => {
  677. const formattedTimestamp = item.startTime
  678. ? dayjs(item.startTime).format('YYYY-MM-DD HH:mm')
  679. : '时间未设置'
  680. const dictItem = taskScheduleDictOptions.value.find((dict) => dict.value === item.status)
  681. const statusLabel = dictItem ? dictItem.label : `未知状态 (${item.status})`
  682. return {
  683. title: `${formattedTimestamp} ${statusLabel}`,
  684. description: '',
  685. status: undefined
  686. }
  687. })
  688. } else {
  689. stepsData.value = []
  690. }
  691. // 处理实际进度数据
  692. if (actualProgress && actualProgress.length > 0) {
  693. const sortedActualProgress = actualProgress.sort(
  694. (a, b) => (a.constructionStartDate || 0) - (b.constructionStartDate || 0)
  695. )
  696. // 根据 deptId 决定使用哪个状态字段
  697. const statusField = row.deptId === 163 ? 'rdStatus' : 'constructionStatus'
  698. actualStepsData.value = sortedActualProgress.map((item: any) => {
  699. const formattedTimestamp = item.constructionStartDate
  700. ? dayjs(item.constructionStartDate).format('YYYY-MM-DD HH:mm')
  701. : '时间未设置'
  702. // 使用动态的状态字段
  703. const statusValue = item[statusField]
  704. const dictItem = taskScheduleDictOptions.value.find((dict) => dict.value === statusValue)
  705. const statusLabel = dictItem ? dictItem.label : `未知状态 (${statusValue})`
  706. return {
  707. title: `${formattedTimestamp} ${statusLabel}`,
  708. description: '',
  709. status: undefined
  710. }
  711. })
  712. } else {
  713. actualStepsData.value = []
  714. }
  715. } catch (error) {
  716. console.error('获取任务进度时间线失败:', error)
  717. message.error('获取任务进度失败')
  718. stepsData.value = []
  719. actualStepsData.value = []
  720. }
  721. }
  722. /** 获取任务进度字典数据 */
  723. const getTaskScheduleDictOptions = async (deptId: number) => {
  724. try {
  725. const dictType = getStatusDictType(deptId)
  726. taskScheduleDictOptions.value = getStrDictOptions(dictType)
  727. } catch (error) {
  728. console.error('获取任务进度字典失败:', error)
  729. taskScheduleDictOptions.value = []
  730. }
  731. }
  732. const openForm = (type: string, id?: number) => {
  733. if (id === undefined) {
  734. push({ name: 'IotProjectInfo', params: { type } })
  735. } else {
  736. push({ name: 'IotProjectInfo', params: { type, id } })
  737. }
  738. }
  739. /** 分配任务操作 */
  740. const assignTask = (row: IotProjectInfoVO) => {
  741. push({
  742. name: 'IotProjectTaskInfo',
  743. params: {
  744. type: 'create',
  745. projectId: row.id,
  746. deptId: row.deptId
  747. },
  748. query: {
  749. contractName: row.contractName
  750. }
  751. })
  752. }
  753. /** 删除按钮操作 */
  754. const handleDelete = async (id: number) => {
  755. try {
  756. queryParams.deptId = useUserStore().getUser.deptId
  757. const data = await IotProjectTaskApi.getIotProjectTaskList(queryParams)
  758. if (data.length === 0) {
  759. // 删除的二次确认
  760. await message.delConfirm()
  761. // 发起删除
  762. await IotProjectInfoApi.deleteIotProjectInfo(id)
  763. message.success(t('common.delSuccess'))
  764. } else {
  765. message.error(t('form.relatedProject'))
  766. }
  767. // 刷新列表
  768. await getList()
  769. } catch {}
  770. }
  771. /** 导出按钮操作 */
  772. const handleExport = async () => {
  773. try {
  774. // 发起导出
  775. exportLoading.value = true
  776. const data = await IotProjectInfoApi.exportIotProjectInfo(queryParams)
  777. download.excel(data, '项目信息.xls')
  778. } catch {
  779. } finally {
  780. exportLoading.value = false
  781. }
  782. }
  783. /** 根据部门ID获取状态字典类型 */
  784. const getStatusDictType = (deptId: number) => {
  785. if (deptId === 163) {
  786. return DICT_TYPE.PMS_PROJECT_RD_STATUS // 瑞都
  787. } else if (deptId === 157) {
  788. return DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE // 瑞恒
  789. }
  790. return DICT_TYPE.PMS_PROJECT_TASK_SCHEDULE // 默认瑞恒
  791. }
  792. /** 初始化 **/
  793. onMounted(async () => {
  794. // 获取公司级的部门 用于 公司 筛选 管理员使用
  795. companyDeptList.value = await DeptApi.companyLevelDepts()
  796. deptList.value = handleTree(await DeptApi.companyLevelChildrenDepts())
  797. if (queryParams.companyDeptId) {
  798. await loadProjectDeptOptions(queryParams.companyDeptId)
  799. }
  800. getList()
  801. // 预加载任务进度字典
  802. // getTaskScheduleDictOptions()
  803. })
  804. watch(
  805. () => queryParams.companyDeptId,
  806. async (newVal, oldVal) => {
  807. if (newVal === oldVal) return
  808. queryParams.projectDeptId = undefined
  809. await loadProjectDeptOptions(newVal)
  810. }
  811. )
  812. </script>
  813. <style scoped>
  814. @media (width <= 1600px) {
  815. .iot-project-info-query .query-row {
  816. grid-template-columns: repeat(3, minmax(240px, 1fr));
  817. }
  818. }
  819. @media (width <= 1200px) {
  820. .iot-project-info-page {
  821. grid-template-rows: auto minmax(480px, 1fr) auto;
  822. height: auto;
  823. min-height: calc(
  824. 100vh - 20px - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height)
  825. );
  826. }
  827. .iot-project-info-query .query-row {
  828. grid-template-columns: repeat(2, minmax(240px, 1fr));
  829. }
  830. }
  831. @media (width <= 768px) {
  832. .iot-project-info-query {
  833. padding: 12px;
  834. }
  835. .iot-project-info-query .query-row {
  836. grid-template-columns: minmax(0, 1fr);
  837. }
  838. .iot-project-info-query .query-actions :deep(.el-form-item__content) {
  839. display: grid;
  840. grid-template-columns: repeat(2, minmax(0, 1fr));
  841. gap: 8px;
  842. width: 100%;
  843. }
  844. .iot-project-info-query .query-actions :deep(.el-button) {
  845. width: 100%;
  846. margin-left: 0;
  847. }
  848. }
  849. .iot-project-info-query {
  850. display: flex;
  851. flex-direction: column;
  852. gap: 12px;
  853. }
  854. .query-row {
  855. display: grid;
  856. grid-template-columns: repeat(4, minmax(220px, 1fr)) minmax(320px, 1.4fr);
  857. gap: 12px 20px;
  858. min-width: 0;
  859. }
  860. .query-actions {
  861. width: 100%;
  862. }
  863. .query-actions :deep(.el-form-item__content) {
  864. display: flex;
  865. flex-wrap: wrap;
  866. justify-content: flex-start;
  867. gap: 8px 10px;
  868. }
  869. .query-actions :deep(.el-button) {
  870. margin-left: 0;
  871. }
  872. .query-control {
  873. width: 100%;
  874. }
  875. .query-control--date {
  876. width: 100%;
  877. }
  878. :deep(.el-form-item) {
  879. margin-bottom: 0;
  880. }
  881. /* 任务列表相关样式 */
  882. .card-header {
  883. display: flex;
  884. justify-content: space-between;
  885. align-items: center;
  886. }
  887. .close-btn {
  888. min-height: auto;
  889. padding: 0;
  890. }
  891. .task-list-panel {
  892. max-height: 360px;
  893. transition: height 0.2s ease;
  894. }
  895. .task-table-body {
  896. flex: 1;
  897. }
  898. .dept-names {
  899. display: inline-block;
  900. max-width: 120px;
  901. overflow: hidden;
  902. text-overflow: ellipsis;
  903. white-space: nowrap;
  904. }
  905. .device-names {
  906. display: inline-block;
  907. max-width: 120px;
  908. overflow: hidden;
  909. text-overflow: ellipsis;
  910. white-space: nowrap;
  911. }
  912. .responsible-names {
  913. display: inline-block;
  914. max-width: 120px;
  915. overflow: hidden;
  916. text-overflow: ellipsis;
  917. white-space: nowrap;
  918. }
  919. :deep(.el-step__description) {
  920. color: red !important;
  921. }
  922. :deep(.timeline-dialog) {
  923. max-width: 90vw;
  924. }
  925. /* 确保步骤标题完全显示 */
  926. :deep(.timeline-dialog .el-step__title) {
  927. max-width: none;
  928. overflow: visible;
  929. text-overflow: clip;
  930. white-space: nowrap;
  931. }
  932. /* 调整步骤容器的布局 */
  933. :deep(.timeline-dialog .el-steps--horizontal) {
  934. flex-wrap: nowrap;
  935. overflow-x: auto;
  936. justify-content: flex-start;
  937. }
  938. /* 调整对话框内容区域的滚动 */
  939. :deep(.timeline-dialog .el-dialog__body) {
  940. overflow-x: auto;
  941. }
  942. /* 进度展示容器样式 */
  943. .progress-container {
  944. display: flex;
  945. flex-direction: column;
  946. gap: 24px;
  947. }
  948. .progress-section {
  949. padding: 16px;
  950. background-color: #fafafa;
  951. border: 1px solid #e6e6e6;
  952. border-radius: 8px;
  953. }
  954. .progress-section:first-child {
  955. background-color: #f0f7ff;
  956. border-color: #409eff;
  957. }
  958. .progress-section:last-child {
  959. background-color: #f0f9eb;
  960. border-color: #67c23a;
  961. }
  962. .progress-title {
  963. margin: 0 0 16px;
  964. font-size: 16px;
  965. font-weight: bold;
  966. color: #606266;
  967. }
  968. .progress-section:first-child .progress-title {
  969. color: #409eff;
  970. }
  971. .progress-section:last-child .progress-title {
  972. color: #67c23a;
  973. }
  974. /* 调整空状态显示 */
  975. :deep(.progress-section .el-empty) {
  976. padding: 20px 0;
  977. }
  978. :deep(.progress-section .el-empty__description) {
  979. margin-top: 8px;
  980. font-size: 14px;
  981. color: #909399;
  982. }
  983. </style>