index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. <template>
  2. <el-row :gutter="20">
  3. <el-col :class="{'leftcontent': true, 'collapsed': isLeftContentCollapsed}" :span="isLeftContentCollapsed ? 0 : 4" :xs="24">
  4. <ContentWrap class="h-1/1">
  5. <DeptTree @node-click="handleDeptNodeClick" />
  6. </ContentWrap>
  7. </el-col>
  8. <el-col class="rightcontent" :span="isLeftContentCollapsed ? 24 : 20" :xs="24" style="position: relative;height: 100vh;">
  9. <div
  10. class="toggle-button"
  11. :style="{ left: isLeftContentCollapsed ? '0px' : '-13px' }"
  12. @click="toggleLeftContent"
  13. @mouseover="handleMouseOver"
  14. @mouseout="handleMouseOut"
  15. :title="hoverText"
  16. >
  17. <span style="font-size: 5px;" :class="{'triangle': true, 'rotated': isLeftContentCollapsed}"></span>
  18. </div>
  19. <ContentWrap>
  20. <!-- 搜索工作栏 -->
  21. <el-form
  22. class="-mb-15px"
  23. :model="queryParams"
  24. ref="queryFormRef"
  25. :inline="true"
  26. label-width="68px"
  27. >
  28. <el-form-item :label="t('bomList.name')" prop="name">
  29. <el-input
  30. v-model="queryParams.name"
  31. :placeholder="t('bomList.nHolder')"
  32. clearable
  33. @keyup.enter="handleQuery"
  34. class="!w-240px"
  35. />
  36. </el-form-item>
  37. <el-form-item :label="t('bomList.status')" prop="result">
  38. <el-select
  39. v-model="queryParams.result"
  40. :placeholder="t('bomList.status')"
  41. clearable
  42. class="!w-240px"
  43. >
  44. <el-option
  45. v-for="dict in resultOptions"
  46. :key="dict.value"
  47. :label="dict.label"
  48. :value="dict.value"
  49. />
  50. </el-select>
  51. </el-form-item>
  52. <el-form-item :label="t('common.createTime')" prop="createTime" label-width="100px">
  53. <el-date-picker
  54. v-model="queryParams.createTime"
  55. value-format="YYYY-MM-DD HH:mm:ss"
  56. type="daterange"
  57. :start-placeholder="t('operationFill.start')"
  58. :end-placeholder="t('operationFill.end')"
  59. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  60. class="!w-220px"
  61. />
  62. </el-form-item>
  63. <el-form-item>
  64. <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />
  65. {{ t('operationFill.search') }}</el-button>
  66. <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> {{ t('operationFill.reset') }}</el-button>
  67. <el-button
  68. type="primary"
  69. plain
  70. @click="openForm('create')"
  71. v-hasPermi="['pms:iot-main-work-order:create']"
  72. >
  73. <Icon icon="ep:plus" class="mr-5px" /> {{ t('operationFill.add') }}
  74. </el-button>
  75. <el-button
  76. type="success"
  77. plain
  78. @click="handleExport"
  79. :loading="exportLoading"
  80. v-hasPermi="['pms:iot-main-work-order:export']"
  81. >
  82. <Icon icon="ep:download" class="mr-5px" /> 导出
  83. </el-button>
  84. </el-form-item>
  85. </el-form>
  86. </ContentWrap>
  87. <!-- 列表 -->
  88. <ContentWrap ref="tableContainerRef" class="table-wrap">
  89. <el-table v-loading="loading" :data="list" :stripe="true" style="width: 100%" ref="tableRef">
  90. <el-table-column :label="t('iotDevice.serial')" align="center" :width="columnWidths.serial">
  91. <template #default="scope">
  92. {{ scope.$index + 1 }}
  93. </template>
  94. </el-table-column>
  95. <el-table-column :label="t('bomList.name')" align="center" prop="name" :width="columnWidths.name"/>
  96. <el-table-column :label="t('iotDevice.dept')" align="center" prop="deptName" :width="columnWidths.deptName"/>
  97. <el-table-column :label="t('bomList.status')" align="center" prop="result" :width="columnWidths.result">
  98. <template #default="scope">
  99. <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT" :value="scope.row.result" />
  100. </template>
  101. </el-table-column>
  102. <el-table-column :label="t('bomList.serviceDue')" align="center" :width="columnWidths.serviceDue">
  103. <template #default="scope">
  104. <span :class="getDistanceClass(scope.row.mainDistance)">
  105. {{ scope.row.mainDistance }}
  106. </span>
  107. </template>
  108. </el-table-column>
  109. <el-table-column :label="t('bomList.type')" align="center" prop="type" :width="columnWidths.type">
  110. <template #default="scope">
  111. <dict-tag :type="DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE" :value="scope.row.type" />
  112. </template>
  113. </el-table-column>
  114. <el-table-column :label="t('iotMaintain.PersonInCharge')" align="center"
  115. prop="responsiblePersonName" :width="columnWidths.responsiblePersonName"/>
  116. <el-table-column
  117. :label="t('dict.createTime')"
  118. align="center"
  119. prop="createTime"
  120. :formatter="dateFormatter"
  121. :width="columnWidths.createTime"
  122. />
  123. <el-table-column
  124. :label="t('dict.fillTime')"
  125. align="center"
  126. prop="updateTime"
  127. :width="columnWidths.updateTime"
  128. >
  129. <template #default="scope">
  130. <span v-if="scope.row.result == 2">
  131. {{ formatCellDate(scope.row.updateTime) }}
  132. </span>
  133. <span v-else></span>
  134. </template>
  135. </el-table-column>
  136. <el-table-column :label="t('iotMaintain.operation')" align="center" :width="columnWidths.operation" fixed="right">
  137. <template #default="scope">
  138. <el-button
  139. v-if="isNegativeMainDistance(scope.row.mainDistance)"
  140. link
  141. :type="getDelayReasonButtonType(scope.row.delayReason)"
  142. @click="openDelayReasonDialog(scope.row)"
  143. v-hasPermi="['pms:iot-main-work-order:update']"
  144. >
  145. {{ t('mainPlan.delayed') || '延时' }}
  146. </el-button>
  147. <el-button
  148. link
  149. type="primary"
  150. @click="openForm('update', scope.row.id)"
  151. v-hasPermi="['pms:iot-main-work-order:update']"
  152. v-if="scope.row.result === 1"
  153. >
  154. {{ t('operationFill.fill') }}
  155. </el-button>
  156. <el-button
  157. link
  158. type="primary"
  159. @click="detail(scope.row.id)"
  160. v-hasPermi="['pms:iot-main-work-order:query']"
  161. >
  162. {{ t('operationFill.view') }}
  163. </el-button>
  164. </template>
  165. </el-table-column>
  166. </el-table>
  167. <!-- 分页 -->
  168. <Pagination
  169. :total="total"
  170. v-model:page="queryParams.pageNo"
  171. v-model:limit="queryParams.pageSize"
  172. @pagination="getList"
  173. />
  174. </ContentWrap>
  175. </el-col>
  176. </el-row>
  177. <!-- 表单弹窗:添加/修改 -->
  178. <IotMainWorkOrderForm ref="formRef" @success="getList" />
  179. <!-- 延保原因弹窗 -->
  180. <el-dialog
  181. v-model="delayReasonDialogVisible"
  182. :title="t('workOrderMaterial.delayReason') || '延时原因'"
  183. width="500px"
  184. :close-on-click-modal="false"
  185. >
  186. <el-form :model="delayReasonForm" label-width="0px" :rules="delayReasonRules"
  187. ref="delayReasonFormRef">
  188. <el-form-item label=" " prop="delayReason" class="required-item" label-width="16px">
  189. <el-input
  190. v-model="delayReasonForm.delayReason"
  191. type="textarea"
  192. :rows="4"
  193. :placeholder="t('workOrderMaterial.inputDelayReason') || '请输入延时原因'"
  194. maxlength="500"
  195. show-word-limit
  196. />
  197. </el-form-item>
  198. </el-form>
  199. <template #footer>
  200. <el-button @click="delayReasonDialogVisible = false">
  201. {{ t('common.cancel') || '取消' }}
  202. </el-button>
  203. <el-button type="primary" @click="saveDelayReason" :loading="saveDelayReasonLoading">
  204. {{ t('common.save') || '保存' }}
  205. </el-button>
  206. </template>
  207. </el-dialog>
  208. </template>
  209. <script setup lang="ts">
  210. import { dateFormatter } from '@/utils/formatTime'
  211. import download from '@/utils/download'
  212. import { IotMainWorkOrderApi, IotMainWorkOrderVO } from '@/api/pms/iotmainworkorder'
  213. import IotMainWorkOrderForm from './IotMainWorkOrderForm.vue'
  214. import {DICT_TYPE, getStrDictOptions} from "@/utils/dict";
  215. import DeptTree from "@/views/system/user/DeptTree.vue";
  216. const { push } = useRouter() // 路由跳转
  217. /** 保养工单 列表 */
  218. defineOptions({ name: 'IotMainWorkOrder' })
  219. // 表单引用
  220. const delayReasonFormRef = ref<InstanceType<typeof ElForm>>()
  221. const message = useMessage() // 消息弹窗
  222. const { t } = useI18n() // 国际化
  223. const tableRef = ref() // 表格引用
  224. const isLeftContentCollapsed = ref(false);
  225. // 表格容器引用 用于获取容器宽度
  226. const tableContainerRef = ref()
  227. const loading = ref(true) // 列表的加载中
  228. const list = ref<IotMainWorkOrderVO[]>([]) // 列表的数据
  229. const total = ref(0) // 列表的总页数
  230. const queryParams = reactive({
  231. pageNo: 1,
  232. pageSize: 10,
  233. planId: undefined,
  234. planSerialNumber: undefined,
  235. deptId: undefined,
  236. orderNumber: undefined,
  237. name: undefined,
  238. type: undefined,
  239. responsiblePerson: undefined,
  240. responsiblePersonName: undefined,
  241. cost: undefined,
  242. result: undefined,
  243. otherCost: undefined,
  244. laborCost: undefined,
  245. outsourcingFlag: undefined,
  246. actualStartTime: [],
  247. actualEndTime: [],
  248. remark: undefined,
  249. status: undefined,
  250. processInstanceId: undefined,
  251. auditStatus: undefined,
  252. createTime: [],
  253. })
  254. const queryFormRef = ref() // 搜索的表单
  255. const exportLoading = ref(false) // 导出的加载中
  256. const hoverText = ref('');
  257. // 定义表单验证规则
  258. const delayReasonRules = {
  259. delayReason: [
  260. { required: true, message: t('workOrderMaterial.inputDelayReason') || '请输入延时原因', trigger: 'blur' }
  261. ]
  262. }
  263. // 列宽度配置
  264. const columnWidths = ref({
  265. serial: '80px',
  266. name: '200px',
  267. deptName: '150px',
  268. result: '120px',
  269. serviceDue: '150px',
  270. type: '120px',
  271. responsiblePersonName: '150px',
  272. createTime: '180px',
  273. updateTime: '180px',
  274. operation: '150px'
  275. })
  276. // 计算文本宽度
  277. const getTextWidth = (text: string, fontSize = 14) => {
  278. const span = document.createElement('span');
  279. span.style.visibility = 'hidden';
  280. span.style.position = 'absolute';
  281. span.style.whiteSpace = 'nowrap';
  282. span.style.fontSize = `${fontSize}px`;
  283. span.style.fontFamily = 'inherit';
  284. span.innerText = text;
  285. document.body.appendChild(span);
  286. const width = span.offsetWidth;
  287. document.body.removeChild(span);
  288. return width;
  289. };
  290. /** 延保原因相关状态 */
  291. const delayReasonDialogVisible = ref(false)
  292. const saveDelayReasonLoading = ref(false)
  293. const delayReasonForm = reactive({
  294. id: undefined as number | undefined,
  295. delayReason: ''
  296. })
  297. /** 判断mainDistance是否为负值 */
  298. const isNegativeMainDistance = (mainDistance: string | null): boolean => {
  299. if (!mainDistance) return false
  300. // 使用正则提取数字部分(包括负号、小数点和科学计数法)
  301. const numericPart = mainDistance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0]
  302. if (numericPart) {
  303. const num = parseFloat(numericPart)
  304. return num < 0
  305. }
  306. return false
  307. }
  308. /** 打开延保原因弹窗 */
  309. const openDelayReasonDialog = (row: IotMainWorkOrderVO) => {
  310. delayReasonForm.id = row.id
  311. delayReasonForm.delayReason = row.delayReason || ''
  312. delayReasonDialogVisible.value = true
  313. }
  314. /** 保存延保原因 */
  315. const saveDelayReason = async () => {
  316. // 表单验证
  317. if (!delayReasonFormRef.value) return
  318. const valid = await delayReasonFormRef.value.validate()
  319. if (!valid) return
  320. if (!delayReasonForm.id) {
  321. message.error('ID不能为空')
  322. return
  323. }
  324. try {
  325. saveDelayReasonLoading.value = true
  326. // 调用更新接口,只传递id和delayReason字段
  327. await IotMainWorkOrderApi.updateIotMainWorkOrder({
  328. id: delayReasonForm.id,
  329. delayReason: delayReasonForm.delayReason
  330. })
  331. message.success(t('common.success') || '保存成功')
  332. delayReasonDialogVisible.value = false
  333. // 刷新列表数据
  334. await getList()
  335. } catch (error) {
  336. console.error('保存延保原因失败:', error)
  337. message.error(t('sys.api.operationFailed') || '保存失败')
  338. } finally {
  339. saveDelayReasonLoading.value = false
  340. }
  341. }
  342. /** 根据delayReason是否有值返回按钮类型 */
  343. const getDelayReasonButtonType = (delayReason: string | null | undefined): string => {
  344. // 如果delayReason有值(不为null、undefined且不是空字符串),返回success,否则返回warning
  345. return delayReason ? 'success' : 'warning'
  346. }
  347. /** 处理部门被点击 */
  348. const handleDeptNodeClick = async (row) => {
  349. queryParams.deptId = row.id
  350. await getList()
  351. }
  352. // 计算列宽度
  353. const calculateColumnWidths = () => {
  354. const MIN_WIDTH = 80; // 最小列宽
  355. const PADDING = 25; // 列内边距
  356. const FLEXIBLE_COLUMN = 'name'; // 可伸缩列
  357. // 确保表格容器存在
  358. if (!tableContainerRef.value?.$el) return;
  359. const container = tableContainerRef.value.$el;
  360. const containerWidth = container.clientWidth;
  361. // 1. 计算所有列的最小宽度
  362. const minWidths: Record<string, number> = {};
  363. let totalMinWidth = 0;
  364. // 计算列最小宽度的函数
  365. const calculateColumnMinWidth = (key: string, label: string, getValue: Function) => {
  366. const headerWidth = getTextWidth(label) * 1.2;
  367. let contentMaxWidth = 0;
  368. // 计算内容最大宽度
  369. list.value.forEach((row, index) => {
  370. const text = String(getValue ? getValue(row, index) : (row[key] || ''));
  371. const textWidth = getTextWidth(text);
  372. if (textWidth > contentMaxWidth) contentMaxWidth = textWidth;
  373. });
  374. const minWidth = Math.max(headerWidth, contentMaxWidth, MIN_WIDTH) + PADDING;
  375. minWidths[key] = minWidth;
  376. totalMinWidth += minWidth;
  377. return minWidth;
  378. };
  379. // 计算各列最小宽度
  380. calculateColumnMinWidth('serial', t('iotDevice.serial'), (row: any, index: number) => `${index + 1}`);
  381. const nameMinWidth = calculateColumnMinWidth('name', t('bomList.name'), (row: any) => row.name);
  382. calculateColumnMinWidth('deptName', t('iotDevice.dept'), (row: any) => row.deptName);
  383. calculateColumnMinWidth('result', t('bomList.status'), (row: any) => {
  384. const dict = getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)
  385. .find(d => d.value === row.result);
  386. return dict ? dict.label : '';
  387. });
  388. calculateColumnMinWidth('serviceDue', t('bomList.serviceDue'), (row: any) => row.mainDistance || '');
  389. calculateColumnMinWidth('type', t('bomList.type'), (row: any) => {
  390. const dict = getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_TYPE)
  391. .find(d => d.value === row.type);
  392. return dict ? dict.label : '';
  393. });
  394. calculateColumnMinWidth('responsiblePersonName', t('iotMaintain.PersonInCharge'), (row: any) => row.responsiblePersonName);
  395. calculateColumnMinWidth('createTime', t('dict.createTime'), (row: any) => dateFormatter(null, null, row.createTime));
  396. calculateColumnMinWidth('updateTime', t('dict.fillTime'), (row: any) => row.result == 2 ? formatCellDate(row.updateTime) : '');
  397. // 操作列固定宽度
  398. minWidths.operation = 160;
  399. totalMinWidth += 160;
  400. // 2. 计算可伸缩列最终宽度
  401. const newWidths: Record<string, string> = {};
  402. const availableWidth = containerWidth - 17; // 减去滚动条宽度
  403. // 应用最小宽度到所有列
  404. Object.keys(minWidths).forEach(key => {
  405. newWidths[key] = `${minWidths[key]}px`;
  406. });
  407. // 计算可伸缩列需要的宽度
  408. if (totalMinWidth < availableWidth) {
  409. // 有剩余空间:分配给可伸缩列
  410. newWidths[FLEXIBLE_COLUMN] = `${minWidths[FLEXIBLE_COLUMN] + (availableWidth - totalMinWidth)}px`;
  411. } else {
  412. // 空间不足:确保可伸缩列至少显示内容
  413. newWidths[FLEXIBLE_COLUMN] = `${nameMinWidth}px`;
  414. }
  415. // 3. 更新列宽配置
  416. columnWidths.value = newWidths;
  417. // 4. 触发表格重新布局
  418. nextTick(() => {
  419. tableRef.value?.doLayout();
  420. });
  421. };
  422. /** 查询列表 */
  423. const getList = async () => {
  424. loading.value = true
  425. try {
  426. const data = await IotMainWorkOrderApi.sortedMainWorkOrderPage(queryParams)
  427. list.value = data.list
  428. total.value = data.total
  429. // 数据加载后计算列宽
  430. nextTick(() => {
  431. calculateColumnWidths()
  432. window.dispatchEvent(new Event('resize'))
  433. })
  434. } finally {
  435. loading.value = false
  436. }
  437. }
  438. // 日期格式化辅助函数
  439. const formatCellDate = (dateString: string | null) => {
  440. if (!dateString) return '';
  441. return dateFormatter(null, null, dateString);
  442. }
  443. /** 搜索按钮操作 */
  444. const handleQuery = () => {
  445. queryParams.pageNo = 1
  446. getList()
  447. }
  448. /** 重置按钮操作 */
  449. const resetQuery = () => {
  450. queryFormRef.value.resetFields()
  451. handleQuery()
  452. }
  453. // 保养状态 下拉列表 添加数据字典外的选项
  454. const resultOptions = computed(() => [
  455. {
  456. label: t('operationFill.all'),
  457. value: '0' // 空值会触发 clearable 效果
  458. },
  459. {
  460. label: t('mainPlan.delayed'),
  461. value: '3' // 空值会触发 clearable 效果
  462. },
  463. ...getStrDictOptions(DICT_TYPE.PMS_MAIN_WORK_ORDER_RESULT)
  464. ])
  465. const getDistanceClass = (distance: number | string | null) => {
  466. if (distance === null || distance === undefined) return '';
  467. // 如果是数字类型,直接处理
  468. if (typeof distance === 'number') {
  469. return distance < 0 ? 'negative-distance' :
  470. distance > 0 ? 'positive-distance' : '';
  471. }
  472. // 如果是字符串,提取数字部分
  473. if (typeof distance === 'string') {
  474. // 使用正则提取数字部分(包括负号、小数点和科学计数法)
  475. const numericPart = distance.match(/[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/)?.[0];
  476. // 如果提取到数字部分,转换为数值
  477. if (numericPart) {
  478. const num = parseFloat(numericPart);
  479. return num < 0 ? 'negative-distance' :
  480. num > 0 ? 'positive-distance' : '';
  481. }
  482. }
  483. return '';
  484. };
  485. const toggleLeftContent = () => {
  486. isLeftContentCollapsed.value = !isLeftContentCollapsed.value;
  487. };
  488. const handleMouseOver = () => {
  489. hoverText.value = isLeftContentCollapsed.value ? '展开' : '收起';
  490. };
  491. const handleMouseOut = () => {
  492. hoverText.value = '';
  493. };
  494. /** 添加/修改操作 */
  495. const formRef = ref()
  496. const openForm = (type: string, id?: number) => {
  497. // 修改
  498. if (typeof id === 'number') {
  499. push({ name: 'IotMainWorkOrderOptimize', params: {id } })
  500. return
  501. } else {
  502. push({ name: 'IotMainWorkOrderAdd', params:{} })
  503. }
  504. }
  505. /** 删除按钮操作 */
  506. const handleDelete = async (id: number) => {
  507. try {
  508. // 删除的二次确认
  509. await message.delConfirm()
  510. // 发起删除
  511. await IotMainWorkOrderApi.deleteIotMainWorkOrder(id)
  512. message.success(t('common.delSuccess'))
  513. // 刷新列表
  514. await getList()
  515. } catch {}
  516. }
  517. const detail = (id?: number) => {
  518. push({ name: 'IotMainWorkOrderDetail', params:{id} })
  519. }
  520. /** 导出按钮操作 */
  521. const handleExport = async () => {
  522. try {
  523. // 导出的二次确认
  524. await message.exportConfirm()
  525. // 发起导出
  526. exportLoading.value = true
  527. const data = await IotMainWorkOrderApi.exportIotMainWorkOrder(queryParams)
  528. download.excel(data, '保养工单.xls')
  529. } catch {
  530. } finally {
  531. exportLoading.value = false
  532. }
  533. }
  534. // 声明 ResizeObserver 实例
  535. let resizeObserver: ResizeObserver | null = null;
  536. /** 初始化 **/
  537. onMounted(() => {
  538. getList()
  539. window.addEventListener('resize', calculateColumnWidths);
  540. // 创建 ResizeObserver 监听表格容器尺寸变化
  541. if (tableContainerRef.value?.$el) {
  542. resizeObserver = new ResizeObserver(() => {
  543. // 使用防抖避免频繁触发
  544. clearTimeout(window.resizeTimer);
  545. window.resizeTimer = setTimeout(() => {
  546. calculateColumnWidths();
  547. }, 100);
  548. });
  549. resizeObserver.observe(tableContainerRef.value.$el);
  550. }
  551. })
  552. onUnmounted(() => {
  553. window.removeEventListener('resize', calculateColumnWidths);
  554. // 清除 ResizeObserver
  555. if (resizeObserver && tableContainerRef.value?.$el) {
  556. resizeObserver.unobserve(tableContainerRef.value.$el);
  557. resizeObserver = null;
  558. }
  559. // 清除定时器
  560. if (window.resizeTimer) {
  561. clearTimeout(window.resizeTimer);
  562. }
  563. })
  564. // 监听列表数据变化重新计算列宽
  565. watch(list, () => {
  566. nextTick(calculateColumnWidths)
  567. }, { deep: true })
  568. // 监听左侧菜单状态变化(展开/收起)
  569. watch(isLeftContentCollapsed, () => {
  570. // 添加延迟以确保 DOM 更新完成
  571. setTimeout(calculateColumnWidths, 50);
  572. })
  573. </script>
  574. <style scoped>
  575. .leftcontent {
  576. transition: width 0.3s ease;
  577. position: relative;
  578. }
  579. .leftcontent.collapsed {
  580. width: 0;
  581. overflow: hidden;
  582. }
  583. /* 正数样式 - 淡绿色 */
  584. .positive-distance {
  585. color: #67c23a; /* element-plus 成功色 */
  586. background-color: rgba(103, 194, 58, 0.1); /* 10% 透明度的淡绿色背景 */
  587. padding: 2px 8px;
  588. border-radius: 4px;
  589. display: inline-block;
  590. }
  591. /* 负数样式 - 淡红色 */
  592. .negative-distance {
  593. color: #f56c6c; /* element-plus 危险色 */
  594. background-color: rgba(245, 108, 108, 0.1); /* 10% 透明度的淡红色背景 */
  595. padding: 2px 8px;
  596. border-radius: 4px;
  597. display: inline-block;
  598. }
  599. .table-wrap {
  600. overflow-x: auto; /* 允许水平滚动 */
  601. }
  602. /* 确保所有内容不换行 */
  603. :deep(.el-table) {
  604. min-width: 100% !important;
  605. width: auto !important;
  606. }
  607. /* 表头和单元格内容不换行 */
  608. :deep(.el-table__header .el-table__cell .cell),
  609. :deep(.el-table__body .el-table__cell .cell) {
  610. white-space: nowrap !important;
  611. overflow: visible !important;
  612. text-overflow: clip !important;
  613. }
  614. /* 防止表头内容换行 */
  615. :deep(.el-table__header-wrapper) .el-table__cell > .cell {
  616. white-space: nowrap;
  617. }
  618. /* 确保表格行不换行 */
  619. :deep(.el-table__row) {
  620. white-space: nowrap;
  621. }
  622. /* 表头特别处理 */
  623. :deep(.el-table__header) {
  624. .cell {
  625. display: inline-block;
  626. white-space: nowrap;
  627. width: auto !important;
  628. }
  629. }
  630. /* 表格使用100%宽度 */
  631. :deep(.el-table__inner-wrapper) {
  632. width: 100% !important;
  633. }
  634. .toggle-button {
  635. position: absolute;
  636. top: 44%;
  637. transform: translate(-65%,-50%);
  638. width: 12px;
  639. height: 40px;
  640. background-color: #f0f0f0;
  641. cursor: pointer;
  642. display: flex;
  643. align-items: center;
  644. justify-content: center;
  645. z-index: 1;
  646. clip-path: polygon(0 0, 100% 18%, 100% 85%, 0 100%);
  647. border-radius: 8px;
  648. }
  649. /* 添加鼠标悬停样式 */
  650. .toggle-button:hover {
  651. background-color: #afafaf;
  652. }
  653. .triangle {
  654. width: 0;
  655. height: 0;
  656. border-top: 4px solid transparent;
  657. border-bottom: 4px solid transparent;
  658. transition: transform 0.4s ease;
  659. border-right: 5px solid gray; /* 修改为右边框显示颜色 */
  660. border-left: none; /* 移除左边框 */
  661. }
  662. .triangle.rotated {
  663. transform: rotate(180deg);
  664. }
  665. /* 延时原因 必填星号样式 */
  666. :deep(.required-item .el-form-item__label:before) {
  667. content: '*';
  668. color: #ff4d4f;
  669. margin-right: 2px;
  670. }
  671. /* 调整标签区域样式 */
  672. :deep(.required-item .el-form-item__label) {
  673. padding-right: 0 !important;
  674. }
  675. </style>