search.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  1. <template>
  2. <view class="page report-page maintenance-search-page">
  3. <z-paging
  4. ref="paging"
  5. v-model="dataList"
  6. class="report-paging"
  7. :default-page-size="10"
  8. @query="queryList">
  9. <view class="report-list">
  10. <view class="report-card" v-for="item in dataList" :key="item.id">
  11. <view class="card-header">
  12. <view class="device-title">
  13. {{ item.deviceName || "--" }}
  14. </view>
  15. <view
  16. class="distance-tag"
  17. :class="getDistanceClass(item.mainDistance)">
  18. {{ formatDistance(item.mainDistance) }}
  19. </view>
  20. </view>
  21. <view class="card-body">
  22. <view class="field-row">
  23. <text class="field-label">{{ $t("device.deviceCode") }}</text>
  24. <text class="field-value">{{ item.deviceCode || "--" }}</text>
  25. </view>
  26. <view class="field-row">
  27. <text class="field-label">{{ $t("ruiDuReport.dept") }}</text>
  28. <text class="field-value">{{ item.deptName || "--" }}</text>
  29. </view>
  30. <view class="field-row">
  31. <text class="field-label">{{
  32. $t("workOrder.responsiblePerson")
  33. }}</text>
  34. <text class="field-value">{{ item.responsibleNames || "--" }}</text>
  35. </view>
  36. <view class="field-row">
  37. <text class="field-label">{{
  38. $t("maintenanceSearch.deviceStatus")
  39. }}</text>
  40. <text class="field-value">{{
  41. getDeviceStatusName(item.deviceStatus)
  42. }}</text>
  43. </view>
  44. <view v-if="item.totalRunTime" class="field-row">
  45. <text class="field-label">{{
  46. $t("maintenanceSearch.totalRunTime")
  47. }}</text>
  48. <text class="field-value">{{ item.totalRunTime }}</text>
  49. </view>
  50. <view v-if="item.totalMileage" class="field-row">
  51. <text class="field-label">{{
  52. $t("maintenanceSearch.totalMileage")
  53. }}</text>
  54. <text class="field-value">{{ item.totalMileage }}</text>
  55. </view>
  56. <view v-if="formatMultiAttrs(item)" class="field-row brief-row">
  57. <text class="field-label">{{
  58. $t("maintenanceSearch.multiAttrs")
  59. }}</text>
  60. <UniTooltip :content="formatMultiAttrs(item)" placement="top">
  61. <text class="field-value brief-text">{{
  62. formatMultiAttrs(item)
  63. }}</text>
  64. </UniTooltip>
  65. </view>
  66. </view>
  67. <view class="card-footer">
  68. <view class="status-text">{{ getWorkOrderStatus(item) }}</view>
  69. <button
  70. v-if="hasDetailSource(item)"
  71. class="detail-btn"
  72. type="primary"
  73. plain
  74. @click="openDetail(item)">
  75. {{ $t("operation.view") }}
  76. </button>
  77. </view>
  78. </view>
  79. </view>
  80. </z-paging>
  81. <UniFab
  82. :pattern="fabPattern"
  83. horizontal="right"
  84. vertical="bottom"
  85. direction="horizontal"
  86. :popMenu="false"
  87. @fabClick="openFilterPopup" />
  88. <uni-popup
  89. ref="filterPopup"
  90. type="bottom"
  91. background-color="#fff"
  92. border-radius="10px 10px 0 0">
  93. <view class="filter-popup">
  94. <view class="filter-header">
  95. <text class="filter-action" @click="closeFilterPopup">
  96. {{ $t("operation.cancel") }}
  97. </text>
  98. <text class="filter-title">{{ $t("ruiDuReport.filterTitle") }}</text>
  99. <text class="filter-action primary" @click="applyFilter">
  100. {{ $t("operation.confirm") }}
  101. </text>
  102. </view>
  103. <view class="filter-body">
  104. <view class="filter-item">
  105. <view class="filter-label">{{
  106. $t("maintenanceSearch.deviceCode")
  107. }}</view>
  108. <uni-easyinput
  109. v-model="filterForm.deviceCode"
  110. :inputBorder="false"
  111. :styles="inputStyles"
  112. :placeholder="$t('maintenanceSearch.deviceCodePlaceholder')" />
  113. </view>
  114. <view class="filter-item">
  115. <view class="filter-label">{{
  116. $t("maintenanceSearch.deviceName")
  117. }}</view>
  118. <uni-easyinput
  119. v-model="filterForm.deviceName"
  120. :inputBorder="false"
  121. :styles="inputStyles"
  122. :placeholder="$t('maintenanceSearch.deviceNamePlaceholder')" />
  123. </view>
  124. <view class="filter-item dept-item">
  125. <view class="filter-label">{{ $t("ruiDuReport.dept") }}</view>
  126. <view class="dept-selected">
  127. {{ selectedDeptName || $t("operation.PleaseSelect") }}
  128. </view>
  129. <view class="tree">
  130. <DaTree
  131. :data="treeData"
  132. labelField="name"
  133. valueField="id"
  134. disabledField="disabled"
  135. defaultExpandAll
  136. checkedDisabled
  137. :defaultCheckedKeys="filterForm.deptId"
  138. @change="handleTreeChange" />
  139. </view>
  140. </view>
  141. </view>
  142. <view class="filter-footer">
  143. <button class="filter-button reset" @click="resetFilter">
  144. {{ $t("inventory.search.reset") }}
  145. </button>
  146. <button class="filter-button" type="primary" @click="applyFilter">
  147. {{ $t("operation.search") }}
  148. </button>
  149. </view>
  150. </view>
  151. </uni-popup>
  152. <uni-popup
  153. ref="detailPopup"
  154. type="bottom"
  155. background-color="#fff"
  156. border-radius="10px 10px 0 0">
  157. <view class="detail-popup">
  158. <view class="filter-header">
  159. <text class="filter-action" @click="closeDetailPopup">
  160. {{ $t("operation.cancel") }}
  161. </text>
  162. <text class="filter-title">{{
  163. $t("maintenanceSearch.detailTitle")
  164. }}</text>
  165. <text class="filter-action primary" @click="closeDetailPopup">
  166. {{ $t("operation.confirm") }}
  167. </text>
  168. </view>
  169. <view class="detail-device">
  170. <view class="detail-device-name">{{ detailDevice.deviceName }}</view>
  171. <view class="detail-device-code">{{ detailDevice.deviceCode }}</view>
  172. </view>
  173. <scroll-view scroll-y class="detail-list">
  174. <view
  175. class="detail-card"
  176. v-for="detail in detailList"
  177. :key="detail.id">
  178. <view class="detail-name">{{ detail.name || "--" }}</view>
  179. <view class="detail-grid">
  180. <view
  181. v-if="detail.runningTimeRule === 0"
  182. class="detail-item">
  183. <text class="detail-label">{{
  184. $t("maintenanceSearch.totalRunTime")
  185. }}</text>
  186. <text>{{ detail.totalRunTime ?? detail.tempTotalRunTime ?? "--" }}</text>
  187. </view>
  188. <view
  189. v-if="detail.mileageRule === 0"
  190. class="detail-item">
  191. <text class="detail-label">{{
  192. $t("maintenanceSearch.totalMileage")
  193. }}</text>
  194. <text>{{ detail.totalMileage ?? detail.tempTotalMileage ?? "--" }}</text>
  195. </view>
  196. </view>
  197. <view
  198. v-if="showTimeColumns && detail.runningTimeRule === 0"
  199. class="detail-section">
  200. <view class="detail-section-title">
  201. {{ $t("maintenanceSearch.maintenanceDuration") }}
  202. </view>
  203. <view class="detail-section-grid">
  204. <view class="detail-item">
  205. <text class="detail-label">{{
  206. $t("maintenanceSearch.lastMaintenanceOperationTime")
  207. }}</text>
  208. <text>{{ detail.lastRunningTime ?? "--" }}</text>
  209. </view>
  210. <view class="detail-item">
  211. <text class="detail-label">{{
  212. $t("maintenanceSearch.runTimeCycle")
  213. }}</text>
  214. <text>{{ detail.nextRunningTime ?? "--" }}</text>
  215. </view>
  216. <view class="detail-item">
  217. <text class="detail-label">{{
  218. $t("maintenanceSearch.nextMaintTime")
  219. }}</text>
  220. <text :class="{ danger: isNegative(calculateTimePeriod(detail)) }">
  221. {{ calculateTimePeriod(detail) }}
  222. </text>
  223. </view>
  224. </view>
  225. </view>
  226. <view
  227. v-if="showMileageColumns && detail.mileageRule === 0"
  228. class="detail-section">
  229. <view class="detail-section-title">
  230. {{ $t("maintenanceSearch.maintenanceMileage") }}
  231. </view>
  232. <view class="detail-section-grid">
  233. <view class="detail-item">
  234. <text class="detail-label">{{
  235. $t("maintenanceSearch.lastMaintenanceMileage")
  236. }}</text>
  237. <text>{{ detail.lastRunningKilometers ?? "--" }}</text>
  238. </view>
  239. <view class="detail-item">
  240. <text class="detail-label">{{
  241. $t("maintenanceSearch.mileageCycle")
  242. }}</text>
  243. <text>{{ detail.nextRunningKilometers ?? "--" }}</text>
  244. </view>
  245. <view class="detail-item">
  246. <text class="detail-label">{{
  247. $t("maintenanceSearch.nextMaintKil")
  248. }}</text>
  249. <text :class="{ danger: isNegative(calculateKiloPeriod(detail)) }">
  250. {{ calculateKiloPeriod(detail) }}
  251. </text>
  252. </view>
  253. </view>
  254. </view>
  255. <view
  256. v-if="showNaturalDateColumns && detail.naturalDateRule === 0"
  257. class="detail-section">
  258. <view class="detail-section-title">
  259. {{ $t("maintenanceSearch.maintenanceDate") }}
  260. </view>
  261. <view class="detail-section-grid">
  262. <view class="detail-item">
  263. <text class="detail-label">{{
  264. $t("maintenanceSearch.lastMaintenanceNaturalDate")
  265. }}</text>
  266. <text>{{ formatDateValue(detail.lastNaturalDate) }}</text>
  267. </view>
  268. <view class="detail-item">
  269. <text class="detail-label">{{
  270. $t("maintenanceSearch.naturalDateCycle")
  271. }}</text>
  272. <text>{{ detail.nextNaturalDate ?? "--" }}</text>
  273. </view>
  274. <view class="detail-item">
  275. <text class="detail-label">{{
  276. $t("maintenanceSearch.nextMaintDate")
  277. }}</text>
  278. <text>{{ calculateNextNaturalDate(detail) }}</text>
  279. </view>
  280. </view>
  281. </view>
  282. </view>
  283. <view v-if="!detailLoading && detailList.length === 0" class="empty">
  284. {{ $t("common.noData") }}
  285. </view>
  286. </scroll-view>
  287. </view>
  288. </uni-popup>
  289. </view>
  290. </template>
  291. <script setup>
  292. import { computed, onMounted, reactive, ref } from "vue";
  293. import { useI18n } from "vue-i18n";
  294. import dayjs from "dayjs";
  295. import DaTree from "@/components/da-tree/index.vue";
  296. import UniFab from "@/uni_modules/uni-fab/components/uni-fab/uni-fab.vue";
  297. import UniTooltip from "@/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue";
  298. import {
  299. getDeviceMainDistances,
  300. getMainPlanBOMs,
  301. getWorkOrderBOMs,
  302. } from "@/api/maintenance";
  303. import { specifiedSimpleDepts } from "@/api";
  304. import { getDeptId } from "@/utils/auth";
  305. import { useDataDictStore } from "@/store/modules/dataDict";
  306. const paging = ref(null);
  307. const { t } = useI18n({ useScope: "global" });
  308. const filterPopup = ref(null);
  309. const detailPopup = ref(null);
  310. const dataList = ref([]);
  311. const deptOptions = ref([]);
  312. const treeData = ref([]);
  313. const detailList = ref([]);
  314. const detailLoading = ref(false);
  315. const detailDevice = reactive({
  316. deviceCode: "",
  317. deviceName: "",
  318. });
  319. const deviceStatusDict = reactive({});
  320. const dictStore = useDataDictStore();
  321. const inputStyles = reactive({
  322. backgroundColor: "#f7f8fa",
  323. color: "#333",
  324. });
  325. const fabPattern = reactive({
  326. color: "#fff",
  327. backgroundColor: "#fff",
  328. selectedColor: "#fff",
  329. buttonColor: "#004098",
  330. iconColor: "#fff",
  331. icon: "search",
  332. });
  333. const filterForm = reactive({
  334. deptId: "",
  335. deviceCode: "",
  336. deviceName: "",
  337. });
  338. const selectedDeptName = computed(() => {
  339. const current = deptOptions.value.find(
  340. (item) => String(item.value) === String(filterForm.deptId)
  341. );
  342. return current?.text || "";
  343. });
  344. const showTimeColumns = computed(() => {
  345. return detailList.value.some((item) => item.runningTimeRule === 0);
  346. });
  347. const showMileageColumns = computed(() => {
  348. return detailList.value.some((item) => item.mileageRule === 0);
  349. });
  350. const showNaturalDateColumns = computed(() => {
  351. return detailList.value.some((item) => item.naturalDateRule === 0);
  352. });
  353. const handleTree = (
  354. data,
  355. id = "id",
  356. parentId = "parentId",
  357. children = "children"
  358. ) => {
  359. if (!Array.isArray(data)) return [];
  360. const childrenListMap = {};
  361. const nodeIds = {};
  362. const tree = [];
  363. for (const item of data) {
  364. const itemParentId = item[parentId];
  365. if (childrenListMap[itemParentId] == null) {
  366. childrenListMap[itemParentId] = [];
  367. }
  368. nodeIds[item[id]] = item;
  369. childrenListMap[itemParentId].push(item);
  370. }
  371. for (const item of data) {
  372. if (nodeIds[item[parentId]] == null) {
  373. tree.push(item);
  374. }
  375. }
  376. const adaptToChildrenList = (node) => {
  377. if (childrenListMap[node[id]] != null) {
  378. node[children] = childrenListMap[node[id]];
  379. }
  380. if (node[children]) {
  381. node[children].forEach(adaptToChildrenList);
  382. }
  383. };
  384. tree.forEach(adaptToChildrenList);
  385. return tree;
  386. };
  387. const sortDeptTree = (nodes) => {
  388. if (!Array.isArray(nodes)) return [];
  389. return [...nodes]
  390. .sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
  391. .map((node) => ({
  392. ...node,
  393. children: sortDeptTree(node.children),
  394. }));
  395. };
  396. const loadDeptOptions = async () => {
  397. try {
  398. const response = await specifiedSimpleDepts(getDeptId());
  399. const list = response?.data || [];
  400. deptOptions.value = list.map((item) => ({
  401. text: item.name,
  402. value: item.id,
  403. }));
  404. treeData.value = sortDeptTree(handleTree(list));
  405. } catch (error) {
  406. treeData.value = [];
  407. deptOptions.value = [];
  408. }
  409. };
  410. const loadDeviceStatusDict = async () => {
  411. if (dictStore.dataDict.length <= 0) {
  412. await dictStore.loadDataDictList();
  413. }
  414. dictStore.getDataDictList("pms_device_status").forEach((item) => {
  415. deviceStatusDict[item.value] = item.label;
  416. });
  417. };
  418. const queryList = (pageNo, pageSize) => {
  419. getDeviceMainDistances({
  420. pageNo,
  421. pageSize,
  422. deptId: filterForm.deptId || undefined,
  423. deviceCode: filterForm.deviceCode || undefined,
  424. deviceName: filterForm.deviceName || undefined,
  425. setFlag: "",
  426. })
  427. .then((res) => {
  428. paging.value?.complete(res.data?.list || []);
  429. })
  430. .catch(() => {
  431. paging.value?.complete(false);
  432. });
  433. };
  434. const parseDistanceNumber = (distance) => {
  435. if (distance === null || distance === undefined || distance === "") {
  436. return undefined;
  437. }
  438. if (typeof distance === "number") return distance;
  439. const numericPart = String(distance).match(
  440. /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/
  441. )?.[0];
  442. return numericPart ? Number(numericPart) : undefined;
  443. };
  444. const hasMaintenancePlan = (mainDistance) => {
  445. return mainDistance !== null && mainDistance !== undefined && mainDistance !== "";
  446. };
  447. const hasDetailSource = (item) => {
  448. return hasMaintenancePlan(item.mainDistance) && (item.workOrderId || item.planId);
  449. };
  450. const formatDistance = (mainDistance) => {
  451. return hasMaintenancePlan(mainDistance)
  452. ? mainDistance
  453. : $tSafe("maintenanceSearch.noMaintenancePlan");
  454. };
  455. const getDistanceClass = (distance) => {
  456. const value = parseDistanceNumber(distance);
  457. if (value === undefined || value === 0) return "";
  458. return value < 0 ? "negative" : "positive";
  459. };
  460. const formatMultiAttrs = (item) => {
  461. const runtime = Object.entries(item.multiAttrsTotalRuntime || {});
  462. const mileage = Object.entries(item.multiAttrsTotalMileage || {});
  463. return runtime
  464. .concat(mileage)
  465. .filter(([key, value]) => key && value)
  466. .map(([key, value]) => `${key}: ${value}`)
  467. .join(" ");
  468. };
  469. const getDeviceStatusName = (code) => {
  470. return deviceStatusDict[code] || code || "--";
  471. };
  472. const getWorkOrderStatus = (item) => {
  473. if (item.shouldWorkOrder && item.runningWorkOrder) {
  474. return $tSafe("maintenanceSearch.generatedNotExecuted");
  475. }
  476. if (item.shouldWorkOrder && !item.runningWorkOrder) {
  477. return $tSafe("maintenanceSearch.notGenerated");
  478. }
  479. return "-";
  480. };
  481. const openFilterPopup = () => {
  482. filterPopup.value?.open();
  483. };
  484. const closeFilterPopup = () => {
  485. filterPopup.value?.close();
  486. };
  487. const handleTreeChange = (value) => {
  488. filterForm.deptId = value;
  489. };
  490. const applyFilter = () => {
  491. closeFilterPopup();
  492. paging.value?.reload();
  493. };
  494. const resetFilter = () => {
  495. filterForm.deptId = "";
  496. filterForm.deviceCode = "";
  497. filterForm.deviceName = "";
  498. };
  499. const openDetail = async (row) => {
  500. detailDevice.deviceCode = row.deviceCode || "";
  501. detailDevice.deviceName = row.deviceName || "";
  502. detailList.value = [];
  503. detailPopup.value?.open();
  504. detailLoading.value = true;
  505. try {
  506. const params = {
  507. pageNo: 1,
  508. pageSize: 100,
  509. deviceId: row.id,
  510. };
  511. const response = row.workOrderId
  512. ? await getWorkOrderBOMs({ ...params, workOrderId: row.workOrderId })
  513. : await getMainPlanBOMs({ ...params, planId: row.planId });
  514. detailList.value = Array.isArray(response?.data) ? response.data : [];
  515. } catch (error) {
  516. detailList.value = [];
  517. } finally {
  518. detailLoading.value = false;
  519. }
  520. };
  521. const closeDetailPopup = () => {
  522. detailPopup.value?.close();
  523. };
  524. const calculateTimePeriod = (item) => {
  525. if (item.runningTimeRule === 0) {
  526. const totalRunVal = item.totalRunTime ?? item.tempTotalRunTime;
  527. const next = Number(item.nextRunningTime) || 0;
  528. const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
  529. const lastRun = Number(item.lastRunningTime) || 0;
  530. return Number((next - (totalRun - lastRun)).toFixed(2));
  531. }
  532. return typeof item.timePeriod === "number"
  533. ? Number(item.timePeriod.toFixed(2))
  534. : item.timePeriod || "--";
  535. };
  536. const calculateKiloPeriod = (item) => {
  537. if (item.mileageRule === 0) {
  538. const totalRunVal = item.totalMileage ?? item.tempTotalMileage;
  539. const next = Number(item.nextRunningKilometers) || 0;
  540. const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
  541. const lastRun = Number(item.lastRunningKilometers) || 0;
  542. return Number((next - (totalRun - lastRun)).toFixed(2));
  543. }
  544. return typeof item.kilometerCycle === "number"
  545. ? Number(item.kilometerCycle.toFixed(2))
  546. : item.kilometerCycle || "--";
  547. };
  548. const calculateNextNaturalDate = (item) => {
  549. if (item.naturalDateRule !== 0 || !item.lastNaturalDate || !item.nextNaturalDate) {
  550. return "--";
  551. }
  552. return dayjs(item.lastNaturalDate)
  553. .add(item.nextNaturalDate, "day")
  554. .format("YYYY-MM-DD");
  555. };
  556. const formatDateValue = (value) => {
  557. return value ? dayjs(value).format("YYYY-MM-DD") : "--";
  558. };
  559. const isNegative = (value) => {
  560. if (value === null || value === undefined || value === "") return false;
  561. const num = Number(value);
  562. return !Number.isNaN(num) && num < 0;
  563. };
  564. const $tSafe = (key) => {
  565. return t(key);
  566. };
  567. onMounted(() => {
  568. loadDeptOptions();
  569. loadDeviceStatusDict();
  570. });
  571. </script>
  572. <style lang="scss" scoped>
  573. .report-page {
  574. padding: 10px !important;
  575. }
  576. .report-paging {
  577. height: 100%;
  578. }
  579. .report-list {
  580. padding: 8px 6px;
  581. box-sizing: border-box;
  582. }
  583. .report-card {
  584. position: relative;
  585. margin-bottom: 14px;
  586. overflow: hidden;
  587. background: #ffffff;
  588. border: 1px solid rgba(0, 64, 152, 0.08);
  589. border-radius: 8px;
  590. box-shadow: 0 6px 18px rgba(35, 54, 79, 0.08);
  591. }
  592. .card-header {
  593. min-height: 52px;
  594. padding: 14px 16px 10px;
  595. display: flex;
  596. align-items: center;
  597. justify-content: space-between;
  598. box-sizing: border-box;
  599. border-bottom: 1px solid #edf1f7;
  600. }
  601. .device-title {
  602. min-width: 0;
  603. flex: 1;
  604. color: #000000;
  605. font-weight: 700;
  606. font-size: 18px;
  607. line-height: 24px;
  608. overflow: hidden;
  609. text-overflow: ellipsis;
  610. white-space: nowrap;
  611. }
  612. .distance-tag {
  613. max-width: 128px;
  614. height: 24px;
  615. line-height: 24px;
  616. padding: 0 9px;
  617. margin-left: 10px;
  618. border-radius: 12px;
  619. background: #f1f3f8;
  620. color: #606266;
  621. font-size: 12px;
  622. font-weight: 600;
  623. overflow: hidden;
  624. text-overflow: ellipsis;
  625. white-space: nowrap;
  626. box-sizing: border-box;
  627. &.positive {
  628. color: #2bbb80;
  629. background: rgba(43, 187, 128, 0.12);
  630. }
  631. &.negative {
  632. color: #f56c6c;
  633. background: rgba(245, 108, 108, 0.12);
  634. }
  635. }
  636. .card-body {
  637. padding: 12px 16px 4px;
  638. box-sizing: border-box;
  639. }
  640. .field-row {
  641. display: flex;
  642. align-items: flex-start;
  643. min-height: 34px;
  644. font-size: 14px;
  645. }
  646. .field-label {
  647. width: 92px;
  648. flex-shrink: 0;
  649. color: #000000;
  650. font-weight: 600;
  651. font-size: 13px;
  652. line-height: 22px;
  653. }
  654. .field-value {
  655. min-width: 0;
  656. flex: 1;
  657. color: #233044;
  658. font-weight: 500;
  659. font-size: 14px;
  660. line-height: 22px;
  661. }
  662. .brief-row {
  663. padding-top: 4px;
  664. }
  665. .brief-row :deep(.uni-tooltip) {
  666. min-width: 0;
  667. flex: 1;
  668. display: block;
  669. }
  670. .brief-text {
  671. display: block;
  672. overflow: hidden;
  673. text-overflow: ellipsis;
  674. white-space: nowrap;
  675. }
  676. .card-footer {
  677. padding: 2px 16px 14px;
  678. display: flex;
  679. align-items: center;
  680. justify-content: space-between;
  681. }
  682. .status-text {
  683. min-width: 0;
  684. flex: 1;
  685. color: #666;
  686. font-size: 13px;
  687. }
  688. .detail-btn {
  689. min-width: 72px;
  690. height: 30px;
  691. line-height: 28px;
  692. margin: 0;
  693. padding: 0 14px;
  694. border-radius: 15px;
  695. font-size: 13px;
  696. }
  697. .filter-popup,
  698. .detail-popup {
  699. height: 86vh;
  700. max-height: 86vh;
  701. display: flex;
  702. flex-direction: column;
  703. background: #fff;
  704. padding-bottom: env(safe-area-inset-bottom);
  705. }
  706. .filter-header {
  707. height: 48px;
  708. padding: 0 16px;
  709. display: flex;
  710. align-items: center;
  711. justify-content: space-between;
  712. border-bottom: 1px solid #f0f0f0;
  713. box-sizing: border-box;
  714. }
  715. .filter-title {
  716. font-weight: 600;
  717. color: #333;
  718. font-size: 16px;
  719. }
  720. .filter-action {
  721. min-width: 48px;
  722. color: #666;
  723. font-size: 14px;
  724. &.primary {
  725. color: #004098;
  726. text-align: right;
  727. }
  728. }
  729. .filter-body {
  730. flex: 1;
  731. overflow-y: auto;
  732. padding: 12px 16px;
  733. box-sizing: border-box;
  734. }
  735. .filter-item {
  736. margin-bottom: 14px;
  737. }
  738. .filter-label {
  739. margin-bottom: 8px;
  740. color: #333;
  741. font-weight: 500;
  742. font-size: 14px;
  743. }
  744. .dept-selected {
  745. min-height: 36px;
  746. line-height: 36px;
  747. padding: 0 10px;
  748. margin-bottom: 8px;
  749. background: #f7f8fa;
  750. color: #666;
  751. border-radius: 4px;
  752. box-sizing: border-box;
  753. }
  754. .tree {
  755. height: 420px;
  756. overflow: hidden;
  757. border: 1px solid #f0f0f0;
  758. border-radius: 4px;
  759. }
  760. .filter-footer {
  761. display: flex;
  762. gap: 10px;
  763. padding: 10px 16px 14px;
  764. border-top: 1px solid #f0f0f0;
  765. box-sizing: border-box;
  766. }
  767. .filter-button {
  768. flex: 1;
  769. height: 38px;
  770. line-height: 38px;
  771. font-size: 14px;
  772. margin: 0;
  773. &.reset {
  774. color: #004098;
  775. background: #fff;
  776. border: 1px solid #004098;
  777. }
  778. }
  779. .detail-device {
  780. padding: 12px 16px;
  781. border-bottom: 1px solid #f0f0f0;
  782. }
  783. .detail-device-name {
  784. color: #000;
  785. font-weight: 700;
  786. font-size: 16px;
  787. line-height: 24px;
  788. }
  789. .detail-device-code {
  790. color: #666;
  791. font-size: 13px;
  792. line-height: 20px;
  793. }
  794. .detail-list {
  795. flex: 1;
  796. height: 0;
  797. padding: 12px 16px;
  798. box-sizing: border-box;
  799. }
  800. .detail-card {
  801. margin-bottom: 12px;
  802. padding: 12px;
  803. background: #f8fafc;
  804. border: 1px solid #edf1f7;
  805. border-radius: 8px;
  806. }
  807. .detail-name {
  808. margin-bottom: 10px;
  809. color: #000;
  810. font-weight: 700;
  811. font-size: 15px;
  812. }
  813. .detail-grid {
  814. display: grid;
  815. grid-template-columns: repeat(2, minmax(0, 1fr));
  816. gap: 8px 10px;
  817. }
  818. .detail-section {
  819. margin-top: 12px;
  820. padding-top: 10px;
  821. border-top: 1px solid #edf1f7;
  822. }
  823. .detail-section-title {
  824. margin-bottom: 8px;
  825. color: #004098;
  826. font-weight: 700;
  827. font-size: 14px;
  828. line-height: 20px;
  829. }
  830. .detail-section-grid {
  831. display: grid;
  832. grid-template-columns: repeat(3, minmax(0, 1fr));
  833. gap: 8px 10px;
  834. }
  835. .detail-item {
  836. min-width: 0;
  837. color: #233044;
  838. font-size: 13px;
  839. line-height: 20px;
  840. }
  841. .detail-label {
  842. display: block;
  843. color: #666;
  844. font-size: 12px;
  845. }
  846. @media screen and (max-width: 360px) {
  847. .detail-section-grid {
  848. grid-template-columns: repeat(2, minmax(0, 1fr));
  849. }
  850. }
  851. .danger {
  852. color: #f56c6c;
  853. }
  854. .empty {
  855. padding: 32px 0;
  856. text-align: center;
  857. color: #999;
  858. font-size: 14px;
  859. }
  860. </style>