|
|
@@ -0,0 +1,853 @@
|
|
|
+<template>
|
|
|
+ <view class="page report-page maintenance-search-page">
|
|
|
+ <z-paging
|
|
|
+ ref="paging"
|
|
|
+ v-model="dataList"
|
|
|
+ class="report-paging"
|
|
|
+ :default-page-size="10"
|
|
|
+ @query="queryList">
|
|
|
+ <view class="report-list">
|
|
|
+ <view class="report-card" v-for="item in dataList" :key="item.id">
|
|
|
+ <view class="card-header">
|
|
|
+ <view class="device-title">
|
|
|
+ {{ item.deviceName || "--" }}
|
|
|
+ </view>
|
|
|
+ <view
|
|
|
+ class="distance-tag"
|
|
|
+ :class="getDistanceClass(item.mainDistance)">
|
|
|
+ {{ formatDistance(item.mainDistance) }}
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="card-body">
|
|
|
+ <view class="field-row">
|
|
|
+ <text class="field-label">{{ $t("device.deviceCode") }}</text>
|
|
|
+ <text class="field-value">{{ item.deviceCode || "--" }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="field-row">
|
|
|
+ <text class="field-label">{{ $t("ruiDuReport.dept") }}</text>
|
|
|
+ <text class="field-value">{{ item.deptName || "--" }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="field-row">
|
|
|
+ <text class="field-label">{{
|
|
|
+ $t("workOrder.responsiblePerson")
|
|
|
+ }}</text>
|
|
|
+ <text class="field-value">{{ item.responsibleNames || "--" }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="field-row">
|
|
|
+ <text class="field-label">{{
|
|
|
+ $t("maintenanceSearch.deviceStatus")
|
|
|
+ }}</text>
|
|
|
+ <text class="field-value">{{
|
|
|
+ getDeviceStatusName(item.deviceStatus)
|
|
|
+ }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="item.totalRunTime" class="field-row">
|
|
|
+ <text class="field-label">{{
|
|
|
+ $t("maintenanceSearch.totalRunTime")
|
|
|
+ }}</text>
|
|
|
+ <text class="field-value">{{ item.totalRunTime }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="item.totalMileage" class="field-row">
|
|
|
+ <text class="field-label">{{
|
|
|
+ $t("maintenanceSearch.totalMileage")
|
|
|
+ }}</text>
|
|
|
+ <text class="field-value">{{ item.totalMileage }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="formatMultiAttrs(item)" class="field-row brief-row">
|
|
|
+ <text class="field-label">{{
|
|
|
+ $t("maintenanceSearch.multiAttrs")
|
|
|
+ }}</text>
|
|
|
+ <UniTooltip :content="formatMultiAttrs(item)" placement="top">
|
|
|
+ <text class="field-value brief-text">{{
|
|
|
+ formatMultiAttrs(item)
|
|
|
+ }}</text>
|
|
|
+ </UniTooltip>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="card-footer">
|
|
|
+ <view class="status-text">{{ getWorkOrderStatus(item) }}</view>
|
|
|
+ <button
|
|
|
+ v-if="hasDetailSource(item)"
|
|
|
+ class="detail-btn"
|
|
|
+ type="primary"
|
|
|
+ plain
|
|
|
+ @click="openDetail(item)">
|
|
|
+ {{ $t("operation.view") }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </z-paging>
|
|
|
+
|
|
|
+ <UniFab
|
|
|
+ :pattern="fabPattern"
|
|
|
+ horizontal="right"
|
|
|
+ vertical="bottom"
|
|
|
+ direction="horizontal"
|
|
|
+ :popMenu="false"
|
|
|
+ @fabClick="openFilterPopup" />
|
|
|
+
|
|
|
+ <uni-popup
|
|
|
+ ref="filterPopup"
|
|
|
+ type="bottom"
|
|
|
+ background-color="#fff"
|
|
|
+ border-radius="10px 10px 0 0">
|
|
|
+ <view class="filter-popup">
|
|
|
+ <view class="filter-header">
|
|
|
+ <text class="filter-action" @click="closeFilterPopup">
|
|
|
+ {{ $t("operation.cancel") }}
|
|
|
+ </text>
|
|
|
+ <text class="filter-title">{{ $t("ruiDuReport.filterTitle") }}</text>
|
|
|
+ <text class="filter-action primary" @click="applyFilter">
|
|
|
+ {{ $t("operation.confirm") }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="filter-body">
|
|
|
+ <view class="filter-item">
|
|
|
+ <view class="filter-label">{{
|
|
|
+ $t("maintenanceSearch.deviceCode")
|
|
|
+ }}</view>
|
|
|
+ <uni-easyinput
|
|
|
+ v-model="filterForm.deviceCode"
|
|
|
+ :inputBorder="false"
|
|
|
+ :styles="inputStyles"
|
|
|
+ :placeholder="$t('maintenanceSearch.deviceCodePlaceholder')" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="filter-item">
|
|
|
+ <view class="filter-label">{{
|
|
|
+ $t("maintenanceSearch.deviceName")
|
|
|
+ }}</view>
|
|
|
+ <uni-easyinput
|
|
|
+ v-model="filterForm.deviceName"
|
|
|
+ :inputBorder="false"
|
|
|
+ :styles="inputStyles"
|
|
|
+ :placeholder="$t('maintenanceSearch.deviceNamePlaceholder')" />
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="filter-item dept-item">
|
|
|
+ <view class="filter-label">{{ $t("ruiDuReport.dept") }}</view>
|
|
|
+ <view class="dept-selected">
|
|
|
+ {{ selectedDeptName || $t("operation.PleaseSelect") }}
|
|
|
+ </view>
|
|
|
+ <view class="tree">
|
|
|
+ <DaTree
|
|
|
+ :data="treeData"
|
|
|
+ labelField="name"
|
|
|
+ valueField="id"
|
|
|
+ disabledField="disabled"
|
|
|
+ defaultExpandAll
|
|
|
+ checkedDisabled
|
|
|
+ :defaultCheckedKeys="filterForm.deptId"
|
|
|
+ @change="handleTreeChange" />
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="filter-footer">
|
|
|
+ <button class="filter-button reset" @click="resetFilter">
|
|
|
+ {{ $t("inventory.search.reset") }}
|
|
|
+ </button>
|
|
|
+ <button class="filter-button" type="primary" @click="applyFilter">
|
|
|
+ {{ $t("operation.search") }}
|
|
|
+ </button>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </uni-popup>
|
|
|
+
|
|
|
+ <uni-popup
|
|
|
+ ref="detailPopup"
|
|
|
+ type="bottom"
|
|
|
+ background-color="#fff"
|
|
|
+ border-radius="10px 10px 0 0">
|
|
|
+ <view class="detail-popup">
|
|
|
+ <view class="filter-header">
|
|
|
+ <text class="filter-action" @click="closeDetailPopup">
|
|
|
+ {{ $t("operation.cancel") }}
|
|
|
+ </text>
|
|
|
+ <text class="filter-title">{{
|
|
|
+ $t("maintenanceSearch.detailTitle")
|
|
|
+ }}</text>
|
|
|
+ <text class="filter-action primary" @click="closeDetailPopup">
|
|
|
+ {{ $t("operation.confirm") }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view class="detail-device">
|
|
|
+ <view class="detail-device-name">{{ detailDevice.deviceName }}</view>
|
|
|
+ <view class="detail-device-code">{{ detailDevice.deviceCode }}</view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <scroll-view scroll-y class="detail-list">
|
|
|
+ <view
|
|
|
+ class="detail-card"
|
|
|
+ v-for="detail in detailList"
|
|
|
+ :key="detail.id">
|
|
|
+ <view class="detail-name">{{ detail.name || "--" }}</view>
|
|
|
+ <view class="detail-grid">
|
|
|
+ <view class="detail-item">
|
|
|
+ <text class="detail-label">{{
|
|
|
+ $t("maintenanceSearch.totalRunTime")
|
|
|
+ }}</text>
|
|
|
+ <text>{{ detail.totalRunTime ?? detail.tempTotalRunTime ?? "--" }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="detail-item">
|
|
|
+ <text class="detail-label">{{
|
|
|
+ $t("maintenanceSearch.totalMileage")
|
|
|
+ }}</text>
|
|
|
+ <text>{{ detail.totalMileage ?? detail.tempTotalMileage ?? "--" }}</text>
|
|
|
+ </view>
|
|
|
+ <view v-if="detail.runningTimeRule === 0" class="detail-item">
|
|
|
+ <text class="detail-label">{{
|
|
|
+ $t("maintenanceSearch.nextMaintTime")
|
|
|
+ }}</text>
|
|
|
+ <text :class="{ danger: isNegative(calculateTimePeriod(detail)) }">
|
|
|
+ {{ calculateTimePeriod(detail) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view v-if="detail.mileageRule === 0" class="detail-item">
|
|
|
+ <text class="detail-label">{{
|
|
|
+ $t("maintenanceSearch.nextMaintKil")
|
|
|
+ }}</text>
|
|
|
+ <text :class="{ danger: isNegative(calculateKiloPeriod(detail)) }">
|
|
|
+ {{ calculateKiloPeriod(detail) }}
|
|
|
+ </text>
|
|
|
+ </view>
|
|
|
+ <view v-if="detail.naturalDateRule === 0" class="detail-item">
|
|
|
+ <text class="detail-label">{{
|
|
|
+ $t("maintenanceSearch.nextMaintDate")
|
|
|
+ }}</text>
|
|
|
+ <text>{{ calculateNextNaturalDate(detail) }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+
|
|
|
+ <view v-if="!detailLoading && detailList.length === 0" class="empty">
|
|
|
+ {{ $t("common.noData") }}
|
|
|
+ </view>
|
|
|
+ </scroll-view>
|
|
|
+ </view>
|
|
|
+ </uni-popup>
|
|
|
+ </view>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { computed, onMounted, reactive, ref } from "vue";
|
|
|
+import { useI18n } from "vue-i18n";
|
|
|
+import dayjs from "dayjs";
|
|
|
+import DaTree from "@/components/da-tree/index.vue";
|
|
|
+import UniFab from "@/uni_modules/uni-fab/components/uni-fab/uni-fab.vue";
|
|
|
+import UniTooltip from "@/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue";
|
|
|
+import {
|
|
|
+ getDeviceMainDistances,
|
|
|
+ getMainPlanBOMs,
|
|
|
+ getWorkOrderBOMs,
|
|
|
+} from "@/api/maintenance";
|
|
|
+import { specifiedSimpleDepts } from "@/api";
|
|
|
+import { getDeptId } from "@/utils/auth";
|
|
|
+import { useDataDictStore } from "@/store/modules/dataDict";
|
|
|
+
|
|
|
+const paging = ref(null);
|
|
|
+const { t } = useI18n({ useScope: "global" });
|
|
|
+const filterPopup = ref(null);
|
|
|
+const detailPopup = ref(null);
|
|
|
+const dataList = ref([]);
|
|
|
+const deptOptions = ref([]);
|
|
|
+const treeData = ref([]);
|
|
|
+const detailList = ref([]);
|
|
|
+const detailLoading = ref(false);
|
|
|
+const detailDevice = reactive({
|
|
|
+ deviceCode: "",
|
|
|
+ deviceName: "",
|
|
|
+});
|
|
|
+const deviceStatusDict = reactive({});
|
|
|
+const dictStore = useDataDictStore();
|
|
|
+
|
|
|
+const inputStyles = reactive({
|
|
|
+ backgroundColor: "#f7f8fa",
|
|
|
+ color: "#333",
|
|
|
+});
|
|
|
+
|
|
|
+const fabPattern = reactive({
|
|
|
+ color: "#fff",
|
|
|
+ backgroundColor: "#fff",
|
|
|
+ selectedColor: "#fff",
|
|
|
+ buttonColor: "#004098",
|
|
|
+ iconColor: "#fff",
|
|
|
+ icon: "search",
|
|
|
+});
|
|
|
+
|
|
|
+const filterForm = reactive({
|
|
|
+ deptId: "",
|
|
|
+ deviceCode: "",
|
|
|
+ deviceName: "",
|
|
|
+});
|
|
|
+
|
|
|
+const selectedDeptName = computed(() => {
|
|
|
+ const current = deptOptions.value.find(
|
|
|
+ (item) => String(item.value) === String(filterForm.deptId)
|
|
|
+ );
|
|
|
+ return current?.text || "";
|
|
|
+});
|
|
|
+
|
|
|
+const handleTree = (
|
|
|
+ data,
|
|
|
+ id = "id",
|
|
|
+ parentId = "parentId",
|
|
|
+ children = "children"
|
|
|
+) => {
|
|
|
+ if (!Array.isArray(data)) return [];
|
|
|
+
|
|
|
+ const childrenListMap = {};
|
|
|
+ const nodeIds = {};
|
|
|
+ const tree = [];
|
|
|
+
|
|
|
+ for (const item of data) {
|
|
|
+ const itemParentId = item[parentId];
|
|
|
+ if (childrenListMap[itemParentId] == null) {
|
|
|
+ childrenListMap[itemParentId] = [];
|
|
|
+ }
|
|
|
+ nodeIds[item[id]] = item;
|
|
|
+ childrenListMap[itemParentId].push(item);
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const item of data) {
|
|
|
+ if (nodeIds[item[parentId]] == null) {
|
|
|
+ tree.push(item);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const adaptToChildrenList = (node) => {
|
|
|
+ if (childrenListMap[node[id]] != null) {
|
|
|
+ node[children] = childrenListMap[node[id]];
|
|
|
+ }
|
|
|
+ if (node[children]) {
|
|
|
+ node[children].forEach(adaptToChildrenList);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ tree.forEach(adaptToChildrenList);
|
|
|
+ return tree;
|
|
|
+};
|
|
|
+
|
|
|
+const sortDeptTree = (nodes) => {
|
|
|
+ if (!Array.isArray(nodes)) return [];
|
|
|
+
|
|
|
+ return [...nodes]
|
|
|
+ .sort((a, b) => (a.sort ?? 999999) - (b.sort ?? 999999))
|
|
|
+ .map((node) => ({
|
|
|
+ ...node,
|
|
|
+ children: sortDeptTree(node.children),
|
|
|
+ }));
|
|
|
+};
|
|
|
+
|
|
|
+const loadDeptOptions = async () => {
|
|
|
+ try {
|
|
|
+ const response = await specifiedSimpleDepts(getDeptId());
|
|
|
+ const list = response?.data || [];
|
|
|
+ deptOptions.value = list.map((item) => ({
|
|
|
+ text: item.name,
|
|
|
+ value: item.id,
|
|
|
+ }));
|
|
|
+ treeData.value = sortDeptTree(handleTree(list));
|
|
|
+ } catch (error) {
|
|
|
+ treeData.value = [];
|
|
|
+ deptOptions.value = [];
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const loadDeviceStatusDict = async () => {
|
|
|
+ if (dictStore.dataDict.length <= 0) {
|
|
|
+ await dictStore.loadDataDictList();
|
|
|
+ }
|
|
|
+ dictStore.getDataDictList("pms_device_status").forEach((item) => {
|
|
|
+ deviceStatusDict[item.value] = item.label;
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const queryList = (pageNo, pageSize) => {
|
|
|
+ getDeviceMainDistances({
|
|
|
+ pageNo,
|
|
|
+ pageSize,
|
|
|
+ deptId: filterForm.deptId || undefined,
|
|
|
+ deviceCode: filterForm.deviceCode || undefined,
|
|
|
+ deviceName: filterForm.deviceName || undefined,
|
|
|
+ setFlag: "",
|
|
|
+ })
|
|
|
+ .then((res) => {
|
|
|
+ paging.value?.complete(res.data?.list || []);
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ paging.value?.complete(false);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const parseDistanceNumber = (distance) => {
|
|
|
+ if (distance === null || distance === undefined || distance === "") {
|
|
|
+ return undefined;
|
|
|
+ }
|
|
|
+ if (typeof distance === "number") return distance;
|
|
|
+ const numericPart = String(distance).match(
|
|
|
+ /[-+]?\d*\.?\d+(?:[eE][-+]?\d+)?/
|
|
|
+ )?.[0];
|
|
|
+ return numericPart ? Number(numericPart) : undefined;
|
|
|
+};
|
|
|
+
|
|
|
+const hasMaintenancePlan = (mainDistance) => {
|
|
|
+ return mainDistance !== null && mainDistance !== undefined && mainDistance !== "";
|
|
|
+};
|
|
|
+
|
|
|
+const hasDetailSource = (item) => {
|
|
|
+ return hasMaintenancePlan(item.mainDistance) && (item.workOrderId || item.planId);
|
|
|
+};
|
|
|
+
|
|
|
+const formatDistance = (mainDistance) => {
|
|
|
+ return hasMaintenancePlan(mainDistance)
|
|
|
+ ? mainDistance
|
|
|
+ : $tSafe("maintenanceSearch.noMaintenancePlan");
|
|
|
+};
|
|
|
+
|
|
|
+const getDistanceClass = (distance) => {
|
|
|
+ const value = parseDistanceNumber(distance);
|
|
|
+ if (value === undefined || value === 0) return "";
|
|
|
+ return value < 0 ? "negative" : "positive";
|
|
|
+};
|
|
|
+
|
|
|
+const formatMultiAttrs = (item) => {
|
|
|
+ const runtime = Object.entries(item.multiAttrsTotalRuntime || {});
|
|
|
+ const mileage = Object.entries(item.multiAttrsTotalMileage || {});
|
|
|
+ return runtime
|
|
|
+ .concat(mileage)
|
|
|
+ .filter(([key, value]) => key && value)
|
|
|
+ .map(([key, value]) => `${key}: ${value}`)
|
|
|
+ .join(" ");
|
|
|
+};
|
|
|
+
|
|
|
+const getDeviceStatusName = (code) => {
|
|
|
+ return deviceStatusDict[code] || code || "--";
|
|
|
+};
|
|
|
+
|
|
|
+const getWorkOrderStatus = (item) => {
|
|
|
+ if (item.shouldWorkOrder && item.runningWorkOrder) {
|
|
|
+ return $tSafe("maintenanceSearch.generatedNotExecuted");
|
|
|
+ }
|
|
|
+ if (item.shouldWorkOrder && !item.runningWorkOrder) {
|
|
|
+ return $tSafe("maintenanceSearch.notGenerated");
|
|
|
+ }
|
|
|
+ return "-";
|
|
|
+};
|
|
|
+
|
|
|
+const openFilterPopup = () => {
|
|
|
+ filterPopup.value?.open();
|
|
|
+};
|
|
|
+
|
|
|
+const closeFilterPopup = () => {
|
|
|
+ filterPopup.value?.close();
|
|
|
+};
|
|
|
+
|
|
|
+const handleTreeChange = (value) => {
|
|
|
+ filterForm.deptId = value;
|
|
|
+};
|
|
|
+
|
|
|
+const applyFilter = () => {
|
|
|
+ closeFilterPopup();
|
|
|
+ paging.value?.reload();
|
|
|
+};
|
|
|
+
|
|
|
+const resetFilter = () => {
|
|
|
+ filterForm.deptId = "";
|
|
|
+ filterForm.deviceCode = "";
|
|
|
+ filterForm.deviceName = "";
|
|
|
+};
|
|
|
+
|
|
|
+const openDetail = async (row) => {
|
|
|
+ detailDevice.deviceCode = row.deviceCode || "";
|
|
|
+ detailDevice.deviceName = row.deviceName || "";
|
|
|
+ detailList.value = [];
|
|
|
+ detailPopup.value?.open();
|
|
|
+ detailLoading.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 100,
|
|
|
+ deviceId: row.id,
|
|
|
+ };
|
|
|
+ const response = row.workOrderId
|
|
|
+ ? await getWorkOrderBOMs({ ...params, workOrderId: row.workOrderId })
|
|
|
+ : await getMainPlanBOMs({ ...params, planId: row.planId });
|
|
|
+ detailList.value = Array.isArray(response?.data) ? response.data : [];
|
|
|
+ } catch (error) {
|
|
|
+ detailList.value = [];
|
|
|
+ } finally {
|
|
|
+ detailLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const closeDetailPopup = () => {
|
|
|
+ detailPopup.value?.close();
|
|
|
+};
|
|
|
+
|
|
|
+const calculateTimePeriod = (item) => {
|
|
|
+ if (item.runningTimeRule === 0) {
|
|
|
+ const totalRunVal = item.totalRunTime ?? item.tempTotalRunTime;
|
|
|
+ const next = Number(item.nextRunningTime) || 0;
|
|
|
+ const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
|
|
|
+ const lastRun = Number(item.lastRunningTime) || 0;
|
|
|
+ return Number((next - (totalRun - lastRun)).toFixed(2));
|
|
|
+ }
|
|
|
+ return typeof item.timePeriod === "number"
|
|
|
+ ? Number(item.timePeriod.toFixed(2))
|
|
|
+ : item.timePeriod || "--";
|
|
|
+};
|
|
|
+
|
|
|
+const calculateKiloPeriod = (item) => {
|
|
|
+ if (item.mileageRule === 0) {
|
|
|
+ const totalRunVal = item.totalMileage ?? item.tempTotalMileage;
|
|
|
+ const next = Number(item.nextRunningKilometers) || 0;
|
|
|
+ const totalRun = totalRunVal != null ? Number(totalRunVal) : 0;
|
|
|
+ const lastRun = Number(item.lastRunningKilometers) || 0;
|
|
|
+ return Number((next - (totalRun - lastRun)).toFixed(2));
|
|
|
+ }
|
|
|
+ return typeof item.kilometerCycle === "number"
|
|
|
+ ? Number(item.kilometerCycle.toFixed(2))
|
|
|
+ : item.kilometerCycle || "--";
|
|
|
+};
|
|
|
+
|
|
|
+const calculateNextNaturalDate = (item) => {
|
|
|
+ if (item.naturalDateRule !== 0 || !item.lastNaturalDate || !item.nextNaturalDate) {
|
|
|
+ return "--";
|
|
|
+ }
|
|
|
+ return dayjs(item.lastNaturalDate)
|
|
|
+ .add(item.nextNaturalDate, "day")
|
|
|
+ .format("YYYY-MM-DD");
|
|
|
+};
|
|
|
+
|
|
|
+const isNegative = (value) => {
|
|
|
+ if (value === null || value === undefined || value === "") return false;
|
|
|
+ const num = Number(value);
|
|
|
+ return !Number.isNaN(num) && num < 0;
|
|
|
+};
|
|
|
+
|
|
|
+const $tSafe = (key) => {
|
|
|
+ return t(key);
|
|
|
+};
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ loadDeptOptions();
|
|
|
+ loadDeviceStatusDict();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.report-page {
|
|
|
+ padding: 10px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.report-paging {
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.report-list {
|
|
|
+ padding: 8px 6px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.report-card {
|
|
|
+ position: relative;
|
|
|
+ margin-bottom: 14px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #ffffff;
|
|
|
+ border: 1px solid rgba(0, 64, 152, 0.08);
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 6px 18px rgba(35, 54, 79, 0.08);
|
|
|
+}
|
|
|
+
|
|
|
+.card-header {
|
|
|
+ min-height: 52px;
|
|
|
+ padding: 14px 16px 10px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ box-sizing: border-box;
|
|
|
+ border-bottom: 1px solid #edf1f7;
|
|
|
+}
|
|
|
+
|
|
|
+.device-title {
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ color: #000000;
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 18px;
|
|
|
+ line-height: 24px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.distance-tag {
|
|
|
+ max-width: 128px;
|
|
|
+ height: 24px;
|
|
|
+ line-height: 24px;
|
|
|
+ padding: 0 9px;
|
|
|
+ margin-left: 10px;
|
|
|
+ border-radius: 12px;
|
|
|
+ background: #f1f3f8;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+ font-weight: 600;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ box-sizing: border-box;
|
|
|
+
|
|
|
+ &.positive {
|
|
|
+ color: #2bbb80;
|
|
|
+ background: rgba(43, 187, 128, 0.12);
|
|
|
+ }
|
|
|
+
|
|
|
+ &.negative {
|
|
|
+ color: #f56c6c;
|
|
|
+ background: rgba(245, 108, 108, 0.12);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.card-body {
|
|
|
+ padding: 12px 16px 4px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.field-row {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ min-height: 34px;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-label {
|
|
|
+ width: 92px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ color: #000000;
|
|
|
+ font-weight: 600;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+.field-value {
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ color: #233044;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 22px;
|
|
|
+}
|
|
|
+
|
|
|
+.brief-row {
|
|
|
+ padding-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.brief-row :deep(.uni-tooltip) {
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ display: block;
|
|
|
+}
|
|
|
+
|
|
|
+.brief-text {
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.card-footer {
|
|
|
+ padding: 2px 16px 14px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+}
|
|
|
+
|
|
|
+.status-text {
|
|
|
+ min-width: 0;
|
|
|
+ flex: 1;
|
|
|
+ color: #666;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-btn {
|
|
|
+ min-width: 72px;
|
|
|
+ height: 30px;
|
|
|
+ line-height: 28px;
|
|
|
+ margin: 0;
|
|
|
+ padding: 0 14px;
|
|
|
+ border-radius: 15px;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-popup,
|
|
|
+.detail-popup {
|
|
|
+ height: 86vh;
|
|
|
+ max-height: 86vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #fff;
|
|
|
+ padding-bottom: env(safe-area-inset-bottom);
|
|
|
+}
|
|
|
+
|
|
|
+.filter-header {
|
|
|
+ height: 48px;
|
|
|
+ padding: 0 16px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-action {
|
|
|
+ min-width: 48px;
|
|
|
+ color: #666;
|
|
|
+ font-size: 14px;
|
|
|
+
|
|
|
+ &.primary {
|
|
|
+ color: #004098;
|
|
|
+ text-align: right;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.filter-body {
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding: 12px 16px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-item {
|
|
|
+ margin-bottom: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-label {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.dept-selected {
|
|
|
+ min-height: 36px;
|
|
|
+ line-height: 36px;
|
|
|
+ padding: 0 10px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ background: #f7f8fa;
|
|
|
+ color: #666;
|
|
|
+ border-radius: 4px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.tree {
|
|
|
+ height: 420px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #f0f0f0;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-footer {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 10px 16px 14px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.filter-button {
|
|
|
+ flex: 1;
|
|
|
+ height: 38px;
|
|
|
+ line-height: 38px;
|
|
|
+ font-size: 14px;
|
|
|
+ margin: 0;
|
|
|
+
|
|
|
+ &.reset {
|
|
|
+ color: #004098;
|
|
|
+ background: #fff;
|
|
|
+ border: 1px solid #004098;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.detail-device {
|
|
|
+ padding: 12px 16px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-device-name {
|
|
|
+ color: #000;
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 16px;
|
|
|
+ line-height: 24px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-device-code {
|
|
|
+ color: #666;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-list {
|
|
|
+ flex: 1;
|
|
|
+ height: 0;
|
|
|
+ padding: 12px 16px;
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-card {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1px solid #edf1f7;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-name {
|
|
|
+ margin-bottom: 10px;
|
|
|
+ color: #000;
|
|
|
+ font-weight: 700;
|
|
|
+ font-size: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
+ gap: 8px 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-item {
|
|
|
+ min-width: 0;
|
|
|
+ color: #233044;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.detail-label {
|
|
|
+ display: block;
|
|
|
+ color: #666;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.danger {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.empty {
|
|
|
+ padding: 32px 0;
|
|
|
+ text-align: center;
|
|
|
+ color: #999;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+</style>
|