From 9646ba4b026e4dbee0c0d5ac3d52b428bfef7b29 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期五, 28 八月 2026 10:49:44 +0800
Subject: [PATCH] 新疆-新聚 1.销售台账中的销售合同按框架合同处理 2.设备台账需按车间/区域汇总设备,车间/位置可自行新增,且支持子父级递归 3.计量器具台账支持通过 Excel 文件批量新增数据 4.设备保养定时任务新增“年度保养”任务频率

---
 src/views/equipmentManagement/upkeep/index.vue                            |   47 +
 src/api/salesManagement/salesLedger.js                                    |   29 +
 src/views/equipmentManagement/measurementEquipment/index.vue              |   65 ++
 src/views/procurementManagement/procurementReport/index.vue               |    2 
 src/views/equipmentManagement/inspectionManagement/components/formDia.vue |  179 ++++++-
 src/views/equipmentManagement/upkeep/Form/formDia.vue                     |   47 ++
 src/views/salesManagement/salesLedger/index.vue                           |  284 +++++++++++
 src/views/equipmentManagement/ledger/index.vue                            |  646 +++++++++++++++++++++------
 src/api/equipmentManagement/ledger.js                                     |    3 
 src/views/equipmentManagement/ledger/Form.vue                             |   26 +
 10 files changed, 1,105 insertions(+), 223 deletions(-)

diff --git a/src/api/equipmentManagement/ledger.js b/src/api/equipmentManagement/ledger.js
index d1b65b0..ccd2c95 100644
--- a/src/api/equipmentManagement/ledger.js
+++ b/src/api/equipmentManagement/ledger.js
@@ -36,9 +36,10 @@
   });
 };
 
-export const getDeviceLedger = () => {
+export const getDeviceLedger = (params) => {
   return request({
     url: "/device/ledger/getDeviceLedger",
     method: "get",
+    params,
   });
 };
diff --git a/src/api/salesManagement/salesLedger.js b/src/api/salesManagement/salesLedger.js
index 6548927..b9ef283 100644
--- a/src/api/salesManagement/salesLedger.js
+++ b/src/api/salesManagement/salesLedger.js
@@ -116,4 +116,31 @@
         method: "get",
         params: query,
     });
-}
\ No newline at end of file
+}
+
+// 鍒涘缓妗嗘灦璁㈠崟
+export function createFrameworkOrder(query) {
+  return request({
+    url: "/sales/ledger/createFrameworkOrder",
+    method: "post",
+    data: query,
+  });
+}
+
+// 鏌ヨ妗嗘灦璁㈠崟鍒楄〃
+export function listFrameworkOrders(query) {
+  return request({
+    url: "/sales/ledger/listFrameworkOrders",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ妗嗘灦鍚堝悓绱鍙戣揣鏁伴噺
+export function frameworkContractShippedQuantity(query) {
+  return request({
+    url: "/sales/ledger/frameworkContractShippedQuantity",
+    method: "get",
+    params: query,
+  });
+}
diff --git a/src/views/equipmentManagement/inspectionManagement/components/formDia.vue b/src/views/equipmentManagement/inspectionManagement/components/formDia.vue
index 74cbb58..e1a0b22 100644
--- a/src/views/equipmentManagement/inspectionManagement/components/formDia.vue
+++ b/src/views/equipmentManagement/inspectionManagement/components/formDia.vue
@@ -5,15 +5,11 @@
       <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
         <el-row>
           <el-col :span="12">
-            <el-form-item label="璁惧鍚嶇О" prop="taskId">
-              <el-select v-model="form.taskId" @change="setDeviceModel">
-                <el-option
-                  v-for="(item, index) in deviceOptions"
-                  :key="index"
-                  :label="item.deviceName"
-                  :value="item.id"
-                ></el-option>
-              </el-select>
+            <el-form-item label="璁惧鍚嶇О" prop="taskName">
+              <div style="display: flex; gap: 8px; width: 100%">
+                <el-input v-model="form.taskName" placeholder="璇烽�夋嫨璁惧" readonly style="flex: 1" />
+                <el-button @click="openDeviceDialog">閫夋嫨璁惧</el-button>
+              </div>
             </el-form-item>
           </el-col>
           <el-col :span="12">
@@ -111,6 +107,50 @@
         </div>
       </template>
     </el-dialog>
+
+    <!-- 閫夋嫨宸℃璁惧寮圭獥 -->
+    <el-dialog v-model="deviceDialogVisible" title="閫夋嫨宸℃璁惧" width="760px" draggable append-to-body>
+      <div class="device-select-wrap">
+        <div class="device-select-tree">
+          <div class="all-device-node"
+               :class="{ active: deviceAreaId === null }"
+               @click="selectAllDevice">
+            鍏ㄩ儴璁惧
+          </div>
+          <el-tree
+            ref="deviceAreaTreeRef"
+            v-loading="deviceAreaLoading"
+            :data="areaTree"
+            node-key="id"
+            :props="{ label: 'label', children: 'children' }"
+            default-expand-all
+            :expand-on-click-node="false"
+            highlight-current
+            :current-node-key="deviceAreaId"
+            @node-click="onDeviceAreaClick"
+          />
+        </div>
+        <div class="device-select-list">
+          <el-table
+            :data="deviceList"
+            v-loading="deviceListLoading"
+            border
+            height="420"
+            style="cursor: pointer"
+            @row-click="selectDevice"
+          >
+            <el-table-column prop="deviceName" label="璁惧鍚嶇О" min-width="140" />
+            <el-table-column prop="deviceModel" label="瑙勬牸鍨嬪彿" min-width="120" />
+            <el-table-column prop="areaName" label="鎵�灞炲尯鍩�" min-width="120" />
+            <el-table-column label="鎿嶄綔" width="80" align="center">
+              <template #default="scope">
+                <el-button link type="primary" @click.stop="selectDevice(scope.row)">閫夋嫨</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -120,6 +160,7 @@
 import {addOrEditTimingTask} from "@/api/inspectionManagement/index.js";
 import {userListNoPageByTenantId} from "@/api/system/user.js";
 import { getDeviceLedger } from "@/api/equipmentManagement/ledger";
+import { getDeviceAreaTree } from "@/api/equipmentManagement/area";
 
 const { proxy } = getCurrentInstance()
 const emit = defineEmits()
@@ -127,6 +168,16 @@
 const dialogVisitable = ref(false);
 const operationType = ref('add');
 const deviceOptions = ref([]);
+
+// 璁惧閫夋嫨寮圭獥鐩稿叧
+const deviceDialogVisible = ref(false);
+const areaTree = ref([]);
+const deviceAreaId = ref(null);
+const deviceAreaLoading = ref(false);
+const deviceAreaTreeRef = ref();
+const deviceList = ref([]);
+const deviceListLoading = ref(false);
+
 const data = reactive({
   form: {
     taskId: undefined,
@@ -142,7 +193,7 @@
     time: ''
   },
 	rules: {
-		taskId: [{ required: true, message: "璇烽�夋嫨璁惧", trigger: "change" },],
+		taskName: [{ required: true, message: "璇烽�夋嫨璁惧", trigger: "change" },],
 		inspector: [{ required: true, message: "璇疯緭鍏ュ贰妫�浜�", trigger: "blur" },],
 		dateStr: [{ required: true, message: "璇烽�夋嫨鐧昏鏃堕棿", trigger: "change" }],
 		frequencyType: [{ required: true, message: "璇烽�夋嫨浠诲姟棰戠巼", trigger: "change" }],
@@ -207,7 +258,7 @@
 
 const loadDeviceName = async () => {
   const { data } = await getDeviceLedger();
-  deviceOptions.value = data;
+  deviceOptions.value = data || [];
 };
 
 const setDeviceModel = (id) => {
@@ -217,29 +268,82 @@
   }
 }
 
+// 鍔犺浇鍖哄煙鏍�
+const loadDeviceAreaTree = () => {
+  deviceAreaLoading.value = true;
+  getDeviceAreaTree()
+    .then((res) => {
+      areaTree.value = res.data || [];
+    })
+    .finally(() => {
+      deviceAreaLoading.value = false;
+    });
+};
+
+// 鍔犺浇璁惧鍒楄〃
+const loadDevices = (areaId) => {
+  deviceListLoading.value = true;
+  getDeviceLedger(areaId ? { areaId } : undefined)
+    .then((res) => {
+      deviceList.value = res.data || [];
+    })
+    .finally(() => {
+      deviceListLoading.value = false;
+    });
+};
+
+// 鎵撳紑璁惧閫夋嫨寮圭獥
+const openDeviceDialog = () => {
+  deviceDialogVisible.value = true;
+  deviceAreaId.value = null;
+  loadDeviceAreaTree();
+  loadDevices();
+};
+
+// 閫夋嫨鍏ㄩ儴璁惧
+const selectAllDevice = () => {
+  deviceAreaId.value = null;
+  loadDevices();
+};
+
+// 鐐瑰嚮鍖哄煙鑺傜偣
+const onDeviceAreaClick = (data) => {
+  deviceAreaId.value = data.id;
+  loadDevices(data.id);
+};
+
+// 閫変腑璁惧鍥炲~
+const selectDevice = (row) => {
+  form.value.taskId = row.id;
+  form.value.taskName = row.deviceName;
+  deviceDialogVisible.value = false;
+};
+
 // 鎵撳紑寮规
 const openDialog = async (type, row) => {
   dialogVisitable.value = true
   operationType.value = type
-  
+
   // 閲嶇疆琛ㄥ崟
   resetForm();
-  
+
   // 鍔犺浇鐢ㄦ埛鍒楄〃
   userListNoPageByTenantId().then((res) => {
     userList.value = res.data;
   });
-  
-  // 鍔犺浇璁惧鍒楄〃
+
+  // 鍔犺浇璁惧鍒楄〃锛堢紪杈戝洖鏄剧敤锛�
   await loadDeviceName();
-  
+
   if (type === 'edit' && row) {
     form.value = {...row}
     form.value.inspector = form.value.inspectorIds.split(',').map(Number)
-    
+
     // 濡傛灉鏈夎澶嘔D锛岃嚜鍔ㄨ缃澶囦俊鎭�
     if (form.value.taskId) {
-      setDeviceModel(form.value.taskId);
+      if (!form.value.taskName) {
+        setDeviceModel(form.value.taskId);
+      }
     }
   }
 }
@@ -279,16 +383,16 @@
       try {
         form.value.inspectorIds = form.value.inspector.join(',')
         delete form.value.inspector
-        
+
         if (form.value.frequencyType === 'WEEKLY') {
           let frequencyDetail = ''
           frequencyDetail = form.value.week + ',' + form.value.time
           form.value.frequencyDetail = frequencyDetail
         }
-        
+
         let res = await userStore.getInfo()
         form.value.registrantId = res.user.userId
-        
+
         await addOrEditTimingTask(form.value)
         cancel()
         proxy.$modal.msgSuccess('鎻愪氦鎴愬姛')
@@ -302,5 +406,34 @@
 </script>
 
 <style scoped>
-
-</style>
\ No newline at end of file
+.device-select-wrap {
+  display: flex;
+}
+.device-select-tree {
+  width: 260px;
+  flex-shrink: 0;
+  padding-right: 12px;
+  border-right: 1px solid #ebeef5;
+  max-height: 460px;
+  overflow-y: auto;
+}
+.device-select-list {
+  flex: 1;
+  min-width: 0;
+  padding-left: 12px;
+}
+.all-device-node {
+  padding: 8px 12px;
+  cursor: pointer;
+  font-size: 14px;
+  border-radius: 4px;
+  margin-bottom: 8px;
+}
+.all-device-node:hover {
+  background: #f5f7fa;
+}
+.all-device-node.active {
+  background: #ecf5ff;
+  color: #409eff;
+}
+</style>
diff --git a/src/views/equipmentManagement/ledger/Form.vue b/src/views/equipmentManagement/ledger/Form.vue
index f1d21f2..46d5c98 100644
--- a/src/views/equipmentManagement/ledger/Form.vue
+++ b/src/views/equipmentManagement/ledger/Form.vue
@@ -38,6 +38,20 @@
         </el-form-item>
       </el-col>
       <el-col :span="12">
+        <el-form-item label="鎵�灞炲尯鍩�" prop="areaId">
+          <el-tree-select
+            v-model="form.areaId"
+            :data="areaTree"
+            node-key="id"
+            :props="{ label: 'label', children: 'children' }"
+            check-strictly
+            clearable
+            placeholder="璇烽�夋嫨鎵�灞炲尯鍩�"
+            style="width: 100%"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="12">
         <el-form-item label="渚涘簲鍟�" prop="supplierName">
           <el-input v-model="form.supplierName" placeholder="璇疯緭鍏ヤ緵搴斿晢" />
         </el-form-item>
@@ -174,6 +188,7 @@
 import useFormData from "@/hooks/useFormData";
 // import useUserStore from "@/store/modules/user";
 import { getLedgerById } from "@/api/equipmentManagement/ledger";
+import { getDeviceAreaTree } from "@/api/equipmentManagement/area";
 import dayjs from "dayjs";
 import {
   calculateTaxIncludeTotalPrice,
@@ -190,6 +205,7 @@
   name: "璁惧鍙拌处琛ㄥ崟",
 });
 const formRef = ref(null);
+const areaTree = ref([]);
 const operationType = ref('');
 // 璁惧绫诲瀷鍥哄畾閫夐」
 const deviceTypeOptions = ref([
@@ -228,6 +244,7 @@
   deviceModel: undefined, // 瑙勬牸鍨嬪彿
   deviceBrand: undefined, // 璁惧鍝佺墝
   type: undefined, // 璁惧绫诲瀷
+  areaId: undefined, // 鎵�灞炲尯鍩�
   supplierName: undefined, // 渚涘簲鍟�
   storageLocation: undefined, // 瀛樻斁浣嶇疆
   isDepr: 2, // 鏄惁鍚敤鎶樻棫 1-鏄� 2-鍚�
@@ -255,7 +272,15 @@
   }
 });
 
+const loadAreaTree = () => {
+  getDeviceAreaTree().then((res) => {
+    areaTree.value = res.data || [];
+  });
+};
+loadAreaTree();
+
 const loadForm = async (id) => {
+  loadAreaTree();
 	if (id) {
 		operationType.value = 'edit'
 	}
@@ -266,6 +291,7 @@
     form.deviceBrand = data.deviceBrand;
     form.type = data.type;
     form.supplierName = data.supplierName;
+    form.areaId = data.areaId;
     form.storageLocation = data.storageLocation;
     form.isDepr = data.isDepr;
     form.annualDepreciationAmount = data.annualDepreciationAmount;
diff --git a/src/views/equipmentManagement/ledger/index.vue b/src/views/equipmentManagement/ledger/index.vue
index af10532..95baaca 100644
--- a/src/views/equipmentManagement/ledger/index.vue
+++ b/src/views/equipmentManagement/ledger/index.vue
@@ -1,84 +1,160 @@
 <template>
   <div class="app-container">
-    <el-form :model="filters" :inline="true">
-      <el-form-item label="璁惧鍚嶇О">
-        <el-input
-          v-model="filters.deviceName"
-          style="width: 200px"
-          placeholder="璇疯緭鍏ヨ澶囧悕绉�"
-          clearable
-          @change="getTableData"
-        />
-      </el-form-item>
-      <el-form-item label="瑙勬牸鍨嬪彿">
-        <el-input
-            v-model="filters.deviceModel"
-            style="width: 200px"
-            placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
-            clearable
-            @change="getTableData"
-        />
-      </el-form-item>
-      <el-form-item label="渚涘簲鍟�">
-        <el-input
-            v-model="filters.supplierName"
-            style="width: 200px"
-            placeholder="璇疯緭鍏ヤ緵搴斿晢"
-            clearable
-            @change="getTableData"
-        />
-      </el-form-item>
-      <el-form-item label="褰曞叆鏃ユ湡:">
-        <el-date-picker v-model="filters.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
-                        placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" @click="getTableData">鎼滅储</el-button>
-        <el-button @click="resetFilters">閲嶇疆</el-button>
-      </el-form-item>
-    </el-form>
-    <div class="table_list">
-      <div class="actions">
-        <div></div>
-        <div>
-          <el-button type="primary" @click="add" icon="Plus"> 鏂板 </el-button>
-          <el-button type="info" @click="handleImport" icon="Upload">瀵煎叆</el-button>
-          <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
-          <el-button
-            type="danger"
-            icon="Delete"
-            :disabled="multipleList.length <= 0"
-            @click="deleteRow(multipleList.map((item) => item.id))"
-          >
-            鎵归噺鍒犻櫎
+    <div class="area-view">
+      <!-- 宸︿晶鍖哄煙鏍� -->
+      <div class="area-left">
+        <div class="area-left-header">
+          <el-button type="primary" @click="openAreaDialog('add')" icon="Plus">
+            鏂板鍖哄煙
           </el-button>
         </div>
+        <div
+          class="all-device-node"
+          :class="{ active: currentAreaId === null }"
+          @click="handleAllDeviceClick"
+        >
+          鍏ㄩ儴璁惧
+        </div>
+        <el-tree
+          ref="areaTreeRef"
+          v-loading="areaTreeLoading"
+          :data="areaTree"
+          node-key="id"
+          :props="{ label: 'label', children: 'children' }"
+          :expand-on-click-node="false"
+          default-expand-all
+          draggable
+          highlight-current
+          :current-node-key="currentAreaId"
+          @node-click="handleAreaNodeClick"
+          @node-drop="handleAreaNodeDrop"
+        >
+          <template #default="{ node, data }">
+            <div class="area-tree-node">
+              <span class="area-node-label">{{ data.label }}</span>
+              <span class="area-node-count">{{ data.deviceCount ?? 0 }}</span>
+              <span class="area-node-actions">
+                <el-button
+                  link
+                  type="primary"
+                  size="small"
+                  @click.stop="openAreaDialog('addChild', data)"
+                >
+                  瀛愮骇
+                </el-button>
+                <el-button
+                  link
+                  type="primary"
+                  size="small"
+                  @click.stop="openAreaDialog('edit', data)"
+                >
+                  缂栬緫
+                </el-button>
+                <el-button
+                  link
+                  type="danger"
+                  size="small"
+                  @click.stop="handleAreaDelete(node, data)"
+                >
+                  鍒犻櫎
+                </el-button>
+              </span>
+            </div>
+          </template>
+        </el-tree>
       </div>
-      <PIMTable
-        rowKey="id"
-        isSelection
-        :column="columns"
-        :tableData="dataList"
-        :page="{
-          current: pagination.currentPage,
-          size: pagination.pageSize,
-          total: pagination.total,
-        }"
-        @selection-change="handleSelectionChange"
-        @pagination="changePage"
-      >
-      </PIMTable>
+
+      <!-- 鍙充晶璁惧鍒楄〃 -->
+      <div class="area-right">
+        <el-form :model="filters" :inline="true">
+          <el-form-item label="璁惧鍚嶇О">
+            <el-input
+              v-model="filters.deviceName"
+              style="width: 200px"
+              placeholder="璇疯緭鍏ヨ澶囧悕绉�"
+              clearable
+              @change="getTableData"
+            />
+          </el-form-item>
+          <el-form-item label="瑙勬牸鍨嬪彿">
+            <el-input
+              v-model="filters.deviceModel"
+              style="width: 200px"
+              placeholder="璇疯緭鍏ヨ鏍煎瀷鍙�"
+              clearable
+              @change="getTableData"
+            />
+          </el-form-item>
+          <el-form-item label="渚涘簲鍟�">
+            <el-input
+              v-model="filters.supplierName"
+              style="width: 200px"
+              placeholder="璇疯緭鍏ヤ緵搴斿晢"
+              clearable
+              @change="getTableData"
+            />
+          </el-form-item>
+          <el-form-item label="褰曞叆鏃ユ湡:">
+            <el-date-picker
+              v-model="filters.entryDate"
+              value-format="YYYY-MM-DD"
+              format="YYYY-MM-DD"
+              type="daterange"
+              placeholder="璇烽�夋嫨"
+              clearable
+              @change="changeDaterange"
+            />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="getTableData">鎼滅储</el-button>
+            <el-button @click="handleReset">閲嶇疆</el-button>
+          </el-form-item>
+        </el-form>
+        <div class="table_list">
+          <div class="actions">
+            <div></div>
+            <div>
+              <el-button type="primary" @click="add" icon="Plus"> 鏂板 </el-button>
+              <el-button type="info" @click="handleImport" icon="Upload">瀵煎叆</el-button>
+              <el-button @click="handleOut" icon="download">瀵煎嚭</el-button>
+              <el-button
+                type="danger"
+                icon="Delete"
+                :disabled="multipleList.length <= 0"
+                @click="deleteRow(multipleList.map((item) => item.id))"
+              >
+                鎵归噺鍒犻櫎
+              </el-button>
+            </div>
+          </div>
+          <PIMTable
+            rowKey="id"
+            isSelection
+            :column="columns"
+            :tableData="dataList"
+            :page="{
+              current: pagination.currentPage,
+              size: pagination.pageSize,
+              total: pagination.total,
+            }"
+            @selection-change="handleSelectionChange"
+            @pagination="changePage"
+          >
+          </PIMTable>
+        </div>
+      </div>
     </div>
+
     <Modal ref="modalRef" @success="getTableData"></Modal>
     <el-dialog v-model="qrDialogVisible" title="浜岀淮鐮�" width="300px" draggable>
-      <div style="text-align:center;">
-        <img :src="qrCodeUrl" alt="浜岀淮鐮�" style="width:200px;height:200px;" />
-        <div style="margin:10px 0;">
+      <div style="text-align: center">
+        <img :src="qrCodeUrl" alt="浜岀淮鐮�" style="width: 200px; height: 200px" />
+        <div style="margin: 10px 0">
           <el-button type="primary" @click="downloadQRCode">涓嬭浇浜岀淮鐮佸浘鐗�</el-button>
         </div>
       </div>
     </el-dialog>
-    
+
     <!-- 瀵煎叆瀵硅瘽妗� -->
     <el-dialog :title="upload.title" v-model="upload.open" width="400px" append-to-body>
       <el-upload
@@ -98,7 +174,14 @@
         <template #tip>
           <div class="el-upload__tip text-center">
             <span>浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�</span>
-            <el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline; margin-left: 5px;" @click="importTemplate">涓嬭浇妯℃澘</el-link>
+            <el-link
+              type="primary"
+              :underline="false"
+              style="font-size: 12px; vertical-align: baseline; margin-left: 5px"
+              @click="importTemplate"
+            >
+              涓嬭浇妯℃澘
+            </el-link>
           </div>
         </template>
       </el-upload>
@@ -118,6 +201,7 @@
         <el-descriptions-item label="璁惧鍝佺墝">{{ detailData.deviceBrand }}</el-descriptions-item>
         <el-descriptions-item label="璁惧绫诲瀷">{{ detailData.type }}</el-descriptions-item>
         <el-descriptions-item label="渚涘簲鍟�">{{ detailData.supplierName }}</el-descriptions-item>
+        <el-descriptions-item label="鎵�灞炲尯鍩�">{{ detailData.areaName }}</el-descriptions-item>
         <el-descriptions-item label="瀛樻斁浣嶇疆">{{ detailData.storageLocation }}</el-descriptions-item>
         <el-descriptions-item label="鍗曚綅">{{ detailData.unit }}</el-descriptions-item>
         <el-descriptions-item label="鏁伴噺">{{ detailData.number }}</el-descriptions-item>
@@ -128,17 +212,22 @@
         <el-descriptions-item label="绋庣巼(%)">{{ detailData.taxRate }}</el-descriptions-item>
         <el-descriptions-item label="涓嶅惈绋庢�讳环">{{ detailData.unTaxIncludingPriceTotal }}</el-descriptions-item>
         <el-descriptions-item label="褰曞叆鏃ユ湡">{{ detailData.createTime }}</el-descriptions-item>
-        <el-descriptions-item label="棰勮杩愯鏃堕棿">{{ detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : '' }}</el-descriptions-item>
+        <el-descriptions-item label="棰勮杩愯鏃堕棿">{{
+          detailData.planRuntimeTime ? dayjs(detailData.planRuntimeTime).format('YYYY-MM-DD') : ''
+        }}</el-descriptions-item>
         <el-descriptions-item label="璁惧鍥剧墖" :span="2">
-          <div v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0" style="display: flex; gap: 10px; flex-wrap: wrap;">
-            <el-image 
-              v-for="(file, index) in detailData.storageBlobVOs" 
-              :key="index" 
-              :src="file.previewURL || file.url" 
-              :preview-src-list="detailData.storageBlobVOs.map(u => u.previewURL || u.url)"
+          <div
+            v-if="detailData.storageBlobVOs && detailData.storageBlobVOs.length > 0"
+            style="display: flex; gap: 10px; flex-wrap: wrap"
+          >
+            <el-image
+              v-for="(file, index) in detailData.storageBlobVOs"
+              :key="index"
+              :src="file.previewURL || file.url"
+              :preview-src-list="detailData.storageBlobVOs.map((u) => u.previewURL || u.url)"
               :initial-index="index"
-              style="width: 100px; height: 100px" 
-              fit="cover" 
+              style="width: 100px; height: 100px"
+              fit="cover"
             />
           </div>
           <span v-else>鏃犲浘鐗�</span>
@@ -148,14 +237,57 @@
         <el-button @click="detailDialogVisible = false">鍏抽棴</el-button>
       </template>
     </el-dialog>
+
+    <!-- 鍖哄煙鏂板/缂栬緫寮圭獥 -->
+    <el-dialog
+      v-model="areaDialogVisible"
+      :title="areaDialogTitle"
+      width="420px"
+      draggable
+      @closed="areaFormRef && areaFormRef.resetFields()"
+    >
+      <el-form :model="areaForm" :rules="areaRules" ref="areaFormRef" label-width="90px">
+        <el-form-item label="涓婄骇鍖哄煙" prop="parentId">
+          <el-tree-select
+            v-model="areaForm.parentId"
+            :data="areaTree"
+            node-key="id"
+            :props="{ label: 'label', children: 'children' }"
+            check-strictly
+            clearable
+            placeholder="涓嶉�夊垯涓烘牴鍖哄煙"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <el-form-item label="鍖哄煙鍚嶇О" prop="name">
+          <el-input v-model="areaForm.name" placeholder="璇疯緭鍏ュ尯鍩�/杞﹂棿/浣嶇疆鍚嶇О" clearable />
+        </el-form-item>
+        <el-form-item label="鎺掑簭" prop="sort">
+          <el-input-number v-model="areaForm.sort" :min="0" style="width: 100%" placeholder="鍙��" />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitAreaForm">纭畾</el-button>
+        <el-button @click="areaDialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
   </div>
 </template>
 
 <script setup>
 import { usePaginationApi } from "@/hooks/usePaginationApi";
-// import { Search } from "@element-plus/icons-vue";
-import { getLedgerPage, delLedger, getLedgerById } from "@/api/equipmentManagement/ledger";
-import { onMounted, getCurrentInstance, ref, reactive } from "vue";
+import {
+  getLedgerPage,
+  delLedger,
+  getLedgerById,
+} from "@/api/equipmentManagement/ledger";
+import {
+  getDeviceAreaTree,
+  addDeviceArea,
+  editDeviceArea,
+  delDeviceArea,
+} from "@/api/equipmentManagement/area";
+import { onMounted, getCurrentInstance, ref, reactive, computed } from "vue";
 import Modal from "./Modal.vue";
 import { ElMessageBox, ElMessage } from "element-plus";
 import { UploadFilled } from "@element-plus/icons-vue";
@@ -178,20 +310,38 @@
 const detailDialogVisible = ref(false);
 const detailData = ref({});
 
+// 鍖哄煙鐩稿叧
+const areaTree = ref([]);
+const areaTreeLoading = ref(false);
+const areaTreeRef = ref();
+const currentAreaId = ref(null);
+const areaDialogVisible = ref(false);
+const areaDialogType = ref("add");
+const areaFormRef = ref();
+const areaForm = reactive({
+  id: undefined,
+  name: "",
+  parentId: null,
+  sort: 0,
+});
+const areaRules = {
+  name: [{ required: true, message: "璇疯緭鍏ュ尯鍩熷悕绉�", trigger: "blur" }],
+};
+const areaDialogTitle = computed(() => {
+  if (areaDialogType.value === "edit") return "缂栬緫鍖哄煙";
+  if (areaDialogType.value === "addChild") return "鏂板瀛愬尯鍩�";
+  return "鏂板鍖哄煙";
+});
+
 // 瀵煎叆鐩稿叧
-const uploadRef = ref(null)
+const uploadRef = ref(null);
 const upload = reactive({
-  // 鏄惁鏄剧ず寮瑰嚭灞�
   open: false,
-  // 寮瑰嚭灞傛爣棰�
   title: "",
-  // 鏄惁绂佺敤涓婁紶
   isUploading: false,
-  // 璁剧疆涓婁紶鐨勮姹傚ご閮�
   headers: { Authorization: "Bearer " + getToken() },
-  // 涓婁紶鐨勫湴鍧�
-  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import"
-})
+  url: import.meta.env.VITE_APP_BASE_API + "/device/ledger/import",
+});
 
 const {
   filters,
@@ -209,6 +359,7 @@
     supplierName: undefined,
     entryDateStart: undefined,
     entryDateEnd: undefined,
+    areaId: undefined,
   },
   [
     {
@@ -226,6 +377,10 @@
     {
       label: "璁惧绫诲瀷",
       prop: "type",
+    },
+    {
+      label: "鎵�灞炲尯鍩�",
+      prop: "areaName",
     },
     {
       label: "渚涘簲鍟�",
@@ -247,43 +402,167 @@
       label: "褰曞叆鏃ユ湡",
       prop: "createTime",
       formatData: (v) => {
-        if (!v) return '';
-        // 濡傛灉鍖呭惈鏃跺垎绉掞紝鍙彇鏃ユ湡閮ㄥ垎
-        if (v.includes(' ')) {
-          return v.split(' ')[0];
+        if (!v) return "";
+        if (v.includes(" ")) {
+          return v.split(" ")[0];
         }
         return v;
       },
     },
-		{
-			dataType: "action",
-			label: "鎿嶄綔",
-			align: "center",
-			fixed: 'right',
-			width: 180,
-			operation: [
+    {
+      dataType: "action",
+      label: "鎿嶄綔",
+      align: "center",
+      fixed: "right",
+      width: 180,
+      operation: [
         {
           name: "璇︽儏",
           clickFun: (row) => {
             handleDetail(row);
           },
         },
-				{
-					name: "缂栬緫",
-					clickFun: (row) => {
-						edit(row.id)
-					},
-				},
-				{
-					name: "浜岀淮鐮�",
-					clickFun: (row) => {
-						showQRCode(row)
-					},
-				},
-			],
-		},
+        {
+          name: "缂栬緫",
+          clickFun: (row) => {
+            edit(row.id);
+          },
+        },
+        {
+          name: "浜岀淮鐮�",
+          clickFun: (row) => {
+            showQRCode(row);
+          },
+        },
+      ],
+    },
   ]
 );
+
+// 鍔犺浇鍖哄煙鏍�
+const getAreaTree = () => {
+  areaTreeLoading.value = true;
+  getDeviceAreaTree()
+    .then((res) => {
+      areaTree.value = res.data || [];
+    })
+    .finally(() => {
+      areaTreeLoading.value = false;
+    });
+};
+
+// 閲嶇疆鎼滅储涓庡尯鍩熺瓫閫�
+const handleReset = () => {
+  currentAreaId.value = null;
+  resetFilters();
+};
+
+// 鐐瑰嚮"鍏ㄩ儴璁惧"
+const handleAllDeviceClick = () => {
+  currentAreaId.value = null;
+  filters.areaId = undefined;
+  getTableData();
+};
+
+// 鐐瑰嚮鍖哄煙鑺傜偣
+const handleAreaNodeClick = (data) => {
+  currentAreaId.value = data.id;
+  filters.areaId = data.id;
+  getTableData();
+};
+
+// 鎷栨嫿璋冩暣鐖跺瓙鍏崇郴
+const handleAreaNodeDrop = (draggingNode, dropNode, dropType) => {
+  const node = draggingNode.data;
+  let parentId = null;
+  if (dropNode && dropType === "inner") {
+    parentId = dropNode.data.id;
+  } else if (dropNode) {
+    parentId = dropNode.data.parentId ?? null;
+  } else {
+    parentId = null;
+  }
+  if (parentId === node.id) {
+    proxy.$modal.msgWarning("涓嶈兘灏嗗尯鍩熸寕杞藉埌鑷韩");
+    getAreaTree();
+    return;
+  }
+  editDeviceArea({
+    id: node.id,
+    name: node.label,
+    parentId,
+    sort: node.sort,
+  })
+    .then(() => {
+      proxy.$modal.msgSuccess("璋冩暣鎴愬姛");
+      getAreaTree();
+    })
+    .catch(() => {
+      getAreaTree();
+    });
+};
+
+// 鎵撳紑鍖哄煙寮圭獥
+const openAreaDialog = (type, data) => {
+  areaDialogType.value = type;
+  areaForm.id = undefined;
+  areaForm.name = "";
+  areaForm.parentId = null;
+  areaForm.sort = 0;
+  if (type === "addChild" && data) {
+    areaForm.parentId = data.id;
+  } else if (type === "edit" && data) {
+    areaForm.id = data.id;
+    areaForm.name = data.label;
+    areaForm.parentId = data.parentId ?? null;
+    areaForm.sort = data.sort ?? 0;
+  }
+  areaDialogVisible.value = true;
+};
+
+// 鎻愪氦鍖哄煙琛ㄥ崟
+const submitAreaForm = () => {
+  if (!areaFormRef.value) return;
+  areaFormRef.value.validate((valid) => {
+    if (!valid) return;
+    const payload = {
+      name: areaForm.name,
+      parentId: areaForm.parentId ?? null,
+      sort: areaForm.sort ?? 0,
+    };
+    const api =
+      areaDialogType.value === "edit"
+        ? editDeviceArea({ ...payload, id: areaForm.id })
+        : addDeviceArea(payload);
+    api
+      .then(() => {
+        proxy.$modal.msgSuccess("鎿嶄綔鎴愬姛");
+        areaDialogVisible.value = false;
+        getAreaTree();
+      })
+      .catch(() => {});
+  });
+};
+
+// 鍒犻櫎鍖哄煙
+const handleAreaDelete = (node, data) => {
+  ElMessageBox.confirm("纭鍒犻櫎璇ュ尯鍩熷悧锛�", "鎻愮ず", {
+    confirmButtonText: "纭畾",
+    cancelButtonText: "鍙栨秷",
+    type: "warning",
+  })
+    .then(() => delDeviceArea(data.id))
+    .then(() => {
+      proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+      if (currentAreaId.value === data.id) {
+        currentAreaId.value = null;
+        filters.areaId = undefined;
+      }
+      getAreaTree();
+      getTableData();
+    })
+    .catch(() => {});
+};
 
 // 澶氶�夊悗鍋氫粈涔�
 const handleSelectionChange = (selectionList) => {
@@ -305,7 +584,7 @@
 };
 const changePage = ({ page, limit }) => {
   pagination.currentPage = page;
-	pagination.pageSize = limit;
+  pagination.pageSize = limit;
   onCurrentChange(page);
 };
 const deleteRow = (id) => {
@@ -351,36 +630,33 @@
 };
 
 const showQRCode = async (row) => {
-  // 鐩存帴浣跨敤URL锛屼笉瑕佺敤JSON.stringify鍖呰
-  const qrContent = proxy.javaApi + '/device-info?deviceId=' + row.id;
+  const qrContent = proxy.javaApi + "/device-info?deviceId=" + row.id;
   const qrDataUrl = await QRCode.toDataURL(qrContent, { width: 200, margin: 2 });
-  
-  // 鍒涘缓canvas鍚堟垚甯﹀悕绉扮殑浜岀淮鐮佸浘鐗�
-  const canvas = document.createElement('canvas');
-  const ctx = canvas.getContext('2d');
+
+  const canvas = document.createElement("canvas");
+  const ctx = canvas.getContext("2d");
   const qrSize = 200;
   const textHeight = 30;
   const padding = 10;
   canvas.width = qrSize + padding * 2;
   canvas.height = qrSize + textHeight + padding * 2;
-  
-  // 濉厖鐧借壊鑳屾櫙
-  ctx.fillStyle = '#ffffff';
+
+  ctx.fillStyle = "#ffffff";
   ctx.fillRect(0, 0, canvas.width, canvas.height);
-  
-  // 缁樺埗浜岀淮鐮�
+
   const qrImg = new Image();
   qrImg.src = qrDataUrl;
-  await new Promise((resolve) => { qrImg.onload = resolve; });
+  await new Promise((resolve) => {
+    qrImg.onload = resolve;
+  });
   ctx.drawImage(qrImg, padding, padding, qrSize, qrSize);
-  
-  // 缁樺埗璁惧鍚嶇О
-  ctx.fillStyle = '#333333';
-  ctx.font = 'bold 14px Arial';
-  ctx.textAlign = 'center';
-  ctx.fillText(row.deviceName || '', canvas.width / 2, qrSize + padding + 20);
-  
-  qrCodeUrl.value = canvas.toDataURL('image/png');
+
+  ctx.fillStyle = "#333333";
+  ctx.font = "bold 14px Arial";
+  ctx.textAlign = "center";
+  ctx.fillText(row.deviceName || "", canvas.width / 2, qrSize + padding + 20);
+
+  qrCodeUrl.value = canvas.toDataURL("image/png");
   qrRowData.value = row;
   qrDialogVisible.value = true;
 };
@@ -392,42 +668,100 @@
   a.click();
 };
 
-// 瀵煎叆鎸夐挳鎿嶄綔
 const handleImport = () => {
-  upload.title = "璁惧鍙拌处瀵煎叆"
-  upload.open = true
-}
+  upload.title = "璁惧鍙拌处瀵煎叆";
+  upload.open = true;
+};
 
-// 涓嬭浇妯℃澘鎿嶄綔
 const importTemplate = () => {
-  proxy.download("/device/ledger/downloadTemplate", {}, `璁惧鍙拌处瀵煎叆妯℃澘_${new Date().getTime()}.xlsx`)
-}
+  proxy.download(
+    "/device/ledger/downloadTemplate",
+    {},
+    `璁惧鍙拌处瀵煎叆妯℃澘_${new Date().getTime()}.xlsx`
+  );
+};
 
-// 鏂囦欢涓婁紶涓鐞�
 const handleFileUploadProgress = (event, file, fileList) => {
-  upload.isUploading = true
-}
+  upload.isUploading = true;
+};
 
-// 鏂囦欢涓婁紶鎴愬姛澶勭悊
 const handleFileSuccess = (response, file, fileList) => {
-  upload.open = false
-  upload.isUploading = false
-  proxy.$refs["uploadRef"].handleRemove(file)
-  proxy.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "瀵煎叆缁撴灉", { dangerouslyUseHTMLString: true })
-  getTableData()
-}
+  upload.open = false;
+  upload.isUploading = false;
+  proxy.$refs["uploadRef"].handleRemove(file);
+  proxy.$alert(
+    "<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" +
+      response.msg +
+      "</div>",
+    "瀵煎叆缁撴灉",
+    { dangerouslyUseHTMLString: true }
+  );
+  getTableData();
+};
 
-// 鎻愪氦涓婁紶鏂囦欢
 const submitFileForm = () => {
-  proxy.$refs["uploadRef"].submit()
-}
+  proxy.$refs["uploadRef"].submit();
+};
 
 onMounted(() => {
+  getAreaTree();
   getTableData();
 });
 </script>
 
 <style lang="scss" scoped>
+.area-view {
+  display: flex;
+}
+.area-left {
+  width: 320px;
+  flex-shrink: 0;
+  padding: 16px;
+  background: #ffffff;
+  margin-right: 16px;
+}
+.area-left-header {
+  margin-bottom: 10px;
+}
+.all-device-node {
+  padding: 8px 12px;
+  cursor: pointer;
+  font-size: 14px;
+  border-radius: 4px;
+  margin-bottom: 8px;
+}
+.all-device-node:hover {
+  background: #f5f7fa;
+}
+.all-device-node.active {
+  background: #ecf5ff;
+  color: #409eff;
+}
+.area-tree-node {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 14px;
+  padding-right: 4px;
+}
+.area-node-count {
+  color: #909399;
+  font-size: 12px;
+  margin-left: 6px;
+}
+.area-node-actions {
+  display: none;
+}
+.area-tree-node:hover .area-node-actions {
+  display: inline-flex;
+}
+.area-right {
+  flex: 1;
+  min-width: 0;
+  padding: 16px;
+  background: #ffffff;
+}
 .table_list {
   margin-top: unset;
 }
diff --git a/src/views/equipmentManagement/measurementEquipment/index.vue b/src/views/equipmentManagement/measurementEquipment/index.vue
index 007eef6..3ece930 100644
--- a/src/views/equipmentManagement/measurementEquipment/index.vue
+++ b/src/views/equipmentManagement/measurementEquipment/index.vue
@@ -37,6 +37,10 @@
       <div>
         <el-button type="primary"
                    @click="openForm('add')">鏂板璁¢噺鍣ㄥ叿</el-button>
+        <el-button type="primary"
+                   plain
+                   @click="handleImport">鎵归噺瀵煎叆</el-button>
+        <el-button @click="handleDownloadTemplate">涓嬭浇妯℃澘</el-button>
         <el-button type="danger"
                    plain
                    @click="handleDelete">鍒犻櫎</el-button>
@@ -61,6 +65,17 @@
                      @close="handleQuery"></calibration-dia>
     <files-dia ref="filesDia"></files-dia>
     <rowClickDataForm ref="rowClickData"></rowClickDataForm>
+    <ImportDialog ref="importDialogRef"
+                  v-model="importDialogVisible"
+                  title="鎵归噺瀵煎叆璁¢噺鍣ㄥ叿"
+                  :action="importAction"
+                  :headers="importHeaders"
+                  :auto-upload="false"
+                  :on-success="handleImportSuccess"
+                  :on-error="handleImportError"
+                  @confirm="handleImportConfirm"
+                  @download-template="handleDownloadTemplate"
+                  @close="handleImportClose" />
   </div>
 </template>
 
@@ -76,6 +91,8 @@
   import FormDia from "@/views/equipmentManagement/measurementEquipment/components/formDia.vue";
   import { ElMessageBox } from "element-plus";
   import useUserStore from "@/store/modules/user.js";
+  import { getToken } from "@/utils/auth";
+  import ImportDialog from "@/components/Dialog/ImportDialog.vue";
   import CalibrationDia from "@/views/equipmentManagement/measurementEquipment/components/calibrationDia.vue";
   import {
     measuringInstrumentDelete,
@@ -336,6 +353,54 @@
         proxy.$modal.msg("宸插彇娑�");
       });
   };
+  // 鎵归噺瀵煎叆
+  const importDialogRef = ref();
+  const importDialogVisible = ref(false);
+  const importAction =
+    import.meta.env.VITE_APP_BASE_API + "/measuringInstrumentLedger/import";
+  const importHeaders = ref({
+    Authorization: `Bearer ${getToken()}`,
+  });
+
+  const handleImport = () => {
+    importDialogVisible.value = true;
+  };
+
+  const handleImportSuccess = response => {
+    if (response.code === 200) {
+      proxy.$modal.msgSuccess(response.msg || "瀵煎叆鎴愬姛");
+      importDialogVisible.value = false;
+      if (importDialogRef.value) {
+        importDialogRef.value.clearFiles();
+      }
+      getList();
+    } else {
+      proxy.$modal.msgError(response.msg || "瀵煎叆澶辫触");
+    }
+  };
+
+  const handleImportError = error => {
+    proxy.$modal.msgError("瀵煎叆澶辫触锛岃妫�鏌ユ枃浠舵牸寮忔槸鍚︽纭�");
+  };
+
+  const handleImportConfirm = () => {
+    if (importDialogRef.value) {
+      importDialogRef.value.submit();
+    }
+  };
+
+  const handleDownloadTemplate = () => {
+    proxy.download(
+      "/measuringInstrumentLedger/downloadTemplate",
+      {},
+      "璁¢噺鍣ㄥ叿鍙拌处瀵煎叆妯℃澘.xlsx"
+    );
+  };
+
+  const handleImportClose = () => {
+    importDialogVisible.value = false;
+  };
+
   onMounted(() => {
     getList();
   });
diff --git a/src/views/equipmentManagement/upkeep/Form/formDia.vue b/src/views/equipmentManagement/upkeep/Form/formDia.vue
index 6856ae1..0415d3a 100644
--- a/src/views/equipmentManagement/upkeep/Form/formDia.vue
+++ b/src/views/equipmentManagement/upkeep/Form/formDia.vue
@@ -95,6 +95,7 @@
 							<el-option label="姣忓懆" value="WEEKLY"/>
 							<el-option label="姣忔湀" value="MONTHLY"/>
 							<el-option label="瀛e害" value="QUARTERLY"/>
+							<el-option label="骞村害淇濆吇" value="YEARLY"/>
 						</el-select>
 					</el-form-item>
 				</el-col>
@@ -143,6 +144,20 @@
 						/>
 					</el-form-item>
 				</el-col>
+				<el-col :span="12" v-if="form.frequencyType === 'YEARLY'">
+					<el-form-item label="鎵ц鏃ユ湡" prop="frequencyDetail">
+						<div style="display: flex; gap: 4px; width: 100%">
+							<el-select v-model="form.yearMonth" placeholder="鏈�" clearable style="flex: 1">
+								<el-option v-for="m in 12" :key="m" :label="m + '鏈�'" :value="m" />
+							</el-select>
+							<el-select v-model="form.yearDay" placeholder="鏃�" clearable style="flex: 1">
+								<el-option v-for="d in 31" :key="d" :label="d + '鏃�'" :value="d" />
+							</el-select>
+							<el-time-picker v-model="form.yearTime" placeholder="鏃堕棿" format="HH:mm" value-format="HH:mm" style="flex: 1.2" />
+						</div>
+					</el-form-item>
+				</el-col>
+
 			</el-row>
 			<el-row>
 				<el-col :span="12">
@@ -182,6 +197,9 @@
 		frequencyDetail: '',
 		week: '',
 		time: '',
+		yearMonth: undefined,
+		yearDay: undefined,
+		yearTime: '',
 		deviceModel: undefined, // 瑙勬牸鍨嬪彿
 		registrationDate: '',
 		maintenancePerson: '' // 淇濆吇浜�
@@ -238,6 +256,13 @@
 		if (form.value.taskId) {
 			setDeviceModel(form.value.taskId);
 		}
+		// YEARLY 棰戠巼鍥炴樉锛氳В鏋� 鏈堜唤,鏃ユ湡,鏃堕棿
+		if (form.value.frequencyType === 'YEARLY' && form.value.frequencyDetail) {
+			const parts = String(form.value.frequencyDetail).split(',');
+			form.value.yearMonth = Number(parts[0]);
+			form.value.yearDay = Number(parts[1]);
+			form.value.yearTime = parts[2] || '';
+		}
 	} else if (type === 'add') {
 		// 鏂板鏃惰缃櫥璁版棩鏈熶负褰撳ぉ
 		form.value.registrationDate = getCurrentDate();
@@ -269,6 +294,9 @@
 		frequencyDetail: '',
 		week: '',
 		time: '',
+		yearMonth: undefined,
+		yearDay: undefined,
+		yearTime: '',
 		deviceModel: undefined,
 		registrationDate: '',
 		maintenancePerson: ''
@@ -300,6 +328,25 @@
 					frequencyDetail = payload.week + ',' + payload.time
 					payload.frequencyDetail = frequencyDetail
 				}
+
+				if (payload.frequencyType === 'YEARLY') {
+					const month = Number(payload.yearMonth);
+					const day = Number(payload.yearDay);
+					const time = payload.yearTime;
+					if (!month || month < 1 || month > 12) {
+						proxy.$modal.msgError('鏈堜唤蹇呴』鍦�1-12涔嬮棿');
+						return;
+					}
+					if (!day || day < 1 || day > 31) {
+						proxy.$modal.msgError('鏃ユ湡蹇呴』鍦�1-31涔嬮棿');
+						return;
+					}
+					if (!time || !/^\d{2}:\d{2}$/.test(time)) {
+						proxy.$modal.msgError('鏃堕棿鏍煎紡蹇呴』涓� HH:mm');
+						return;
+					}
+					payload.frequencyDetail = month + ',' + day + ',' + time;
+				}
 				
 				// 褰曞叆鏃ユ湡锛氱洿鎺ヤ娇鐢ㄨ〃鍗曢噷鐨� registrationDate 瀛楁
 				// 涓�浜涢粯璁ょ姸鎬佸瓧娈�
diff --git a/src/views/equipmentManagement/upkeep/index.vue b/src/views/equipmentManagement/upkeep/index.vue
index 245a162..560b70d 100644
--- a/src/views/equipmentManagement/upkeep/index.vue
+++ b/src/views/equipmentManagement/upkeep/index.vue
@@ -63,6 +63,9 @@
             }"
                     @selection-change="handleScheduledSelectionChange"
                     @pagination="changeScheduledPage">
+            <template #frequencyDetailRef="{ row }">
+              {{ formatFrequencyDetail(row) }}
+            </template>
             <template #statusRef="{ row }">
               <el-tag v-if="row.status === 1"
                       type="success">鍚敤</el-tag>
@@ -316,31 +319,15 @@
           WEEKLY: "姣忓懆",
           MONTHLY: "姣忔湀",
           QUARTERLY: "瀛e害",
+          YEARLY: "骞村害淇濆吇",
         }[cell] || ""),
     },
     {
       prop: "frequencyDetail",
       label: "寮�濮嬫棩鏈熶笌鏃堕棿",
       minWidth: 150,
-      // 鍚屾牱鏀圭敤 formatData锛孭IMTable 鍐呴儴浼氭妸鍗曞厓鏍煎�间紶杩涙潵
-      formatData: cell => {
-        if (typeof cell !== "string") return "";
-        let val = cell;
-        const replacements = {
-          MON: "鍛ㄤ竴",
-          TUE: "鍛ㄤ簩",
-          WED: "鍛ㄤ笁",
-          THU: "鍛ㄥ洓",
-          FRI: "鍛ㄤ簲",
-          SAT: "鍛ㄥ叚",
-          SUN: "鍛ㄦ棩",
-        };
-        // 浣跨敤姝e垯涓�娆℃�ф浛鎹㈡墍鏈夊尮閰嶉」
-        return val.replace(
-          /MON|TUE|WED|THU|FRI|SAT|SUN/g,
-          match => replacements[match]
-        );
-      },
+      dataType: "slot",
+      slot: "frequencyDetailRef",
     },
     { prop: "maintenancePerson", label: "淇濆吇浜�", minWidth: 100 },
     { prop: "registrant", label: "鐧昏浜�", minWidth: 100 },
@@ -362,6 +349,28 @@
   ]);
 
   // 淇濆吇璁板綍琛ㄦ牸鍒楅厤缃紙鍘熻澶囦繚鍏昏〃鏍煎垪锛�
+  const formatFrequencyDetail = (row) => {
+    if (!row) return "";
+    const val = String(row.frequencyDetail || "");
+    if (row.frequencyType === "YEARLY") {
+      const parts = val.split(",");
+      if (parts.length === 3) {
+        return `姣忓勾${parts[0]}鏈�${parts[1]}鏃� ${parts[2]}`;
+      }
+      return val;
+    }
+    const replacements = {
+      MON: "鍛ㄤ竴",
+      TUE: "鍛ㄤ簩",
+      WED: "鍛ㄤ笁",
+      THU: "鍛ㄥ洓",
+      FRI: "鍛ㄤ簲",
+      SAT: "鍛ㄥ叚",
+      SUN: "鍛ㄦ棩",
+    };
+    return val.replace(/MON|TUE|WED|THU|FRI|SAT|SUN/g, (m) => replacements[m]);
+  };
+
   const columns = ref([
     {
       label: "璁惧鍚嶇О",
diff --git a/src/views/procurementManagement/procurementReport/index.vue b/src/views/procurementManagement/procurementReport/index.vue
index 26e682d..bf07e6e 100644
--- a/src/views/procurementManagement/procurementReport/index.vue
+++ b/src/views/procurementManagement/procurementReport/index.vue
@@ -139,7 +139,7 @@
     }
   },
   {
-    label: '閫�娆惧偍閲�',
+    label: '閲囪喘閲戦',
     prop: 'purchaseAmount',
     formatData: (val) => {
       return val ? `楼${parseFloat(val).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : '楼0.00'
diff --git a/src/views/salesManagement/salesLedger/index.vue b/src/views/salesManagement/salesLedger/index.vue
index 610c17c..ccf90b4 100644
--- a/src/views/salesManagement/salesLedger/index.vue
+++ b/src/views/salesManagement/salesLedger/index.vue
@@ -177,7 +177,7 @@
                   <el-button
                     link
                     type="primary"
-                    :disabled="!canShip(scope.row)"
+                    :disabled="!canShip(scope.row) || props.row.contractType === 2"
                     @click="openDeliveryForm(scope.row)"
                   >
                     鍙戣揣
@@ -261,7 +261,7 @@
           width="200"
           show-overflow-tooltip
         />
-        <el-table-column fixed="right" label="鎿嶄綔" width="220" align="center">
+        <el-table-column fixed="right" label="鎿嶄綔" width="320" align="center">
           <template #default="scope">
             <el-button link type="primary" @click="openForm('view', scope.row)"
               >璇︽儏
@@ -276,6 +276,20 @@
                 !canEditLedger(scope.row)
               "
               >缂栬緫
+            </el-button>
+            <el-button
+              v-if="scope.row.contractType === 2"
+              link
+              type="success"
+              @click="openFrameworkOrderDialog(scope.row)"
+              >鍒涘缓妗嗘灦璁㈠崟
+            </el-button>
+            <el-button
+              v-if="scope.row.contractType === 2"
+              link
+              type="primary"
+              @click="openFrameworkOrderList(scope.row)"
+              >妗嗘灦璁㈠崟
             </el-button>
             <el-button link type="primary" @click="openFileDialog(scope.row)"
               >闄勪欢
@@ -320,6 +334,21 @@
             <el-button type="primary" plain @click="openQuotationDialog">
               浠庨攢鍞姤浠峰鍏�
             </el-button>
+          </el-col>
+        </el-row>
+        <el-row :gutter="30">
+          <el-col :span="12">
+            <el-form-item label="鍚堝悓绫诲瀷锛�" prop="contractType">
+              <el-select
+                v-model="form.contractType"
+                placeholder="璇烽�夋嫨"
+                clearable
+                :disabled="operationType === 'view' || operationType === 'edit'"
+              >
+                <el-option label="鏅�氶攢鍞悎鍚�" :value="1" />
+                <el-option label="妗嗘灦鍚堝悓" :value="2" />
+              </el-select>
+            </el-form-item>
           </el-col>
         </el-row>
         <el-row :gutter="30">
@@ -467,7 +496,7 @@
             </el-form-item>
           </el-col>
         </el-row>
-        <el-row :gutter="30">
+        <el-row :gutter="30" v-if="!isFrameworkContract">
           <el-col :span="12">
             <el-form-item label="浜よ揣鏃ユ湡锛�" prop="entryDate">
               <el-date-picker
@@ -750,7 +779,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item label="绋庣巼(%)锛�" prop="taxRate">
               <el-select
                 v-model="productForm.taxRate"
@@ -783,7 +812,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item label="鏁伴噺锛�" prop="quantity">
               <el-input-number
                 :step="0.1"
@@ -799,7 +828,7 @@
           </el-col>
         </el-row>
         <el-row :gutter="30">
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item label="鍚◣鎬讳环(鍏�)锛�" prop="taxInclusiveTotalPrice">
               <el-input
                 v-model="productForm.taxInclusiveTotalPrice"
@@ -809,7 +838,7 @@
               />
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item
               label="涓嶅惈绋庢�讳环(鍏�)锛�"
               prop="taxExclusiveTotalPrice"
@@ -824,7 +853,7 @@
           </el-col>
         </el-row>
         <el-row :gutter="30">
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item label="鍙戠エ绫诲瀷锛�" prop="invoiceType">
               <el-select
                 v-model="productForm.invoiceType"
@@ -836,7 +865,7 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
+          <el-col :span="12" v-if="!isFrameworkContract">
             <el-form-item label="鏄惁鐢熶骇锛�" prop="isProduction">
               <el-radio-group v-model="productForm.isProduction">
                 <el-radio label="鏄�" :value="true" />
@@ -1177,6 +1206,104 @@
         </div>
       </template>
     </el-dialog>
+    <!-- 鍒涘缓妗嗘灦璁㈠崟寮圭獥 -->
+    <el-dialog
+      v-model="frameworkOrderDialogVisible"
+      title="鍒涘缓妗嗘灦璁㈠崟"
+      width="70%"
+      :close-on-click-modal="false"
+    >
+      <el-form label-width="120px" label-position="top">
+        <el-form-item label="鐖舵鏋跺悎鍚岋細">
+          <el-input
+            :model-value="currentFrameworkContract?.salesContractNo"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="澶囨敞锛�">
+          <el-input
+            v-model="frameworkOrderForm.remarks"
+            type="textarea"
+            :rows="2"
+            placeholder="璇疯緭鍏ュ娉�"
+          />
+        </el-form-item>
+        <el-form-item label="浜у搧锛堝~鍐欎氦璐ф棩鏈燂級锛�">
+          <el-table :data="frameworkOrderProducts" border>
+            <el-table-column label="浜у搧澶х被" prop="productCategory" />
+            <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+            <el-table-column label="鍗曚綅" prop="unit" />
+            <el-table-column label="浜よ揣鏃ユ湡" align="center">
+              <template #default="scope">
+                <el-date-picker
+                  v-model="scope.row.deliveryDate"
+                  value-format="YYYY-MM-DD"
+                  format="YYYY-MM-DD"
+                  type="date"
+                  placeholder="璇烽�夋嫨"
+                  clearable
+                />
+              </template>
+            </el-table-column>
+          </el-table>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button type="primary" @click="submitFrameworkOrder"
+          >纭鍒涘缓
+        </el-button>
+        <el-button @click="frameworkOrderDialogVisible = false">鍙栨秷</el-button>
+      </template>
+    </el-dialog>
+    <!-- 妗嗘灦璁㈠崟鍒楄〃寮圭獥 -->
+    <el-dialog
+      v-model="frameworkOrderListVisible"
+      title="妗嗘灦璁㈠崟鍒楄〃"
+      width="80%"
+      :close-on-click-modal="false"
+    >
+      <el-alert
+        v-if="frameworkContractShippedQty !== null"
+        type="info"
+        :closable="false"
+        style="margin-bottom: 12px"
+        :title="'鐖舵鏋跺悎鍚岀疮璁″凡鍙戣揣鏁伴噺锛�' + frameworkContractShippedQty"
+      />
+      <el-table :data="frameworkOrderList" border :row-key="(row) => row.id">
+        <el-table-column type="expand">
+          <template #default="props">
+            <el-table :data="props.row.products" border size="small">
+              <el-table-column label="浜у搧澶х被" prop="productCategory" />
+              <el-table-column label="瑙勬牸鍨嬪彿" prop="specificationModel" />
+              <el-table-column label="鍗曚綅" prop="unit" />
+              <el-table-column label="鎿嶄綔" width="80" align="center">
+                <template #default="scope">
+                  <el-button
+                    link
+                    type="primary"
+                    @click="shipFrameworkOrderProduct(scope.row, props.row)"
+                    >鍙戣揣
+                  </el-button>
+                </template>
+              </el-table-column>
+            </el-table>
+          </template>
+        </el-table-column>
+        <el-table-column
+          label="璁㈠崟缂栧彿"
+          prop="salesContractNo"
+          min-width="180"
+        />
+        <el-table-column label="椤圭洰鍚嶇О" prop="projectName" min-width="160" />
+        <el-table-column label="澶囨敞" prop="remarks" min-width="160" />
+        <el-table-column
+          label="绱鍙戣揣鏁伴噺"
+          prop="shippedQuantity"
+          width="140"
+          align="center"
+        />
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
@@ -1201,6 +1328,9 @@
   delProduct,
   delLedgerFile,
   getProductInventory,
+  createFrameworkOrder,
+  listFrameworkOrders,
+  frameworkContractShippedQuantity,
 } from "@/api/salesManagement/salesLedger.js";
 import { getStockInventoryByModelId } from "@/api/inventoryManagement/stockInventory.js";
 import { modelList, productTreeList } from "@/api/basicData/product.js";
@@ -1250,6 +1380,7 @@
   },
   form: {
     salesContractNo: "",
+    contractType: 2,
     autoGenerateContractNo: true,
     salesman: "",
     customerId: "",
@@ -1267,11 +1398,31 @@
     customerId: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
     entryPerson: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
     entryDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
-    deliveryDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+    deliveryDate: [
+      {
+        validator: (rule, value, callback) => {
+          if (form.value.contractType === 2) return callback();
+          if (!value) return callback(new Error("璇烽�夋嫨"));
+          callback();
+        },
+        trigger: "change",
+      },
+    ],
     executionDate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
   },
 });
 const { form, rules } = toRefs(data);
+const isFrameworkContract = computed(() => form.value.contractType === 2);
+const frameworkOptional = (message, trigger = "blur") => ({
+  validator: (_rule, value, callback) => {
+    if (isFrameworkContract.value) return callback();
+    if (value === undefined || value === null || value === "") {
+      return callback(new Error(message));
+    }
+    callback();
+  },
+  trigger,
+});
 const { form: searchForm } = useFormData(data.searchForm);
 // 浜у搧琛ㄥ崟寮规鏁版嵁
 const productFormVisible = ref(false);
@@ -1297,18 +1448,12 @@
       { required: true, message: "璇烽�夋嫨", trigger: "change" },
     ],
     unit: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
-    quantity: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
-    taxInclusiveUnitPrice: [
-      { required: true, message: "璇疯緭鍏�", trigger: "blur" },
-    ],
-    taxRate: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
-    taxInclusiveTotalPrice: [
-      { required: true, message: "璇疯緭鍏�", trigger: "blur" },
-    ],
-    taxExclusiveTotalPrice: [
-      { required: true, message: "璇疯緭鍏�", trigger: "blur" },
-    ],
-    invoiceType: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
+    quantity: [frameworkOptional("璇疯緭鍏�")],
+    taxInclusiveUnitPrice: [{ required: true, message: "璇疯緭鍏�", trigger: "blur" }],
+    taxRate: [frameworkOptional("璇烽�夋嫨", "change")],
+    taxInclusiveTotalPrice: [frameworkOptional("璇疯緭鍏�")],
+    taxExclusiveTotalPrice: [frameworkOptional("璇疯緭鍏�")],
+    invoiceType: [frameworkOptional("璇烽�夋嫨", "change")],
     isProduction: [{ required: true, message: "璇烽�夋嫨", trigger: "change" }],
   },
 });
@@ -1740,6 +1885,7 @@
   form.value.entryPerson = userStore.id;
   if (type === "add") {
     // 鏂板鏃惰缃綍鍏ユ棩鏈熶负褰撳ぉ
+    form.value.contractType = 2;
     form.value.entryDate = getCurrentDate();
     // 绛捐鏃ユ湡榛樿涓哄綋澶�
     form.value.executionDate = getCurrentDate();
@@ -1892,6 +2038,14 @@
       }
       form.value.storageBlobDTOs = fileList;
       form.value.type = 1;
+      if (isFrameworkContract.value) {
+        form.value.productData = form.value.productData.map((item) => ({
+          ...item,
+          quantity: null,
+          deliveryDate: null,
+          isProduction: false,
+        }));
+      }
       if (form.value.autoGenerateContractNo) {
         form.value.salesContractNo = "";
       }
@@ -2933,6 +3087,92 @@
   deliveryFormVisible.value = false;
   currentDeliveryRow.value = null;
 };
+
+// 妗嗘灦鍚堝悓 / 妗嗘灦璁㈠崟鐩稿叧
+const frameworkOrderDialogVisible = ref(false);
+const frameworkOrderListVisible = ref(false);
+const currentFrameworkContract = ref(null);
+const frameworkOrderForm = reactive({
+  remarks: "",
+});
+const frameworkOrderProducts = ref([]);
+const frameworkOrderList = ref([]);
+const frameworkContractShippedQty = ref(null);
+
+const openFrameworkOrderDialog = async (row) => {
+  currentFrameworkContract.value = row;
+  frameworkOrderForm.remarks = "";
+  frameworkOrderProducts.value = [];
+  try {
+    const res = await productList({ salesLedgerId: row.id, type: 1 });
+    frameworkOrderProducts.value = (res.data || []).map((p) => ({
+      parentProductId: p.id,
+      productCategory: p.productCategory,
+      specificationModel: p.specificationModel,
+      unit: p.unit,
+      deliveryDate: "",
+    }));
+  } catch (e) {
+    frameworkOrderProducts.value = [];
+  }
+  frameworkOrderDialogVisible.value = true;
+};
+
+const submitFrameworkOrder = async () => {
+  const selected = frameworkOrderProducts.value.filter((p) => p.deliveryDate);
+  if (selected.length === 0) {
+    proxy.$modal.msgWarning("璇疯嚦灏戜负涓�涓骇鍝佸~鍐欎氦璐ф棩鏈�");
+    return;
+  }
+  try {
+    await createFrameworkOrder({
+      id: currentFrameworkContract.value.id,
+      remarks: frameworkOrderForm.remarks,
+      productData: selected.map((p) => ({
+        parentProductId: p.parentProductId,
+        deliveryDate: p.deliveryDate,
+      })),
+    });
+    proxy.$modal.msgSuccess("妗嗘灦璁㈠崟鍒涘缓鎴愬姛");
+    frameworkOrderDialogVisible.value = false;
+  } catch (e) {
+    // 閿欒宸茬敱璇锋眰鎷︽埅鍣ㄧ粺涓�鎻愮ず
+  }
+};
+
+const openFrameworkOrderList = async (row) => {
+  currentFrameworkContract.value = row;
+  frameworkContractShippedQty.value = null;
+  frameworkOrderList.value = [];
+  frameworkOrderListVisible.value = true;
+  listFrameworkOrders({ parentContractId: row.id }).then(async (res) => {
+    const orders = res.data || [];
+    const withProducts = [];
+    for (const order of orders) {
+      let products = [];
+      try {
+        const productRes = await productList({ salesLedgerId: order.id, type: 1 });
+        products = productRes.data || [];
+      } catch (e) {
+        products = [];
+      }
+      withProducts.push({ ...order, products });
+    }
+    frameworkOrderList.value = withProducts;
+  });
+  frameworkContractShippedQuantity({ parentContractId: row.id }).then((res) => {
+    frameworkContractShippedQty.value = res.data ?? 0;
+  });
+};
+
+const shipFrameworkOrderProduct = (productRow, orderRow) => {
+  if (!orderRow || !orderRow.id) return;
+  frameworkOrderListVisible.value = false;
+  openDeliveryForm({
+    ...productRow,
+    salesLedgerId: orderRow.id,
+  });
+};
 const currentFactoryName = ref("");
 const getCurrentFactoryName = async () => {
   let res = await userStore.getInfo();

--
Gitblit v1.9.3