diff --git a/docs/下载调整.md b/docs/下载调整.md new file mode 100644 index 0000000..58f7d08 --- /dev/null +++ b/docs/下载调整.md @@ -0,0 +1,235 @@ +# 下载案例资料调整计划 + +> 用途:学生端"下载案例资料"功能调整涉及的所有文件清单。信息来源:`TrainingMaterialButton.vue` 组件本身、`TrainingTaskToolbar.vue` 内嵌引用及各子页面 `` 使用情况。 +> 生成日期:2026-09-22 +> 共计:**2 个共享组件 + 27 个学生端子页面**(23 个直接引入 + 4 个间接引入) + +--- + +## 1. 调整范围总览 + +| 分类 | 数量 | 说明 | +| -------------------- | ------ | ------------------------------------------- | +| 共享组件 | 2 | 按钮本体 + 顶部任务工具栏(间接渲染按钮) | +| 直接引入按钮的子页面 | 23 | 在模板中显式使用 `` | +| 间接引入按钮的子页面 | 4 | 通过 `TrainingTaskToolbar` 自动渲染按钮 | +| **合计** | **29** | — | + +> 修改共享组件会**级联影响**所有引用页面,需谨慎评估边界;调整单页面样式时,优先使用页面级 `:deep()` 选择器,避免污染共享组件。 + +--- + +## 2. 共享组件(2 个) + +| # | 文件 | 关键行 | 说明 | +| --- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| 1 | [src/views/components/TrainingMaterialButton.vue](../src/views/components/TrainingMaterialButton.vue) | L11 `下载案例资料` | "下载案例资料"按钮本体,含预览弹窗与下载逻辑。**修改此文件会级联影响所有 25 个子页面** | +| 2 | [src/views/components/TrainingTaskToolbar.vue](../src/views/components/TrainingTaskToolbar.vue) | L3 `` | 顶部任务工具栏,内部嵌套 TrainingMaterialButton,被 4 个子页面间接引用 | + +--- + +## 3. 直接引入 `` 的子页面(21 个) + +### 3.1 Foundation 基础模块(2 个) + +| # | 文件 | 引入行 | +| --- | --------------------------------------------------------------------------------------------------------------- | ------ | +| 1 | [src/views/foundation/product-development-factors.vue](../src/views/foundation/product-development-factors.vue) | L31 | +| 2 | [src/views/foundation/product-development-process.vue](../src/views/foundation/product-development-process.vue) | L28 | + +### 3.2 Demand 需求分析模块(8 个) + +| # | 文件 | 引入行 | +| --- | ----------------------------------------------------------------------------------------------------------------- | ------ | +| 1 | [src/views/demand/index.vue](../src/views/demand/index.vue) | L19 | +| 2 | [src/views/demand/environment.vue](../src/views/demand/environment.vue) | L19 | +| 3 | [src/views/demand/people.vue](../src/views/demand/people.vue) | — | +| 4 | [src/views/demand/consumer-scenario.vue](../src/views/demand/consumer-scenario.vue) | L26 | +| 5 | [src/views/demand/consumer-behavior.vue](../src/views/demand/consumer-behavior.vue) | L22 | +| 6 | [src/views/demand/business-feasibility.vue](../src/views/demand/business-feasibility.vue) | L22 | +| 7 | [src/views/demand/feature-conversion.vue](../src/views/demand/feature-conversion.vue) | L26 | +| 8 | [src/views/demand/competitive-environment-analysis.vue](../src/views/demand/competitive-environment-analysis.vue) | L22 | + +### 3.3 Product 产品开发模块(9 个) + +| # | 文件 | 引入行 | +| --- | ------------------------------------------------------------------------------------------------------------------------------- | ------ | +| 1 | [src/views/product/index.vue](../src/views/product/index.vue) | L19 | +| 2 | [src/views/product/structure.vue](../src/views/product/structure.vue) | L22 | +| 3 | [src/views/product/requirements-doc.vue](../src/views/product/requirements-doc.vue) | L22 | +| 4 | [src/views/product/hardware-cost.vue](../src/views/product/hardware-cost.vue) | L29 | +| 5 | [src/views/product/price.vue](../src/views/product/price.vue) | L22 | +| 6 | [src/views/product/differentiation.vue](../src/views/product/differentiation.vue) | L29 | +| 7 | [src/views/product/after-sales-service.vue](../src/views/product/after-sales-service.vue) | L22 | +| 8 | [src/views/product/market-opportunity-selection.vue](../src/views/product/market-opportunity-selection.vue) | L22 | +| 9 | [src/views/product/components/ProductStandardTrainingPage.vue](../src/views/product/components/ProductStandardTrainingPage.vue) | L14 | + +### 3.4 Channel 渠道模块(2 个) + +| # | 文件 | 引入行 | +| --- | ------------------------------------------------------------------- | ------ | +| 1 | [src/views/channel/index.vue](../src/views/channel/index.vue) | L22 | +| 2 | [src/views/channel/supplier.vue](../src/views/channel/supplier.vue) | L22 | + +### 3.5 Test 产品上线与运营推广模块(1 个) + +| # | 文件 | 引入行 | +| --- | ------------------------------------------------------- | ------ | +| 1 | [src/views/test/index.vue](../src/views/test/index.vue) | L21 | + +### 3.6 Assessment 投放效果评估与优化模块(1 个) + +| # | 文件 | 引入行 | +| --- | --------------------------------------------------------------------------------- | ------ | +| 1 | [src/views/assessment/optimization.vue](../src/views/assessment/optimization.vue) | L30 | + +--- + +## 4. 通过 TrainingTaskToolbar 间接引入按钮的子页面(4 个) + +> 这些页面**没有**显式使用 ``,但通过 `` 自动获得下载按钮。同时这些页面的题干中也常常引用"案例资料"作为作答依据(与下载功能配套)。 + +| # | 文件 | 工具栏引入行 | 题干引用"案例资料"位置 | +| --- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------- | +| 1 | [src/views/foundation/new-product-survey.vue](../src/views/foundation/new-product-survey.vue) | L26 `` | — | +| 2 | [src/views/training/GenericTrainingPage.vue](../src/views/training/GenericTrainingPage.vue) | L33 `` | L151/L248/L311/L319/L329/L336/L342 等 | +| 3 | [src/views/test/promotion.vue](../src/views/test/promotion.vue) | L29 `` | L118/L121/L130 | +| 4 | [src/views/assessment/diagnosis.vue](../src/views/assessment/diagnosis.vue) | L21 `` | L81/L89 | + +--- + +## 5. 调整注意事项 + +1. **优先级**:先评估共享组件(第 2 节),修改后全量回归 25 个引用页;如需差异化样式,再回到各子页面单独处理。 +2. **样式作用域**:学生端浅色化务必使用页面级 `:deep()` 选择器(不能嵌套),避免修改 `TrainingMaterialButton.vue` 源码影响其他端。 +3. **禁用态保护**:`TrainingMaterialButton.vue` L32 对 `案例资料.rar + /file/example.rar` 默认占位做了禁用兜底,调整时需保留此逻辑。 +4. **题干文本**:第 4 节页面中"案例资料"既指下载按钮,也指题干说明(如 L151/L118/L81),调整按钮文案时勿误改题干文本。 +5. **数据来源**:`material-name` 与 `material-url` 由后端 `taskConfig` 下发,调整前端时无需改动数据契约。 +6. **HMR 缓存**:KeepAlive 缓存会保留旧组件实例状态,HMR 更新后需硬刷新(Ctrl+Shift+R)验证。 + +--- + +## 6. 文件清单(仅路径) + +``` +src/views/components/TrainingMaterialButton.vue +src/views/components/TrainingTaskToolbar.vue +src/views/foundation/product-development-factors.vue +src/views/foundation/product-development-process.vue +src/views/foundation/new-product-survey.vue +src/views/demand/index.vue +src/views/demand/environment.vue +src/views/demand/people.vue +src/views/demand/consumer-scenario.vue +src/views/demand/consumer-behavior.vue +src/views/demand/business-feasibility.vue +src/views/demand/feature-conversion.vue +src/views/demand/competitive-environment-analysis.vue +src/views/product/index.vue +src/views/product/structure.vue +src/views/product/requirements-doc.vue +src/views/product/hardware-cost.vue +src/views/product/price.vue +src/views/product/differentiation.vue +src/views/product/after-sales-service.vue +src/views/product/market-opportunity-selection.vue +src/views/product/components/ProductStandardTrainingPage.vue +src/views/channel/index.vue +src/views/channel/supplier.vue +src/views/test/index.vue +src/views/test/promotion.vue +src/views/assessment/optimization.vue +src/views/assessment/diagnosis.vue +src/views/training/GenericTrainingPage.vue +``` + +--- + +## 7. 修改执行记录(2026-09-22 完成) + +### 7.1 调整目标 + +将原本单一的"下载案例资料"按钮扩展为**四个独立下载按钮**: + +1. **案例资料**(保留原 `materialName` / `materialUrl` 字段) +2. **实训数据**(新增 `trainingDataName` / `trainingDataUrl` 字段) +3. **实训指导手册**(新增 `guideManualName` / `guideManualUrl` 字段) +4. **AI辅助指导手册**(新增 `aiManualName` / `aiManualUrl` 字段) + +后端 `taskConfig` 接口需对应扩展 6 个字段,未下发时按钮自动显示禁用态(plain)。 + +### 7.2 共享组件改造 + +| # | 文件 | 改动要点 | 状态 | +| --- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------- | +| 1 | [src/views/components/TrainingMaterialButton.vue](../src/views/components/TrainingMaterialButton.vue) | 模板渲染 4 个下载按钮 + 1 个预览按钮;新增 8 个 prop(4 组 name/url);保留 `materialName/materialUrl` 作案例资料向后兼容;新增 `downloadMaterial(material, label)` 通用下载函数;按钮 padding 由 18px 调整为 14px 以适配 4 按钮布局 | ✅ 完成 | +| 2 | [src/views/components/TrainingTaskToolbar.vue](../src/views/components/TrainingTaskToolbar.vue) | 模板 L3-L12 改为多行写法,传递 4 组字段;props 新增 6 个;`loadedMaterial` ref 扩展为 8 字段;新增 `resolvedTrainingData` / `resolvedGuideManual` / `resolvedAiManual` 三个 computed;`loadTaskConfig` 同步读取后端 6 个新字段 | ✅ 完成 | + +### 7.3 子页面批量调整(4 个并行子代理) + +#### 子代理 A — Foundation 模块(2/2 ✅) + +| 文件 | 修改前 | 修改后 | +| ------------------------------------------ | -------- | ---------------------- | +| foundation/product-development-factors.vue | L31 单行 | L31–L40 多行(8 字段) | +| foundation/product-development-process.vue | L28 单行 | L28–L37 多行(8 字段) | + +#### 子代理 B — Demand 模块(8/8 ✅) + +| 文件 | 修改前 | 修改后 | +| ------------------------------------------- | -------- | ------------ | +| demand/index.vue | L19 单行 | L19–L28 多行 | +| demand/environment.vue | L19 单行 | L19–L28 多行 | +| demand/people.vue | L28 单行 | L28–L37 多行 | +| demand/consumer-scenario.vue | L26 单行 | L26–L35 多行 | +| demand/consumer-behavior.vue | L22 单行 | L22–L31 多行 | +| demand/business-feasibility.vue | L22 单行 | L22–L31 多行 | +| demand/feature-conversion.vue | L26 单行 | L26–L35 多行 | +| demand/competitive-environment-analysis.vue | L22 单行 | L22–L31 多行 | + +#### 子代理 C — Product 模块(9/9 ✅) + +| 文件 | 修改前 | 修改后 | +| -------------------------------------------------- | --------------------- | ------------------------------- | +| product/index.vue | L19 单行 | L19–L28 多行 | +| product/structure.vue | L22 单行 | L22–L31 多行 | +| product/requirements-doc.vue | L22 单行 | L22–L31 多行 | +| product/hardware-cost.vue | L29 单行 | L29–L38 多行 | +| product/price.vue | L22 多行(仅 2 字段) | L22–L31 多行(插入 6 行新字段) | +| product/differentiation.vue | L29 单行 | L29–L38 多行 | +| product/after-sales-service.vue | L22 单行 | L22–L31 多行 | +| product/market-opportunity-selection.vue | L22 单行 | L22–L31 多行 | +| product/components/ProductStandardTrainingPage.vue | L14 单行 | L14–L23 多行 | + +#### 子代理 D — Channel / Test / Assessment 模块(4/4 ✅) + +| 文件 | 修改前 | 修改后 | +| --------------------------- | ---------------------------------------------- | ----------------------------------- | +| channel/index.vue | L22 单行 | L22–L31 多行(10 空格缩进) | +| channel/supplier.vue | L22 单行 | L22–L31 多行(10 空格缩进) | +| test/index.vue | L21 单行(带 class="release-channel-toolbar") | L21–L31 多行(保留 class 在标签后) | +| assessment/optimization.vue | L30 单行 | L30–L39 多行(12 空格缩进) | + +### 7.4 间接引入页面(无需修改) + +以下 4 个页面通过 `` 间接渲染按钮,组件改造后自动获得四按钮支持,无需修改模板: + +| 文件 | 工具栏引入位置 | 状态 | +| --------------------------------- | -------------- | ----------- | +| foundation/new-product-survey.vue | L26 | ✅ 自动生效 | +| training/GenericTrainingPage.vue | L33 | ✅ 自动生效 | +| test/promotion.vue | L29 | ✅ 自动生效 | +| assessment/diagnosis.vue | L21 | ✅ 自动生效 | + +### 7.5 全局验证 + +执行 Grep 验证:搜索 `:ai-manual-url="taskConfig?\.aiManualUrl"` 模式,命中 **23 个文件**(与直接引入按钮的子页面数量一致),每个文件各 1 处,共 23 处 ✅。同时验证 6 个新字段绑定行(`:training-data-name` / `:training-data-url` / `:guide-manual-name` / `:guide-manual-url` / `:ai-manual-name` / `:ai-manual-url`)在 23 个文件中均存在。 + +### 7.6 注意事项 + +1. **HMR 缓存**:KeepAlive 缓存会保留旧组件实例状态,HMR 更新后需硬刷新(Ctrl+Shift+R)验证四按钮渲染。 +2. **后端字段契约**:后端 `getTrainingTaskByKey` 接口需返回 6 个新字段(`trainingDataName`/`trainingDataUrl`/`guideManualName`/`guideManualUrl`/`aiManualName`/`aiManualUrl`),未返回时对应按钮显示禁用态。 +3. **禁用态保护**:`案例资料.rar + /file/example.rar` 默认占位文件已被 `resolveMaterial` 函数过滤为 null,按钮显示禁用态,保留此逻辑避免误下载示例占位文件。 +4. **题干文本**:第 4 节页面题干中"案例资料"指的是作答依据(如 L151/L118/L81),与按钮文案独立,未做改动。 +5. **样式作用域**:`TrainingMaterialButton.vue` 是共享组件,被深色页(demand 下)和浅色页(foundation/test/assessment)共用;样式修改需同时验证两端主题。 +6. **预览功能**:仅案例资料保留预览弹窗(图片/PDF),其他三类按钮仅支持下载,不支持预览。 diff --git a/src/views/assessment/optimization.vue b/src/views/assessment/optimization.vue index ace1571..1fec12f 100644 --- a/src/views/assessment/optimization.vue +++ b/src/views/assessment/optimization.vue @@ -27,7 +27,16 @@

{{ activeStepName }}

- +
diff --git a/src/views/channel/index.vue b/src/views/channel/index.vue index 294e7b9..02f804f 100644 --- a/src/views/channel/index.vue +++ b/src/views/channel/index.vue @@ -19,7 +19,16 @@

- +
diff --git a/src/views/channel/supplier.vue b/src/views/channel/supplier.vue index 0a85870..c6f5e67 100644 --- a/src/views/channel/supplier.vue +++ b/src/views/channel/supplier.vue @@ -19,7 +19,16 @@

- +
diff --git a/src/views/components/TrainingMaterialButton.vue b/src/views/components/TrainingMaterialButton.vue index 9a240ea..5ecb1c3 100644 --- a/src/views/components/TrainingMaterialButton.vue +++ b/src/views/components/TrainingMaterialButton.vue @@ -1,21 +1,36 @@ @@ -24,18 +39,40 @@ import { computed, ref } from "vue" import { ElMessage } from "element-plus" import { Download, View } from "@element-plus/icons-vue" -const props = defineProps({ materialName: { type: String, default: "" }, materialUrl: { type: String, default: "" } }) -const previewVisible = ref(false) -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: normalizeMaterialUrl(url) } +const props = defineProps({ + // 案例资料(向后兼容旧 materialName / materialUrl) + materialName: { type: String, default: "" }, + materialUrl: { type: String, default: "" }, + caseMaterialName: { type: String, default: "" }, + caseMaterialUrl: { type: String, default: "" }, + // 实训数据 + trainingDataName: { type: String, default: "" }, + trainingDataUrl: { type: String, default: "" }, + // 实训指导手册 + guideManualName: { type: String, default: "" }, + guideManualUrl: { type: String, default: "" }, + // AI 辅助指导手册 + aiManualName: { type: String, default: "" }, + aiManualUrl: { type: String, default: "" } }) -const extension = computed(() => String(material.value?.name || material.value?.url || "").split(/[?#]/)[0].split(".").pop().toLowerCase()) +const previewVisible = ref(false) + +function resolveMaterial(name, url) { + const n = String(name || "").trim() + const u = String(url || "").trim() + if (!n || !u || (n === "案例资料.rar" && u === "/file/example.rar")) return null + return { name: n, url: normalizeMaterialUrl(u) } +} + +// 案例资料优先读取新增的 caseMaterialName/Url,缺省时回退到旧 materialName/Url 字段 +const caseMaterial = computed(() => resolveMaterial(props.caseMaterialName || props.materialName, props.caseMaterialUrl || props.materialUrl)) +const trainingData = computed(() => resolveMaterial(props.trainingDataName, props.trainingDataUrl)) +const guideManual = computed(() => resolveMaterial(props.guideManualName, props.guideManualUrl)) +const aiManual = computed(() => resolveMaterial(props.aiManualName, props.aiManualUrl)) + +const extension = computed(() => String(caseMaterial.value?.name || caseMaterial.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 normalizeMaterialUrl(rawUrl) { const url = String(rawUrl || "").trim() @@ -51,22 +88,25 @@ function normalizeMaterialUrl(rawUrl) { return url } -async function handleDownload() { - if (!material.value?.url) { ElMessage.warning("暂无案例资料"); return } +async function downloadMaterial(material, label) { + if (!material?.url) { + ElMessage.warning(`暂无${label}`) + return + } try { - const response = await fetch(material.value.url) + const response = await fetch(material.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 + link.download = material.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("案例资料文件不存在或暂不可下载,请联系管理员重新上传。") + console.warn(`${label} download failed`, error) + ElMessage.error(`${label}文件不存在或暂不可下载,请联系管理员重新上传。`) } } @@ -84,7 +124,7 @@ async function handleDownload() { justify-content: center; gap: 6px; min-height: 40px; - padding: 0 18px; + padding: 0 14px; border: 1px solid rgba(31, 41, 55, 0.4); border-radius: 10px; color: #1f2937; diff --git a/src/views/components/TrainingTaskToolbar.vue b/src/views/components/TrainingTaskToolbar.vue index 630d817..c4c72e0 100644 --- a/src/views/components/TrainingTaskToolbar.vue +++ b/src/views/components/TrainingTaskToolbar.vue @@ -1,6 +1,15 @@