项目详情页打标状态轮询改为1秒刷新
This commit is contained in:
@@ -0,0 +1,67 @@
|
|||||||
|
# 项目详情打标状态轮询前端实施计划
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在项目详情页中,当项目状态为“打标中”时自动轮询项目详情接口,并在状态变化后及时刷新页面展示。
|
||||||
|
|
||||||
|
**Architecture:** 轮询逻辑收敛到项目详情父组件 `detail.vue`,由父组件统一维护 1 秒轮询定时器、请求节流与销毁清理,子组件继续只消费 `projectInfo.projectStatus`。这样可以保证详情页各子标签页共享同一份最新项目状态,并在状态脱离“打标中”后自动停止轮询。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 2、Element UI、现有 `@/api/ccdiProject` 接口层、Node `assert` 单测脚本
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 补充失败单测
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js`
|
||||||
|
- Test: `ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 编写失败单测**
|
||||||
|
|
||||||
|
校验 `detail.vue` 已具备:
|
||||||
|
- 轮询定时器状态字段
|
||||||
|
- 仅在 `projectStatus === "3"` 时启动轮询
|
||||||
|
- 状态变更后停止轮询
|
||||||
|
- 组件销毁时清理轮询
|
||||||
|
|
||||||
|
- [ ] **Step 2: 运行单测确认失败**
|
||||||
|
|
||||||
|
Run: `node ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js`
|
||||||
|
Expected: FAIL,提示缺少详情页打标状态轮询逻辑
|
||||||
|
|
||||||
|
### Task 2: 在详情页实现最短路径轮询
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `ruoyi-ui/src/views/ccdiProject/detail.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 增加轮询状态字段与清理逻辑**
|
||||||
|
|
||||||
|
新增页级定时器、轮询间隔、请求中的互斥标记,并在组件销毁前统一关闭定时器。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 在项目详情加载后按状态启停轮询**
|
||||||
|
|
||||||
|
首次加载和手动刷新项目详情后,根据接口返回的 `projectStatus` 判断:
|
||||||
|
- 状态为 `3` 时启动轮询
|
||||||
|
- 状态不是 `3` 时关闭轮询
|
||||||
|
|
||||||
|
- [ ] **Step 3: 轮询期间复用项目详情接口更新页面**
|
||||||
|
|
||||||
|
轮询调用 `getProject(projectId)`,更新 `projectInfo`、页面标题和状态标签;如果状态已不再是 `3`,则立即停止轮询。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 处理路由切换与重复启动**
|
||||||
|
|
||||||
|
切换 `projectId`、离开页面或重复进入轮询分支时,确保不会叠加多个定时器,也不会在已有请求未结束时并发重复请求。
|
||||||
|
|
||||||
|
### Task 3: 回归验证与记录
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/reports/implementation/2026-03-19-project-detail-tagging-status-polling-record.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 运行相关单测**
|
||||||
|
|
||||||
|
Run: `node ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js`
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 2: 补充实施记录**
|
||||||
|
|
||||||
|
记录本次修改内容、测试命令和验证结论,便于后续追踪。
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 项目详情打标状态轮询实施记录
|
||||||
|
|
||||||
|
## 修改背景
|
||||||
|
|
||||||
|
项目详情页需要在项目状态为“打标中”时自动轮询项目状态,保证页面头部状态标签及依赖该状态的子页面禁用逻辑能够及时刷新。
|
||||||
|
|
||||||
|
## 本次修改
|
||||||
|
|
||||||
|
### 1. 详情页增加页级项目状态轮询
|
||||||
|
|
||||||
|
- 文件:`ruoyi-ui/src/views/ccdiProject/detail.vue`
|
||||||
|
- 变更点:
|
||||||
|
- 新增 `projectStatusPollingTimer`、`projectStatusPollingInterval`、`projectStatusPollingLoading`
|
||||||
|
- 抽取 `fetchProjectDetail` 统一项目详情请求与数据归一化
|
||||||
|
- 新增 `syncProjectStatusPolling`、`startProjectStatusPolling`、`stopProjectStatusPolling`、`pollProjectStatus`
|
||||||
|
- 在项目状态为 `3`(打标中)时按 1 秒间隔启动轮询
|
||||||
|
- 在状态脱离 `3`、路由切换或组件销毁时关闭轮询
|
||||||
|
|
||||||
|
### 2. 补充轮询回归单测
|
||||||
|
|
||||||
|
- 文件:`ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js`
|
||||||
|
- 校验点:
|
||||||
|
- 详情页存在轮询定时器字段
|
||||||
|
- 销毁前清理轮询
|
||||||
|
- 按 `projectStatus === "3"` 启停轮询
|
||||||
|
- 轮询请求后状态变化会停止轮询
|
||||||
|
|
||||||
|
### 3. 补充实施计划
|
||||||
|
|
||||||
|
- 文件:`docs/plans/frontend/2026-03-19-project-detail-tagging-status-polling-frontend-implementation.md`
|
||||||
|
|
||||||
|
## 验证记录
|
||||||
|
|
||||||
|
### 单测
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js
|
||||||
|
node ruoyi-ui/tests/unit/upload-data-disabled-cards.test.js
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
- `project-detail-tagging-polling test passed`
|
||||||
|
- `upload-data-disabled-cards test passed`
|
||||||
|
|
||||||
|
### 构建验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ruoyi-ui && npm run build:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
结果:
|
||||||
|
- 构建成功,退出码 `0`
|
||||||
|
- 存在既有产物体积告警,但不影响本次功能构建通过
|
||||||
|
|
||||||
|
## 工作区说明
|
||||||
|
|
||||||
|
- 当前工作区存在未由本次任务引入的改动:`.DS_Store`、`ry.sh`
|
||||||
|
- 本次提交时应忽略 `.DS_Store`,并避免将无关文件纳入暂存区
|
||||||
@@ -69,7 +69,7 @@ import ParamConfig from "./components/detail/ParamConfig";
|
|||||||
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
|
import PreliminaryCheck from "./components/detail/PreliminaryCheck";
|
||||||
import SpecialCheck from "./components/detail/SpecialCheck";
|
import SpecialCheck from "./components/detail/SpecialCheck";
|
||||||
import DetailQuery from "./components/detail/DetailQuery";
|
import DetailQuery from "./components/detail/DetailQuery";
|
||||||
import {getProject} from "@/api/ccdiProject";
|
import { getProject } from "@/api/ccdiProject";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "ProjectDetail",
|
name: "ProjectDetail",
|
||||||
@@ -102,10 +102,15 @@ export default {
|
|||||||
warningThreshold: 60,
|
warningThreshold: 60,
|
||||||
projectStatus: "0",
|
projectStatus: "0",
|
||||||
},
|
},
|
||||||
|
projectStatusPollingTimer: null,
|
||||||
|
projectStatusPollingInterval: 1000,
|
||||||
|
projectStatusPollingLoading: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
"$route.params.projectId"(newId) {
|
"$route.params.projectId"(newId) {
|
||||||
|
this.stopProjectStatusPolling();
|
||||||
|
this.projectStatusPollingLoading = false;
|
||||||
if (newId) {
|
if (newId) {
|
||||||
this.projectId = newId;
|
this.projectId = newId;
|
||||||
this.projectInfo.projectId = newId;
|
this.projectInfo.projectId = newId;
|
||||||
@@ -116,12 +121,18 @@ export default {
|
|||||||
"$route.query.tab"() {
|
"$route.query.tab"() {
|
||||||
this.initActiveTabFromRoute();
|
this.initActiveTabFromRoute();
|
||||||
},
|
},
|
||||||
|
"projectInfo.projectStatus"() {
|
||||||
|
this.syncProjectStatusPolling();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
// 初始化页面数据
|
// 初始化页面数据
|
||||||
this.initActiveTabFromRoute();
|
this.initActiveTabFromRoute();
|
||||||
this.initPageData();
|
this.initPageData();
|
||||||
},
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.stopProjectStatusPolling();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
initActiveTabFromRoute() {
|
initActiveTabFromRoute() {
|
||||||
const tab = (this.$route.query && this.$route.query.tab) || "";
|
const tab = (this.$route.query && this.$route.query.tab) || "";
|
||||||
@@ -142,35 +153,87 @@ export default {
|
|||||||
},
|
},
|
||||||
/** 初始化页面数据 */
|
/** 初始化页面数据 */
|
||||||
initPageData() {
|
initPageData() {
|
||||||
// 这里应该从API获取项目详细信息
|
return this.fetchProjectDetail();
|
||||||
|
},
|
||||||
|
async fetchProjectDetail(options = {}) {
|
||||||
|
const { silent = false } = options;
|
||||||
if (!this.projectId) {
|
if (!this.projectId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!silent) {
|
||||||
|
this.projectInfo.projectName = "";
|
||||||
|
this.updatePageTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await getProject(this.projectId);
|
||||||
|
const data = res.data || {};
|
||||||
|
this.projectInfo = {
|
||||||
|
...this.projectInfo,
|
||||||
|
...data,
|
||||||
|
projectId: data.projectId || this.projectId,
|
||||||
|
projectName: data.projectName || "",
|
||||||
|
projectDesc: data.projectDesc || data.description || "",
|
||||||
|
projectStatus: String(
|
||||||
|
data.projectStatus !== undefined && data.projectStatus !== null
|
||||||
|
? data.projectStatus
|
||||||
|
: data.status !== undefined && data.status !== null
|
||||||
|
? data.status
|
||||||
|
: this.projectInfo.projectStatus
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this.updatePageTitle();
|
||||||
|
this.syncProjectStatusPolling();
|
||||||
|
return this.projectInfo;
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) {
|
||||||
|
this.$message.error("加载项目详情失败");
|
||||||
|
} else {
|
||||||
|
console.error("轮询项目状态失败:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
this.updatePageTitle();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
syncProjectStatusPolling() {
|
||||||
|
if (String(this.projectInfo.projectStatus) === "3") {
|
||||||
|
this.startProjectStatusPolling();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.projectInfo.projectName = "";
|
this.stopProjectStatusPolling();
|
||||||
this.updatePageTitle();
|
},
|
||||||
getProject(this.projectId)
|
startProjectStatusPolling() {
|
||||||
.then((res) => {
|
if (this.projectStatusPollingTimer) {
|
||||||
const data = res.data || {};
|
return;
|
||||||
this.projectInfo = {
|
}
|
||||||
...this.projectInfo,
|
this.projectStatusPollingTimer = setInterval(() => {
|
||||||
...data,
|
this.pollProjectStatus();
|
||||||
projectId: data.projectId || this.projectId,
|
}, this.projectStatusPollingInterval);
|
||||||
projectName: data.projectName || "",
|
},
|
||||||
projectDesc: data.projectDesc || data.description || "",
|
stopProjectStatusPolling() {
|
||||||
projectStatus: String(
|
if (!this.projectStatusPollingTimer) {
|
||||||
data.projectStatus !== undefined && data.projectStatus !== null
|
return;
|
||||||
? data.projectStatus
|
}
|
||||||
: data.status !== undefined && data.status !== null
|
clearInterval(this.projectStatusPollingTimer);
|
||||||
? data.status
|
this.projectStatusPollingTimer = null;
|
||||||
: this.projectInfo.projectStatus
|
},
|
||||||
),
|
async pollProjectStatus() {
|
||||||
};
|
if (this.projectStatusPollingLoading || !this.projectId) {
|
||||||
this.updatePageTitle();
|
return;
|
||||||
})
|
}
|
||||||
.catch(() => {
|
|
||||||
this.$message.error("Failed to load project details");
|
this.projectStatusPollingLoading = true;
|
||||||
this.updatePageTitle();
|
try {
|
||||||
});
|
await this.fetchProjectDetail({ silent: true });
|
||||||
|
if (String(this.projectInfo.projectStatus) !== "3") {
|
||||||
|
this.stopProjectStatusPolling();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("项目状态轮询请求失败:", error);
|
||||||
|
} finally {
|
||||||
|
this.projectStatusPollingLoading = false;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updatePageTitle() {
|
updatePageTitle() {
|
||||||
const title = this.projectInfo.projectName || `ProjectDetail-${this.projectId}`;
|
const title = this.projectInfo.projectName || `ProjectDetail-${this.projectId}`;
|
||||||
|
|||||||
47
ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js
Normal file
47
ruoyi-ui/tests/unit/project-detail-tagging-polling.test.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const assert = require("assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const componentPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../src/views/ccdiProject/detail.vue"
|
||||||
|
);
|
||||||
|
const source = fs.readFileSync(componentPath, "utf8");
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/projectStatusPollingTimer:\s*null/.test(source),
|
||||||
|
"详情页应声明项目状态轮询定时器"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/projectStatusPollingInterval:\s*1000/.test(source),
|
||||||
|
"详情页轮询间隔应为 1000ms"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/beforeDestroy\(\)\s*\{[\s\S]*?this\.stopProjectStatusPolling\(\)/.test(source),
|
||||||
|
"详情页销毁前应停止项目状态轮询"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/syncProjectStatusPolling\(\)\s*\{[\s\S]*?String\(this\.projectInfo\.projectStatus\)\s*===\s*"3"[\s\S]*?this\.startProjectStatusPolling\(\)[\s\S]*?this\.stopProjectStatusPolling\(\)/.test(
|
||||||
|
source
|
||||||
|
),
|
||||||
|
"详情页应根据项目状态启停轮询"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/startProjectStatusPolling\(\)\s*\{[\s\S]*?setInterval\([\s\S]*?this\.pollProjectStatus\(\)[\s\S]*?this\.projectStatusPollingInterval/.test(
|
||||||
|
source
|
||||||
|
),
|
||||||
|
"详情页应按固定间隔轮询项目状态"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
/pollProjectStatus\(\)\s*\{[\s\S]*?await this\.fetchProjectDetail\(\{ silent: true \}\)[\s\S]*?if\s*\(String\(this\.projectInfo\.projectStatus\)\s*!==\s*"3"\)\s*\{[\s\S]*?this\.stopProjectStatusPolling\(\)/.test(
|
||||||
|
source
|
||||||
|
),
|
||||||
|
"详情页轮询后应在状态脱离打标中时停止"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("project-detail-tagging-polling test passed");
|
||||||
Reference in New Issue
Block a user