feat: add product development process step four

dev-QQq
chenyuan 3 weeks ago
parent d0c9211c9e
commit e740fee32c

@ -165,6 +165,23 @@ export function checkProductDevelopmentProcessStepThree(payload) {
});
}
export function checkProductDevelopmentProcessStepFour(payload) {
if (isStudentDemo()) {
const value = payload || {};
if (!String(value.parallelWorkAnalysis || "").trim()) return Promise.reject(new Error("请填写可提前介入并同步完成的电商运营工作"));
if (!String(value.aiScenarioOne || "").trim() || !String(value.aiScenarioTwo || "").trim()) {
return Promise.reject(new Error("请至少填写 2 个 AI 工具提效应用场景"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/product-development-process/step-4/validate",
method: "post",
data: payload,
headers: { repeatSubmit: false },
});
}
function validateNewProductSurveyStepTwoDemo(form) {
const value = form || {};
const images = Array.isArray(value.images) ? value.images : [];

@ -119,6 +119,28 @@
<button type="button" class="btn-nav" :disabled="uploadingGantt" @click="resetStepThree"><el-icon><RefreshLeft /></el-icon></button>
</div>
</template>
<template v-else-if="currentStep === 4">
<div class="step-title-row">
<div><p class="section-eyebrow">Step 04</p><h3>{{ trainingSteps[3] }}</h3></div>
<span class="step-chip">并行协同复盘</span>
</div>
<p v-if="!stepThreePassed" class="step-warning"> 3 </p>
<section class="process-reflection" :class="{ disabled: !stepThreePassed }">
<label class="reflection-field">
<span>1观察甘特图思考电商运营在产品开发过程中可以提前介入同步完成哪些工作以缩短运营周期提高效率预热蓄水内容种草</span>
<textarea v-model.trim="stepFourForm.parallelWorkAnalysis" :disabled="!stepThreePassed || checking" placeholder="请结合甘特图说明可同步推进的电商运营工作、介入节点及预期效率提升" />
</label>
<div class="reflection-question">2在互联网产品开发管理中你认为哪些环节可以借助AI工具提升效率请列举至少2个具体应用场景</div>
<div class="scenario-grid">
<label class="reflection-field"><span>场景1</span><textarea v-model.trim="stepFourForm.aiScenarioOne" :disabled="!stepThreePassed || checking" placeholder="例如:使用 AI 汇总竞品评价与用户需求辅助生成PRD初稿" /></label>
<label class="reflection-field"><span>场景2</span><textarea v-model.trim="stepFourForm.aiScenarioTwo" :disabled="!stepThreePassed || checking" placeholder="例如:使用 AI 生成电商详情页文案、主图脚本或运营素材" /></label>
</div>
</section>
<div class="task-actions">
<button type="button" class="btn-nav btn-submit" :disabled="checking || !stepThreePassed" @click="submitTraining"></button>
<button type="button" class="btn-nav" :disabled="checking" @click="resetStepFour"><el-icon><RefreshLeft /></el-icon></button>
</div>
</template>
<div v-else class="step-placeholder"><p> {{ currentStep }} 步内容待配置</p><span> 1 步答案已保存可从上方步骤栏返回查看</span></div>
</section>
</section>
@ -136,7 +158,7 @@
<script setup>
import { Download, Operation, RefreshLeft, Upload } from "@element-plus/icons-vue";
import * as XLSX from "xlsx";
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { checkProductDevelopmentProcessStepOne, checkProductDevelopmentProcessStepTwo, checkProductDevelopmentProcessStepThree, checkProductDevelopmentProcessStepFour, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import useUserStore from "@/store/modules/user";
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
@ -170,8 +192,10 @@ const draftReady = ref(false);
const checkResult = ref(null);
const stepOnePassed = ref(false);
const stepTwoPassed = ref(false);
const stepThreePassed = ref(false);
const ganttImage = ref({ url: "", name: "" });
const uploadingGantt = ref(false);
const stepFourForm = ref({ parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" });
const taskTitle = computed(() => taskConfig.value?.taskName || defaultTask.title);
const taskBackground = computed(() => taskConfig.value?.background || defaultTask.background);
const taskGoals = computed(() => parseArray(taskConfig.value?.objectives, defaultTask.goals));
@ -224,7 +248,11 @@ async function loadSavedAnswer() {
}
if (answer?.step2Answer) restoreStepTwoOutcome(answer.step2Answer);
if (answer?.step2Answer) stepTwoPassed.value = true;
if (answer?.step3Answer) restoreStepThreeOutcome(answer.step3Answer);
if (answer?.step3Answer) {
restoreStepThreeOutcome(answer.step3Answer);
stepThreePassed.value = true;
}
if (answer?.step4Answer) restoreStepFourOutcome(answer.step4Answer);
const savedStep = Number(answer?.currentStep);
if (Number.isInteger(savedStep) && savedStep > 0) currentStep.value = savedStep;
} catch (error) {
@ -267,6 +295,19 @@ function restoreStepThreeOutcome(value) {
}
}
function restoreStepFourOutcome(value) {
try {
const parsed = typeof value === "string" ? JSON.parse(value) : value;
stepFourForm.value = {
parallelWorkAnalysis: parsed?.parallelWorkAnalysis || "",
aiScenarioOne: parsed?.aiScenarioOne || "",
aiScenarioTwo: parsed?.aiScenarioTwo || "",
};
} catch (error) {
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" };
}
}
function getLocalDraftKey() {
const info = userStore.userInfo || {};
return `${TASK_KEY}:draft:${info.userId || info.user_id || info.username || info.userName || "anonymous"}`;
@ -287,7 +328,11 @@ function buildStepThreeOutcome() {
return { title: taskTitle.value, ganttImageUrl: ganttImage.value.url, ganttImageName: ganttImage.value.name };
}
function buildDraft() { return { step1: buildOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), currentStep: currentStep.value }; }
function buildStepFourOutcome() {
return { ...stepFourForm.value };
}
function buildDraft() { return { step1: buildOutcome(), step2: buildStepTwoOutcome(), step3: buildStepThreeOutcome(), step4: buildStepFourOutcome(), currentStep: currentStep.value }; }
function persistDraft() {
if (!draftReady.value) return;
@ -302,6 +347,7 @@ function loadDraft() {
restoreOutcome(draft?.step1 || draft);
if (draft?.step2) restoreStepTwoOutcome(draft.step2);
if (draft?.step3) restoreStepThreeOutcome(draft.step3);
if (draft?.step4) restoreStepFourOutcome(draft.step4);
if (Number(draft?.currentStep) > 0) currentStep.value = Number(draft.currentStep);
}
} catch (error) { clearDraft(); }
@ -349,7 +395,9 @@ async function resetTraining() {
checkResult.value = null;
stepOnePassed.value = false;
stepTwoPassed.value = false;
stepThreePassed.value = false;
ganttImage.value = { url: "", name: "" };
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" };
clearDraft();
try {
checking.value = true;
@ -419,6 +467,7 @@ async function saveAndGoToStepFour() {
await checkProductDevelopmentProcessStepThree(buildStepThreeOutcome());
await saveStudentTrainingAnswer(TASK_KEY, { step3Answer: JSON.stringify(buildStepThreeOutcome()), currentStep: 4, submitted: false, saveAction: "SAVE" });
clearDraft();
stepThreePassed.value = true;
currentStep.value = 4;
proxy?.$modal?.msgSuccess("已保存,已进入第 4 步");
} catch (error) {
@ -433,6 +482,26 @@ function resetStepThree() {
proxy?.$modal?.msgSuccess("已清空第 3 步上传内容");
}
async function submitTraining() {
if (checking.value || !stepThreePassed.value) return;
checking.value = true;
try {
await checkProductDevelopmentProcessStepFour(buildStepFourOutcome());
await saveStudentTrainingAnswer(TASK_KEY, { step4Answer: JSON.stringify(buildStepFourOutcome()), currentStep: 4, submitted: true, saveAction: "SUBMIT" });
clearDraft();
proxy?.$modal?.msgSuccess("任务提交成功");
} catch (error) {
proxy?.$modal?.msgError?.(error?.message || "请完成并行协同复盘与两个AI应用场景");
} finally {
checking.value = false;
}
}
function resetStepFour() {
stepFourForm.value = { parallelWorkAnalysis: "", aiScenarioOne: "", aiScenarioTwo: "" };
proxy?.$modal?.msgSuccess("已清空第 4 步填写内容");
}
function previewGanttImage() {
window.open(ganttImage.value.url, "_blank", "noopener,noreferrer");
}
@ -501,6 +570,7 @@ function exportOutcome() {
.process-step-card { padding: 24px; border: 1px solid rgba(31, 185, 249, .32); border-radius: 18px; background: rgba(1, 21, 34, .65); }.step-title-row { display: flex; justify-content: space-between; align-items: flex-start; }.step-title-row h3 { margin: 5px 0 0; color: #fff; font-size: 24px; }.step-chip { border: 1px solid #38caff; border-radius: 999px; padding: 7px 12px; color: #9aeaff; }.process-intro { margin: 24px 0; line-height: 1.8; color: #c5e4ef; }.training-table-wrap { overflow-x: auto; }.process-table { width: 100%; min-width: 740px; border-collapse: collapse; color: #dff5ff; }.process-table th { padding: 14px; color: #fff; background: #079ed9; font-size: 16px; }.process-table td, .process-table th { border: 1px solid rgba(139, 213, 241, .45); text-align: center; }.process-table td { padding: 9px 12px; }.stage-cell { width: 16%; font-weight: 700; }.task-no-cell { width: 15%; }.task-cell { width: 37%; }.select-cell { width: 32%; }.select-cell select { width: 170px; max-width: 100%; padding: 6px 10px; border: 1px solid #6ea9bd; color: #132c36; background: #fff; font-size: 15px; }.classification-feedback { margin: 6px 0 0; color: #ff9b8f; font-size: 12px; }.is-correct { background: rgba(55, 196, 147, .08); }.is-incorrect { background: rgba(255, 91, 82, .1); }.classification-summary { margin: 16px 0 0; color: #ffbc86; }.classification-summary.is-success { color: #6ee8b1; }.task-actions { justify-content: center; margin-top: 26px; }.btn-submit { border-color: #36d9ff; background: linear-gradient(110deg, #0eaae4, #0572ac); font-weight: 700; }.btn-nav:disabled { opacity: .6; cursor: not-allowed; }.step-placeholder { padding: 90px 20px; text-align: center; color: #a8cfdd; }.step-placeholder p { color: #fff; font-size: 20px; }
.step-warning { margin: 0 0 16px; padding: 10px 14px; border: 1px solid rgba(255, 173, 93, .62); border-radius: 8px; color: #ffd19f; background: rgba(187, 102, 27, .16); }.schedule-table { min-width: 1180px; }.schedule-table .stage-cell { width: 13%; }.schedule-table .task-no-cell { width: 9%; }.schedule-table .task-cell { width: 20%; }.classification-cell { width: 13%; font-weight: 700; }.schedule-table input { box-sizing: border-box; width: 150px; max-width: 100%; padding: 7px 8px; border: 1px solid #6ea9bd; color: #132c36; background: #fff; }.schedule-table .dependency-input { width: 190px; }.schedule-table input:disabled { cursor: not-allowed; opacity: .6; }
.gantt-upload-panel { display: grid; justify-items: center; gap: 22px; min-height: 320px; padding: 32px 16px; border: 1px dashed rgba(71, 204, 255, .55); border-radius: 14px; background: radial-gradient(circle at center, rgba(15, 119, 163, .16), transparent 55%); }.gantt-upload-trigger { display: grid; place-items: center; align-content: center; gap: 9px; width: 280px; height: 138px; border-radius: 10px; color: #fff; background: linear-gradient(135deg, #0ba6e0, #0877ad); cursor: pointer; box-shadow: 0 14px 30px rgba(5, 126, 180, .24); }.gantt-upload-trigger input { position: absolute; width: 1px; height: 1px; opacity: 0; }.gantt-upload-trigger .el-icon { font-size: 26px; }.gantt-upload-trigger span { color: #cbefff; font-size: 12px; }.gantt-upload-trigger.disabled { cursor: not-allowed; opacity: .55; }.gantt-preview { width: min(100%, 760px); margin: 0; text-align: center; }.gantt-preview img { display: block; width: 100%; max-height: 410px; object-fit: contain; border: 1px solid rgba(84, 211, 255, .5); border-radius: 10px; background: #fff; cursor: zoom-in; }.gantt-preview figcaption { margin-top: 8px; color: #a8dce9; font-size: 13px; }.gantt-guide { margin-top: 26px; padding: 18px 22px; border-left: 3px solid #23c4ff; background: rgba(12, 75, 104, .22); color: #d4edf6; }.gantt-guide h4 { margin: 0 0 14px; color: #fff; }.gantt-guide ol { margin: 0; padding-left: 20px; }.gantt-guide li { margin: 11px 0; line-height: 1.7; }.gantt-guide button { margin-left: 12px; border: 0; border-radius: 6px; padding: 5px 10px; color: #fff; background: #109ed4; cursor: pointer; }
.process-reflection { display: grid; gap: 28px; }.reflection-field { display: grid; gap: 12px; color: #d8edf4; font-size: 16px; line-height: 1.7; }.reflection-field span { color: #f1fbff; font-weight: 600; }.reflection-field textarea { min-height: 190px; resize: vertical; border: 1px solid rgba(126, 212, 243, .6); border-radius: 8px; padding: 14px; color: #eafaff; background: rgba(3, 35, 53, .78); font: inherit; line-height: 1.7; }.reflection-field textarea:focus { outline: 2px solid rgba(47, 204, 255, .45); border-color: #45d2ff; }.reflection-field textarea:disabled { cursor: not-allowed; opacity: .55; }.reflection-question { color: #f1fbff; font-size: 16px; font-weight: 600; line-height: 1.8; }.scenario-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px; }.scenario-grid .reflection-field textarea { min-height: 180px; }.process-reflection.disabled { opacity: .72; }
@media (max-width: 1280px) { .product-process { grid-template-columns: 1fr; }.step-indicator { overflow-x: auto; }.step-tab { min-width: 190px; } }
@media (max-width: 720px) { .product-process { padding: 12px; }.process-workbench { margin: 12px; padding: 14px; }.task-header { padding: 18px; }.workbench-actions { justify-content: flex-start; flex-wrap: wrap; }.step-title-row { gap: 12px; }.step-chip { display: none; } }
@media (max-width: 720px) { .product-process { padding: 12px; }.process-workbench { margin: 12px; padding: 14px; }.task-header { padding: 18px; }.workbench-actions { justify-content: flex-start; flex-wrap: wrap; }.step-title-row { gap: 12px; }.step-chip { display: none; }.scenario-grid { grid-template-columns: 1fr; gap: 18px; } }
</style>

Loading…
Cancel
Save