Zimo 1 deň pred
rodič
commit
9683b1971f
9 zmenil súbory, kde vykonal 1030 pridanie a 44 odobranie
  1. 27 9
      api/maintenance.js
  2. 20 2
      locale/en.json
  3. 20 2
      locale/ja.json
  4. 20 2
      locale/ru.json
  5. 22 3
      locale/zh-Hans.json
  6. 20 2
      locale/zh-Hant.json
  7. 19 12
      pages.json
  8. 29 12
      pages/home/index.vue
  9. 853 0
      pages/maintenance/search.vue

+ 27 - 9
api/maintenance.js

@@ -11,14 +11,32 @@ export function getMaintenanceCount(params) {
 }
 
 // 获取保养工单列表
-export function getMaintenanceList(params) {
-  return request({
-    url: '/pms/iot-main-work-order/sortedMainWorkOrderPage',
-    // url: '/pms/iot-main-work-order/page',
-    method: 'get',
-    params
-  })
-}
+export function getMaintenanceList(params) {
+  return request({
+    url: '/pms/iot-main-work-order/sortedMainWorkOrderPage',
+    // url: '/pms/iot-main-work-order/page',
+    method: 'get',
+    params
+  })
+}
+
+// 以设备为维度查询最近保养距离
+export function getDeviceMainDistances(params) {
+  return request({
+    url: '/pms/iot-main-work-order/deviceMainDistances',
+    method: 'get',
+    params
+  })
+}
+
+// 根据保养计划查询保养项
+export function getMainPlanBOMs(params) {
+  return request({
+    url: '/pms/iot-maintenance-bom/getMainPlanBOMs',
+    method: 'get',
+    params
+  })
+}
 // 根据设备id获取设备保养项
 export function getDeviceAssociateBomList(params) {
   return request({
@@ -68,4 +86,4 @@ export function getBomMaterialsByWorkOrderId(params) {
     method: 'get',
     params
   })
-}
+}

+ 20 - 2
locale/en.json

@@ -100,8 +100,10 @@
 	"home.ruiDuReport": "RuiDu report",
 	"home.ruiDuReportTip": "View daily report data",
 	"home.inventoryQuery": "Inventory query",
-	"home.clickToQueryInventoryData": "Click to query inventory data",
-	"home.equipmentLedger": "Equipment ledger",
+	"home.clickToQueryInventoryData": "Click to query inventory data",
+	"home.maintenanceSearch": "Maintenance query",
+	"home.maintenanceSearchTip": "View equipment maintenance distance",
+	"home.equipmentLedger": "Equipment ledger",
 	"home.viewEquipmentLedger": "View equipment ledger",
 	"home.equipmentStatusChange": "Equipment status change",
 	"home.deviceUser": "Equipment responsible person",
@@ -147,6 +149,22 @@
 	"ruiDuReport.dept": "Department",
 	"ruiDuReport.constructionBrief": "Brief",
 	"ruiDuReport.createTime": "Create time",
+	"maintenanceSearch.title": "Maintenance query",
+	"maintenanceSearch.deviceCode": "Equipment code",
+	"maintenanceSearch.deviceCodePlaceholder": "Please enter equipment code",
+	"maintenanceSearch.deviceName": "Equipment name",
+	"maintenanceSearch.deviceNamePlaceholder": "Please enter equipment name",
+	"maintenanceSearch.deviceStatus": "Equipment status",
+	"maintenanceSearch.totalRunTime": "Total runtime H",
+	"maintenanceSearch.totalMileage": "Total mileage KM",
+	"maintenanceSearch.multiAttrs": "Multi-attribute totals",
+	"maintenanceSearch.noMaintenancePlan": "No maintenance plan",
+	"maintenanceSearch.generatedNotExecuted": "Generated, not executed",
+	"maintenanceSearch.notGenerated": "Not generated",
+	"maintenanceSearch.detailTitle": "Maintenance item details",
+	"maintenanceSearch.nextMaintTime": "Time to next maintenance",
+	"maintenanceSearch.nextMaintKil": "Mileage to next maintenance",
+	"maintenanceSearch.nextMaintDate": "Next maintenance date",
 	"workOrder.addDevice": "Add equipment",
 	"workOrder.addMaterial": "Add material",
 	"workOrder.selectMaterial": "Select material",

+ 20 - 2
locale/ja.json

@@ -100,8 +100,10 @@
 	"home.ruiDuReport": "瑞都レポート",
 	"home.ruiDuReportTip": "日報データを表示",
 	"home.inventoryQuery": "在庫照会",
-	"home.clickToQueryInventoryData": "クリックして在庫データを照会",
-	"home.equipmentLedger": "機器台帳",
+	"home.clickToQueryInventoryData": "クリックして在庫データを照会",
+	"home.maintenanceSearch": "保養照会",
+	"home.maintenanceSearchTip": "機器の保養距離を表示",
+	"home.equipmentLedger": "機器台帳",
 	"home.viewEquipmentLedger": "機器台帳を表示",
 	"home.equipmentStatusChange": "機器状態変更",
 	"home.deviceUser": "機器担当者",
@@ -147,6 +149,22 @@
 	"ruiDuReport.dept": "部門",
 	"ruiDuReport.constructionBrief": "施工概要",
 	"ruiDuReport.createTime": "作成時間",
+	"maintenanceSearch.title": "保養照会",
+	"maintenanceSearch.deviceCode": "機器コード",
+	"maintenanceSearch.deviceCodePlaceholder": "機器コードを入力してください",
+	"maintenanceSearch.deviceName": "機器名",
+	"maintenanceSearch.deviceNamePlaceholder": "機器名を入力してください",
+	"maintenanceSearch.deviceStatus": "機器状態",
+	"maintenanceSearch.totalRunTime": "累計運行時間H",
+	"maintenanceSearch.totalMileage": "累計運行距離KM",
+	"maintenanceSearch.multiAttrs": "複数属性の累計値",
+	"maintenanceSearch.noMaintenancePlan": "保養計画なし",
+	"maintenanceSearch.generatedNotExecuted": "生成済み未実行",
+	"maintenanceSearch.notGenerated": "未生成",
+	"maintenanceSearch.detailTitle": "保養項目詳細",
+	"maintenanceSearch.nextMaintTime": "次回保養までの時間",
+	"maintenanceSearch.nextMaintKil": "次回保養までの距離",
+	"maintenanceSearch.nextMaintDate": "次回保養日",
 	"workOrder.addDevice": "機器を追加",
 	"workOrder.addMaterial": "資材を追加",
 	"workOrder.selectMaterial": "資材を選択",

+ 20 - 2
locale/ru.json

@@ -100,8 +100,10 @@
 	"home.ruiDuReport": "Отчет RuiDu",
 	"home.ruiDuReportTip": "Просмотр данных ежедневного отчета",
 	"home.inventoryQuery": "Запрос инвентаря",
-	"home.clickToQueryInventoryData": "Нажмите, чтобы запросить данные инвентаря",
-	"home.equipmentLedger": "Книга учета оборудования",
+	"home.clickToQueryInventoryData": "Нажмите, чтобы запросить данные инвентаря",
+	"home.maintenanceSearch": "Запрос обслуживания",
+	"home.maintenanceSearchTip": "Просмотр расстояния до обслуживания",
+	"home.equipmentLedger": "Книга учета оборудования",
 	"home.viewEquipmentLedger": "Просмотр книги учета оборудования",
 	"home.equipmentStatusChange": "Изменение статуса оборудования",
 	"home.deviceUser": "Ответственный за оборудование",
@@ -147,6 +149,22 @@
 	"ruiDuReport.dept": "Отдел",
 	"ruiDuReport.constructionBrief": "Сводка",
 	"ruiDuReport.createTime": "Время создания",
+	"maintenanceSearch.title": "Запрос обслуживания",
+	"maintenanceSearch.deviceCode": "Код оборудования",
+	"maintenanceSearch.deviceCodePlaceholder": "Введите код оборудования",
+	"maintenanceSearch.deviceName": "Название оборудования",
+	"maintenanceSearch.deviceNamePlaceholder": "Введите название оборудования",
+	"maintenanceSearch.deviceStatus": "Статус оборудования",
+	"maintenanceSearch.totalRunTime": "Общее время работы H",
+	"maintenanceSearch.totalMileage": "Общий пробег KM",
+	"maintenanceSearch.multiAttrs": "Итоги по нескольким атрибутам",
+	"maintenanceSearch.noMaintenancePlan": "Нет плана обслуживания",
+	"maintenanceSearch.generatedNotExecuted": "Создано, не выполнено",
+	"maintenanceSearch.notGenerated": "Не создано",
+	"maintenanceSearch.detailTitle": "Детали обслуживания",
+	"maintenanceSearch.nextMaintTime": "Время до обслуживания",
+	"maintenanceSearch.nextMaintKil": "Пробег до обслуживания",
+	"maintenanceSearch.nextMaintDate": "Дата следующего обслуживания",
 	"workOrder.addDevice": "Добавить оборудование",
 	"workOrder.addMaterial": "Добавить материал",
 	"workOrder.selectMaterial": "Выбрать материал",

+ 22 - 3
locale/zh-Hans.json

@@ -139,9 +139,11 @@
   "home.dailyReportRuiYingX": "瑞鹰修井日报",
   "home.dailyReportRuiYingXTip": "填写日报",
   "home.dailyReportRuiYingXApproval": "审批日报",
-  "home.inventoryQuery": "库存查询",
-  "home.clickToQueryInventoryData": "点击查询库存数据",
-  "home.equipmentLedger": "设备台账",
+  "home.inventoryQuery": "库存查询",
+  "home.clickToQueryInventoryData": "点击查询库存数据",
+  "home.maintenanceSearch": "保养查询",
+  "home.maintenanceSearchTip": "查看设备保养距离",
+  "home.equipmentLedger": "设备台账",
   "home.viewEquipmentLedger": "查看设备台账",
   "home.equipmentStatusChange": "设备状态变更",
   "home.deviceUser": "设备责任人",
@@ -195,6 +197,23 @@
   "ruiDuReport.dept": "部门",
   "ruiDuReport.constructionBrief": "施工简报",
   "ruiDuReport.createTime": "创建时间",
+  // --------------------------------------- 保养查询 ----------------------------------------
+  "maintenanceSearch.title": "保养查询",
+  "maintenanceSearch.deviceCode": "设备编码",
+  "maintenanceSearch.deviceCodePlaceholder": "请输入设备编码",
+  "maintenanceSearch.deviceName": "设备名称",
+  "maintenanceSearch.deviceNamePlaceholder": "请输入设备名称",
+  "maintenanceSearch.deviceStatus": "设备状态",
+  "maintenanceSearch.totalRunTime": "累计运行时长H",
+  "maintenanceSearch.totalMileage": "累计运行里程KM",
+  "maintenanceSearch.multiAttrs": "多属性累计值",
+  "maintenanceSearch.noMaintenancePlan": "无保养计划",
+  "maintenanceSearch.generatedNotExecuted": "已生成未执行",
+  "maintenanceSearch.notGenerated": "未生成",
+  "maintenanceSearch.detailTitle": "保养项详情",
+  "maintenanceSearch.nextMaintTime": "距下次保养时长",
+  "maintenanceSearch.nextMaintKil": "距下次保养里程",
+  "maintenanceSearch.nextMaintDate": "下次保养日期",
   // --------------------------------------- 状态相关 ----------------------------------------
   "status.enable": "启用",
   "status.disable": "停用",

+ 20 - 2
locale/zh-Hant.json

@@ -86,8 +86,10 @@
   "home.ruiDuReport": "瑞都报表",
   "home.ruiDuReportTip": "查看日报报表",
   "home.inventoryQuery": "库存查询",
-  "home.clickToQueryInventoryData": "点击查询库存数据",
-  "home.equipmentLedger": "设备台账",
+  "home.clickToQueryInventoryData": "点击查询库存数据",
+  "home.maintenanceSearch": "保养查询",
+  "home.maintenanceSearchTip": "查看设备保养距离",
+  "home.equipmentLedger": "设备台账",
   "home.viewEquipmentLedger": "查看设备台账",
   "home.equipmentStatusChange": "设备状态变更",
   "home.adjustEquipmentStatus": "调整设备状态",
@@ -131,6 +133,22 @@
   "ruiDuReport.dept": "部门",
   "ruiDuReport.constructionBrief": "施工简报",
   "ruiDuReport.createTime": "创建时间",
+  "maintenanceSearch.title": "保养查询",
+  "maintenanceSearch.deviceCode": "设备编码",
+  "maintenanceSearch.deviceCodePlaceholder": "请输入设备编码",
+  "maintenanceSearch.deviceName": "设备名称",
+  "maintenanceSearch.deviceNamePlaceholder": "请输入设备名称",
+  "maintenanceSearch.deviceStatus": "设备状态",
+  "maintenanceSearch.totalRunTime": "累计运行时长H",
+  "maintenanceSearch.totalMileage": "累计运行里程KM",
+  "maintenanceSearch.multiAttrs": "多属性累计值",
+  "maintenanceSearch.noMaintenancePlan": "无保养计划",
+  "maintenanceSearch.generatedNotExecuted": "已生成未执行",
+  "maintenanceSearch.notGenerated": "未生成",
+  "maintenanceSearch.detailTitle": "保养项详情",
+  "maintenanceSearch.nextMaintTime": "距下次保养时长",
+  "maintenanceSearch.nextMaintKil": "距下次保养里程",
+  "maintenanceSearch.nextMaintDate": "下次保养日期",
   // ----------------------------------------------------
   "workOrder.addDevice": "新增设备",
   "workOrder.addMaterial": "新增物料",

+ 19 - 12
pages.json

@@ -86,18 +86,25 @@
         "navigationBarTitleText": "%maintenanceWorkOrder.editMaintenanceWorkOrder%"
       }
     },
-    {
-      // 保养工单详情
-      "path": "pages/maintenance/detail",
-      "style": {
-        "navigationBarTitleText": "%maintenanceWorkOrder.viewMaintenanceWorkOrder%"
-      }
-    },
-    {
-      // 设备维修
-      "path": "pages/repair/index",
-      "style": {
-        "navigationStyle": "custom"
+    {
+      // 保养工单详情
+      "path": "pages/maintenance/detail",
+      "style": {
+        "navigationBarTitleText": "%maintenanceWorkOrder.viewMaintenanceWorkOrder%"
+      }
+    },
+    {
+      // 保养查询
+      "path": "pages/maintenance/search",
+      "style": {
+        "navigationBarTitleText": "%maintenanceSearch.title%"
+      }
+    },
+    {
+      // 设备维修
+      "path": "pages/repair/index",
+      "style": {
+        "navigationStyle": "custom"
       }
     },
     {

+ 29 - 12
pages/home/index.vue

@@ -302,11 +302,11 @@
             <uni-icons type="right" :color="'#CACCCF'" size="15" />
           </view>
         </view>
-        <!-- 库存查询 -->
-        <view
-          class="card-cell flex-row align-center justify-between"
-          @click="navigatorTo('/pages/inventory/index')">
-          <image src="/static/home/kucun.svg" mode="aspectFill"></image>
+        <!-- 库存查询 -->
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="navigatorTo('/pages/inventory/index')">
+          <image src="/static/home/kucun.svg" mode="aspectFill"></image>
           <view class="cell-con flex-row align-center justify-between">
             <view class="cell-text flex-row align-center justify-start">
               <view class="title">
@@ -316,13 +316,30 @@
                 {{ $t("home.clickToQueryInventoryData") }}
               </view>
             </view>
-            <uni-icons type="right" :color="'#CACCCF'" size="15" />
-          </view>
-        </view>
-        <!-- 设备台账 -->
-        <view
-          class="card-cell flex-row align-center justify-between"
-          @click="navigatorTo('/pages/ledger/index')">
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
+        <!-- 保养查询 -->
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="navigatorTo('/pages/maintenance/search')">
+          <image src="/static/home/taizhang.svg" mode="aspectFill"></image>
+          <view class="cell-con flex-row align-center justify-between">
+            <view class="cell-text flex-row align-center justify-start">
+              <view class="title">
+                {{ $t("home.maintenanceSearch") }}
+              </view>
+              <view class="subtitle">
+                {{ $t("home.maintenanceSearchTip") }}
+              </view>
+            </view>
+            <uni-icons type="right" :color="'#CACCCF'" size="15" />
+          </view>
+        </view>
+        <!-- 设备台账 -->
+        <view
+          class="card-cell flex-row align-center justify-between"
+          @click="navigatorTo('/pages/ledger/index')">
           <image src="/static/home/taizhang.svg" mode="aspectFill"></image>
           <view class="cell-con flex-row align-center justify-between">
             <view class="cell-text flex-row align-center justify-start">

+ 853 - 0
pages/maintenance/search.vue

@@ -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>