fix: make case material downloads deployment-safe

dev-QQq^2
陈沅 4 days ago
parent c05ea5ed3f
commit 4cb3b8756b

@ -25,24 +25,44 @@ const material = computed(() => {
const name = String(props.materialName || "").trim();
const url = String(props.materialUrl || "").trim();
if (!name || !url || (name === "案例资料.rar" && url === "/file/example.rar")) return null;
return { name, url };
return { name, url: normalizeMaterialUrl(url) };
});
const extension = computed(() => String(material.value?.name || material.value?.url || "").split(/[?#]/)[0].split(".").pop().toLowerCase());
const isImage = computed(() => ["jpg", "jpeg", "png", "gif", "webp", "bmp", "svg"].includes(extension.value));
const isPdf = computed(() => extension.value === "pdf");
const previewable = computed(() => isImage.value || isPdf.value);
function handleDownload() {
function normalizeMaterialUrl(rawUrl) {
const url = String(rawUrl || "").trim();
if (!url) return "";
try {
const parsed = new URL(url);
if (["localhost", "127.0.0.1", "::1"].includes(parsed.hostname) && parsed.pathname.startsWith("/file/")) {
return `${parsed.pathname}${parsed.search}${parsed.hash}`;
}
} catch {
// Relative file paths are already resolved through the current deployment or Vite proxy.
}
return url;
}
async function handleDownload() {
if (!material.value?.url) { ElMessage.warning("暂无案例资料"); return; }
const link = document.createElement("a");
link.href = material.value.url;
link.target = "_blank";
link.rel = "noopener";
link.download = material.value.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
if (!previewable.value) ElMessage.info("该文件类型暂不支持在线预览,已为您打开下载。");
try {
const response = await fetch(material.value.url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const objectUrl = URL.createObjectURL(await response.blob());
const link = document.createElement("a");
link.href = objectUrl;
link.download = material.value.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(objectUrl);
} catch (error) {
console.warn("Case material download failed", error);
ElMessage.error("案例资料文件不存在或暂不可下载,请联系管理员重新上传。");
}
}
</script>

@ -8,6 +8,7 @@ const studentPage = fs.readFileSync(path.join(root, "src/views/training/GenericT
const newProductSurveyPage = fs.readFileSync(path.join(root, "src/views/foundation/new-product-survey.vue"), "utf8");
const productDevelopmentFactorsPage = fs.readFileSync(path.join(root, "src/views/foundation/product-development-factors.vue"), "utf8");
const trainingIntro = fs.readFileSync(path.join(root, "src/views/components/TrainingIntro.vue"), "utf8");
const materialButton = fs.readFileSync(path.join(root, "src/views/components/TrainingMaterialButton.vue"), "utf8");
const directStudentBriefFiles = [
"src/views/demand/index.vue",
"src/views/demand/environment.vue",
@ -47,6 +48,35 @@ assert(
/TrainingMaterialButton/.test(productDevelopmentFactorsPage),
"product development factors page should render the case material button"
);
assert.match(
materialButton,
/function normalizeMaterialUrl\(rawUrl\)/,
"case material downloads must normalize legacy absolute URLs"
);
assert.ok(
materialButton.includes('["localhost", "127.0.0.1", "::1"]'),
"localhost case material URLs must not be sent to each student's own browser port"
);
assert.match(
materialButton,
/url: normalizeMaterialUrl\(url\)/,
"the download button must use the normalized material URL"
);
assert.match(
materialButton,
/async function handleDownload\(\)/,
"case material downloads must validate the file response before opening a download"
);
assert.match(
materialButton,
/await fetch\(material\.value\.url\)/,
"case material downloads must request the configured file URL"
);
assert.match(
materialButton,
/案例资料文件不存在或暂不可下载/,
"a missing case material file must show a clear recovery message"
);
const studentBriefPages = [studentPage, newProductSurveyPage, productDevelopmentFactorsPage, trainingIntro, ...directStudentBriefPages];
assert(/实训背景/.test(trainingTaskBrief) && /实训目标/.test(trainingTaskBrief) && /实训要求/.test(trainingTaskBrief), "common TrainingTaskBrief component should render the three student brief sections");

Loading…
Cancel
Save