zzw
20 小时以前 1332cc511936d7dbe804921bc733c83d5e4d1400
feat: add completion query and configurable system identity

- add fault-grouped completion query with merged detail cells
- make system name and version keyword-configurable
- allow editing menu parent with cycle validation
- update handoff records for requirements 23-25
2个文件已添加
19个文件已修改
681 ■■■■■ 已修改文件
HANDOFF.md 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/bootstrap/DataInitializer.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/controller/WorkOrderController.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/business/service/BusinessService.java 155 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/ButtonPermissionCatalog.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-server/src/main/java/com/emergencyrepair/system/service/SystemService.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/index.html 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/public/sensitive-words.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/AppHeader.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/components/RepairLogo.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/layout/AppLayout.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/main.js 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/router/index.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/utils/sensitive.js 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/Login.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/completion/CompletionQuery.vue 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
emergency-repair-web/src/views/system/MenuManage.vue 53 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/交接文件_2026.10.3.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/改进20261003.txt 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
requirement/敏感词字典.json 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
HANDOFF.md
@@ -781,3 +781,48 @@
- é”™è¯¯æ—¥å¿—当前覆盖进入 Spring MVC å…¨å±€å¼‚常处理的异常;启动阶段异常、异步线程中未进入该处理器的异常暂不会自动落库。
- ä¸è¦æäº¤ `.codex-run`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
## 20. æ”¶å°¾å¿«ç…§ï¼ˆ2026-10-06 å‘¨ä¸€ï¼Œéœ€æ±‚ 23-25 ç³»ç»Ÿé…ç½®ã€å®Œå·¥æŸ¥é˜…与菜单父节点)
**本轮完成内容**
- éœ€æ±‚ 23:系统名称和版本改为关键词配置,codename åˆ†åˆ«ä¸º `rk_SystemName`、`rk_SystemVer`。登录页、页头、Logo æ›¿ä»£æ–‡æœ¬å’Œæµè§ˆå™¨æ ‡é¢˜ä½¿ç”¨é…ç½®å€¼ï¼›é¡µå¤´ç‰ˆæœ¬ä½¿ç”¨çº¢è‰²æ˜¾ç¤ºã€‚
- éœ€æ±‚ 24:新增“完工信息查阅”功能,菜单 `/completion/query`,接口 `GET /api/work-orders/completion/query`。查询结果按故障汇总,一个故障展开多张派工单;故障编号、rk_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`。临时数据库记录已清理。
- èœå•父节点接口实测:临时创建父子菜单成功;把父菜单移动到其子菜单下返回“上级菜单不能选择当前菜单的下级”;把子菜单移动到其他父节点成功;临时菜单已清理。
**当前运行状态(2026-10-06)**
- å‰ç«¯ `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge 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`、Office é”æ–‡ä»¶æˆ– `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt`。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
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"));
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));
emergency-repair-server/src/main/java/com/emergencyrepair/business/dto/CompletionFaultQueryItem.java
New file
@@ -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<>();
    }
}
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) {
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"));
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));
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>
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",
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; }
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>
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'
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')
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' } },
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.
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">
emergency-repair-web/src/views/completion/CompletionQuery.vue
New file
@@ -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>
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))
requirement/½»½ÓÎļþ_2026.10.3.md
@@ -577,3 +577,53 @@
- é”™è¯¯æ—¥å¿—目前覆盖进入 Spring MVC å…¨å±€å¼‚常处理的异常;启动阶段异常、异步线程异常若绕过该处理器,暂不会自动记录。
- æœ¬è½®ä»£ç ä¸Žäº¤æŽ¥è®°å½•一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
## åå››ã€2026-10-06 è”调收尾(需求 23-25)
| åºå· | éœ€æ±‚ | çŠ¶æ€ | å®žçŽ°ä¸ŽéªŒè¯ |
| --- | --- | --- | --- |
| 23 | ç³»ç»Ÿåç§°ã€ç‰ˆæœ¬æ”¹ä¸ºå…³é”®è¯é…ç½®ï¼Œç‰ˆæœ¬çº¢è‰²æ˜¾ç¤º | å·²å®Œæˆ | codename ä¸º `rk_SystemName`、`rk_SystemVer`;登录页、页头、Logo å’Œæµè§ˆå™¨æ ‡é¢˜ç»Ÿä¸€è¯»å–配置,页头版本为红色 |
| 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 å®Œå·¥ä¿¡æ¯æŸ¥é˜…口径
- ç»“果按故障汇总,不按派工单汇总。
- ä¸€ä¸ªæ•…障有多张已填报完工信息的派工单时,表格展开为多行;序号、故障编号、rk_X号、设备名称、工程名称及损坏情况、上报单位、上报日期这 7 åˆ—共用故障行,使用单元格纵向合并。
- æ´¾å·¥å•编号、承修单位、完工状态、工程编号、开工/完工、承修人、完工处理情况、更换器材、修后质量、原因分析、提交时间按派工单展示。
- æŸ¥è¯¢æ”¯æŒå…³é”®è¯ã€æ‰¿ä¿®å•位、完工状态和完工日期;关键词同时匹配故障、派工单、工程编号、承修人、处理情况和器材。
### 14.3 éªŒè¯ç»“æžœ
- åŽç«¯ `mvn -B -DskipTests compile` é€šè¿‡ï¼›å‰ç«¯ `npm run build` é€šè¿‡ï¼Œä»…有既有包体积告警。
- `node .codex-run/verify-req23-25.js` é€šè¿‡ï¼šç³»ç»Ÿåç§°ã€ç‰ˆæœ¬å’Œçº¢è‰²æ ·å¼æ­£å¸¸ï¼›å®Œå·¥æŸ¥é˜…正常;菜单编辑父节点“故障管理”可编辑。
- ä¸´æ—¶æž„造同一故障两张派工单、两条完工记录,接口返回 1 ä¸ªæ•…障节点和 2 å¼ æ´¾å·¥å•,各带处理情况和器材明细。
- æµè§ˆå™¨æ˜¾ç¤º 2 å¼ æ´¾å·¥å•行,故障编号只出现 1 æ¬¡ï¼Œ7 ä¸ªæ•…障信息列均为 `rowspan=2`;两张单的明细均正常;截图 `.codex-run/completion-query-merge-verified.png`。临时数据已清理。
- èœå•级联父节点接口实测可正常移动子菜单;选择自身或下级时被后端拒绝,临时菜单已清理。
### 14.4 å½“前服务与接手注意
- å‰ç«¯ `http://localhost:8082`;后端 `http://127.0.0.1:8091`;DM8 `127.0.0.1:5236`;Edge è°ƒè¯•端口 `9222`。
- ç™»å½• `admin / admin123`;数据库 `SYSDBA/SYSDBA`,业务 schema `LX`。
- å®Œå·¥æŸ¥é˜…当前只展示已有 `T_COMPLETION_INFO` å®Œå·¥å¡«æŠ¥è®°å½•的派工单,并且接口未分页。
- æœ¬è½®ä»£ç ã€éœ€æ±‚文件和交接记录一并提交并推送公司 Git ä»“库 `main` åˆ†æ”¯ã€‚
- `.codex-run`、Office é”æ–‡ä»¶åŠ `emergency-repair-web/public/新建 æ–‡æœ¬æ–‡æ¡£.txt` ä»ä¸ºæœ¬åœ°ä¸´æ—¶æ–‡ä»¶ï¼Œä¸åº”提交。
requirement/¸Ä½ø20261003.txt
@@ -26,4 +26,14 @@
17、角色管理用户设置增加一个全部/已设置切换功能
18、增加功能显示组织机构(部门树表)
19、承修单位应该到部门表里找DEPT_TYPE列为cj的记录
19、承修单位应该到部门表里找DEPT_TYPE列为cj的记录
20、菜单权限管理,双权限管理应该分2步, ç¬¬1步设置租户权限,第2步设置角色权限, ç›®å‰ç”¨æˆ·æœ¬èº«æ˜¯æ²¡æœ‰ç§Ÿæˆ·çš„,所以这个租户权限设置就没必要了,保留角色权限设置就可以了,权限检查框要反显
21、菜单权限管理,这个菜单不要,在角色管理增加一个权限设置按钮,类似用户设置那个按钮那样
22、系统管理下面增加2级菜单组节点, ç³»ç»Ÿé…ç½®ï¼ˆé™¤äº†æƒé™é…ç½®ã€æ—¥å¿—管理管理下面哪些菜单外,其他菜单放到系统配置下面)、权限配置(用户管理、角色管理、菜单管理)、日志管理(登录日志、操作日志、错误日志)
   é”™è¯¯æ—¥å¿—为新增功能,记录程序运行出现的bug
23、系统名称、网络版等做成关键词配置,codename分别为rk_SystemName、rk_SystemVer ç‰ˆæœ¬ç”¨çº¢è‰²å­—体显示
24、增加完工信息查阅功能,在一个表展示多个故障及完工信息展示(其中完工处理情况、更换器材都是多个)
25、菜单管理编辑时,应该可以修改父节点
requirement/Ãô¸Ð´Ê×Öµä.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",