feat: add consumer behavior hypotheses

dev-QQq
chenyuan 3 weeks ago
parent 7a3b2a8b02
commit 0c63009c88

@ -335,3 +335,20 @@ export function checkProductDevelopmentFactorsStepFour(payload) {
}
return request({ url: "/api/student-training-answers/product-development-factors/step-4/validate", method: "post", data: payload, headers: { repeatSubmit: false } });
}
export function checkConsumerBehaviorStepOne(payload) {
const hypotheses = Array.isArray(payload?.hypotheses) ? payload.hypotheses : [];
if (isStudentDemo()) {
const incomplete = hypotheses.find((item) => ["targetGroup", "keyFinding", "coreHypothesis"].some((field) => !String(item?.[field] || "").trim()));
if (!hypotheses.length || incomplete) {
return Promise.reject(new Error("请完整填写目标人群、实训8关键发现和问卷核心假设"));
}
return Promise.resolve({ code: 200, data: { valid: true, message: "校验通过" } });
}
return request({
url: "/api/student-training-answers/consumer-behavior/step-1/validate",
method: "post",
data: { hypotheses },
headers: { repeatSubmit: false },
});
}

@ -58,32 +58,35 @@
</div>
</div>
<section v-if="form.activeStep === 0" class="step-panel">
<div class="questionnaire-head">
<p class="task-desc questionnaire-desc">
借助AI生成智能产品购买决策因素调研问卷提示词参考设计问卷用于分析用户购买智能产品的决策因素问卷完成后复制到下述编辑框并上传问卷pdf文档
<div class="hypothesis-head">
<p class="task-desc hypothesis-desc">
基于实训 8 的用户画像结论明确本次问卷调查的目标人群与核心假设填写下表
</p>
<el-button class="upload-action questionnaire-upload" :loading="uploading" @click="triggerFileSelect('questionnaire')">
<el-icon><Upload /></el-icon>
上传问卷
</el-button>
</div>
<div v-if="form.questionnaireFileName" class="file-chip questionnaire-file">
<el-icon><Document /></el-icon>
<span>{{ form.questionnaireFileName }}</span>
<div class="hypothesis-actions">
<el-button class="hypothesis-action" @click="addTargetHypothesis"></el-button>
<el-button class="hypothesis-action hypothesis-action--quiet" :disabled="form.targetHypotheses.length <= 1" @click="removeTargetHypothesis"></el-button>
</div>
<el-button class="training-eight-action" @click="viewTargetUserProfile">8</el-button>
</div>
<div class="field-block questionnaire-field">
<span>示例</span>
<div
ref="questionnaireEditorRef"
class="large-textarea questionnaire-textarea"
contenteditable="true"
role="textbox"
aria-multiline="true"
data-placeholder="请将借助AI设计完成的问卷内容复制到这里"
@input="syncQuestionnaireText"
@blur="syncQuestionnaireText"
></div>
<div class="target-hypothesis-table-wrap">
<table class="target-hypothesis-table">
<thead>
<tr>
<th>目标人群</th>
<th>实训8关键发现</th>
<th>本次问卷需验证的核心假设</th>
</tr>
</thead>
<tbody>
<tr v-for="(hypothesis, index) in form.targetHypotheses" :key="index">
<td><input v-model="hypothesis.targetGroup" class="hypothesis-input" placeholder="例如职场白领25-35岁" /></td>
<td><textarea v-model="hypothesis.keyFinding" class="hypothesis-textarea" placeholder="例如:高压工作、入睡困难、看重效率" /></td>
<td><textarea v-model="hypothesis.coreHypothesis" class="hypothesis-textarea" placeholder="例如:是否更关注白噪声质量与智能场景联动?对价格的敏感度如何?" /></td>
</tr>
</tbody>
</table>
</div>
<p class="hypothesis-tip">可编辑表格内容请基于实训 8 的结论明确每类目标人群的问卷验证方向</p>
</section>
<section v-else-if="form.activeStep === 1" class="step-panel">
@ -179,7 +182,7 @@ import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
import TrainingMaterialButton from "@/views/components/TrainingMaterialButton.vue";
import TrainingTaskBrief from "@/views/components/TrainingTaskBrief.vue";
import { getTrainingTaskByKey } from "@/api/trainingTask";
import { getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
import { checkConsumerBehaviorStepOne, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
const TASK_KEY = "consumer-behavior";
const { proxy } = getCurrentInstance();
@ -380,6 +383,7 @@ const uploadAccept = computed(() => {
function createForm() {
return {
activeStep: 0,
targetHypotheses: createTargetHypotheses(),
questionnaireText: questionnaireTemplate,
questionnaireFileName: "",
questionnaireFileUrl: "",
@ -390,6 +394,18 @@ function createForm() {
};
}
function createTargetHypotheses() {
return [
{ targetGroup: "职场白领25-35岁", keyFinding: "", coreHypothesis: "" },
{ targetGroup: "精致妈妈28-38岁", keyFinding: "", coreHypothesis: "" },
{ targetGroup: "银发人群50-65岁", keyFinding: "", coreHypothesis: "" },
];
}
function createEmptyTargetHypothesis() {
return { targetGroup: "", keyFinding: "", coreHypothesis: "" };
}
const form = ref(createForm());
const taskTitle = computed(() => taskConfig.value?.taskName || "消费者需求与行为");
@ -458,6 +474,7 @@ function applySavedAnswer(value) {
...parsed,
activeStep: Number.isInteger(parsed.activeStep) ? parsed.activeStep : 0,
questionnaireText: normalizeQuestionnaireContent(parsed.questionnaireText || questionnaireTemplate),
targetHypotheses: normalizeTargetHypotheses(parsed.targetHypotheses),
chartFiles: parsed.chartFiles || {},
report: normalizeReport(parsed.report),
};
@ -467,6 +484,15 @@ function applySavedAnswer(value) {
}
}
function normalizeTargetHypotheses(rows) {
if (!Array.isArray(rows) || !rows.length) return createTargetHypotheses();
return rows.map((row) => ({
targetGroup: String(row?.targetGroup || ""),
keyFinding: String(row?.keyFinding || ""),
coreHypothesis: String(row?.coreHypothesis || ""),
}));
}
function normalizeReport(value = {}) {
return Object.keys(emptyReport).reduce((report, key) => {
const currentValue = value?.[key] || "";
@ -528,6 +554,19 @@ function setActiveStep(index) {
}
}
function addTargetHypothesis() {
form.value.targetHypotheses.push(createEmptyTargetHypothesis());
}
function removeTargetHypothesis() {
if (form.value.targetHypotheses.length <= 1) return;
form.value.targetHypotheses.pop();
}
function viewTargetUserProfile() {
window.open("/demand/people", "_blank", "noopener");
}
async function handleFileChange(event) {
const file = event.target.files?.[0];
if (!file) return;
@ -561,7 +600,8 @@ function goPrev() {
if (form.value.activeStep > 0) setActiveStep(form.value.activeStep - 1);
}
function goNext() {
async function goNext() {
if (form.value.activeStep === 0) await persist("IN_PROGRESS");
if (form.value.activeStep < flowSteps.value.length - 1) setActiveStep(form.value.activeStep + 1);
}
@ -589,6 +629,9 @@ function buildAnswerPayload(status) {
async function persist(status = "IN_PROGRESS") {
saving.value = true;
try {
if (status !== "RESET" && (form.value.activeStep === 0 || status === "SUBMITTED")) {
await checkConsumerBehaviorStepOne({ hypotheses: form.value.targetHypotheses });
}
await saveStudentTrainingAnswer(TASK_KEY, buildAnswerPayload(status));
proxy?.$modal?.msgSuccess(status === "SUBMITTED" ? "提交成功" : "保存成功");
} finally {
@ -618,6 +661,9 @@ function exportOutcome() {
"",
`当前步骤:${flowSteps.value[form.value.activeStep] || ""}`,
"",
"问卷调查目标人群与核心假设:",
...form.value.targetHypotheses.map((item, index) => `${index + 1}. 目标人群:${item.targetGroup || ""}\n实训8关键发现${item.keyFinding || ""}\n核心假设${item.coreHypothesis || ""}`),
"",
"问卷设计:",
questionnaireTextForExport(),
"",
@ -1009,6 +1055,127 @@ function exportOutcome() {
margin-bottom: 12px;
}
.hypothesis-head {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 18px 28px;
margin-bottom: 20px;
}
.hypothesis-desc {
margin: 0;
color: #d9eaf4;
font-size: 16px;
font-weight: 600;
}
.hypothesis-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 14px;
}
.hypothesis-actions :deep(.hypothesis-action),
.training-eight-action {
min-width: 146px;
height: 42px;
border: 1px solid rgba(92, 224, 255, 0.7);
border-radius: 10px;
color: #ffffff;
background: linear-gradient(95deg, #0b84bd, #096491);
font-size: 15px;
font-weight: 800;
}
.hypothesis-actions :deep(.hypothesis-action--quiet) {
border-color: rgba(115, 191, 220, 0.58);
color: #d9f4ff;
background: rgba(13, 85, 117, 0.72);
}
.training-eight-action {
grid-column: 2;
grid-row: 1;
min-width: 136px;
}
.target-hypothesis-table-wrap {
overflow-x: auto;
border: 1px solid rgba(0, 180, 255, 0.24);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.target-hypothesis-table {
width: 100%;
min-width: 920px;
border-collapse: collapse;
table-layout: fixed;
th,
td {
padding: 12px;
border-right: 1px solid rgba(77, 167, 220, 0.26);
border-bottom: 1px solid rgba(77, 167, 220, 0.26);
vertical-align: middle;
}
th {
color: #ecfbff;
background: linear-gradient(100deg, #158fc4, #08739f);
font-size: 16px;
font-weight: 800;
}
th:nth-child(1) { width: 28%; }
th:nth-child(2) { width: 28%; }
th:nth-child(3) { width: 44%; }
tr:last-child td { border-bottom: 0; }
th:last-child,
td:last-child { border-right: 0; }
}
.hypothesis-input,
.hypothesis-textarea {
box-sizing: border-box;
width: 100%;
border: 1px solid rgba(67, 134, 166, 0.8);
outline: none;
color: #eaf7ff;
background: rgba(7, 29, 43, 0.92);
font-family: inherit;
font-size: 14px;
line-height: 1.65;
&::placeholder { color: #8da5b2; opacity: 1; }
&:focus {
border-color: #5fe7ff;
box-shadow: 0 0 0 3px rgba(34, 207, 255, 0.14);
}
}
.hypothesis-input {
height: 42px;
padding: 0 12px;
border-radius: 10px;
}
.hypothesis-textarea {
min-height: 86px;
padding: 10px 12px;
border-radius: 10px;
resize: vertical;
}
.hypothesis-tip {
margin: 12px 0 0;
color: #75dff7;
font-size: 13px;
}
.questionnaire-desc {
flex: 1;
margin: 0;
@ -1399,6 +1566,24 @@ function exportOutcome() {
width: 100%;
}
.hypothesis-head {
grid-template-columns: 1fr;
}
.training-eight-action {
grid-column: auto;
grid-row: auto;
width: 100%;
}
.hypothesis-actions {
justify-content: stretch;
}
.hypothesis-actions :deep(.hypothesis-action) {
flex: 1;
}
.step-indicator {
align-items: stretch;
flex-direction: column;

Loading…
Cancel
Save