diff --git a/src/api/studentTrainingAnswer.js b/src/api/studentTrainingAnswer.js index a45c351..7d19707 100644 --- a/src/api/studentTrainingAnswer.js +++ b/src/api/studentTrainingAnswer.js @@ -304,8 +304,8 @@ export function validateReleaseChannelEvaluationStepTwo(payload) { && [row.dailySearchVolume, row.supplyDemandRatio, row.averageCpc, row.existingVisitorCount] .every((value) => Number.isFinite(value) && value >= 0) && [row.averageClickRate, row.averageConversionRate].every((value) => Number.isFinite(value) && value >= 0 && value <= 100) - && Number.isInteger(row.matchingScore) && row.matchingScore >= 1 && row.matchingScore <= 5); - if (!valid) return Promise.reject(new Error("请完整填写渠道数据;点击率和转化率为0-100%,匹配度为1-5分")); + && Number.isInteger(row.matchingScore) && row.matchingScore >= 1 && row.matchingScore <= 100); + if (!valid) return Promise.reject(new Error("请完整填写渠道数据;点击率和转化率为0-100%,匹配度为1-100分")); return Promise.resolve({ code: 200, data: { valid: true, message: "渠道数据校验通过", rowCount: normalizedRows.length } }); } return request({ @@ -332,6 +332,8 @@ export function validateReleaseChannelEvaluationStepThree(payload) { ]; if (!sourceRows.length || rows.length !== sourceRows.length) throw new Error("请先完成第二步全部渠道数据"); const submitted = new Map(rows.map((row) => [String(row?.channel || "").trim(), row])); + const incorrectItems = []; + let totalItemCount = 0; for (const source of sourceRows) { const channel = String(source?.channel || "").trim(); const row = submitted.get(channel); @@ -346,12 +348,16 @@ export function validateReleaseChannelEvaluationStepThree(payload) { const max = Math.max(...values); const expectedScore = max === min ? 10 : Number((((reverse ? max - value : value - min) * 10) / (max - min)).toFixed(1)); const supplied = dimensions.get(dimension); - if (!supplied || Number(supplied.weight) !== weight || Number(supplied.score) !== expectedScore) throw new Error(`“${channel}”的“${dimension}”得分或权重不正确`); + if (!supplied || Number(supplied.weight) !== weight) throw new Error(`“${channel}”的“${dimension}”权重不完整`); + totalItemCount += 1; + if (Number(supplied.score) !== expectedScore) incorrectItems.push(`“${channel}”的“${dimension}”得分`); expectedTotal += expectedScore * weight / 100; }); - if (Number(row.weightedScore) !== Number(expectedTotal.toFixed(2))) throw new Error(`“${channel}”的加权得分不正确`); + totalItemCount += 1; + if (Number(row.weightedScore) !== Number(expectedTotal.toFixed(2))) incorrectItems.push(`“${channel}”的加权得分`); } - return Promise.resolve({ code: 200, data: { valid: true, message: "归一化得分与加权得分校验通过", rowCount: rows.length } }); + const calculationCorrect = incorrectItems.length === 0; + return Promise.resolve({ code: 200, data: { valid: true, calculationCorrect, totalItemCount, incorrectItemCount: incorrectItems.length, incorrectItems, message: calculationCorrect ? "计算结果已校验正确,可进入下一步" : `已保存并进入下一步。系统发现${incorrectItems.length}项计算结果与参考值不一致,AI助评时将据此扣分。`, rowCount: rows.length } }); } catch (error) { return Promise.reject(error); } diff --git a/src/views/assessment/optimization.vue b/src/views/assessment/optimization.vue index c215a45..765a8be 100644 --- a/src/views/assessment/optimization.vue +++ b/src/views/assessment/optimization.vue @@ -38,7 +38,7 @@
- + @@ -46,8 +46,8 @@ - - + + @@ -226,7 +226,13 @@ const selectedProduct = computed(() => categoryRows.value.find((row) => row.cate const currentClassificationLabel = computed(() => selectedProduct.value?.abcClassification ? `${selectedProduct.value.abcClassification}类${selectedProduct.value.abcClassification === "A" ? "核心品类" : selectedProduct.value.abcClassification === "B" ? "潜力品类" : "长尾品类"}` : "选择新品后自动带出"); const progressItems = computed(() => configuredSteps.value.map((text, index) => ({ text, status: index + 1 < activeStep.value ? "done" : index + 1 === activeStep.value ? "doing" : "" }))); -function createRow(source = {}) { return { id: source.id || `${Date.now()}-${Math.random().toString(16).slice(2)}`, categoryName: source.categoryName ?? "", revenue: source.revenue ?? "", revenueShare: source.revenueShare ?? "", cumulativeShare: source.cumulativeShare ?? "", inventoryCapital: source.inventoryCapital ?? "", capitalOccupancyRate: source.capitalOccupancyRate ?? "", sellThroughRate: source.sellThroughRate ?? "", grossMargin: source.grossMargin ?? "", abcClassification: source.abcClassification ?? "" }; } +function normalizePercentageValue(value) { + const text = String(value ?? "").trim().replace(/%/g, ""); + if (!text) return null; + const number = Number(text); + return Number.isFinite(number) ? Number(number.toFixed(2)) : null; +} +function createRow(source = {}) { return { id: source.id || `${Date.now()}-${Math.random().toString(16).slice(2)}`, categoryName: source.categoryName ?? "", revenue: source.revenue ?? "", revenueShare: source.revenueShare ?? "", cumulativeShare: source.cumulativeShare ?? "", inventoryCapital: source.inventoryCapital ?? "", capitalOccupancyRate: normalizePercentageValue(source.capitalOccupancyRate), sellThroughRate: normalizePercentageValue(source.sellThroughRate), grossMargin: source.grossMargin ?? "", abcClassification: source.abcClassification ?? "" }; } function createStepFour(source = {}) { return { productName: source.productName ?? "", matrixQuadrant: source.matrixQuadrant ?? "", matrixReason: source.matrixReason ?? "", necessityAnalysis: source.necessityAnalysis ?? "", recommendedClassification: source.recommendedClassification ?? "", trafficSupportMeasures: source.trafficSupportMeasures ?? "" }; } function createStrategyRows(savedRows = []) { const saved = new Map((Array.isArray(savedRows) ? savedRows : []).map((row) => [row?.layer, row])); return [{ layer: "A", label: "A类核心品类", operationPlaceholder: "例如:重点投入流量与推广预算,持续优化转化率", inventoryPlaceholder: "例如:安全库存上限,高频补货,优先保障供货稳定" }, { layer: "B", label: "B类潜力品类", operationPlaceholder: "例如:开展AB测试,针对潜力品类配置流量扶持", inventoryPlaceholder: "例如:密切关注运营效果,活动前适当备货" }, { layer: "C", label: "C类长尾品类", operationPlaceholder: "例如:控制流量预算,通过组合活动逐步清退滞销SKU", inventoryPlaceholder: "例如:停止主动补货,清退滞销SKU" }, { layer: "NEW", label: "新产品", operationPlaceholder: "例如:设置新品测试活动,重点推广运营", inventoryPlaceholder: "例如:提前备货,根据渠道测试情况重点备货" }].map((row) => ({ ...row, operationStrategy: saved.get(row.layer)?.operationStrategy ?? "", inventoryStrategy: saved.get(row.layer)?.inventoryStrategy ?? "", resourcePriority: saved.get(row.layer)?.resourcePriority ?? "" })); } function normalizeActiveStep(value) { return !configuredSteps.value.length ? 1 : Math.min(Math.max(Number(value) || 1, 1), configuredSteps.value.length); } @@ -378,7 +384,7 @@ onMounted(async () => { .training-core { min-width:0; padding:28px; border:1px solid var(--line); border-radius:28px; background:rgba(5,25,37,.82); box-shadow:0 22px 48px rgba(0,0,0,.22); }.task-header{margin-bottom:20px}.task-badge{display:inline-flex;align-items:center;gap:8px;padding:5px 12px;border:1px solid rgba(99,217,255,.55);border-radius:999px;color:#dff5ff;background:rgba(17,126,178,.3);font-size:12px;font-weight:800}.task-header h1{margin:10px 0 0;font-size:30px}.task-card{margin-top:24px;overflow:hidden;border:1px solid var(--line);border-radius:20px;background:rgba(3,22,34,.78)} .workbench-head{display:flex;justify-content:space-between;gap:18px;padding:22px 24px;border-bottom:1px solid var(--line)}.section-eyebrow{margin:0;color:#71dfff;font-size:12px;font-weight:800;letter-spacing:.12em}.workbench-head h2{margin:5px 0 0;font-size:24px}.workbench-actions,.table-toolbar>div,.step-navigation,.training-actions{display:flex;flex-wrap:wrap;align-items:center;gap:10px}.workbench-actions{justify-content:flex-end}.outline-action,.action-button,.nav-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;min-height:40px;padding:0 16px;border:1px solid rgba(99,217,255,.5);border-radius:9px;color:#e9fbff;background:rgba(17,126,178,.34);font:inherit;font-weight:700;cursor:pointer}.outline-action:hover,.action-button:hover,.nav-button:hover{border-color:#7deaff;background:rgba(20,148,193,.48)}button:disabled{cursor:not-allowed;opacity:.45} .step-indicator{display:flex;gap:9px;padding:16px 24px;border-bottom:1px solid var(--line);overflow-x:auto}.step-tab{display:inline-flex;align-items:center;gap:8px;min-width:max-content;padding:8px 12px;border:1px solid var(--line);border-radius:999px;color:var(--muted);background:transparent;font:inherit;cursor:pointer}.step-tab span{display:grid;width:21px;height:21px;place-items:center;border-radius:50%;color:#bdefff;background:rgba(16,117,154,.35);font-size:11px;font-weight:800}.step-tab.active{border-color:#71dfff;color:white;background:rgba(12,114,159,.48)}.step-tab.complete{color:#d5fff8}.step-tab.complete span{background:#2ba995} -.category-step{padding:24px}.instruction{margin:0 0 20px;color:#d8edf5;font-size:15px;line-height:1.75}.table-toolbar{display:flex;justify-content:space-between;gap:14px;margin-bottom:12px;color:var(--muted);font-size:13px}.action-button--muted{border-color:rgba(158,192,207,.35);background:rgba(75,101,112,.25)}.category-table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px}.category-table{width:100%;min-width:1260px;border-collapse:collapse}.category-table th,.category-table td{padding:10px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);text-align:center}.category-table th{color:#e6f9ff;background:rgba(23,112,150,.42);font-size:13px;white-space:nowrap}.category-table td{background:rgba(2,18,28,.32)}.category-table tr:last-child td{border-bottom:0}.category-table th:last-child,.category-table td:last-child{border-right:0}.empty-cell{height:88px;color:var(--muted)}.abc-table{min-width:900px}.abc-table :deep(.el-select){width:100%;min-width:180px} +.category-step{padding:24px}.instruction{margin:0 0 20px;color:#d8edf5;font-size:15px;line-height:1.75}.table-toolbar{display:flex;justify-content:space-between;gap:14px;margin-bottom:12px;color:var(--muted);font-size:13px}.action-button--muted{border-color:rgba(158,192,207,.35);background:rgba(75,101,112,.25)}.category-table-wrap{overflow-x:auto;border:1px solid var(--line);border-radius:12px}.category-table{width:100%;min-width:1260px;border-collapse:collapse}.category-table th,.category-table td{padding:10px;border-right:1px solid var(--line);border-bottom:1px solid var(--line);text-align:center}.category-table th{color:#e6f9ff;background:rgba(23,112,150,.42);font-size:13px;white-space:nowrap}.category-table td{background:rgba(2,18,28,.32)}.category-table tr:last-child td{border-bottom:0}.category-table th:last-child,.category-table td:last-child{border-right:0}.category-table :deep(.el-input-number){width:100%}.empty-cell{height:88px;color:var(--muted)}.abc-table{min-width:900px}.abc-table :deep(.el-select){width:100%;min-width:180px} .rule-notice{margin-top:20px;padding:16px 18px;border:1px solid rgba(255,112,112,.46);border-radius:12px;background:rgba(126,31,35,.15);color:#ffd5d5}.rule-notice strong{color:#ff9292}.rule-notice p{margin:8px 0 0;line-height:1.65}.analysis-step{padding:24px}.analysis-tabs{display:flex;gap:10px;margin-bottom:18px}.analysis-tabs button{padding:9px 14px;border:1px solid var(--line);border-radius:8px;color:var(--muted);background:transparent;font:inherit;cursor:pointer}.analysis-tabs button.active{color:white;border-color:#71dfff;background:rgba(12,114,159,.48)}.business-workspace{overflow:hidden;border:1px solid var(--line);border-radius:14px}.step-navigation{justify-content:center;padding:20px 24px 0}.nav-button--save,.nav-button--primary{border-color:#71dfff;background:linear-gradient(100deg,#0d80b7,#09638f)}.training-actions{justify-content:center;gap:28px;padding:26px 24px}.training-actions :deep(.training-action){min-width:160px;height:50px;border-color:#2c83aa;color:#dff7ff;background:rgba(13,64,88,.62);font-size:17px;font-weight:800} .pareto-upload-panel{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;min-height:330px;padding:26px;border:1px dashed rgba(113,223,255,.7);border-radius:16px;background:linear-gradient(135deg,rgba(5,47,69,.7),rgba(4,23,35,.58));text-align:center}.file-input{display:none}.upload-icon{font-size:42px;color:#71dfff}.pareto-upload-panel>strong{font-size:20px}.pareto-upload-panel>span,.upload-file-meta span{color:var(--muted);font-size:13px}.pareto-preview{width:min(100%,760px);height:270px;border:1px solid var(--line);border-radius:10px;background:rgba(0,10,18,.65);cursor:zoom-in}.upload-file-meta{display:grid;gap:5px;max-width:100%;word-break:break-all}.pareto-actions{display:flex;flex-wrap:wrap;justify-content:center;gap:10px}.danger-action{border-color:rgba(255,145,145,.6);color:#ffd7d7;background:rgba(130,43,43,.23)} .pareto-materials{display:flex;align-items:center;justify-content:space-between;gap:22px;margin-top:20px;padding:18px 20px;border-left:3px solid var(--accent);border-radius:0 14px 14px 0;background:rgba(11,79,109,.22);color:#d8edf5}.pareto-materials h3{margin:0 0 8px;font-size:16px}.pareto-materials ol{margin:0;padding-left:20px;line-height:1.8}.pareto-materials__actions{display:grid;flex:0 0 178px;gap:10px}.action-button--primary{border-color:#71dfff;background:linear-gradient(100deg,#0d80b7,#09638f)} diff --git a/src/views/components/TrainingAiSidebar.vue b/src/views/components/TrainingAiSidebar.vue index eb339e7..1f19858 100644 --- a/src/views/components/TrainingAiSidebar.vue +++ b/src/views/components/TrainingAiSidebar.vue @@ -36,12 +36,31 @@ AI助评 +
+ 综合评分 + {{ displayFinalScore }} + / 100 +
+
-

{{ displayEvalMessage }}

-
- 综合评分:{{ displayFinalScore }} / 100 +
+

{{ reports.assessmentSummary }}

+
+
+
+ {{ item.name }} + {{ item.score }} / {{ item.maxScore }} +
+

{{ item.description }}

+
+
+
+ 优先改进 +
  • {{ item }}
+
+

{{ displayEvalMessage }}

请先至少填写并保存一项实训内容,再请求 AI 助评。

@@ -109,13 +128,20 @@ const route = useRoute(); const currentTaskKey = computed(() => resolveTrainingTaskKey(route, props.taskKey)); const reports = computed(() => normalizeEvaluationReports(evaluation.value)); const currentStepContext = computed(() => [props.studyTip, props.referenceTitle, plainText(props.referenceText)].filter(Boolean).join("\n")); +const displayScoreCriteria = computed(() => { + const criteria = reports.value.scoreCriteria; + const stepNames = props.progressItems.map((item) => String(item?.text || "").trim()).filter(Boolean); + if (!stepNames.length || stepNames.length !== criteria.length) return criteria; + return criteria.map((item, index) => ({ ...item, name: stepNames[index] })); +}); const displayAssistMessage = computed(() => assistMessage.value || props.studyTip); const displayScoreVisible = computed(() => props.scoreVisible || internalScoreVisible.value || reports.value.score !== null); const displayFinalScore = computed(() => (props.scoreVisible ? props.finalScore : reports.value.score ?? internalScore.value)); +const hasStructuredAssessment = computed(() => Boolean(reports.value.assessmentSummary || displayScoreCriteria.value.length || reports.value.improvements.length)); const displayEvalMessage = computed(() => { if (props.scoreVisible) return props.evalMessage; - return reports.value.assessmentReport || internalEvalMessage.value || props.evalMessage; + return internalEvalMessage.value || props.evalMessage; }); onMounted(loadEvaluation); @@ -174,7 +200,7 @@ async function requestEvaluation() { await loadEvaluation(); internalScoreVisible.value = reports.value.score !== null; internalScore.value = reports.value.score ?? "--"; - internalEvalMessage.value = reports.value.assessmentReport || props.evalMessage; + internalEvalMessage.value = reports.value.assessmentSummary || "AI 评分已生成,请查看评分明细与改进建议。"; } catch (error) { internalEvalMessage.value = "AI 助评暂时不可用,请稍后重试。"; } finally { assessmentLoading.value = false; } @@ -266,6 +292,61 @@ function plainText(value) { margin: 0; } } +.ai-output--review { + max-height: 500px; + overflow-y: auto; +} +.assessment-result { + position: relative; + z-index: 1; + display: grid; + gap: 12px; +} +.assessment-summary { + margin: 0; + color: #e6f7ff; + font-weight: 650; + line-height: 1.8; +} +.assessment-criteria { + display: grid; + gap: 8px; +} +.assessment-criterion { + padding: 10px 11px; + border: 1px solid rgba(113, 225, 255, 0.22); + border-radius: 10px; + background: rgba(10, 43, 61, 0.4); +} +.assessment-criterion__heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + color: #f0fbff; +} +.assessment-criterion__heading span { + flex: 0 0 auto; + color: #71e8ff; + font-weight: 800; + font-variant-numeric: tabular-nums; +} +.assessment-criterion__heading small { color: #91bbcf; font-weight: 600; } +.assessment-criterion p { + margin: 6px 0 0; + color: #afd3e3; + font-size: 13px; + line-height: 1.65; +} +.assessment-actions { + padding: 10px 11px; + border-left: 2px solid #5ce4ff; + color: #cceafa; + background: rgba(29, 114, 149, 0.14); +} +.assessment-actions strong { color: #7cecff; font-size: 13px; } +.assessment-actions ul { margin: 6px 0 0; padding-left: 18px; } +.assessment-actions li + li { margin-top: 4px; } .study-messages { position: relative; z-index: 1; @@ -380,17 +461,30 @@ function plainText(value) { z-index: 1; display: inline-flex; align-items: center; - gap: 6px; - margin-top: 14px; - padding: 8px 12px; - border: 1px solid rgba(126, 233, 255, 0.38); - border-radius: 999px; + width: 100%; + gap: 8px; + margin: -2px 0 14px; + padding: 12px 14px; + border: 1px solid rgba(101, 238, 255, 0.7); + border-radius: 12px; color: #dff8ff; - background: rgba(11, 42, 56, 0.78); + background: + linear-gradient(90deg, rgba(20, 169, 216, 0.4), rgba(8, 50, 70, 0.75)), + rgba(11, 42, 56, 0.78); + box-shadow: inset 0 1px 0 rgba(220, 253, 255, 0.23), 0 0 24px rgba(39, 209, 255, 0.16); + font-size: 14px; + font-weight: 800; + letter-spacing: 0.04em; + + span { color: #dff8ff; } + em { color: #b8dce9; font-size: 13px; font-style: normal; font-weight: 700; } strong { color: #7ee9ff; - font-size: 20px; + margin-left: auto; + font-size: 28px; + line-height: 1; + text-shadow: 0 0 14px rgba(102, 238, 255, 0.6); } } diff --git a/src/views/components/trainingAiEvaluationReport.js b/src/views/components/trainingAiEvaluationReport.js index 4961461..01e2c6c 100644 --- a/src/views/components/trainingAiEvaluationReport.js +++ b/src/views/components/trainingAiEvaluationReport.js @@ -8,6 +8,11 @@ function safeParseJson(value) { } } +function criterionDisplayName(value) { + const name = String(value || "").trim(); + return name || "评分项"; +} + function formatReport(value) { if (value == null) return ""; const parsed = typeof value === "string" ? safeParseJson(value) : value; @@ -24,13 +29,19 @@ function normalizeCriteria(value) { return value.map((item) => { if (typeof item === "string") return { name: item, score: "", description: "" }; return { - name: String(item?.name || item?.criterion || item?.title || "评分项"), + name: criterionDisplayName(item?.name || item?.criterion || item?.title), score: item?.score ?? item?.weight ?? "", - description: String(item?.description || item?.detail || ""), + maxScore: item?.maxScore ?? item?.max ?? "", + description: String(item?.rationale || item?.description || item?.detail || ""), }; }); } +function normalizeTextList(value) { + if (!Array.isArray(value)) return []; + return value.map((item) => String(item || "").trim()).filter(Boolean); +} + function normalizeOperation(operation, root, type) { const value = operation && typeof operation === "object" ? operation : {}; const prefix = type === "help" ? "help" : "assessment"; @@ -47,15 +58,19 @@ 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); + const assessmentReportValue = assessment.report || assessment.content || assessment.feedback; + const assessmentReportData = safeParseJson(root.assessmentReportJson) || safeParseJson(assessmentReportValue) || {}; return { help, assessment, helpReport: formatReport(help.report || help.content || help.suggestion), - assessmentReport: formatReport(assessment.report || assessment.content || assessment.feedback), + assessmentReport: formatReport(assessmentReportValue), score: root.assessmentScore ?? assessment.score ?? assessmentReportData?.score ?? root.score ?? null, scoreCriteria: normalizeCriteria(assessmentReportData?.criteria || assessment.scoreCriteria || root.scoreCriteria), + assessmentSummary: String(assessmentReportData?.overallComment || assessmentReportData?.summary || "").trim(), + strengths: normalizeTextList(assessmentReportData?.strengths), + improvements: normalizeTextList(assessmentReportData?.improvements), }; } diff --git a/src/views/test/index.vue b/src/views/test/index.vue index 503e59c..39f518a 100644 --- a/src/views/test/index.vue +++ b/src/views/test/index.vue @@ -56,7 +56,7 @@
品类名称近12个月营收(万元)营收占比(%)累计占比(%)库存资金占用(万元)资金占用率近30天动销率毛利率
品类名称近12个月营收(万元)营收占比(%)累计占比(%)库存资金占用(万元)资金占用率(%)近30天动销率(%)毛利率
暂无数据,请根据案例资料增加行并填写。
当前权重合计{{ displayWeightTotal }}%
-

默认展示建议的评估维度和权重占比。可修改任意单元格,也可逐行新增、删除;点击下一步时系统校验所有权重之和必须等于 100%。

+

默认展示建议的评估维度和建议权重范围,具体权重需由学生自行填写。可修改任意单元格,也可逐行新增、删除;点击下一步时系统校验所有权重之和必须等于 100%。