| | |
| | | # HANDOFF:临抢修软件需求与第一版实现 |
| | | |
| | | > 交接日期:2026-08-25(2026-08-26 晚 / 2026-09-07 / 2026-09-08 / 2026-09-09 核对 / 2026-09-10 / 2026-09-11 / 2026-09-14 / 2026-09-16 / 2026-09-17 / 2026-09-28 新会话准备 / 2026-09-28 专业配置与故障编号自动生成 / 2026-09-29 代码入库公司 Git 更新)| 用途:新开会话时先读本文件,即可无缝接续 |
| | | > 项目目录:`C:\Users\jcxiong\Documents\Codex\MyProject\EmergencyRepair` |
| | | > 交接日期:2026-08-25(2026-08-26 晚 / 2026-09-07 / 2026-09-08 / 2026-09-09 核对 / 2026-09-10 / 2026-09-11 / 2026-09-14 / 2026-09-16 / 2026-09-17 / 2026-09-28 新会话准备 / 2026-09-28 专业配置与故障编号自动生成 / 2026-09-29 代码入库公司 Git 更新 / 2026-10-02 本机环境迁移与前后端启动)| 用途:新开会话时先读本文件,即可无缝接续 |
| | | > 项目目录(2026-10-02 起为本机 `D:\projectJava\lqx`;行内旧路径 `C:\Users\jcxiong\Documents\Codex\MyProject\EmergencyRepair` 为上一台机器) |
| | | > 注意:2026-10-02 本机工具链与旧记录不同,构建、启动与工作方式约定以第 16 节为准。 |
| | | |
| | | ## 0. 最新交接(2026-09-28 新会话准备,优先阅读) |
| | | |
| | |
| | | 2. 决策表 B6–B10 待用户答复(`requirement\工程状态机设计.md` 第 4 节 + `requirement\决策表_待确认项.md` B 组),答完固化为正式设计。 |
| | | 3. AXU15 剩余待确认:字段来源与可改性(待确认清单第 13 条)、完工验收各环节角色与打印输出、修后质量评级与 AXU14 T员评分字段的关系。 |
| | | 4. 后续改动完成后直接推送到公司 Git 仓库 `main` 分支。 |
| | | |
| | | ## 16. 收尾快照(2026-10-02 周五,本机环境迁移与前后端启动) |
| | | |
| | | **环境迁移(重要,与第 0 节旧记录不同)** |
| | | - 代码已在本机重新拉取,工作目录改为 `D:\projectJava\lqx`,不再是 `D:\Codex\MyProject\EmergencyRepair`。 |
| | | - 仓库:`http://61.183.254.94:3000/r/EmergencyRepair.git`,分支 `main`,本地 HEAD `5d015d7`(docs: add shutdown snapshot for 2026-09-29),与 `origin/main` 一致。 |
| | | - 克隆时远端 HEAD 指向不存在的 ref,不会自动检出,需手动 `git checkout main`。凭据已存入 Windows 凭据管理器,`.git/config` 中不再有明文密码。 |
| | | - 本机工具链与旧机器不同,**第 0.9 节和第 8 节里“`apply_patch` 不可用、必须用 Python 读改写、不能用 `Start-Process`”的约定只适用于上一台机器**,本机 `apply_patch` 与 `Start-Process` 均可正常使用。 |
| | | - JDK:`C:\Program Files\Java\jdk1.8.0_172`。PATH 里的 java 是 JRE 1.8.0_481(没有 javac),构建前必须显式设置 `JAVA_HOME`。 |
| | | - Maven:`D:\solidDir\apache-maven-3.6.1\bin\mvn.cmd`(不在 PATH,需绝对路径调用)。 |
| | | - Node/npm:`C:\Program Files\nodejs`(Node v14.8.0 / npm 6.14.7,不在 PATH,需绝对路径调用)。 |
| | | - 达梦驱动 `dm.jdbc:DmJdbcDriver18:1.0` 在默认本地仓库缺失(只有下载失败标记),已从 `D:\solidDir\repo` 同名构件安装进 `C:\Users\Lenovo\.m2`;属环境准备,未改项目文件。 |
| | | - DM8:服务 `DmServiceDMSERVER` 运行中,监听 `127.0.0.1:5236`(PID 5692),账号 `SYSDBA/SYSDBA`,schema `LX`。 |
| | | |
| | | **本次完成的构建与运行** |
| | | - 后端 `mvn -B -DskipTests clean package` 通过,产物 `emergency-repair-server\target\emergency-repair-server-1.0.0-SNAPSHOT.jar`(约 27MB)。 |
| | | - 前端 `npm install` + `npm run build` 通过,产物 `emergency-repair-web\dist`,仅两个既有包体积告警。 |
| | | - 后端用 `java -jar target\emergency-repair-server-1.0.0-SNAPSHOT.jar` 后台运行,监听 `8091`(PID 61648);前端用 `node node_modules\@vue\cli-service\bin\vue-cli-service.js serve` 后台运行,监听 `8082`(PID 57052)。日志为项目根的 `backend.log` / `frontend.log`(`*.log` 已被 `.gitignore` 忽略)。 |
| | | - HTTP 验证:`POST /api/auth/login`(`admin/admin123`)返回 `code=200`;`GET http://localhost:8082/` 返回 200;前端 `/api` 代理到 8091 链路通。登录 `admin / admin123`。 |
| | | |
| | | **本次唯一代码改动(已获用户确认)** |
| | | - `emergency-repair-web\src\utils\sensitive.js`:敏感词运行时替换改为跳过表格数据单元格。 |
| | | - 口径:`<td>` 内的文本与属性保持代称;`<th>` 表头、页面标题、筛选标签、placeholder 照常替换。 |
| | | - 实现:新增 `isInsideTableCell()`(`closest('td')`),接入文本遍历、属性替换、`MutationObserver` 字符变化回调,以及 `replaceDom()` 直接收到文本节点的分支。 |
| | | - 验证:向运行页面注入探针表格(表头与数据格均写 `X号`),结果为 `<th>县号</th>`、`<td>X号</td>`;`npm run build` 通过。 |
| | | - 效率影响:表格场景通常更快;没有表格且高频更新普通文本时,仅增加 `closest('td')` 的少量判断开销。 |
| | | |
| | | **用户本次明确的工作方式要求(务必遵守)** |
| | | - 用户问“可不可以……”时只是要调查结论和方案,**不要直接改代码**;只有用户明确要求实施时才动文件。 |
| | | - 改动前先说明要改什么,口径不明确时先问清楚。 |
| | | |
| | | **下次接续** |
| | | 1. 先确认 `5236 / 8091 / 8082` 是否在监听,不在再按上面的命令重启(本机启动需显式设置 `JAVA_HOME`,Maven / Node 用绝对路径)。 |
| | | 2. `sensitive.js` 改动已获确认并随本次提交推送;`requirement\~$原始需求.docx` 是 Word 锁文件,不要提交。 |
| | | 3. 需求侧待确认项不变:决策表 B6–B10、AXU15 字段来源与可改性、完工验收各环节角色与打印输出、修后质量评级与 AXU14 T员评分字段的关系。 |
| | | 4. 后续改动完成后推送到公司 Git 仓库 `main` 分支。 |
| | |
| | | <repair-logo></repair-logo> |
| | | <span>临抢修管理系统</span> |
| | | <em>网络版</em> |
| | | <span v-if="menuPathText" class="menu-path" :title="menuPathText">{{ menuPathText }}</span> |
| | | </div> |
| | | <div class="header-right"> |
| | | <span><i class="el-icon-user-solid"></i> {{ realName }}</span> |
| | |
| | | export default { |
| | | name: 'AppHeader', |
| | | components: { RepairLogo }, |
| | | props: { |
| | | menuPath: { type: Array, default: () => [] } |
| | | }, |
| | | computed: { |
| | | menuPathText() { |
| | | return this.menuPath.join(' / ') |
| | | }, |
| | | realName() { |
| | | return sessionStorage.getItem('realName') || '系统管理员' |
| | | } |
| | |
| | | |
| | | <style scoped> |
| | | .app-header { |
| | | gap: 20px; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | |
| | | box-shadow: 0 1px 4px rgba(18, 41, 63, 0.24); |
| | | flex-shrink: 0; |
| | | } |
| | | .brand { display: flex; align-items: center; gap: 9px; font-size: 18px; font-weight: 600; } |
| | | .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; } |
| | | .header-right { display: flex; align-items: center; gap: 14px; font-size: 14px; } |
| | | .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; } |
| | | .header-right .el-button { color: #d8e9f7; } |
| | | .header-right .el-button:hover { color: #fff; } |
| | | </style> |
| | |
| | | <template> |
| | | <el-container class="layout"> |
| | | <app-header></app-header> |
| | | <app-header :menu-path="activeMenuPath"></app-header> |
| | | <el-container class="layout-body"> |
| | | <el-aside width="228px" class="layout-aside"> |
| | | <div class="aside-caption">业务导航</div> |
| | |
| | | </el-menu> |
| | | </el-aside> |
| | | <el-main class="layout-main"> |
| | | <transition name="fade" mode="out-in"> |
| | | <router-view></router-view> |
| | | </transition> |
| | | <div class="mdi-tabbar"> |
| | | <div ref="tabScroll" class="mdi-tabs-scroll"> |
| | | <div class="mdi-tabs"> |
| | | <div |
| | | v-for="doc in openDocs" |
| | | :key="doc.fullPath" |
| | | class="mdi-tab" |
| | | :class="{ 'is-active': isActiveDoc(doc) }" |
| | | :title="doc.title" |
| | | @click="activateDoc(doc)" |
| | | > |
| | | <i :class="doc.icon"></i> |
| | | <span class="mdi-tab-title">{{ doc.title }}</span> |
| | | <i v-if="doc.pinned" class="el-icon-lock mdi-tab-pin"></i> |
| | | <i v-else class="el-icon-close mdi-tab-close" title="关闭" @click.stop="closeDoc(doc)"></i> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div class="mdi-actions"> |
| | | <el-dropdown trigger="click" @command="handleCommand"> |
| | | <button class="mdi-actions-button" type="button" title="文档操作"><i class="el-icon-more"></i></button> |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <el-dropdown-item command="closeCurrent" :disabled="!activeDoc || activeDoc.pinned">关闭当前</el-dropdown-item> |
| | | <el-dropdown-item command="closeOthers" :disabled="!canCloseOthers">关闭其他</el-dropdown-item> |
| | | <el-dropdown-item command="closeAll" :disabled="openDocs.length <= 1">关闭全部</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </el-dropdown> |
| | | </div> |
| | | </div> |
| | | <div ref="workspace" class="mdi-workspace" @scroll="rememberScroll"> |
| | | <router-view v-slot="{ Component, route }"> |
| | | <keep-alive :include="cachedNames" :max="20"> |
| | | <component :is="Component" :key="viewKey(route)" /> |
| | | </keep-alive> |
| | | </router-view> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | |
| | | import AppHeader from '@/components/AppHeader.vue' |
| | | import MenuNode from '@/components/MenuNode.vue' |
| | | |
| | | const HOME_DOC = { |
| | | fullPath: '/dashboard', |
| | | title: '首页门户', |
| | | icon: 'el-icon-s-home', |
| | | cacheName: 'Dashboard', |
| | | pinned: true, |
| | | version: 1 |
| | | } |
| | | |
| | | const REPORT_TITLES = { |
| | | overview: '装备故障总体分析', |
| | | 'top-equipment': '故障高发设备Top', |
| | | 'by-team': '按T统计', |
| | | contractors: '厂家维修情况', |
| | | trend: '年度趋势' |
| | | } |
| | | |
| | | export default { |
| | | name: 'AppLayout', |
| | | components: { AppHeader, MenuNode }, |
| | | data() { |
| | | return { menus: [] } |
| | | return { |
| | | menus: [], |
| | | openDocs: [], |
| | | currentFullPath: '', |
| | | cacheVersions: {}, |
| | | scrollTops: {} |
| | | } |
| | | }, |
| | | computed: { |
| | | activeDoc() { |
| | | return this.openDocs.find(doc => doc.fullPath === this.currentFullPath) || null |
| | | }, |
| | | cachedNames() { |
| | | return [...new Set(this.openDocs.map(doc => doc.cacheName).filter(Boolean))] |
| | | }, |
| | | canCloseOthers() { |
| | | const keepCount = this.activeDoc && this.activeDoc.pinned ? 1 : 2 |
| | | return this.openDocs.length > keepCount |
| | | }, |
| | | activeMenuPath() { |
| | | const path = this.findMenuPath(this.menus, this.$route.path) |
| | | if (path.length) return path |
| | | return this.$route.meta.title ? [this.$route.meta.title] : [] |
| | | } |
| | | }, |
| | | watch: { |
| | | '$route': { |
| | | immediate: true, |
| | | handler(route) { this.syncRoute(route) } |
| | | } |
| | | }, |
| | | created() { |
| | | this.loadMenus() |
| | |
| | | api.get('/system/menu/nav').then(res => { |
| | | this.menus = (res.data || []).filter(item => item.visible !== 0) |
| | | }).catch(() => {}) |
| | | }, |
| | | syncRoute(route) { |
| | | if (!route || route.path === '/login') return |
| | | if (this.currentFullPath && this.$refs.workspace) { |
| | | this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop |
| | | } |
| | | this.ensureHomeDoc() |
| | | const fullPath = route.fullPath |
| | | let doc = this.openDocs.find(item => item.fullPath === fullPath) |
| | | if (!doc) { |
| | | doc = { |
| | | fullPath, |
| | | title: this.resolveTitle(route), |
| | | icon: this.resolveIcon(route.path), |
| | | cacheName: route.meta.cacheName || route.path, |
| | | pinned: false, |
| | | version: (this.cacheVersions[fullPath] || 0) + 1 |
| | | } |
| | | this.openDocs.push(doc) |
| | | } else { |
| | | doc.title = this.resolveTitle(route) |
| | | doc.icon = this.resolveIcon(route.path) |
| | | doc.cacheName = route.meta.cacheName || doc.cacheName |
| | | } |
| | | this.currentFullPath = fullPath |
| | | this.$nextTick(() => { |
| | | this.restoreScroll() |
| | | this.scrollActiveTab() |
| | | }) |
| | | }, |
| | | ensureHomeDoc() { |
| | | if (!this.openDocs.some(doc => doc.pinned)) { |
| | | this.openDocs.unshift({ ...HOME_DOC }) |
| | | } |
| | | }, |
| | | findMenuPath(items, targetPath, trail = []) { |
| | | for (const item of items || []) { |
| | | const nextTrail = [...trail, item.name] |
| | | if (item.url === targetPath) return nextTrail |
| | | const childPath = this.findMenuPath(item.children, targetPath, nextTrail) |
| | | if (childPath.length) return childPath |
| | | } |
| | | return [] |
| | | }, |
| | | resolveTitle(route) { |
| | | if (route.path.startsWith('/reports/')) { |
| | | return REPORT_TITLES[route.params.type] || route.meta.title || '统计报表' |
| | | } |
| | | return route.meta.title || route.name || route.path |
| | | }, |
| | | resolveIcon(path) { |
| | | if (path === '/dashboard') return 'el-icon-s-home' |
| | | 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('/acceptance/')) return 'el-icon-circle-check' |
| | | if (path.startsWith('/reports/')) return 'el-icon-data-analysis' |
| | | if (path.startsWith('/system/')) return 'el-icon-setting' |
| | | return 'el-icon-document' |
| | | }, |
| | | isActiveDoc(doc) { return doc.fullPath === this.currentFullPath }, |
| | | viewKey(route) { |
| | | const doc = this.openDocs.find(item => item.fullPath === route.fullPath) |
| | | return route.fullPath + '#' + (doc ? doc.version : 0) |
| | | }, |
| | | activateDoc(doc) { |
| | | if (!this.isActiveDoc(doc)) this.$router.push(doc.fullPath).catch(() => {}) |
| | | }, |
| | | closeDoc(doc) { |
| | | if (!doc || doc.pinned) return |
| | | const index = this.openDocs.findIndex(item => item.fullPath === doc.fullPath) |
| | | if (index < 0) return |
| | | const wasActive = this.isActiveDoc(doc) |
| | | this.cacheVersions[doc.fullPath] = doc.version + 1 |
| | | this.openDocs.splice(index, 1) |
| | | if (wasActive) { |
| | | const target = this.openDocs[index - 1] || this.openDocs[index] || this.openDocs[0] |
| | | this.$router.push(target.fullPath).catch(() => {}) |
| | | } else { |
| | | this.$nextTick(this.scrollActiveTab) |
| | | } |
| | | }, |
| | | handleCommand(command) { |
| | | if (command === 'closeCurrent') this.closeDoc(this.activeDoc) |
| | | if (command === 'closeOthers') this.closeOtherDocs() |
| | | if (command === 'closeAll') this.closeAllDocs() |
| | | }, |
| | | closeOtherDocs() { |
| | | if (!this.activeDoc) return |
| | | const keepPath = this.activeDoc.fullPath |
| | | this.openDocs.filter(doc => !doc.pinned && doc.fullPath !== keepPath).forEach(doc => { |
| | | this.cacheVersions[doc.fullPath] = doc.version + 1 |
| | | }) |
| | | this.openDocs = this.openDocs.filter(doc => doc.pinned || doc.fullPath === keepPath) |
| | | this.$nextTick(this.scrollActiveTab) |
| | | }, |
| | | closeAllDocs() { |
| | | this.openDocs.filter(doc => !doc.pinned).forEach(doc => { |
| | | this.cacheVersions[doc.fullPath] = doc.version + 1 |
| | | }) |
| | | this.openDocs = this.openDocs.filter(doc => doc.pinned) |
| | | this.$router.push('/dashboard').catch(() => {}) |
| | | }, |
| | | rememberScroll() { |
| | | if (this.currentFullPath && this.$refs.workspace) { |
| | | this.scrollTops[this.currentFullPath] = this.$refs.workspace.scrollTop |
| | | } |
| | | }, |
| | | restoreScroll() { |
| | | if (this.$refs.workspace) this.$refs.workspace.scrollTop = this.scrollTops[this.currentFullPath] || 0 |
| | | }, |
| | | scrollActiveTab() { |
| | | const active = this.$refs.tabScroll && this.$refs.tabScroll.querySelector('.mdi-tab.is-active') |
| | | if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' }) |
| | | } |
| | | } |
| | | } |
| | |
| | | .layout-body { flex: 1; min-height: 0; } |
| | | .layout-aside { border-right: 1px solid #d9e2eb; background: #f5f8fb; overflow-x: hidden; overflow-y: auto; } |
| | | .aside-caption { padding: 14px 18px 8px; color: #8090a2; font-size: 12px; letter-spacing: .08em; } |
| | | .layout-main { min-width: 0; padding: 0; overflow: auto; background: #eef2f6; } |
| | | .layout-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 0; overflow: hidden; background: #eef2f6; } |
| | | .el-menu { border-right: 0; } |
| | | .layout-aside ::v-deep .el-menu-item, |
| | | .layout-aside ::v-deep .el-submenu__title { |
| | |
| | | overflow-wrap: anywhere; |
| | | word-break: break-word; |
| | | } |
| | | .fade-enter-active, .fade-leave-active { transition: opacity .12s ease; } |
| | | .fade-enter, .fade-leave-to { opacity: 0; } |
| | | .mdi-tabbar { display: flex; flex: 0 0 40px; align-items: stretch; height: 40px; padding-top: 5px; border-bottom: 1px solid #cfdae5; background: #e3eaf1; } |
| | | .mdi-tabs-scroll { flex: 1; min-width: 0; overflow-x: auto; overflow-y: hidden; } |
| | | .mdi-tabs { display: inline-flex; align-items: flex-end; min-width: 100%; height: 100%; padding: 0 4px; } |
| | | .mdi-tab { display: inline-flex; flex: 0 0 auto; align-items: center; gap: 6px; max-width: 220px; height: 34px; margin-right: 2px; padding: 0 9px; border: 1px solid transparent; border-bottom: 0; border-radius: 4px 4px 0 0; color: #5c6f82; background: rgba(255, 255, 255, .42); cursor: pointer; font-size: 13px; user-select: none; } |
| | | .mdi-tab:hover { color: #245275; background: rgba(255, 255, 255, .72); } |
| | | .mdi-tab.is-active { height: 35px; color: #1f3348; background: #eef2f6; border-color: #cfdae5; box-shadow: inset 0 2px 0 #2c7fb8; } |
| | | .mdi-tab > i { flex: 0 0 auto; } |
| | | .mdi-tab-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| | | .mdi-tab-close, .mdi-tab-pin { width: 16px; color: #8191a2; text-align: center; } |
| | | .mdi-tab-close { border-radius: 50%; font-size: 12px; } |
| | | .mdi-tab-close:hover { color: #fff; background: #d9534f; } |
| | | .mdi-tab-pin { font-size: 12px; } |
| | | .mdi-actions { display: flex; flex: 0 0 auto; align-items: center; padding: 0 6px; border-left: 1px solid rgba(125, 147, 169, .22); } |
| | | .mdi-actions-button { width: 30px; height: 28px; padding: 0; border: 0; border-radius: 3px; color: #587087; background: transparent; cursor: pointer; font-size: 17px; } |
| | | .mdi-actions-button:hover { color: #1f5f91; background: rgba(255, 255, 255, .68); } |
| | | .mdi-workspace { position: relative; flex: 1; min-height: 0; overflow: auto; background: #eef2f6; } |
| | | </style> |
| | |
| | | path: '/', |
| | | component: AppLayout, |
| | | children: [ |
| | | { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, |
| | | { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue') }, |
| | | { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue') }, |
| | | { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'CMB' } }, |
| | | { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'JCC_ASSISTANT' } }, |
| | | { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), meta: { stage: 'JCC_DIRECTOR' } }, |
| | | { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue') }, |
| | | { path: 'fault/completion', component: () => import('@/views/completion/CompletionList.vue') }, |
| | | { path: 'completion/report', component: () => import('@/views/completion/CompletionReport.vue') }, |
| | | { path: 'acceptance/application', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'APPLICATION' } }, |
| | | { path: 'acceptance/inspector', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'INSPECTOR' } }, |
| | | { path: 'acceptance/leader', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'LEADER' } }, |
| | | { path: 'acceptance/print', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'PRINT' } }, |
| | | { path: 'acceptance/rating', component: () => import('@/views/acceptance/AcceptanceList.vue'), meta: { stage: 'RATING' } }, |
| | | { path: 'reports/:type', component: () => import('@/views/report/ReportPage.vue') }, |
| | | { path: 'system/contractors', component: () => import('@/views/system/ContractorManage.vue') }, |
| | | { path: 'system/product-tree', component: () => import('@/views/system/TreeManage.vue'), meta: { treeType: 'product' } }, |
| | | { path: 'system/base-equipment', component: () => import('@/views/system/TreeManage.vue'), meta: { treeType: 'base' } }, |
| | | { path: 'system/engineering-majors', component: () => import('@/views/system/EngineeringMajorManage.vue') }, |
| | | { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue') }, |
| | | { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue') }, |
| | | { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue') }, |
| | | { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue') }, |
| | | { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue') } |
| | | { path: 'dashboard', component: () => import('@/views/Dashboard.vue'), meta: { title: '首页门户', cacheName: 'Dashboard' } }, |
| | | { path: 'fault/report', component: () => import('@/views/fault/FaultReport.vue'), meta: { title: '故障上报', cacheName: 'FaultReport' } }, |
| | | { path: 'fault/review', component: () => import('@/views/fault/XcbReview.vue'), meta: { title: 'XCB助理审核', cacheName: 'XcbReview' } }, |
| | | { path: 'fault/approval/cmb', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'CMB' }, meta: { title: 'CMB领导审批', stage: 'CMB', cacheName: 'SimpleApproval' } }, |
| | | { path: 'fault/approval/jcc-assistant', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_ASSISTANT' }, meta: { title: 'JCC助理审批', stage: 'JCC_ASSISTANT', cacheName: 'SimpleApproval' } }, |
| | | { path: 'fault/approval/jcc-director', component: () => import('@/views/fault/SimpleApproval.vue'), props: { stage: 'JCC_DIRECTOR' }, meta: { title: 'JCC处长审批', stage: 'JCC_DIRECTOR', cacheName: 'SimpleApproval' } }, |
| | | { path: 'fault/dispatch', component: () => import('@/views/fault/DispatchPage.vue'), meta: { title: '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: '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' } }, |
| | | { path: 'acceptance/print', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'PRINT' }, meta: { title: '完工验收打印输出', stage: 'PRINT', cacheName: 'AcceptanceList' } }, |
| | | { path: 'acceptance/rating', component: () => import('@/views/acceptance/AcceptanceList.vue'), props: { stage: 'RATING' }, meta: { title: '修后质量评级', stage: 'RATING', cacheName: 'AcceptanceList' } }, |
| | | { path: 'reports/:type', component: () => import('@/views/report/ReportPage.vue'), props: route => ({ type: route.params.type }), meta: { title: '统计报表', cacheName: 'ReportPage' } }, |
| | | { path: 'system/contractors', component: () => import('@/views/system/ContractorManage.vue'), meta: { title: '承修单位管理', cacheName: 'ContractorManage' } }, |
| | | { path: 'system/product-tree', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'product' }, meta: { title: '产品结构树管理', treeType: 'product', cacheName: 'TreeManage' } }, |
| | | { path: 'system/base-equipment', component: () => import('@/views/system/TreeManage.vue'), props: { treeType: 'base' }, meta: { title: '设备基础库管理', treeType: 'base', cacheName: 'TreeManage' } }, |
| | | { path: 'system/engineering-majors', component: () => import('@/views/system/EngineeringMajorManage.vue'), meta: { title: '工程专业配置', cacheName: 'EngineeringMajorManage' } }, |
| | | { path: 'system/team-majors', component: () => import('@/views/system/TeamMajorManage.vue'), meta: { title: 'T队专业配置', cacheName: 'TeamMajorManage' } }, |
| | | { path: 'system/roles', component: () => import('@/views/system/RoleManage.vue'), meta: { title: '角色管理', cacheName: 'RoleManage' } }, |
| | | { path: 'system/menus', component: () => import('@/views/system/MenuManage.vue'), meta: { title: '菜单管理', cacheName: 'MenuManage' } }, |
| | | { path: 'system/menu-permissions', component: () => import('@/views/system/MenuPermission.vue'), meta: { title: '菜单权限', cacheName: 'MenuPermission' } }, |
| | | { path: 'system/three-member', component: () => import('@/views/system/ThreeMember.vue'), meta: { title: '三员管理', cacheName: 'ThreeMember' } } |
| | | ] |
| | | } |
| | | ] |
| | |
| | | !(realName.startsWith('[') && realName.endsWith(']')) |
| | | } |
| | | |
| | | // Table body cells render stored data, so they keep the codenames from the |
| | | // source instead of being rewritten like headers, labels and placeholders. |
| | | function isInsideTableCell(node) { |
| | | const element = node && node.nodeType === 1 ? node : node && node.parentElement |
| | | if (!element || !element.closest) { |
| | | return false |
| | | } |
| | | return !!element.closest('td') |
| | | } |
| | | |
| | | function replaceAsciiCodename(value, codename, realName) { |
| | | const pattern = new RegExp('(^|[^A-Za-z0-9])' + codename + '(?=$|[^A-Za-z0-9])', 'g') |
| | | return value.replace(pattern, function (match, prefix) { |
| | |
| | | return |
| | | } |
| | | if (root.nodeType === 1) { |
| | | attributes.forEach(function (attribute) { |
| | | replaceAttribute(root, attribute) |
| | | }) |
| | | if (!isInsideTableCell(root)) { |
| | | attributes.forEach(function (attribute) { |
| | | replaceAttribute(root, attribute) |
| | | }) |
| | | } |
| | | } |
| | | root.querySelectorAll('[placeholder], [title], [aria-label]').forEach(function (element) { |
| | | if (isInsideTableCell(element)) { |
| | | return |
| | | } |
| | | attributes.forEach(function (attribute) { |
| | | replaceAttribute(element, attribute) |
| | | }) |
| | |
| | | } |
| | | |
| | | if (root.nodeType === 3) { |
| | | replaceTextNode(root) |
| | | if (!isInsideTableCell(root)) { |
| | | replaceTextNode(root) |
| | | } |
| | | return |
| | | } |
| | | |
| | |
| | | { |
| | | acceptNode: function (node) { |
| | | const parent = node.parentElement |
| | | return parent && !SKIPPED_TAGS[parent.tagName] |
| | | ? NodeFilter.FILTER_ACCEPT |
| | | : NodeFilter.FILTER_REJECT |
| | | if (!parent || SKIPPED_TAGS[parent.tagName] || isInsideTableCell(parent)) { |
| | | return NodeFilter.FILTER_REJECT |
| | | } |
| | | return NodeFilter.FILTER_ACCEPT |
| | | } |
| | | } |
| | | ) |
| | |
| | | observer = new MutationObserver(function (mutations) { |
| | | mutations.forEach(function (mutation) { |
| | | if (mutation.type === 'characterData') { |
| | | replaceTextNode(mutation.target) |
| | | if (!isInsideTableCell(mutation.target)) { |
| | | replaceTextNode(mutation.target) |
| | | } |
| | | return |
| | | } |
| | | mutation.addedNodes.forEach(function (node) { |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 230px)"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 270px)"> |
| | | <el-table-column prop="workOrderNo" label="派工单编号" width="155" fixed></el-table-column> |
| | | <el-table-column prop="status" label="验收状态" width="155" fixed> |
| | | <template slot-scope="{ row }"><el-tag size="mini" :type="acceptanceMeta(row.status).type">{{ acceptanceMeta(row.status).text }}</el-tag></template> |
| | |
| | | |
| | | export default { |
| | | name: 'AcceptanceList', |
| | | props: { |
| | | stage: { type: String, required: true } |
| | | }, |
| | | data() { |
| | | return { |
| | | rows: [], |
| | |
| | | } |
| | | }, |
| | | computed: { |
| | | stage() { return this.$route.meta.stage }, |
| | | pageTitle() { |
| | | return { APPLICATION: '验收申请', INSPECTOR: '质检员验收', LEADER: '质检负责人验收', PRINT: '完工验收打印输出', RATING: '修后质量评级' }[this.stage] |
| | | }, |
| | |
| | | } |
| | | }, |
| | | watch: { |
| | | '$route.path': 'onRouteChange' |
| | | stage: 'onRouteChange' |
| | | }, |
| | | created() { this.load() }, |
| | | methods: { |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 285px)" highlight-current-row @current-change="selectedRow = $event"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 325px)" highlight-current-row @current-change="selectedRow = $event"> |
| | | <el-table-column width="46" align="center"> |
| | | <template slot-scope="{ row }"><el-radio v-model="selectedId" :label="row.id" @change="selectRow(row)"><span></span></el-radio></template> |
| | | </el-table-column> |
| | |
| | | import api from '@/api' |
| | | export default { |
| | | name: 'CompletionReport', |
| | | props: { |
| | | workOrderId: { type: [String, Number], default: null } |
| | | }, |
| | | data() { |
| | | return { |
| | | workOrder: null, |
| | |
| | | }, |
| | | computed: { |
| | | routeWorkOrderId() { |
| | | const value = this.$route.query.workOrderId |
| | | const value = this.workOrderId |
| | | return value ? Number(value) : null |
| | | } |
| | | }, |
| | |
| | | <el-alert :closable="false" type="info" show-icon title="一个故障有多个承修单位时,将分别生成派工单;编号从“故障编号/01”开始依次编号。" style="margin-bottom:12px"></el-alert> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 285px)" @selection-change="selection = $event"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 325px)" @selection-change="selection = $event"> |
| | | <el-table-column type="selection" width="46"></el-table-column> |
| | | <el-table-column prop="faultCode" label="故障编号" width="180" fixed></el-table-column> |
| | | <el-table-column prop="xNo" label="X号" width="64"></el-table-column> |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 355px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 395px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table-column type="selection" width="46" :selectable="row => canEdit(row)"></el-table-column> |
| | | <el-table-column prop="taskStatus" label="任务状态" width="145" fixed> |
| | | <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template> |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 230px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 270px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table-column type="selection" width="46"></el-table-column> |
| | | <el-table-column prop="taskStatus" label="任务状态" width="145" fixed> |
| | | <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template> |
| | |
| | | |
| | | export default { |
| | | name: 'SimpleApproval', |
| | | props: { |
| | | stage: { type: String, required: true } |
| | | }, |
| | | data() { |
| | | return { rows: [], selection: [], keyword: '', dialogVisible: false, saving: false, form: { result: 'PASS', opinion: '' } } |
| | | }, |
| | | computed: { |
| | | stage() { return this.$route.meta.stage }, |
| | | pageTitle() { |
| | | return { CMB: 'CMB领导审批', JCC_ASSISTANT: 'JCC助理审批', JCC_DIRECTOR: 'JCC处长审批' }[this.stage] |
| | | }, |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 355px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 395px)" :header-cell-style="{ textAlign: 'center' }" @selection-change="selection = $event"> |
| | | <el-table-column type="selection" width="46" :selectable="canProcess"></el-table-column> |
| | | <el-table-column prop="taskStatus" label="任务状态" width="145" fixed> |
| | | <template slot-scope="{ row }"><el-tag size="mini" :type="taskMeta(row.taskStatus).type">{{ taskMeta(row.taskStatus).text }}</el-tag></template> |
| | |
| | | |
| | | export default { |
| | | name: 'ReportPage', |
| | | props: { |
| | | type: { type: String, required: true } |
| | | }, |
| | | data() { |
| | | return { stats: {}, rows: [], chart: null } |
| | | return { stats: {}, rows: [], chart: null, loadSeq: 0 } |
| | | }, |
| | | computed: { |
| | | type() { return this.$route.params.type }, |
| | | title() { |
| | | return { |
| | | overview: '装备故障总体分析', |
| | |
| | | } |
| | | }, |
| | | watch: { |
| | | '$route.params.type': 'load' |
| | | type: 'load' |
| | | }, |
| | | mounted() { |
| | | this.load() |
| | |
| | | }, |
| | | methods: { |
| | | load() { |
| | | const requests = { |
| | | overview: api.get('/reports/overview').then(res => { this.stats = res.data || {}; this.rows = this.stats.chart || [] }), |
| | | 'top-equipment': api.get('/reports/top-equipment').then(res => { this.rows = res.data || []; this.stats = {} }), |
| | | 'by-team': api.get('/reports/by-team').then(res => { this.rows = res.data || []; this.stats = {} }), |
| | | contractors: api.get('/reports/contractors').then(res => { this.rows = res.data || []; this.stats = {} }), |
| | | trend: api.get('/reports/yearly-trend').then(res => { this.rows = res.data || []; this.stats = {} }) |
| | | const loaders = { |
| | | overview: () => api.get('/reports/overview').then(res => ({ |
| | | stats: res.data || {}, |
| | | rows: (res.data && res.data.chart) || [] |
| | | })), |
| | | 'top-equipment': () => api.get('/reports/top-equipment').then(res => ({ stats: {}, rows: res.data || [] })), |
| | | 'by-team': () => api.get('/reports/by-team').then(res => ({ stats: {}, rows: res.data || [] })), |
| | | contractors: () => api.get('/reports/contractors').then(res => ({ stats: {}, rows: res.data || [] })), |
| | | trend: () => api.get('/reports/yearly-trend').then(res => ({ stats: {}, rows: res.data || [] })) |
| | | } |
| | | ;(requests[this.type] || Promise.resolve()).then(() => this.$nextTick(this.renderChart)) |
| | | const seq = ++this.loadSeq |
| | | const loader = loaders[this.type] |
| | | const request = loader ? loader() : Promise.resolve({ stats: {}, rows: [] }) |
| | | request.then(result => { |
| | | if (seq !== this.loadSeq) return |
| | | this.stats = result.stats |
| | | this.rows = result.rows |
| | | this.$nextTick(this.renderChart) |
| | | }).catch(() => {}) |
| | | }, |
| | | renderChart() { |
| | | if (!this.$refs.chart) return |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)"> |
| | | <el-table-column prop="sortNo" label="排序" width="80" align="center"></el-table-column> |
| | | <el-table-column label="常用" width="80" align="center"> |
| | | <template slot-scope="{ $index }"><el-tag v-if="$index < 7" size="mini" type="success">前7</el-tag><span v-else>-</span></template> |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)"> |
| | | <el-table-column type="index" label="序号" width="70" align="center"></el-table-column> |
| | | <el-table-column prop="name" label="名称" min-width="240"></el-table-column> |
| | | <el-table-column label="操作" width="140" fixed="right"> |
| | |
| | | </script> |
| | | <style scoped> |
| | | .permission-grid { display: grid; grid-template-columns: 1fr 1fr 320px; gap: 12px; } |
| | | .permission-panel { min-height: calc(100vh - 180px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; } |
| | | .permission-panel { min-height: calc(100vh - 220px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; overflow: auto; } |
| | | .panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } |
| | | .panel-head h3 { margin: 0; color: #31465d; font-size: 15px; } |
| | | .intersection-list { display: flex; flex-wrap: wrap; gap: 6px; } |
| | |
| | | <el-button type="primary" icon="el-icon-plus" @click="openCreate">新增角色</el-button> |
| | | </div> |
| | | <div class="table-panel"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 150px)"> |
| | | <el-table :data="rows" border stripe height="calc(100vh - 190px)"> |
| | | <el-table-column prop="sortNo" label="排序" width="80" align="center"></el-table-column> |
| | | <el-table-column prop="name" label="角色名称" min-width="180"></el-table-column> |
| | | <el-table-column prop="code" label="角色编码" width="170"></el-table-column> |
| | |
| | | </div> |
| | | |
| | | <div class="table-panel"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 205px)"> |
| | | <el-table :data="filteredRows" border stripe height="calc(100vh - 245px)"> |
| | | <el-table-column type="index" label="序号" width="70" align="center"></el-table-column> |
| | | <el-table-column prop="name" label="名称" min-width="240"></el-table-column> |
| | | <el-table-column prop="category" label="分类" width="120" align="center"> |
| | |
| | | import api from '@/api' |
| | | export default { |
| | | name: 'TreeManage', |
| | | props: { |
| | | treeType: { type: String, required: true } |
| | | }, |
| | | data() { |
| | | return { tree: [], keyword: '', form: {}, saving: false, baseDevices: [] } |
| | | }, |
| | | computed: { |
| | | treeType() { return this.$route.meta.treeType }, |
| | | isProduct() { return this.treeType === 'product' }, |
| | | maxLevel() { return this.isProduct ? 5 : 3 }, |
| | | levelNames() { return this.isProduct ? ['型号', 'X号', '一级系统', '二级系统', '设备'] : ['一级系统', '二级系统', '设备'] } |
| | | }, |
| | | watch: { |
| | | keyword(value) { this.$refs.tree && this.$refs.tree.filter(value) }, |
| | | '$route.meta.treeType'() { this.form = {}; this.load() } |
| | | treeType() { this.form = {}; this.load() } |
| | | }, |
| | | created() { this.load() }, |
| | | methods: { |
| | |
| | | </script> |
| | | <style scoped> |
| | | .tree-layout { display: grid; grid-template-columns: 420px minmax(0, 1fr); gap: 12px; } |
| | | .tree-panel, .detail-panel { min-height: calc(100vh - 140px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; } |
| | | .tree-panel, .detail-panel { min-height: calc(100vh - 180px); padding: 14px; border: 1px solid #dce4ec; border-radius: 4px; background: #fff; } |
| | | .tree-node { display: flex; align-items: center; gap: 7px; } |
| | | .tree-node small { color: #95a2b0; } |
| | | .level-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 50px; height: 20px; padding: 0 5px; border-radius: 3px; color: #2f658f; background: #e9f3fb; font-size: 11px; } |