feat: unify student training page layout
parent
33b113e9cb
commit
0235a1eafd
@ -0,0 +1,62 @@
|
||||
function safeParseJson(value) {
|
||||
if (typeof value !== "string" || !value.trim()) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function formatReport(value) {
|
||||
if (value == null) return "";
|
||||
const parsed = typeof value === "string" ? safeParseJson(value) : value;
|
||||
if (parsed == null) return typeof value === "string" ? value : "";
|
||||
try {
|
||||
return JSON.stringify(parsed, null, 2);
|
||||
} catch (error) {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCriteria(value) {
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.map((item) => {
|
||||
if (typeof item === "string") return { name: item, score: "", description: "" };
|
||||
return {
|
||||
name: String(item?.name || item?.criterion || item?.title || "评分项"),
|
||||
score: item?.score ?? item?.weight ?? "",
|
||||
description: String(item?.description || item?.detail || ""),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeOperation(operation, root, type) {
|
||||
const value = operation && typeof operation === "object" ? operation : {};
|
||||
const prefix = type === "help" ? "help" : "assessment";
|
||||
const flatReportKey = type === "help" ? "helpReportJson" : "assessmentReportJson";
|
||||
return {
|
||||
...value,
|
||||
status: String(value.status || root?.[`${prefix}Status`] || "PENDING").toUpperCase(),
|
||||
report: value.report || root?.[flatReportKey] || root?.[`${prefix}Report`] || "",
|
||||
scoreCriteria: value.scoreCriteria || root?.scoreCriteria || [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeEvaluationReports(evaluation) {
|
||||
const root = evaluation && typeof evaluation === "object" ? evaluation : {};
|
||||
const help = normalizeOperation(root.help || root.helpEvaluation, root, "help");
|
||||
const assessment = normalizeOperation(root.assessment || root.assessmentEvaluation, root, "assessment");
|
||||
const assessmentReportData = safeParseJson(root.assessmentReportJson);
|
||||
|
||||
return {
|
||||
help,
|
||||
assessment,
|
||||
helpReport: formatReport(help.report || help.content || help.suggestion),
|
||||
assessmentReport: formatReport(assessment.report || assessment.content || assessment.feedback),
|
||||
score: root.assessmentScore ?? assessment.score ?? assessmentReportData?.score ?? root.score ?? null,
|
||||
scoreCriteria: normalizeCriteria(assessmentReportData?.criteria || assessment.scoreCriteria || root.scoreCriteria),
|
||||
};
|
||||
}
|
||||
|
||||
export { formatReport, normalizeCriteria, normalizeEvaluationReports, normalizeOperation, safeParseJson };
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,97 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const pages = ["demand/index.vue", "demand/environment.vue", "demand/people.vue", "demand/consumer-behavior.vue", "demand/consumer-scenario.vue", "demand/feature-conversion.vue", "product/index.vue", "product/differentiation.vue", "product/structure.vue", "product/after-sales-service.vue", "product/components/ProductStandardTrainingPage.vue", "channel/supplier.vue", "channel/index.vue", "product/price.vue", "assessment/optimization.vue", "assessment/diagnosis.vue"];
|
||||
|
||||
for (const pageName of pages) {
|
||||
const page = fs.readFileSync(path.resolve(__dirname, `../src/views/${pageName}`), "utf8");
|
||||
const tab = page;
|
||||
const connector = page;
|
||||
|
||||
assert.match(tab, /min-width:\s*0;/, `${pageName} should let standard step tabs share the available width`);
|
||||
assert.match(page, /background:\s*linear-gradient\(115deg,\s*rgba\(4,\s*109,\s*155,\s*0\.78\),\s*rgba\(5,\s*62,\s*102,\s*0\.64\)\);/, `${pageName} should use the standard active-step gradient`);
|
||||
assert.match(connector, /overflow:\s*visible;/, `${pageName} should not clip the standard connector arrow`);
|
||||
assert.match(page, /right:\s*-1px;/, `${pageName} should position the standard connector arrow outside the line end`);
|
||||
}
|
||||
|
||||
const peoplePage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/people.vue"), "utf8");
|
||||
assert.match(peoplePage, /class="step-connector" aria-hidden="true"><\/span>/, "people.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const behaviorPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/consumer-behavior.vue"), "utf8");
|
||||
assert.match(behaviorPage, /class="step-connector" aria-hidden="true"><\/span>/, "consumer-behavior.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const scenarioPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/consumer-scenario.vue"), "utf8");
|
||||
assert.match(scenarioPage, /class="step-connector" aria-hidden="true"><\/span>/, "consumer-scenario.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const conversionPage = fs.readFileSync(path.resolve(__dirname, "../src/views/demand/feature-conversion.vue"), "utf8");
|
||||
assert.match(conversionPage, /class="step-connector" aria-hidden="true"><\/span>/, "feature-conversion.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const differentiationPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/differentiation.vue"), "utf8");
|
||||
assert.match(differentiationPage, /class="step-connector" aria-hidden="true"><\/span>/, "differentiation.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const structurePage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/structure.vue"), "utf8");
|
||||
assert.match(structurePage, /class="step-connector" aria-hidden="true"><\/span>/, "structure.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const afterSalesPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/after-sales-service.vue"), "utf8");
|
||||
assert.match(afterSalesPage, /class="step-connector" aria-hidden="true"><\/span>/, "after-sales-service.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const productStandardPage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/components/ProductStandardTrainingPage.vue"), "utf8");
|
||||
assert.match(productStandardPage, /class="step-connector" aria-hidden="true"><\/span>/, "ProductStandardTrainingPage.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const supplierPage = fs.readFileSync(path.resolve(__dirname, "../src/views/channel/supplier.vue"), "utf8");
|
||||
assert.match(supplierPage, /class="step-connector" aria-hidden="true"><\/span>/, "supplier.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const channelPage = fs.readFileSync(path.resolve(__dirname, "../src/views/channel/index.vue"), "utf8");
|
||||
assert.match(channelPage, /class="step-connector" aria-hidden="true"><\/span>/, "channel/index.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const pricePage = fs.readFileSync(path.resolve(__dirname, "../src/views/product/price.vue"), "utf8");
|
||||
assert.match(pricePage, /class="step-connector" aria-hidden="true"><\/span>/, "price.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const optimizationPage = fs.readFileSync(path.resolve(__dirname, "../src/views/assessment/optimization.vue"), "utf8");
|
||||
assert.match(optimizationPage, /class="step-connector" aria-hidden="true"><\/span>/, "optimization.vue should use the standard CSS connector instead of a text arrow");
|
||||
|
||||
const diagnosisPage = fs.readFileSync(path.resolve(__dirname, "../src/views/assessment/diagnosis.vue"), "utf8");
|
||||
assert.match(diagnosisPage, /class="step-connector" aria-hidden="true"><\/span>/, "diagnosis.vue should use the standard CSS connector instead of a text arrow");
|
||||
assert.doesNotMatch(diagnosisPage, /<p class="section-eyebrow">Training Task<\/p><h3>任务实训<\/h3><p class="workbench-description">/, "diagnosis.vue should not render the removed task-training heading or description");
|
||||
assert.match(diagnosisPage, /<TrainingMaterialButton :material-name="taskConfig\?\.materialName" :material-url="taskConfig\?\.materialUrl" \/>/, "diagnosis.vue should retain the training file download control");
|
||||
|
||||
const newProductSurveyPage = fs.readFileSync(path.resolve(__dirname, "../src/views/foundation/new-product-survey.vue"), "utf8");
|
||||
assert.doesNotMatch(newProductSurveyPage, /<p class="section-eyebrow">Training Task<\/p>[\s\S]*?<p v-if="taskRequirement" class="workbench-description">/, "new-product-survey.vue should not render the removed task-training heading or description");
|
||||
assert.match(newProductSurveyPage, /<TrainingMaterialButton :material-name="taskConfig\?\.materialName" :material-url="taskConfig\?\.materialUrl" \/>/, "new-product-survey.vue should retain the training file download control");
|
||||
|
||||
const testIndexPage = fs.readFileSync(path.resolve(__dirname, "../src/views/test/index.vue"), "utf8");
|
||||
assert.match(testIndexPage, /@media \(max-width: 1100px\)[\s\S]*?grid-template-columns: 1fr;/, "test/index.vue should keep the standard desktop two-column layout until the 1100px breakpoint");
|
||||
|
||||
const promotionPage = fs.readFileSync(path.resolve(__dirname, "../src/views/test/promotion.vue"), "utf8");
|
||||
assert.match(promotionPage, /@media \(max-width: 900px\)[\s\S]*?\.promotion-phase :deep\(\.el-button\) \{ width: 100%; \}/, "test/promotion.vue should switch the standard training controls to the mobile layout at 900px");
|
||||
|
||||
const studentTaskWorkbenchPages = [
|
||||
"training/GenericTrainingPage.vue",
|
||||
"product/components/ProductStandardTrainingPage.vue",
|
||||
"foundation/new-product-survey.vue",
|
||||
"foundation/product-development-factors.vue",
|
||||
"demand/index.vue",
|
||||
"demand/environment.vue",
|
||||
"demand/people.vue",
|
||||
"demand/consumer-behavior.vue",
|
||||
"demand/consumer-scenario.vue",
|
||||
"demand/feature-conversion.vue",
|
||||
"product/index.vue",
|
||||
"product/price.vue",
|
||||
"product/differentiation.vue",
|
||||
"product/structure.vue",
|
||||
"product/after-sales-service.vue",
|
||||
"channel/index.vue",
|
||||
"channel/supplier.vue",
|
||||
"assessment/diagnosis.vue",
|
||||
"assessment/optimization.vue"
|
||||
];
|
||||
|
||||
for (const pageName of studentTaskWorkbenchPages) {
|
||||
const page = fs.readFileSync(path.resolve(__dirname, `../src/views/${pageName}`), "utf8");
|
||||
assert.doesNotMatch(page, /class="workbench-head">\s*<div>[\s\S]*?workbench-description/, `${pageName} should not render the task-workbench heading or description`);
|
||||
assert.match(page, /TrainingMaterialButton|class="material-download"/, `${pageName} should retain the file download control`);
|
||||
}
|
||||
|
||||
console.log("Demand index step navigation matches the standard-page treatment.");
|
||||
@ -0,0 +1,13 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const source = fs.readFileSync(path.join(root, "src/views/training/GenericTrainingPage.vue"), "utf8");
|
||||
const shell = source.match(/\.student-training-page \{([\s\S]*?)\n\}/)?.[1] || "";
|
||||
|
||||
assert.match(shell, /padding:\s*24px 24px 32px;/, "generic training pages must use the standard shell padding");
|
||||
assert.match(shell, /background:\s*#06111d;/, "generic training pages must keep the standard continuous deep-blue canvas");
|
||||
assert.match(source, /@media \(max-width: 1100px\) \{\s*\.student-training-page \{\s*grid-template-columns:\s*1fr;/, "generic training pages must stack the AI column at the standard 1100px breakpoint");
|
||||
|
||||
console.log("Generic training page uses the standard shell spacing.");
|
||||
@ -0,0 +1,23 @@
|
||||
const assert = require("assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const root = path.resolve(__dirname, "..");
|
||||
const pages = [
|
||||
"src/views/training/GenericTrainingPage.vue",
|
||||
"src/views/foundation/new-product-survey.vue",
|
||||
"src/views/foundation/product-development-factors.vue",
|
||||
"src/views/demand/index.vue",
|
||||
"src/views/demand/environment.vue",
|
||||
];
|
||||
|
||||
for (const page of pages) {
|
||||
const source = fs.readFileSync(path.join(root, page), "utf8");
|
||||
const connector = source.match(/\.step-connector \{([\s\S]*?)\n\}/)?.[1] || "";
|
||||
|
||||
assert.match(connector, /flex:\s*0 1 40px;/, `${page} should use a compact 40px connector basis`);
|
||||
assert.match(connector, /min-width:\s*18px;/, `${page} should retain an 18px visible connector minimum`);
|
||||
assert.match(connector, /margin:\s*0 8px;/, `${page} should use compact connector spacing`);
|
||||
}
|
||||
|
||||
console.log("Step indicator desktop spacing is compact.");
|
||||
Loading…
Reference in New Issue