From 1332cc511936d7dbe804921bc733c83d5e4d1400 Mon Sep 17 00:00:00 2001
From: zzw <228544128@qq.com>
Date: 星期二, 06 十月 2026 17:17:49 +0800
Subject: [PATCH] feat: add completion query and configurable system identity

---
 emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java   |   52 ++++
 emergency-repair-web/public/sensitive-words.json                                                       |   12 +
 emergency-repair-web/src/components/RepairLogo.vue                                                     |    2 
 emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java               |   12 +
 emergency-repair-web/src/views/Login.vue                                                               |    2 
 emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java        |  155 ++++++++++++
 emergency-repair-web/src/router/index.js                                                               |    1 
 emergency-repair-web/src/utils/sensitive.js                                                            |    1 
 emergency-repair-web/src/views/system/MenuManage.vue                                                   |   53 +++
 HANDOFF.md                                                                                             |   45 +++
 emergency-repair-web/public/index.html                                                                 |    2 
 emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java  |    2 
 emergency-repair-web/src/layout/AppLayout.vue                                                          |    2 
 requirement/交接文件_2026.10.3.md                                                                          |   50 ++++
 emergency-repair-web/src/main.js                                                                       |    2 
 emergency-repair-web/src/views/completion/CompletionQuery.vue                                          |  209 +++++++++++++++++
 requirement/改进20261003.txt                                                                             |   12 
 emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java |   13 +
 emergency-repair-web/src/components/AppHeader.vue                                                      |    5 
 emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java            |   37 ++
 requirement/敏感词字典.json                                                                                 |   12 +
 21 files changed, 653 insertions(+), 28 deletions(-)

diff --git a/HANDOFF.md b/HANDOFF.md
index 3c3b292..bdd4431 100644
--- a/HANDOFF.md
+++ b/HANDOFF.md
@@ -781,3 +781,48 @@
 - 閿欒鏃ュ織褰撳墠瑕嗙洊杩涘叆 Spring MVC 鍏ㄥ眬寮傚父澶勭悊鐨勫紓甯革紱鍚姩闃舵寮傚父銆佸紓姝ョ嚎绋嬩腑鏈繘鍏ヨ澶勭悊鍣ㄧ殑寮傚父鏆備笉浼氳嚜鍔ㄨ惤搴撱��
 - 涓嶈鎻愪氦 `.codex-run`銆丱ffice 閿佹枃浠舵垨 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt`銆�
 - 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
+
+## 20. 鏀跺熬蹇収锛�2026-10-06 鍛ㄤ竴锛岄渶姹� 23-25 绯荤粺閰嶇疆銆佸畬宸ユ煡闃呬笌鑿滃崟鐖惰妭鐐癸級
+
+**鏈疆瀹屾垚鍐呭**
+- 闇�姹� 23锛氱郴缁熷悕绉板拰鐗堟湰鏀逛负鍏抽敭璇嶉厤缃紝codename 鍒嗗埆涓� `rk_SystemName`銆乣rk_SystemVer`銆傜櫥褰曢〉銆侀〉澶淬�丩ogo 鏇夸唬鏂囨湰鍜屾祻瑙堝櫒鏍囬浣跨敤閰嶇疆鍊硷紱椤靛ご鐗堟湰浣跨敤绾㈣壊鏄剧ず銆�
+- 闇�姹� 24锛氭柊澧炩�滃畬宸ヤ俊鎭煡闃呪�濆姛鑳斤紝鑿滃崟 `/completion/query`锛屾帴鍙� `GET /api/work-orders/completion/query`銆傛煡璇㈢粨鏋滄寜鏁呴殰姹囨�伙紝涓�涓晠闅滃睍寮�澶氬紶娲惧伐鍗曪紱鏁呴殰缂栧彿銆乺k_X鍙枫�佽澶囥�佸伐绋嬪悕绉板強鎹熷潖鎯呭喌銆佷笂鎶ュ崟浣嶃�佷笂鎶ユ棩鏈熴�佸簭鍙疯繖 7 鍒椾娇鐢ㄥ崟鍏冩牸绾靛悜鍚堝苟銆傛瘡寮犳淳宸ュ崟鐨勫畬宸ュ鐞嗘儏鍐靛拰鏇存崲鍣ㄦ潗鏀寔澶氭潯鏄庣粏銆�
+- 闇�姹� 24 鐨勬煡璇㈡潯浠跺寘鎷叧閿瘝銆佹壙淇崟浣嶃�佸畬宸ョ姸鎬佸拰瀹屽伐鏃ユ湡銆傚叧閿瘝鍖归厤鏁呴殰銆佹淳宸ュ崟銆佸伐绋嬬紪鍙枫�佹壙淇汉鍜屾槑缁嗗唴瀹广��
+- 闇�姹� 25锛氳彍鍗曠鐞嗙紪杈戞椂锛屸�滀笂绾ц彍鍗曗�濇敼涓哄彲缂栬緫绾ц仈閫夋嫨鍣紱缂栬緫鏃舵帓闄ゅ綋鍓嶈彍鍗曞強鍏跺叏閮ㄤ笅绾э紝鎸夐挳绫诲瀷鍙兘閫夋嫨鑿滃崟绫诲瀷鐖惰妭鐐广��
+- 闇�姹� 25 鍚庣鍚屾澧炲姞鐖惰妭鐐瑰瓨鍦ㄦ�с�佽嚜寮曠敤鍜屽惊鐜眰绾ф牎楠岋紝閬垮厤閫氳繃鎺ュ彛鏋勯�犲惊鐜彍鍗曟爲銆�
+
+**涓昏鏂囦欢**
+鍚庣锛�
+
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java`锛堟柊澧烇級
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java`
+
+鍓嶇锛�
+
+- `emergency-repair-web/src/views/completion/CompletionQuery.vue`锛堟柊澧烇級
+- `emergency-repair-web/src/views/system/MenuManage.vue`
+- `emergency-repair-web/src/components/AppHeader.vue`銆乣Login.vue`銆乣RepairLogo.vue`銆乣layout/AppLayout.vue`銆乣router/index.js`銆乣utils/sensitive.js`銆乣main.js`
+- `emergency-repair-web/public/sensitive-words.json`銆乣public/index.html`
+- `requirement/鏁忔劅璇嶅瓧鍏�.json`
+
+**楠岃瘉缁撴灉**
+- 鍚庣 `mvn -B -DskipTests compile` 閫氳繃锛涘墠绔� `npm run build` 閫氳繃锛屼粎鏈夋棦鏈夊寘浣撶Н鍛婅銆�
+- `node .codex-run/verify-req23-25.js`锛氶〉闈㈡爣棰樺拰椤靛ご浣跨敤鈥滀复鎶慨绠$悊绯荤粺 / 缃戠粶鐗堚�濓紝鐗堟湰棰滆壊涓� `rgb(255, 107, 107)`锛涘畬宸ユ煡闃呮樉绀烘晠闅� `GZ-2026-012`銆佸鐞嗘儏鍐靛拰鍣ㄦ潗锛涜彍鍗曠紪杈戠埗鑺傜偣鏄剧ず鈥滄晠闅滅鐞嗏�濅笖鍙紪杈戙��
+- 鎺ュ彛瀹炴祴锛氫复鏃跺悓涓�鏁呴殰鎻掑叆涓ゅ紶娲惧伐鍗曞拰涓ゆ潯瀹屽伐璁板綍鍚庯紝`GET /api/work-orders/completion/query?keyword=ZZ-MERGE-TEST-20261006` 杩斿洖 1 涓晠闅溿��2 寮犳淳宸ュ崟锛屾瘡鏉″悇 1 鏉″鐞嗘儏鍐靛拰 1 鏉″櫒鏉愩��
+- 娴忚鍣ㄥ悎骞跺疄娴嬶細椤甸潰鐢熸垚 2 寮犳淳宸ュ崟琛岋紝鏁呴殰缂栧彿鍗曞厓鏍煎彧鍑虹幇 1 娆★紝7 涓晠闅滀俊鎭垪鍧囨湁 `rowspan=2`锛屼袱寮犲崟鍚勮嚜鐨勫鐞嗘儏鍐靛拰鍣ㄦ潗鍧囨甯告樉绀猴紝鏃犵綉缁滃紓甯革紱鎴浘 `.codex-run/completion-query-merge-verified.png`銆備复鏃舵暟鎹簱璁板綍宸叉竻鐞嗐��
+- 鑿滃崟鐖惰妭鐐规帴鍙e疄娴嬶細涓存椂鍒涘缓鐖跺瓙鑿滃崟鎴愬姛锛涙妸鐖惰彍鍗曠Щ鍔ㄥ埌鍏跺瓙鑿滃崟涓嬭繑鍥炩�滀笂绾ц彍鍗曚笉鑳介�夋嫨褰撳墠鑿滃崟鐨勪笅绾р�濓紱鎶婂瓙鑿滃崟绉诲姩鍒板叾浠栫埗鑺傜偣鎴愬姛锛涗复鏃惰彍鍗曞凡娓呯悊銆�
+
+**褰撳墠杩愯鐘舵�侊紙2026-10-06锛�**
+- 鍓嶇 `http://localhost:8082`锛涘悗绔� `http://127.0.0.1:8091`锛汥M8 `127.0.0.1:5236`锛汦dge CDP `9222`銆�
+- 鐧诲綍 `admin / admin123`锛涙暟鎹簱 `SYSDBA/SYSDBA`锛屼笟鍔� schema `LX`銆�
+- 鍚庣鏃ュ織锛歚.codex-run/backend-20261006-req23-25-v2.out.log`銆乣.codex-run/backend-20261006-req23-25-v2.err.log`銆�
+
+**鎺ユ墜娉ㄦ剰**
+- 瀹屽伐淇℃伅鏌ラ槄褰撳墠鍙眹鎬诲凡缁忓瓨鍦ㄥ畬宸ュ~鎶ヨ褰� `T_COMPLETION_INFO` 鐨勬淳宸ュ崟锛涗粎瀹屽伐澶勭悊浣嗘湭濉姤瀹屽伐淇℃伅鐨勬淳宸ュ崟涓嶄細鍑虹幇鍦ㄦ煡闃呯粨鏋滀腑銆�
+- 瀹屽伐鏌ラ槄鎺ュ彛褰撳墠涓�娆¤繑鍥炲叏閮ㄥ尮閰嶇粨鏋滐紝娌℃湁鍒嗛〉锛涙暟鎹噺杩涗竴姝ュ澶ф椂寤鸿澧炲姞鍒嗛〉鎴栧鍑虹瓥鐣ャ��
+- 涓嶈鎻愪氦 `.codex-run`銆丱ffice 閿佹枃浠舵垨 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt`銆�
+- 鏈疆浠g爜銆侀渶姹傛枃浠跺拰浜ゆ帴璁板綍涓�骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
index 4c5edb4..7a63284 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java
@@ -84,6 +84,7 @@
     public void run(String... args) {
         seedMenusAndRoles();
         ensureSystemMenus();
+        ensureCompletionMenus();
         normalizeSensitiveTerms();
         normalizeEquipmentTerms();
         seedButtonMenus();
@@ -116,6 +117,7 @@
             createMenu("dispatch", fault, "rk_XCB鍔╃悊娲惧崟", "/fault/dispatch", null, 1, 60);
             createMenu("completion-list", fault, "瀹屽伐鎯呭喌澶勭悊", "/fault/completion", null, 1, 70);
             createMenu("completion-report", fault, "瀹屽伐淇℃伅涓婃姤", "/completion/report", null, 1, 80);
+            createMenu("completion-query", fault, "瀹屽伐淇℃伅鏌ラ槄", "/completion/query", null, 1, 90);
 
             Long acceptance = createMenu("acceptance", null, "瀹屽伐楠屾敹", null, "el-icon-circle-check", 0, 30);
             createMenu("accept-application", acceptance, "楠屾敹鐢宠", "/acceptance/application", null, 1, 10);
@@ -264,6 +266,16 @@
         return menu.getId();
     }
 
+    private void ensureCompletionMenus() {
+        SysMenu fault = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
+                .eq(SysMenu::getName, "鏁呴殰绠$悊")
+                .eq(SysMenu::getMenuType, 0)
+                .last("LIMIT 1"));
+        Long pid = fault == null ? 0L : fault.getId();
+        ensureMenu(pid, "瀹屽伐淇℃伅鏌ラ槄", "/completion/query", 90);
+        grantAdminMenus("/completion/query");
+    }
+
     private void ensureSystemMenus() {
         SysMenu system = menuMapper.selectOne(Wrappers.<SysMenu>lambdaQuery()
                 .eq(SysMenu::getName, "\u7cfb\u7edf\u7ba1\u7406").last("LIMIT 1"));
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java b/emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java
index 619860e..0bfdcc0 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java
@@ -1,11 +1,13 @@
 package com.emergencyrepair.business.controller;
 
 import com.emergencyrepair.business.dto.Requests;
+import com.emergencyrepair.business.dto.CompletionFaultQueryItem;
 import com.emergencyrepair.business.entity.CompletionInfo;
 import com.emergencyrepair.business.entity.WorkOrder;
 import com.emergencyrepair.business.service.BusinessService;
 import com.emergencyrepair.common.Result;
 import lombok.RequiredArgsConstructor;
+import org.springframework.format.annotation.DateTimeFormat;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -14,6 +16,7 @@
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
+import java.time.LocalDate;
 import java.util.List;
 
 @RestController
@@ -29,6 +32,16 @@
         return Result.ok(businessService.listWorkOrders(status, keyword, completionReportable));
     }
 
+    @GetMapping("/completion/query")
+    public Result<List<CompletionFaultQueryItem>> completionQuery(
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) String completionStatus,
+            @RequestParam(required = false) String contractorName,
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
+            @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
+        return Result.ok(businessService.listCompletionQueries(keyword, completionStatus, contractorName, startDate, endDate));
+    }
+
     @GetMapping("/{id}")
     public Result<WorkOrder> detail(@PathVariable Long id) {
         return Result.ok(businessService.getWorkOrder(id));
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java b/emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java
new file mode 100644
index 0000000..6b7450a
--- /dev/null
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java
@@ -0,0 +1,52 @@
+package com.emergencyrepair.business.dto;
+
+import lombok.Data;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class CompletionFaultQueryItem {
+    private String faultId;
+    private String faultCode;
+    @JsonProperty("xNo")
+    private String xNo;
+    private String equipmentName;
+    private String parentSystem;
+    private String engineeringMajor;
+    private String teamMajor;
+    private String reportUnit;
+    private LocalDate reportDate;
+    private String owner;
+    private String engineeringNameDesc;
+    private String taskStatus;
+    private List<WorkOrderCompletionItem> workOrders = new ArrayList<>();
+
+    @Data
+    public static class WorkOrderCompletionItem {
+        private String workOrderId;
+        private String workOrderNo;
+        private String contractorName;
+        private String completionStatus;
+        private String engineeringNo;
+        private LocalDate startDate;
+        private LocalDate finishDate;
+        private String contractorPerson;
+        @JsonProperty("tInternalConstruction")
+        private String tInternalConstruction;
+        private String relatedProject;
+        private Integer durationDays;
+        private Integer personCount;
+        private String acceptanceMode;
+        private String qualityInfo;
+        private String reasonAnalysis;
+        private String faultDescription;
+        private String itemName;
+        private LocalDateTime submitTime;
+        private List<Requests.SolutionItem> solutions = new ArrayList<>();
+        private List<Requests.MaterialItem> materials = new ArrayList<>();
+    }
+}
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java b/emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
index b36be6c..f2c5329 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java
@@ -4,6 +4,7 @@
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.emergencyrepair.business.dto.Requests;
+import com.emergencyrepair.business.dto.CompletionFaultQueryItem;
 import com.emergencyrepair.business.entity.Acceptance;
 import com.emergencyrepair.business.entity.CompletionInfo;
 import com.emergencyrepair.business.entity.Fault;
@@ -32,6 +33,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.Objects;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
@@ -406,6 +408,159 @@
         return created;
     }
 
+    public List<CompletionFaultQueryItem> listCompletionQueries(String keyword, String completionStatus,
+                                                               String contractorName, LocalDate startDate,
+                                                               LocalDate endDate) {
+        List<CompletionInfo> completions = completionInfoMapper.selectList(Wrappers.<CompletionInfo>lambdaQuery()
+                .orderByDesc(CompletionInfo::getSubmitTime).orderByDesc(CompletionInfo::getUpdateTime));
+        if (completions.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        Set<Long> workOrderIds = completions.stream().map(CompletionInfo::getWorkOrderId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        if (workOrderIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, WorkOrder> workOrderMap = workOrderMapper.selectBatchIds(workOrderIds).stream()
+                .collect(Collectors.toMap(WorkOrder::getId, item -> item));
+        Set<Long> faultIds = workOrderMap.values().stream().map(WorkOrder::getFaultId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        if (faultIds.isEmpty()) {
+            return Collections.emptyList();
+        }
+        Map<Long, Fault> faultMap = faultMapper.selectBatchIds(faultIds).stream()
+                .collect(Collectors.toMap(Fault::getId, item -> item));
+
+        String normalizedKeyword = StringUtils.hasText(keyword) ? keyword.trim().toLowerCase() : null;
+        String normalizedContractor = StringUtils.hasText(contractorName) ? contractorName.trim().toLowerCase() : null;
+        Map<Long, CompletionFaultQueryItem> grouped = new LinkedHashMap<>();
+        for (CompletionInfo completion : completions) {
+            WorkOrder workOrder = workOrderMap.get(completion.getWorkOrderId());
+            if (workOrder == null) {
+                continue;
+            }
+            if (StringUtils.hasText(completionStatus)
+                    && !completionStatus.equalsIgnoreCase(workOrder.getCompletionStatus())) {
+                continue;
+            }
+            if (normalizedContractor != null && !containsIgnoreCase(workOrder.getContractorName(), normalizedContractor)) {
+                continue;
+            }
+            LocalDate completionDate = completion.getFinishDate() != null
+                    ? completion.getFinishDate()
+                    : (completion.getSubmitTime() == null ? null : completion.getSubmitTime().toLocalDate());
+            if (startDate != null && (completionDate == null || completionDate.isBefore(startDate))) {
+                continue;
+            }
+            if (endDate != null && (completionDate == null || completionDate.isAfter(endDate))) {
+                continue;
+            }
+
+            Fault fault = faultMap.get(workOrder.getFaultId());
+            if (fault == null) {
+                continue;
+            }
+            CompletionFaultQueryItem.WorkOrderCompletionItem workOrderItem = buildCompletionWorkOrderItem(workOrder, completion);
+            if (normalizedKeyword != null && !matchesCompletionKeyword(fault, workOrderItem, normalizedKeyword)) {
+                continue;
+            }
+
+            CompletionFaultQueryItem item = grouped.get(fault.getId());
+            if (item == null) {
+                item = new CompletionFaultQueryItem();
+                item.setFaultId(String.valueOf(fault.getId()));
+                item.setFaultCode(fault.getFaultCode());
+                item.setXNo(fault.getXNo());
+                item.setEquipmentName(fault.getEquipmentName());
+                item.setParentSystem(fault.getParentSystem());
+                item.setEngineeringMajor(fault.getEngineeringMajor());
+                item.setTeamMajor(fault.getTeamMajor());
+                item.setReportUnit(fault.getReportUnit());
+                item.setReportDate(fault.getReportDate());
+                item.setOwner(fault.getOwner());
+                item.setEngineeringNameDesc(fault.getEngineeringNameDesc());
+                item.setTaskStatus(fault.getTaskStatus());
+                grouped.put(fault.getId(), item);
+            }
+            item.getWorkOrders().add(workOrderItem);
+        }
+
+        List<CompletionFaultQueryItem> result = new ArrayList<>(grouped.values());
+        result.forEach(item -> item.getWorkOrders().sort(Comparator.comparing(
+                CompletionFaultQueryItem.WorkOrderCompletionItem::getWorkOrderNo,
+                Comparator.nullsLast(String::compareTo))));
+        result.sort(Comparator.comparing(CompletionFaultQueryItem::getReportDate,
+                Comparator.nullsLast(Comparator.reverseOrder())));
+        return result;
+    }
+
+    private CompletionFaultQueryItem.WorkOrderCompletionItem buildCompletionWorkOrderItem(
+            WorkOrder workOrder, CompletionInfo completion) {
+        CompletionFaultQueryItem.WorkOrderCompletionItem item = new CompletionFaultQueryItem.WorkOrderCompletionItem();
+        item.setWorkOrderId(String.valueOf(workOrder.getId()));
+        item.setWorkOrderNo(workOrder.getWorkOrderNo());
+        item.setContractorName(workOrder.getContractorName());
+        item.setCompletionStatus(workOrder.getCompletionStatus());
+        item.setEngineeringNo(completion.getEngineeringNo());
+        item.setStartDate(completion.getStartDate());
+        item.setFinishDate(completion.getFinishDate());
+        item.setContractorPerson(completion.getContractorPerson());
+        item.setTInternalConstruction(completion.getTInternalConstruction());
+        item.setRelatedProject(completion.getRelatedProject());
+        item.setDurationDays(completion.getDurationDays());
+        item.setPersonCount(completion.getPersonCount());
+        item.setAcceptanceMode(completion.getAcceptanceMode());
+        item.setQualityInfo(completion.getQualityInfo());
+        item.setReasonAnalysis(completion.getReasonAnalysis());
+        item.setFaultDescription(completion.getFaultDescription());
+        item.setItemName(completion.getItemName());
+        item.setSubmitTime(completion.getSubmitTime());
+        List<Requests.SolutionItem> solutions = parseJsonList(completion.getSolutionsJson(), Requests.SolutionItem.class);
+        if (solutions.isEmpty() && StringUtils.hasText(completion.getSolution())) {
+            Requests.SolutionItem legacy = new Requests.SolutionItem();
+            legacy.setContent(completion.getSolution());
+            solutions.add(legacy);
+        }
+        item.setSolutions(solutions);
+        item.setMaterials(parseJsonList(completion.getConsumablesJson(), Requests.MaterialItem.class));
+        return item;
+    }
+
+    private boolean matchesCompletionKeyword(Fault fault, CompletionFaultQueryItem.WorkOrderCompletionItem item,
+                                             String keyword) {
+        if (containsIgnoreCase(fault.getFaultCode(), keyword)
+                || containsIgnoreCase(fault.getXNo(), keyword)
+                || containsIgnoreCase(fault.getEquipmentName(), keyword)
+                || containsIgnoreCase(fault.getEngineeringNameDesc(), keyword)
+                || containsIgnoreCase(fault.getReportUnit(), keyword)
+                || containsIgnoreCase(item.getWorkOrderNo(), keyword)
+                || containsIgnoreCase(item.getContractorName(), keyword)
+                || containsIgnoreCase(item.getEngineeringNo(), keyword)
+                || containsIgnoreCase(item.getItemName(), keyword)
+                || containsIgnoreCase(item.getContractorPerson(), keyword)) {
+            return true;
+        }
+        return item.getSolutions().stream().anyMatch(solution -> containsIgnoreCase(solution.getContent(), keyword))
+                || item.getMaterials().stream().anyMatch(material -> containsIgnoreCase(material.getName(), keyword)
+                || containsIgnoreCase(material.getSpecification(), keyword));
+    }
+
+    private boolean containsIgnoreCase(String value, String keyword) {
+        return StringUtils.hasText(value) && value.toLowerCase().contains(keyword);
+    }
+
+    private <T> List<T> parseJsonList(String json, Class<T> type) {
+        if (!StringUtils.hasText(json)) {
+            return new ArrayList<>();
+        }
+        try {
+            return objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(List.class, type));
+        } catch (Exception exception) {
+            return new ArrayList<>();
+        }
+    }
+
     public List<WorkOrder> listWorkOrders(String status, String keyword, boolean completionReportable) {
         LambdaQueryWrapper<WorkOrder> query = Wrappers.lambdaQuery();
         if (completionReportable) {
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
index 982796d..7e6b6fa 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java
@@ -59,6 +59,8 @@
         put("/completion/report",
                 def("completion:report:save", "\u4fdd\u5b58"),
                 def("completion:report:submit", "\u63d0\u4ea4"));
+        put("/completion/query",
+                def("completion:query:query", "\u67e5\u8be2"));
         put("/acceptance/application",
                 def("acceptance:application:query", "\u67e5\u8be2"),
                 def("acceptance:application:apply", "\u7533\u8bf7"));
diff --git a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
index 9f0cfd1..e06db50 100644
--- a/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
+++ b/emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java
@@ -108,11 +108,20 @@
             menu.setMenuType(menu.getId() == null ? 1 : menu.getMenuType());
         }
         boolean isRoot = menu.getPid() == null || menu.getPid() == 0L;
-        if (menu.getMenuType() == 2) {
-            if (isRoot) {
-                throw new BusinessException("鎸夐挳绫诲瀷鍙兘寤哄湪鑿滃崟绫诲瀷涓嬬骇");
+        if (menu.getId() != null && !isRoot && menu.getId().equals(menu.getPid())) {
+            throw new BusinessException("涓婄骇鑿滃崟涓嶈兘閫夋嫨褰撳墠鑿滃崟");
+        }
+        SysMenu parent = null;
+        if (!isRoot) {
+            parent = menuMapper.selectById(menu.getPid());
+            if (parent == null) {
+                throw new BusinessException("涓婄骇鑿滃崟涓嶅瓨鍦�");
             }
-            SysMenu parent = menuMapper.selectById(menu.getPid());
+            if (menu.getId() != null && isMenuDescendant(menu.getPid(), menu.getId())) {
+                throw new BusinessException("涓婄骇鑿滃崟涓嶈兘閫夋嫨褰撳墠鑿滃崟鐨勪笅绾�");
+            }
+        }
+        if (menu.getMenuType() == 2) {
             if (parent == null || parent.getMenuType() == null || parent.getMenuType() != 1) {
                 throw new BusinessException("鎸夐挳绫诲瀷鍙兘寤哄湪鑿滃崟绫诲瀷涓嬬骇");
             }
@@ -121,11 +130,8 @@
             }
             menu.setUrl(null);
             menu.setIcon(null);
-        } else if (!isRoot) {
-            SysMenu parent = menuMapper.selectById(menu.getPid());
-            if (parent != null && parent.getMenuType() != null && parent.getMenuType() == 2) {
-                throw new BusinessException("鎸夐挳绫诲瀷涓嬬骇涓嶈兘鍐嶅缓鑿滃崟");
-            }
+        } else if (parent != null && parent.getMenuType() != null && parent.getMenuType() == 2) {
+            throw new BusinessException("鎸夐挳绫诲瀷涓嬬骇涓嶈兘鍐嶅缓鑿滃崟");
         }
         LocalDateTime now = LocalDateTime.now();
         menu.setUpdateTime(now);
@@ -144,6 +150,19 @@
         return menuMapper.selectById(menu.getId());
     }
 
+    private boolean isMenuDescendant(Long candidateId, Long ancestorId) {
+        Long cursor = candidateId;
+        int guard = 0;
+        while (cursor != null && cursor != 0L && guard++ < 64) {
+            if (cursor.equals(ancestorId)) {
+                return true;
+            }
+            SysMenu current = menuMapper.selectById(cursor);
+            cursor = current == null ? null : current.getPid();
+        }
+        return false;
+    }
+
     @Transactional
     public void deleteMenu(Long id) {
         long children = menuMapper.selectCount(Wrappers.<SysMenu>lambdaQuery().eq(SysMenu::getPid, id));
diff --git a/emergency-repair-web/public/index.html b/emergency-repair-web/public/index.html
index 73ef908..df7bdc1 100644
--- a/emergency-repair-web/public/index.html
+++ b/emergency-repair-web/public/index.html
@@ -3,7 +3,7 @@
 <head>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width,initial-scale=1">
-  <title>涓存姠淇鐞嗙郴缁�</title>
+  <title>rk_SystemName</title>
 </head>
 <body>
   <noscript>璇峰惎鐢� JavaScript 鍚庝娇鐢ㄤ复鎶慨绠$悊绯荤粺銆�</noscript>
diff --git a/emergency-repair-web/public/sensitive-words.json b/emergency-repair-web/public/sensitive-words.json
index 1c84cef..9c7c1ab 100644
--- a/emergency-repair-web/public/sensitive-words.json
+++ b/emergency-repair-web/public/sensitive-words.json
@@ -1,6 +1,18 @@
 {
   "sensitive_words": [
     {
+      "codename": "rk_SystemName",
+      "real_name": "涓存姠淇鐞嗙郴缁�",
+      "scope": "ui",
+      "note": "System title shown in the login page and application header."
+    },
+    {
+      "codename": "rk_SystemVer",
+      "real_name": "缃戠粶鐗�",
+      "scope": "ui",
+      "note": "System edition shown in red in the application header."
+    },
+    {
       "codename": "rk_X鍙�",
       "real_name": "鍘垮彿",
       "scope": "ui",
diff --git a/emergency-repair-web/src/components/AppHeader.vue b/emergency-repair-web/src/components/AppHeader.vue
index 146bf5c..1b19f76 100644
--- a/emergency-repair-web/src/components/AppHeader.vue
+++ b/emergency-repair-web/src/components/AppHeader.vue
@@ -2,8 +2,8 @@
   <header class="app-header">
     <div class="brand">
       <repair-logo></repair-logo>
-      <span>涓存姠淇鐞嗙郴缁�</span>
-      <em>缃戠粶鐗�</em>
+      <span>rk_SystemName</span>
+      <em class="system-version">rk_SystemVer</em>
       <span v-if="menuPathText" class="menu-path" :title="menuPathText">{{ menuPathText }}</span>
     </div>
     <div class="header-right">
@@ -57,6 +57,7 @@
 .brand { display: flex; flex: 1; align-items: center; min-width: 0; gap: 9px; font-size: 18px; font-weight: 600; }
 .brand .repair-logo { color: #7fc4ff; font-size: 25px; transform: translateY(2px); }
 .brand em { padding: 1px 6px; border: 1px solid rgba(255,255,255,.35); border-radius: 3px; font-size: 11px; font-style: normal; font-weight: 400; opacity: .85; }
+.brand .system-version { border-color: rgba(255, 107, 107, .65); color: #ff6b6b; opacity: 1; }
 .brand > .repair-logo, .brand > span:not(.menu-path), .brand > em { flex: 0 0 auto; }
 .menu-path { flex: 0 1 auto; min-width: 0; padding-left: 14px; border-left: 1px solid rgba(255,255,255,.28); overflow: hidden; color: #d8e9f7; font-size: 13px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
 .header-right { display: flex; flex: 0 0 auto; align-items: center; gap: 14px; font-size: 14px; }
diff --git a/emergency-repair-web/src/components/RepairLogo.vue b/emergency-repair-web/src/components/RepairLogo.vue
index 39c40ef..3972620 100644
--- a/emergency-repair-web/src/components/RepairLogo.vue
+++ b/emergency-repair-web/src/components/RepairLogo.vue
@@ -1,5 +1,5 @@
 <template>
-  <img class="repair-logo" :src="logoSrc" alt="涓存姠淇鐞嗙郴缁�">
+  <img class="repair-logo" :src="logoSrc" alt="rk_SystemName">
 </template>
 
 <script>
diff --git a/emergency-repair-web/src/layout/AppLayout.vue b/emergency-repair-web/src/layout/AppLayout.vue
index 891bb52..04a8ed5 100644
--- a/emergency-repair-web/src/layout/AppLayout.vue
+++ b/emergency-repair-web/src/layout/AppLayout.vue
@@ -170,7 +170,7 @@
       if (path === '/fault/report') return 'el-icon-edit-outline'
       if (path.includes('/approval/') || path === '/fault/review') return 'el-icon-s-check'
       if (path === '/fault/dispatch') return 'el-icon-s-promotion'
-      if (path.startsWith('/fault/completion') || path === '/completion/report') return 'el-icon-finished'
+      if (path.startsWith('/fault/completion') || path.startsWith('/completion/')) return 'el-icon-finished'
       if (path.startsWith('/acceptance/')) return 'el-icon-circle-check'
       if (path.startsWith('/reports/')) return 'el-icon-data-analysis'
       if (path.startsWith('/system/')) return 'el-icon-setting'
diff --git a/emergency-repair-web/src/main.js b/emergency-repair-web/src/main.js
index 381a415..829ed5a 100644
--- a/emergency-repair-web/src/main.js
+++ b/emergency-repair-web/src/main.js
@@ -9,7 +9,7 @@
 Vue.use(ElementUI, { size: 'small' })
 Vue.config.productionTip = false
 
-Vue.prototype.$appName = '涓存姠淇鐞嗙郴缁�'
+Vue.prototype.$appName = 'rk_SystemName'
 
 Vue.prototype.$hasPermission = function(code) {
   const raw = sessionStorage.getItem('permissions')
diff --git a/emergency-repair-web/src/router/index.js b/emergency-repair-web/src/router/index.js
index 5ba2af1..f376755 100644
--- a/emergency-repair-web/src/router/index.js
+++ b/emergency-repair-web/src/router/index.js
@@ -20,6 +20,7 @@
       { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: 'rk_XCB鍔╃悊娲惧崟', cacheName: 'DispatchPage' } },
       { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue'), meta: { title: '瀹屽伐鎯呭喌澶勭悊', cacheName: 'CompletionList' } },
       { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue'), props: route => ({ workOrderId: route.query.workOrderId || null }), meta: { title: '瀹屽伐淇℃伅涓婃姤', cacheName: 'CompletionReport' } },
+      { path: 'completion/query', component: () => import('@/views/completion/CompletionQuery.vue'), meta: { title: '瀹屽伐淇℃伅鏌ラ槄', cacheName: 'CompletionQuery' } },
       { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'APPLICATION' }, meta: { title: '楠屾敹鐢宠', stage: 'APPLICATION', cacheName: 'AcceptanceList' } },
       { path: 'acceptance/inspector', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'INSPECTOR' }, meta: { title: '璐ㄦ鍛橀獙鏀�', stage: 'INSPECTOR', cacheName: 'AcceptanceList' } },
       { path: 'acceptance/leader', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'LEADER' }, meta: { title: '璐ㄦ璐熻矗浜洪獙鏀�', stage: 'LEADER', cacheName: 'AcceptanceList' } },
diff --git a/emergency-repair-web/src/utils/sensitive.js b/emergency-repair-web/src/utils/sensitive.js
index d2bee8b..2be439a 100644
--- a/emergency-repair-web/src/utils/sensitive.js
+++ b/emergency-repair-web/src/utils/sensitive.js
@@ -182,6 +182,7 @@
     })
 
     replaceDom(document.body)
+    document.title = replaceSensitiveText(document.title)
     watchDom()
   } catch (error) {
     // The application still works with development codenames if the dictionary is unavailable.
diff --git a/emergency-repair-web/src/views/Login.vue b/emergency-repair-web/src/views/Login.vue
index bbe197e..a12939a 100644
--- a/emergency-repair-web/src/views/Login.vue
+++ b/emergency-repair-web/src/views/Login.vue
@@ -4,7 +4,7 @@
       <div class="login-brand">
         <repair-logo></repair-logo>
         <div>
-          <h1>涓存姠淇鐞嗙郴缁�</h1>
+          <h1>{{ $appName }}</h1>
         </div>
       </div>
       <el-form ref="form" :model="form" :rules="rules" label-position="top" @keyup.enter.native="submit">
diff --git a/emergency-repair-web/src/views/completion/CompletionQuery.vue b/emergency-repair-web/src/views/completion/CompletionQuery.vue
new file mode 100644
index 0000000..38044cf
--- /dev/null
+++ b/emergency-repair-web/src/views/completion/CompletionQuery.vue
@@ -0,0 +1,209 @@
+<template>
+  <div class="page">
+    <div class="page-title">
+      <div>
+        <h2>瀹屽伐淇℃伅鏌ラ槄</h2>
+        <p>鎸夋晠闅滄眹鎬伙紝鍚屼竴鏁呴殰鐨勫寮犳淳宸ュ崟鍏辩敤鏁呴殰淇℃伅锛屽畬宸ュ鐞嗘儏鍐靛拰鏇存崲鍣ㄦ潗鎸夊鏉℃槑缁嗗睍绀�</p>
+      </div>
+      <el-tag type="info">鍏� {{ rows.length }} 涓晠闅�</el-tag>
+    </div>
+
+    <div class="toolbar">
+      <el-input v-model="filters.keyword" placeholder="鏁呴殰缂栧彿 / rk_X鍙� / 娲惧伐鍗曠紪鍙� / 宸ョ▼鍚嶇О / 鎵夸慨鍗曚綅" clearable style="width:380px" @keyup.enter.native="load"></el-input>
+      <el-input v-model="filters.contractorName" placeholder="鎵夸慨鍗曚綅" clearable style="width:190px" @keyup.enter.native="load"></el-input>
+      <el-select v-model="filters.completionStatus" placeholder="瀹屽伐鐘舵��" clearable style="width:140px">
+        <el-option v-for="(meta, key) in completionStatuses" :key="key" :label="meta.text" :value="key"></el-option>
+      </el-select>
+      <el-date-picker v-model="filters.completionDate" type="daterange" value-format="yyyy-MM-dd" range-separator="鑷�" start-placeholder="瀹屽伐寮�濮嬫棩鏈�" end-placeholder="瀹屽伐缁撴潫鏃ユ湡" style="width:270px"></el-date-picker>
+      <el-button type="primary" icon="el-icon-search" v-permission="'completion:query:query'" @click="load">鏌ヨ</el-button>
+      <el-button icon="el-icon-refresh" @click="reset">閲嶇疆</el-button>
+    </div>
+
+    <div class="table-panel">
+      <el-table v-loading="loading" :data="displayRows" border size="small" :span-method="spanMethod" row-key="rowKey">
+        <el-table-column label="搴忓彿" width="62" align="center">
+          <template slot-scope="{ row }">{{ row.faultIndex }}</template>
+        </el-table-column>
+        <el-table-column prop="faultCode" label="鏁呴殰缂栧彿" width="150"></el-table-column>
+        <el-table-column prop="xNo" label="rk_X鍙�" width="72"></el-table-column>
+        <el-table-column prop="equipmentName" label="璁惧鍚嶇О" width="130" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="engineeringNameDesc" label="宸ョ▼鍚嶇О鍙婃崯鍧忔儏鍐�" min-width="190" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="reportUnit" label="涓婃姤鍗曚綅" width="130" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="reportDate" label="涓婃姤鏃ユ湡" width="105"></el-table-column>
+
+        <el-table-column prop="workOrderNo" label="娲惧伐鍗曠紪鍙�" width="155"></el-table-column>
+        <el-table-column prop="contractorName" label="鎵夸慨鍗曚綅" width="150" show-overflow-tooltip></el-table-column>
+        <el-table-column label="瀹屽伐鐘舵��" width="100">
+          <template slot-scope="{ row }">
+            <el-tag size="mini" :type="completionMeta(row.completionStatus).type">{{ completionMeta(row.completionStatus).text }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="engineeringNo" label="宸ョ▼缂栧彿" width="145"></el-table-column>
+        <el-table-column label="寮�宸� / 瀹屽伐" width="190">
+          <template slot-scope="{ row }">{{ formatPeriod(row.startDate, row.finishDate) }}</template>
+        </el-table-column>
+        <el-table-column prop="contractorPerson" label="鎵夸慨浜�" width="100"></el-table-column>
+        <el-table-column label="瀹屽伐澶勭悊鎯呭喌" min-width="260">
+          <template slot-scope="{ row }">
+            <div v-if="row.solutions && row.solutions.length" class="detail-list">
+              <div v-for="(item, index) in row.solutions" :key="index" class="detail-line">
+                <span class="detail-index">{{ index + 1 }}</span>
+                <span>{{ item.content || '-' }}</span>
+              </div>
+            </div>
+            <span v-else class="muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="鏇存崲鍣ㄦ潗" min-width="300">
+          <template slot-scope="{ row }">
+            <div v-if="row.materials && row.materials.length" class="detail-list">
+              <div v-for="(item, index) in row.materials" :key="index" class="material-line">
+                <span class="detail-index">{{ index + 1 }}</span>
+                <div>
+                  <div><strong>{{ item.name || '-' }}</strong><span v-if="item.specification"> / {{ item.specification }}</span><span v-if="item.quantity != null"> 脳 {{ item.quantity }}{{ item.unit || '' }}</span></div>
+                  <div class="material-meta">{{ materialMeta(item) }}</div>
+                </div>
+              </div>
+            </div>
+            <span v-else class="muted">-</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="qualityInfo" label="淇悗璐ㄩ噺鎯呭喌" min-width="180" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="reasonAnalysis" label="鍘熷洜鍒嗘瀽" min-width="180" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="submitTime" label="鎻愪氦鏃堕棿" width="160"></el-table-column>
+      </el-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import api from '@/api'
+import { completionStatuses, statusMeta } from '@/utils/dict'
+
+export default {
+  name: 'CompletionQuery',
+  data() {
+    return {
+      loading: false,
+      completionStatuses,
+      rows: [],
+      filters: {
+        keyword: '',
+        contractorName: '',
+        completionStatus: '',
+        completionDate: []
+      }
+    }
+  },
+  computed: {
+    displayRows() {
+      const rows = []
+      this.rows.forEach((fault, faultIndex) => {
+        const workOrders = fault.workOrders || []
+        const total = workOrders.length || 1
+        if (!workOrders.length) {
+          rows.push({ ...fault, faultIndex: faultIndex + 1, faultRowspan: 1, rowKey: fault.faultId + '-empty' })
+          return
+        }
+        workOrders.forEach((workOrder, index) => {
+          rows.push({
+            ...fault,
+            ...workOrder,
+            faultIndex: faultIndex + 1,
+            faultRowspan: index === 0 ? total : 0,
+            rowKey: fault.faultId + '-' + workOrder.workOrderId
+          })
+        })
+      })
+      return rows
+    }
+  },
+  created() {
+    this.load()
+  },
+  methods: {
+    load() {
+      this.loading = true
+      const range = this.filters.completionDate || []
+      api.get('/work-orders/completion/query', {
+        params: {
+          keyword: this.filters.keyword || undefined,
+          contractorName: this.filters.contractorName || undefined,
+          completionStatus: this.filters.completionStatus || undefined,
+          startDate: range[0] || undefined,
+          endDate: range[1] || undefined
+        }
+      }).then(res => {
+        this.rows = res.data || []
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    reset() {
+      this.filters = { keyword: '', contractorName: '', completionStatus: '', completionDate: [] }
+      this.load()
+    },
+    completionMeta(value) {
+      return statusMeta(completionStatuses, value)
+    },
+    spanMethod({ row, columnIndex }) {
+      if (columnIndex <= 6) {
+        return row.faultRowspan === 0 ? { rowspan: 0, colspan: 0 } : { rowspan: row.faultRowspan, colspan: 1 }
+      }
+      return { rowspan: 1, colspan: 1 }
+    },
+    formatPeriod(startDate, finishDate) {
+      return [startDate || '-', finishDate || '-'].join(' 鑷� ')
+    },
+    materialMeta(item) {
+      const parts = []
+      if (item.technicalParameter) parts.push(item.technicalParameter)
+      if (item.source) parts.push('鏉ユ簮锛�' + item.source)
+      if (item.outboundNo) parts.push('鍑哄簱鍗曪細' + item.outboundNo)
+      if (item.certificate) parts.push('鍚堟牸璇佹槑锛�' + item.certificate)
+      if (item.remark) parts.push('澶囨敞锛�' + item.remark)
+      return parts.join('锛�') || '-'
+    }
+  }
+}
+</script>
+
+<style scoped>
+.table-panel {
+  overflow-x: auto;
+}
+.detail-list {
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+}
+.detail-line,
+.material-line {
+  display: flex;
+  align-items: flex-start;
+  gap: 6px;
+  line-height: 20px;
+}
+.material-line > div {
+  min-width: 0;
+}
+.detail-index {
+  flex: 0 0 auto;
+  min-width: 16px;
+  height: 16px;
+  margin-top: 2px;
+  border-radius: 50%;
+  color: #fff;
+  background: #7b8fa6;
+  font-size: 11px;
+  line-height: 16px;
+  text-align: center;
+}
+.material-meta {
+  color: #7b8a9a;
+  font-size: 12px;
+}
+.muted {
+  color: #a3afbc;
+}
+</style>
diff --git a/emergency-repair-web/src/views/system/MenuManage.vue b/emergency-repair-web/src/views/system/MenuManage.vue
index 095bfda..ec72216 100644
--- a/emergency-repair-web/src/views/system/MenuManage.vue
+++ b/emergency-repair-web/src/views/system/MenuManage.vue
@@ -44,7 +44,9 @@
 
     <el-dialog :title="form.id ? '缂栬緫鑿滃崟' : '鏂板鑿滃崟'" :visible.sync="dialogVisible" width="640px" append-to-body>
       <el-form ref="form" :model="form" :rules="rules" label-width="105px">
-        <el-form-item label="涓婄骇鑿滃崟"><el-input :value="parentName" readonly></el-input></el-form-item>
+        <el-form-item label="涓婄骇鑿滃崟">
+          <el-cascader v-model="form.pid" :options="parentOptions" :props="parentProps" :show-all-levels="false" filterable clearable placeholder="璇烽�夋嫨涓婄骇鑿滃崟" style="width:100%"></el-cascader>
+        </el-form-item>
         <el-form-item label="鑿滃崟绫诲瀷">
           <el-radio-group v-model="form.menuType">
             <el-radio-button v-for="item in typeOptions" :key="item.value" :label="item.value">{{ item.label }}</el-radio-button>
@@ -89,7 +91,6 @@
       dialogVisible: false,
       saving: false,
       form: {},
-      parent: null,
       rules: { name: [{ required: true, message: '璇疯緭鍏ヨ彍鍗曞悕绉�', trigger: 'blur' }] },
       buttonDialogVisible: false,
       buttonSaving: false,
@@ -99,15 +100,25 @@
     }
   },
   computed: {
-    parentName() {
-      if (!this.parent) return this.form.pid && this.form.pid !== 0 ? '涓婄骇鑿滃崟' : '鏃狅紙涓�绾ц彍鍗曪級'
-      return this.parent.name
+    parentProps() {
+      return { value: 'id', label: 'name', children: 'children', checkStrictly: true, emitPath: false, expandTrigger: 'hover' }
+    },
+    selectedParent() {
+      const pid = String(this.form.pid || '0')
+      if (pid === '0') return null
+      return this.flat.find(item => String(item.id) === pid) || null
+    },
+    parentOptions() {
+      const excluded = this.excludedMenuIds()
+      const allowedType = this.form.menuType === 2 ? 1 : 0
+      const nodes = this.buildParentOptions(this.tree, allowedType, excluded)
+      return this.form.menuType === 2 ? nodes : [{ id: '0', name: '鏃狅紙涓�绾ц彍鍗曪級', children: nodes }]
     },
     typeOptions() {
-      if (this.parent && this.parent.menuType === 1) {
+      if (this.selectedParent && this.selectedParent.menuType === 1) {
         return [{ label: '鎸夐挳', value: 2 }]
       }
-      if (this.parent && this.parent.menuType === 0) {
+      if (this.selectedParent && this.selectedParent.menuType === 0) {
         return [{ label: '鐩綍', value: 0 }, { label: '鑿滃崟', value: 1 }]
       }
       if (this.form.id && this.form.menuType === 2) {
@@ -131,10 +142,9 @@
       return type === 2 ? 'warning' : type === 0 ? 'info' : 'primary'
     },
     openCreate(parent) {
-      this.parent = parent || null
       const menuType = parent && parent.menuType === 1 ? 2 : parent ? 1 : 0
       this.form = {
-        pid: parent ? parent.id : 0,
+        pid: parent ? String(parent.id) : '0',
         name: '',
         url: '',
         icon: menuType === 2 ? '' : 'el-icon-tickets',
@@ -146,8 +156,7 @@
       this.dialogVisible = true
     },
     openEdit(row) {
-      this.parent = this.flat.find(item => item.id === row.pid) || null
-      this.form = { ...row }
+      this.form = { ...row, pid: row.pid == null || row.pid === 0 ? '0' : String(row.pid) }
       this.dialogVisible = true
     },
     save() {
@@ -158,6 +167,7 @@
           return
         }
         const payload = { ...this.form }
+        if (payload.pid === null || payload.pid === undefined || payload.pid === '') payload.pid = '0'
         if (payload.menuType === 2) {
           payload.url = null
           payload.icon = null
@@ -172,6 +182,27 @@
         }).finally(() => { this.saving = false })
       })
     },
+    excludedMenuIds() {
+      const excluded = new Set()
+      const walk = (nodes, underSelf) => {
+        ;(nodes || []).forEach(node => {
+          const next = underSelf || String(node.id) === String(this.form.id)
+          if (next) excluded.add(String(node.id))
+          walk(node.children, next)
+        })
+      }
+      walk(this.tree, false)
+      return excluded
+    },
+    buildParentOptions(nodes, allowedType, excluded) {
+      return (nodes || [])
+        .filter(node => node.menuType === allowedType && !excluded.has(String(node.id)))
+        .map(node => ({
+          id: String(node.id),
+          name: node.name,
+          children: this.buildParentOptions(node.children, allowedType, excluded)
+        }))
+    },
     remove(row) {
       this.$confirm(`纭畾鍒犻櫎鈥�${row.name}鈥濆悧锛焋, '鍒犻櫎纭', { type: 'warning' })
         .then(() => api.delete('/system/menu/' + row.id))
diff --git "a/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md" "b/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
index b1b34d0..0aae7c0 100644
--- "a/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
+++ "b/requirement/\344\272\244\346\216\245\346\226\207\344\273\266_2026.10.3.md"
@@ -577,3 +577,53 @@
 - 閿欒鏃ュ織鐩墠瑕嗙洊杩涘叆 Spring MVC 鍏ㄥ眬寮傚父澶勭悊鐨勫紓甯革紱鍚姩闃舵寮傚父銆佸紓姝ョ嚎绋嬪紓甯歌嫢缁曡繃璇ュ鐞嗗櫒锛屾殏涓嶄細鑷姩璁板綍銆�
 - 鏈疆浠g爜涓庝氦鎺ヨ褰曚竴骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
 - `.codex-run`銆丱ffice 閿佹枃浠跺強 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt` 浠嶄负鏈湴涓存椂鏂囦欢锛屼笉搴旀彁浜ゃ��
+
+## 鍗佸洓銆�2026-10-06 鑱旇皟鏀跺熬锛堥渶姹� 23-25锛�
+
+| 搴忓彿 | 闇�姹� | 鐘舵�� | 瀹炵幇涓庨獙璇� |
+| --- | --- | --- | --- |
+| 23 | 绯荤粺鍚嶇О銆佺増鏈敼涓哄叧閿瘝閰嶇疆锛岀増鏈孩鑹叉樉绀� | 宸插畬鎴� | codename 涓� `rk_SystemName`銆乣rk_SystemVer`锛涚櫥褰曢〉銆侀〉澶淬�丩ogo 鍜屾祻瑙堝櫒鏍囬缁熶竴璇诲彇閰嶇疆锛岄〉澶寸増鏈负绾㈣壊 |
+| 24 | 瀹屽伐淇℃伅鏌ラ槄锛屽鏁呴殰鍙婂鏉″畬宸ユ槑缁嗗睍绀� | 宸插畬鎴� | 澧炲姞 `/completion/query` 鍜� `GET /api/work-orders/completion/query`锛涙寜鏁呴殰姹囨�伙紝鍚屼竴鏁呴殰鐨勫寮犳淳宸ュ崟灞曞紑涓哄琛岋紝鏁呴殰淇℃伅鍒椾娇鐢� `rowspan` 绾靛悜鍚堝苟锛屽鐞嗘儏鍐靛拰鍣ㄦ潗鏀寔澶氭潯 |
+| 25 | 鑿滃崟绠$悊缂栬緫鏃跺彲淇敼鐖惰妭鐐� | 宸插畬鎴� | 涓婄骇鑿滃崟鏀逛负绾ц仈閫夋嫨鍣紝鎺掗櫎鑷韩鍜屽叏閮ㄤ笅绾э紱鍚庣澧炲姞鐖惰妭鐐广�佽嚜寮曠敤鍜屽惊鐜眰绾ф牎楠� |
+
+### 14.1 涓昏鍙樻洿鏂囦欢
+
+鍚庣锛�
+
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java`锛堟柊澧烇級
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java`
+- `emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java`
+
+鍓嶇锛�
+
+- `emergency-repair-web/src/views/completion/CompletionQuery.vue`锛堟柊澧烇級
+- `emergency-repair-web/src/views/system/MenuManage.vue`
+- `emergency-repair-web/src/components/AppHeader.vue`銆乣Login.vue`銆乣RepairLogo.vue`銆乣layout/AppLayout.vue`銆乣router/index.js`銆乣utils/sensitive.js`銆乣main.js`
+- `emergency-repair-web/public/sensitive-words.json`銆乣public/index.html`
+- `requirement/鏁忔劅璇嶅瓧鍏�.json`
+
+### 14.2 瀹屽伐淇℃伅鏌ラ槄鍙e緞
+
+- 缁撴灉鎸夋晠闅滄眹鎬伙紝涓嶆寜娲惧伐鍗曟眹鎬汇��
+- 涓�涓晠闅滄湁澶氬紶宸插~鎶ュ畬宸ヤ俊鎭殑娲惧伐鍗曟椂锛岃〃鏍煎睍寮�涓哄琛岋紱搴忓彿銆佹晠闅滅紪鍙枫�乺k_X鍙枫�佽澶囧悕绉般�佸伐绋嬪悕绉板強鎹熷潖鎯呭喌銆佷笂鎶ュ崟浣嶃�佷笂鎶ユ棩鏈熻繖 7 鍒楀叡鐢ㄦ晠闅滆锛屼娇鐢ㄥ崟鍏冩牸绾靛悜鍚堝苟銆�
+- 娲惧伐鍗曠紪鍙枫�佹壙淇崟浣嶃�佸畬宸ョ姸鎬併�佸伐绋嬬紪鍙枫�佸紑宸�/瀹屽伐銆佹壙淇汉銆佸畬宸ュ鐞嗘儏鍐点�佹洿鎹㈠櫒鏉愩�佷慨鍚庤川閲忋�佸師鍥犲垎鏋愩�佹彁浜ゆ椂闂存寜娲惧伐鍗曞睍绀恒��
+- 鏌ヨ鏀寔鍏抽敭璇嶃�佹壙淇崟浣嶃�佸畬宸ョ姸鎬佸拰瀹屽伐鏃ユ湡锛涘叧閿瘝鍚屾椂鍖归厤鏁呴殰銆佹淳宸ュ崟銆佸伐绋嬬紪鍙枫�佹壙淇汉銆佸鐞嗘儏鍐靛拰鍣ㄦ潗銆�
+
+### 14.3 楠岃瘉缁撴灉
+
+- 鍚庣 `mvn -B -DskipTests compile` 閫氳繃锛涘墠绔� `npm run build` 閫氳繃锛屼粎鏈夋棦鏈夊寘浣撶Н鍛婅銆�
+- `node .codex-run/verify-req23-25.js` 閫氳繃锛氱郴缁熷悕绉般�佺増鏈拰绾㈣壊鏍峰紡姝e父锛涘畬宸ユ煡闃呮甯革紱鑿滃崟缂栬緫鐖惰妭鐐光�滄晠闅滅鐞嗏�濆彲缂栬緫銆�
+- 涓存椂鏋勯�犲悓涓�鏁呴殰涓ゅ紶娲惧伐鍗曘�佷袱鏉″畬宸ヨ褰曪紝鎺ュ彛杩斿洖 1 涓晠闅滆妭鐐瑰拰 2 寮犳淳宸ュ崟锛屽悇甯﹀鐞嗘儏鍐靛拰鍣ㄦ潗鏄庣粏銆�
+- 娴忚鍣ㄦ樉绀� 2 寮犳淳宸ュ崟琛岋紝鏁呴殰缂栧彿鍙嚭鐜� 1 娆★紝7 涓晠闅滀俊鎭垪鍧囦负 `rowspan=2`锛涗袱寮犲崟鐨勬槑缁嗗潎姝e父锛涙埅鍥� `.codex-run/completion-query-merge-verified.png`銆備复鏃舵暟鎹凡娓呯悊銆�
+- 鑿滃崟绾ц仈鐖惰妭鐐规帴鍙e疄娴嬪彲姝e父绉诲姩瀛愯彍鍗曪紱閫夋嫨鑷韩鎴栦笅绾ф椂琚悗绔嫆缁濓紝涓存椂鑿滃崟宸叉竻鐞嗐��
+
+### 14.4 褰撳墠鏈嶅姟涓庢帴鎵嬫敞鎰�
+
+- 鍓嶇 `http://localhost:8082`锛涘悗绔� `http://127.0.0.1:8091`锛汥M8 `127.0.0.1:5236`锛汦dge 璋冭瘯绔彛 `9222`銆�
+- 鐧诲綍 `admin / admin123`锛涙暟鎹簱 `SYSDBA/SYSDBA`锛屼笟鍔� schema `LX`銆�
+- 瀹屽伐鏌ラ槄褰撳墠鍙睍绀哄凡鏈� `T_COMPLETION_INFO` 瀹屽伐濉姤璁板綍鐨勬淳宸ュ崟锛屽苟涓旀帴鍙f湭鍒嗛〉銆�
+- 鏈疆浠g爜銆侀渶姹傛枃浠跺拰浜ゆ帴璁板綍涓�骞舵彁浜ゅ苟鎺ㄩ�佸叕鍙� Git 浠撳簱 `main` 鍒嗘敮銆�
+- `.codex-run`銆丱ffice 閿佹枃浠跺強 `emergency-repair-web/public/鏂板缓 鏂囨湰鏂囨。.txt` 浠嶄负鏈湴涓存椂鏂囦欢锛屼笉搴旀彁浜ゃ��
diff --git "a/requirement/\346\224\271\350\277\23320261003.txt" "b/requirement/\346\224\271\350\277\23320261003.txt"
index 9959a47..25191e8 100644
--- "a/requirement/\346\224\271\350\277\23320261003.txt"
+++ "b/requirement/\346\224\271\350\277\23320261003.txt"
@@ -26,4 +26,14 @@
 
 17銆佽鑹茬鐞嗙敤鎴疯缃鍔犱竴涓叏閮�/宸茶缃垏鎹㈠姛鑳�
 18銆佸鍔犲姛鑳芥樉绀虹粍缁囨満鏋勶紙閮ㄩ棬鏍戣〃锛�
-19銆佹壙淇崟浣嶅簲璇ュ埌閮ㄩ棬琛ㄩ噷鎵綝EPT_TYPE鍒椾负cj鐨勮褰�
\ No newline at end of file
+19銆佹壙淇崟浣嶅簲璇ュ埌閮ㄩ棬琛ㄩ噷鎵綝EPT_TYPE鍒椾负cj鐨勮褰�
+
+20銆佽彍鍗曟潈闄愮鐞嗭紝鍙屾潈闄愮鐞嗗簲璇ュ垎2姝ワ紝 绗�1姝ヨ缃鎴锋潈闄愶紝绗�2姝ヨ缃鑹叉潈闄愶紝 鐩墠鐢ㄦ埛鏈韩鏄病鏈夌鎴风殑锛屾墍浠ヨ繖涓鎴锋潈闄愯缃氨娌″繀瑕佷簡锛屼繚鐣欒鑹叉潈闄愯缃氨鍙互浜嗭紝鏉冮檺妫�鏌ユ瑕佸弽鏄�
+21銆佽彍鍗曟潈闄愮鐞嗭紝杩欎釜鑿滃崟涓嶈锛屽湪瑙掕壊绠$悊澧炲姞涓�涓潈闄愯缃寜閽紝绫讳技鐢ㄦ埛璁剧疆閭d釜鎸夐挳閭f牱
+22銆佺郴缁熺鐞嗕笅闈㈠鍔�2绾ц彍鍗曠粍鑺傜偣锛� 绯荤粺閰嶇疆锛堥櫎浜嗘潈闄愰厤缃�佹棩蹇楃鐞嗙鐞嗕笅闈㈠摢浜涜彍鍗曞锛屽叾浠栬彍鍗曟斁鍒扮郴缁熼厤缃笅闈級銆佹潈闄愰厤缃紙鐢ㄦ埛绠$悊銆佽鑹茬鐞嗐�佽彍鍗曠鐞嗭級銆佹棩蹇楃鐞嗭紙鐧诲綍鏃ュ織銆佹搷浣滄棩蹇椼�侀敊璇棩蹇楋級
+   閿欒鏃ュ織涓烘柊澧炲姛鑳斤紝璁板綍绋嬪簭杩愯鍑虹幇鐨刡ug
+
+23銆佺郴缁熷悕绉般�佺綉缁滅増绛夊仛鎴愬叧閿瘝閰嶇疆锛宑odename鍒嗗埆涓簉k_SystemName銆乺k_SystemVer 鐗堟湰鐢ㄧ孩鑹插瓧浣撴樉绀�
+24銆佸鍔犲畬宸ヤ俊鎭煡闃呭姛鑳斤紝鍦ㄤ竴涓〃灞曠ず澶氫釜鏁呴殰鍙婂畬宸ヤ俊鎭睍绀猴紙鍏朵腑瀹屽伐澶勭悊鎯呭喌銆佹洿鎹㈠櫒鏉愰兘鏄涓級
+25銆佽彍鍗曠鐞嗙紪杈戞椂锛屽簲璇ュ彲浠ヤ慨鏀圭埗鑺傜偣
+
diff --git "a/requirement/\346\225\217\346\204\237\350\257\215\345\255\227\345\205\270.json" "b/requirement/\346\225\217\346\204\237\350\257\215\345\255\227\345\205\270.json"
index b879053..bef1d57 100644
--- "a/requirement/\346\225\217\346\204\237\350\257\215\345\255\227\345\205\270.json"
+++ "b/requirement/\346\225\217\346\204\237\350\257\215\345\255\227\345\205\270.json"
@@ -1,6 +1,18 @@
 {
   "sensitive_words": [
     {
+      "codename": "rk_SystemName",
+      "real_name": "涓存姠淇鐞嗙郴缁�",
+      "scope": "documents,ui,code",
+      "note": "绯荤粺鍚嶇О锛岀櫥褰曢〉銆侀〉澶村強娴忚鍣ㄦ爣棰樼粺涓�浣跨敤璇ラ厤缃�"
+    },
+    {
+      "codename": "rk_SystemVer",
+      "real_name": "缃戠粶鐗�",
+      "scope": "documents,ui,code",
+      "note": "绯荤粺鐗堟湰锛岄〉澶翠腑浣跨敤绾㈣壊瀛椾綋鏄剧ず"
+    },
+    {
       "codename": "rk_X鍙�",
       "real_name": "鑸峰彿",
       "scope": "documents,ui,code",

--
Gitblit v1.9.1