feat: track and visualize project study time
parent
95d4bf87ff
commit
ad9adf71b2
@ -0,0 +1,17 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
export function recordProjectStudyTime(params) {
|
||||||
|
return request({
|
||||||
|
url: "/api/student/project-study-time/record",
|
||||||
|
method: "post",
|
||||||
|
params,
|
||||||
|
headers: { repeatSubmit: false },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentProjectStudyTime() {
|
||||||
|
return request({
|
||||||
|
url: "/api/student/project-study-time/current",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
import { recordProjectStudyTime } from "@/api/projectStudyTime";
|
||||||
|
import { isStudentDemo } from "@/utils/studentDemo";
|
||||||
|
|
||||||
|
const TRACKING_INTERVAL_MS = 60 * 1000;
|
||||||
|
const PROJECT_BY_TASK_KEY = {
|
||||||
|
"new-product-survey": "互联网产品开发基础认知",
|
||||||
|
"product-development-factors": "互联网产品开发基础认知",
|
||||||
|
"product-development-process": "互联网产品开发基础认知",
|
||||||
|
"industry-demand-analysis": "市场洞察与需求分析",
|
||||||
|
"competitive-environment-analysis": "市场洞察与需求分析",
|
||||||
|
"market-opportunity-selection": "市场洞察与需求分析",
|
||||||
|
"business-feasibility": "市场洞察与需求分析",
|
||||||
|
"target-user-profile": "市场洞察与需求分析",
|
||||||
|
"consumer-behavior": "市场洞察与需求分析",
|
||||||
|
"consumer-scenario": "市场洞察与需求分析",
|
||||||
|
"feature-conversion": "市场洞察与需求分析",
|
||||||
|
"product-value-positioning": "市场洞察与需求分析",
|
||||||
|
"product-structure": "产品规划与设计",
|
||||||
|
"after-sales-service": "产品规划与设计",
|
||||||
|
"requirements-doc": "产品规划与设计",
|
||||||
|
"hardware-cost": "产品开发与测试验证",
|
||||||
|
"supplier-evaluation": "产品开发与测试验证",
|
||||||
|
"purchase-demand-forecast": "产品开发与测试验证",
|
||||||
|
"product-pricing": "产品开发与测试验证",
|
||||||
|
"category-optimization": "产品上线与运营推广",
|
||||||
|
"release-channel-evaluation": "产品上线与运营推广",
|
||||||
|
"promotion-effect-optimization": "产品上线与运营推广",
|
||||||
|
"product-diagnosis-iteration": "产品上线与运营推广",
|
||||||
|
"comprehensive-case-training": "综合实训",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveProjectName(taskKey, configuredProjectName, fallback = "") {
|
||||||
|
return String(configuredProjectName || "").trim()
|
||||||
|
|| PROJECT_BY_TASK_KEY[String(taskKey || "").trim()]
|
||||||
|
|| String(fallback || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectStudyTime(projectName) {
|
||||||
|
let activeSince = Date.now();
|
||||||
|
let trackedProjectName = String(projectName.value || "").trim();
|
||||||
|
let timerId;
|
||||||
|
|
||||||
|
const flush = (allowHidden = false) => {
|
||||||
|
if (isStudentDemo() || (document.hidden && !allowHidden)) return;
|
||||||
|
const name = trackedProjectName;
|
||||||
|
const now = Date.now();
|
||||||
|
const durationSeconds = Math.floor((now - activeSince) / 1000);
|
||||||
|
activeSince = now;
|
||||||
|
if (!name || durationSeconds <= 0) return;
|
||||||
|
recordProjectStudyTime({ projectName: name, durationSeconds }).catch(() => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
if (document.hidden) {
|
||||||
|
flush(true);
|
||||||
|
} else {
|
||||||
|
activeSince = Date.now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(projectName, (nextProjectName) => {
|
||||||
|
flush(true);
|
||||||
|
trackedProjectName = String(nextProjectName || "").trim();
|
||||||
|
activeSince = Date.now();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
activeSince = Date.now();
|
||||||
|
trackedProjectName = String(projectName.value || "").trim();
|
||||||
|
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
timerId = window.setInterval(flush, TRACKING_INTERVAL_MS);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
flush(true);
|
||||||
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||||
|
window.clearInterval(timerId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { flush };
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
const fs = require("node:fs");
|
||||||
|
const path = require("node:path");
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, "..");
|
||||||
|
const api = fs.readFileSync(path.join(root, "src/api/projectStudyTime.js"), "utf8");
|
||||||
|
const genericPage = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8");
|
||||||
|
const customPage = fs.readFileSync(path.join(root, "src/views/training/CustomTrainingStepPage.vue"), "utf8");
|
||||||
|
const dashboard = fs.readFileSync(path.join(root, "src/views/student/index.vue"), "utf8");
|
||||||
|
const analysis = fs.readFileSync(path.join(root, "src/views/analysis-of-students/analysis.vue"), "utf8");
|
||||||
|
|
||||||
|
test("project study time API records and loads current student's module durations", () => {
|
||||||
|
assert.match(api, /recordProjectStudyTime/);
|
||||||
|
assert.match(api, /getCurrentProjectStudyTime/);
|
||||||
|
assert.match(api, /\/api\/student\/project-study-time\/record/);
|
||||||
|
assert.match(api, /\/api\/student\/project-study-time\/current/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("training pages flush time spent in the active project", () => {
|
||||||
|
for (const page of [genericPage, customPage]) {
|
||||||
|
assert.match(page, /useProjectStudyTime/);
|
||||||
|
assert.match(page, /projectName/);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("student dashboard visualizes per-project study time and keeps task progress beside it", () => {
|
||||||
|
assert.match(dashboard, /studyTimeChartRef/);
|
||||||
|
assert.match(dashboard, /renderStudyTimeChart/);
|
||||||
|
assert.match(dashboard, /getCurrentProjectStudyTime/);
|
||||||
|
assert.match(dashboard, /progressChartRef/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("AI learning analysis reads current project study durations", () => {
|
||||||
|
assert.match(analysis, /getCurrentProjectStudyTime/);
|
||||||
|
assert.match(analysis, /projectStudyTimes/);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue