|
|
|
@ -92,33 +92,57 @@
|
|
|
|
<p class="section-eyebrow">Step 02</p>
|
|
|
|
<p class="section-eyebrow">Step 02</p>
|
|
|
|
<h3>{{ getStepName(2) }}</h3>
|
|
|
|
<h3>{{ getStepName(2) }}</h3>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<span class="step-chip">平台与关键词</span>
|
|
|
|
<span class="step-chip">实地调研记录</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<p class="step-desc">
|
|
|
|
<p class="step-desc">请以真实消费者身份进入电商平台,通过搜索、浏览和对比,采集 1 个 AI 产品的真实数据,并完成产品三层次分析。</p>
|
|
|
|
优先选择京东、天猫、抖音等主流电商平台,至少选择 3 个高流量使用场景,采用“核心词 + 属性词 + 场景词”的电商搜索热词逻辑记录产品。
|
|
|
|
|
|
|
|
</p>
|
|
|
|
<div class="survey-form">
|
|
|
|
|
|
|
|
<section class="survey-field-group">
|
|
|
|
|
|
|
|
<p class="survey-field-title"><b>01</b> 选择调研平台</p>
|
|
|
|
|
|
|
|
<el-radio-group v-model="step2Form.platform" class="survey-options">
|
|
|
|
|
|
|
|
<el-radio v-for="option in STEP_TWO_PLATFORM_OPTIONS" :key="option.value" :label="option.value" class="survey-option">
|
|
|
|
|
|
|
|
<strong>{{ option.label }}</strong><span>{{ option.description }}</span>
|
|
|
|
|
|
|
|
</el-radio>
|
|
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="training-table-wrap">
|
|
|
|
<section class="survey-field-group">
|
|
|
|
<table class="training-data-table">
|
|
|
|
<p class="survey-field-title"><b>02</b> 确定调研场景</p>
|
|
|
|
<thead>
|
|
|
|
<el-radio-group v-model="step2Form.scene" class="survey-options">
|
|
|
|
<tr>
|
|
|
|
<el-radio v-for="option in STEP_TWO_SCENE_OPTIONS" :key="option.value" :label="option.value" class="survey-option">
|
|
|
|
<th>使用场景</th>
|
|
|
|
<strong>{{ option.label }}</strong><span>{{ option.description }}</span>
|
|
|
|
<th>搜索平台</th>
|
|
|
|
</el-radio>
|
|
|
|
<th>搜索关键词(电商热词)</th>
|
|
|
|
</el-radio-group>
|
|
|
|
<th>平台类目</th>
|
|
|
|
<input v-if="step2Form.scene === '其他场景'" v-model.trim="step2Form.customScene" class="input-single custom-scene-input" placeholder="请填写其他调研场景" maxlength="60" />
|
|
|
|
<th>销量参考(近30天)</th>
|
|
|
|
</section>
|
|
|
|
</tr>
|
|
|
|
|
|
|
|
</thead>
|
|
|
|
<section class="survey-field-group">
|
|
|
|
<tbody>
|
|
|
|
<p class="survey-field-title"><b>03</b> 设计搜索关键词</p>
|
|
|
|
<tr v-for="(row, index) in step2Rows" :key="index">
|
|
|
|
<p class="survey-field-hint">遵循“核心词 + 属性词 + 场景词”。例如:“AI智能音箱 语音控制 家居”“AI办公鼠标 语音转写 会议”。</p>
|
|
|
|
<td><input v-model="row.scene" class="cell-input" placeholder="如:家庭学习" /></td>
|
|
|
|
<input v-model.trim="step2Form.keyword" class="input-single" placeholder="请输入搜索关键词" maxlength="120" />
|
|
|
|
<td><input v-model="row.platform" class="cell-input" placeholder="京东 / 天猫 / 抖音" /></td>
|
|
|
|
</section>
|
|
|
|
<td><input v-model="row.keyword" class="cell-input" placeholder="AI学习机 小学生 护眼" /></td>
|
|
|
|
|
|
|
|
<td><input v-model="row.category" class="cell-input" placeholder="学习机 / 智能硬件" /></td>
|
|
|
|
<section class="survey-field-group">
|
|
|
|
<td><input v-model="row.sales" class="cell-input" placeholder="如:月销 5000+" /></td>
|
|
|
|
<p class="survey-field-title"><b>04</b> 记录产品信息并上传图片</p>
|
|
|
|
</tr>
|
|
|
|
<p class="survey-field-hint">请在搜索结果中按“销量”排序,从排名前 10 的产品中选择 1 款产品;上传主图及详情页截图,最多 5 张。</p>
|
|
|
|
</tbody>
|
|
|
|
<label class="product-name-field">
|
|
|
|
</table>
|
|
|
|
<span>产品名称</span>
|
|
|
|
|
|
|
|
<input v-model.trim="step2Form.productName" class="input-single" placeholder="请输入产品名称" maxlength="120" />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="image-upload-trigger" :class="{ disabled: uploadingImages || step2Form.images.length >= MAX_STEP_TWO_IMAGES }">
|
|
|
|
|
|
|
|
<input type="file" accept="image/*" multiple :disabled="uploadingImages || step2Form.images.length >= MAX_STEP_TWO_IMAGES" @change="uploadStepTwoImages" />
|
|
|
|
|
|
|
|
<span>{{ uploadingImages ? '图片上传中…' : `上传产品图片(${step2Form.images.length}/${MAX_STEP_TWO_IMAGES})` }}</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<div v-if="step2Form.images.length" class="product-image-list">
|
|
|
|
|
|
|
|
<figure v-for="(image, index) in step2Form.images" :key="`${image.url}-${index}`" class="product-image-card">
|
|
|
|
|
|
|
|
<button type="button" class="product-image-preview" @click="previewStepTwoImage(image.url)">
|
|
|
|
|
|
|
|
<img :src="image.url" :alt="`${step2Form.productName || '产品'}图片 ${index + 1}`" />
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
<figcaption :title="image.name">{{ image.name }}</figcaption>
|
|
|
|
|
|
|
|
<button type="button" class="remove-image-button" :aria-label="`删除图片 ${index + 1}`" @click="removeStepTwoImage(index)">删除</button>
|
|
|
|
|
|
|
|
</figure>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</section>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
@ -194,11 +218,11 @@
|
|
|
|
<el-icon><ArrowLeft /></el-icon>
|
|
|
|
<el-icon><ArrowLeft /></el-icon>
|
|
|
|
上一步
|
|
|
|
上一步
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving || checking" @click="saveCurrentProgress">
|
|
|
|
<button type="button" class="btn-nav btn-save" :disabled="saving || checking || uploadingImages" @click="saveCurrentProgress">
|
|
|
|
<el-icon><CircleCheck /></el-icon>
|
|
|
|
<el-icon><CircleCheck /></el-icon>
|
|
|
|
保存当前进度
|
|
|
|
保存当前进度
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
<button type="button" class="btn-nav btn-primary" :disabled="currentStep === steps.length || saving || checking" @click="nextStep">
|
|
|
|
<button type="button" class="btn-nav btn-primary" :disabled="currentStep === steps.length || saving || checking || uploadingImages" @click="nextStep">
|
|
|
|
{{ checking ? "正在判题" : "下一步" }}
|
|
|
|
{{ checking ? "正在判题" : "下一步" }}
|
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
|
<el-icon><ArrowRight /></el-icon>
|
|
|
|
</button>
|
|
|
|
</button>
|
|
|
|
@ -217,6 +241,10 @@
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
</main>
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<el-dialog v-model="imagePreviewOpen" title="产品图片预览" width="min(760px, 92vw)" append-to-body>
|
|
|
|
|
|
|
|
<img v-if="imagePreviewUrl" :src="imagePreviewUrl" alt="产品图片大图" class="product-image-full" />
|
|
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
|
|
<TrainingAiSidebar
|
|
|
|
<TrainingAiSidebar
|
|
|
|
:study-tip="currentStudyTip"
|
|
|
|
:study-tip="currentStudyTip"
|
|
|
|
reference-title="产品三层次分析参考"
|
|
|
|
reference-title="产品三层次分析参考"
|
|
|
|
@ -241,7 +269,7 @@ import {
|
|
|
|
TrendCharts,
|
|
|
|
TrendCharts,
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
} from "@element-plus/icons-vue";
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
import { getTrainingTaskByKey } from "@/api/trainingTask";
|
|
|
|
import { checkNewProductSurveyStepOne, getStudentTrainingAnswer, saveStudentTrainingAnswer } from "@/api/studentTrainingAnswer";
|
|
|
|
import { checkNewProductSurveyStepOne, checkNewProductSurveyStepTwo, getStudentTrainingAnswer, saveStudentTrainingAnswer, uploadStudentTrainingFile } from "@/api/studentTrainingAnswer";
|
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
import useUserStore from "@/store/modules/user";
|
|
|
|
import { extractTrainingStepNames } from "@/views/training/taskKeyMap";
|
|
|
|
import { extractTrainingStepNames } from "@/views/training/taskKeyMap";
|
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
import TrainingAiSidebar from "@/views/components/TrainingAiSidebar.vue";
|
|
|
|
@ -255,6 +283,9 @@ const userStore = useUserStore();
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
const taskConfig = ref(null);
|
|
|
|
const saving = ref(false);
|
|
|
|
const saving = ref(false);
|
|
|
|
const checking = ref(false);
|
|
|
|
const checking = ref(false);
|
|
|
|
|
|
|
|
const uploadingImages = ref(false);
|
|
|
|
|
|
|
|
const imagePreviewOpen = ref(false);
|
|
|
|
|
|
|
|
const imagePreviewUrl = ref("");
|
|
|
|
const draftReady = ref(false);
|
|
|
|
const draftReady = ref(false);
|
|
|
|
const currentStep = ref(1);
|
|
|
|
const currentStep = ref(1);
|
|
|
|
|
|
|
|
|
|
|
|
@ -293,6 +324,22 @@ const steps = computed(() => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const LAYER_OPTIONS = ["核心层", "有形层", "延伸层"];
|
|
|
|
const LAYER_OPTIONS = ["核心层", "有形层", "延伸层"];
|
|
|
|
|
|
|
|
const MAX_STEP_TWO_IMAGES = 5;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const STEP_TWO_PLATFORM_OPTIONS = [
|
|
|
|
|
|
|
|
{ value: "京东", label: "京东", description: "3C/智能硬件类目数据全、评价真实" },
|
|
|
|
|
|
|
|
{ value: "天猫", label: "天猫", description: "品牌 AI 产品丰富,详情页设计成熟" },
|
|
|
|
|
|
|
|
{ value: "抖音电商", label: "抖音电商", description: "爆款 AI 玩具/家居新品多,价格带清晰" },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const STEP_TWO_SCENE_OPTIONS = [
|
|
|
|
|
|
|
|
{ value: "AI智能家居", label: "AI智能家居", description: "如 AI 音箱、AI 台灯、AI 门锁" },
|
|
|
|
|
|
|
|
{ value: "AI智能办公", label: "AI智能办公", description: "如 AI 鼠标、AI 录音笔、AI 翻译机" },
|
|
|
|
|
|
|
|
{ value: "AI儿童玩具", label: "AI儿童玩具", description: "如 AI 早教机器人、AI 对话玩偶" },
|
|
|
|
|
|
|
|
{ value: "AI学习设备", label: "AI学习设备", description: "如 AI 学习机、AI 点读笔" },
|
|
|
|
|
|
|
|
{ value: "AI美妆辅助", label: "AI美妆辅助", description: "如 AI 皮肤检测仪、AI 试妆镜" },
|
|
|
|
|
|
|
|
{ value: "其他场景", label: "其他场景", description: "填写你发现的其他高流量 AI 应用场景" },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const STEP_ONE_SELLING_POINTS = [
|
|
|
|
const STEP_ONE_SELLING_POINTS = [
|
|
|
|
{ id: "selling-point-1", text: "生日礼物女孩子毕业儿童3到6岁10小女童" },
|
|
|
|
{ id: "selling-point-1", text: "生日礼物女孩子毕业儿童3到6岁10小女童" },
|
|
|
|
@ -308,11 +355,7 @@ const STEP_ONE_SELLING_POINTS = [
|
|
|
|
{ id: "selling-point-11", text: "店铺优惠后169元起" },
|
|
|
|
{ id: "selling-point-11", text: "店铺优惠后169元起" },
|
|
|
|
];
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const createStep2Rows = () => [
|
|
|
|
const createStep2Form = () => ({ platform: "", scene: "", customScene: "", keyword: "", productName: "", images: [] });
|
|
|
|
{ scene: "", platform: "", keyword: "", category: "", sales: "" },
|
|
|
|
|
|
|
|
{ scene: "", platform: "", keyword: "", category: "", sales: "" },
|
|
|
|
|
|
|
|
{ scene: "", platform: "", keyword: "", category: "", sales: "" },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const createStep3Rows = () => [
|
|
|
|
const createStep3Rows = () => [
|
|
|
|
{
|
|
|
|
{
|
|
|
|
@ -349,7 +392,7 @@ const createStep4Form = () => ({
|
|
|
|
|
|
|
|
|
|
|
|
const classifications = ref({});
|
|
|
|
const classifications = ref({});
|
|
|
|
const checkResult = ref(null);
|
|
|
|
const checkResult = ref(null);
|
|
|
|
const step2Rows = ref(createStep2Rows());
|
|
|
|
const step2Form = ref(createStep2Form());
|
|
|
|
const step3Rows = ref(createStep3Rows());
|
|
|
|
const step3Rows = ref(createStep3Rows());
|
|
|
|
const step4Form = ref(createStep4Form());
|
|
|
|
const step4Form = ref(createStep4Form());
|
|
|
|
|
|
|
|
|
|
|
|
@ -362,7 +405,7 @@ const getLocalDraftKey = () => {
|
|
|
|
const currentStudyTip = computed(() => {
|
|
|
|
const currentStudyTip = computed(() => {
|
|
|
|
const tips = {
|
|
|
|
const tips = {
|
|
|
|
1: "当前步骤建议:先从案例或电商详情页中拆出核心层、有形层、延伸层,不要把功能和用户利益混在一起。",
|
|
|
|
1: "当前步骤建议:先从案例或电商详情页中拆出核心层、有形层、延伸层,不要把功能和用户利益混在一起。",
|
|
|
|
2: "当前步骤建议:选择主流电商平台,并用“核心词 + 属性词 + 场景词”的组合记录关键词和销量参考。",
|
|
|
|
2: "当前步骤建议:选择一个主流电商平台与真实 AI 产品,用“核心词 + 属性词 + 场景词”搜索并保留主图、详情页截图。",
|
|
|
|
3: "当前步骤建议:同一维度横向比较 3 个产品,重点找出 AI 技术在哪一层创造了差异化。",
|
|
|
|
3: "当前步骤建议:同一维度横向比较 3 个产品,重点找出 AI 技术在哪一层创造了差异化。",
|
|
|
|
4: "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。",
|
|
|
|
4: "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。",
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -384,7 +427,7 @@ onMounted(async () => {
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
watch(
|
|
|
|
[classifications, checkResult, step2Rows, step3Rows, step4Form, currentStep],
|
|
|
|
[classifications, checkResult, step2Form, step3Rows, step4Form, currentStep],
|
|
|
|
() => persistDraft(),
|
|
|
|
() => persistDraft(),
|
|
|
|
{ deep: true }
|
|
|
|
{ deep: true }
|
|
|
|
);
|
|
|
|
);
|
|
|
|
@ -450,7 +493,7 @@ function applySavedAnswer(value) {
|
|
|
|
if (parsed.step1Answer || parsed.step2Answer || parsed.step3Answer || parsed.step4Answer) {
|
|
|
|
if (parsed.step1Answer || parsed.step2Answer || parsed.step3Answer || parsed.step4Answer) {
|
|
|
|
let loaded = false;
|
|
|
|
let loaded = false;
|
|
|
|
const step1Parsed = safeParse(parsed.step1Answer);
|
|
|
|
const step1Parsed = safeParse(parsed.step1Answer);
|
|
|
|
if (step1Parsed?.step1Form || step1Parsed?.step2Rows || step1Parsed?.step3Rows || step1Parsed?.step4Form) {
|
|
|
|
if (step1Parsed?.step1Form || step1Parsed?.step2Form || step1Parsed?.step2Rows || step1Parsed?.step3Rows || step1Parsed?.step4Form) {
|
|
|
|
loaded = applySavedAnswer(step1Parsed) || loaded;
|
|
|
|
loaded = applySavedAnswer(step1Parsed) || loaded;
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
loaded = applyStep1Answer(step1Parsed || parsed.step1Answer) || loaded;
|
|
|
|
loaded = applyStep1Answer(step1Parsed || parsed.step1Answer) || loaded;
|
|
|
|
@ -464,7 +507,7 @@ function applySavedAnswer(value) {
|
|
|
|
|
|
|
|
|
|
|
|
let loaded = false;
|
|
|
|
let loaded = false;
|
|
|
|
loaded = applyStep1Answer(parsed.step1Form || parsed) || loaded;
|
|
|
|
loaded = applyStep1Answer(parsed.step1Form || parsed) || loaded;
|
|
|
|
loaded = applyStep2Answer(parsed.step2Rows || parsed.rows2 || parsed.platformRows) || loaded;
|
|
|
|
loaded = applyStep2Answer(parsed.step2Form || parsed.step2Answer || parsed.step2Rows || parsed.rows2 || parsed.platformRows) || loaded;
|
|
|
|
loaded = applyStep3Answer(parsed.step3Rows || parsed.rows3 || parsed.layerRows) || loaded;
|
|
|
|
loaded = applyStep3Answer(parsed.step3Rows || parsed.rows3 || parsed.layerRows) || loaded;
|
|
|
|
loaded = applyStep4Answer(parsed.step4Form || parsed.aiValue || parsed.valueAnalysis) || loaded;
|
|
|
|
loaded = applyStep4Answer(parsed.step4Form || parsed.aiValue || parsed.valueAnalysis) || loaded;
|
|
|
|
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
|
|
|
|
currentStep.value = Math.min(Math.max(Number(parsed.currentStep || 1), 1), 4);
|
|
|
|
@ -521,11 +564,58 @@ function buildCheckItems() {
|
|
|
|
return STEP_ONE_SELLING_POINTS.map((point) => ({ id: point.id, category: classifications.value[point.id] }));
|
|
|
|
return STEP_ONE_SELLING_POINTS.map((point) => ({ id: point.id, category: classifications.value[point.id] }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function uploadStepTwoImages(event) {
|
|
|
|
|
|
|
|
const input = event.target;
|
|
|
|
|
|
|
|
const files = Array.from(input?.files || []);
|
|
|
|
|
|
|
|
input.value = "";
|
|
|
|
|
|
|
|
if (!files.length) return;
|
|
|
|
|
|
|
|
if (files.some((file) => !file.type?.startsWith("image/"))) {
|
|
|
|
|
|
|
|
proxy?.$modal?.msgWarning("只能上传图片文件");
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if (step2Form.value.images.length + files.length > MAX_STEP_TWO_IMAGES) {
|
|
|
|
|
|
|
|
proxy?.$modal?.msgWarning(`产品图片最多上传 ${MAX_STEP_TWO_IMAGES} 张`);
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
uploadingImages.value = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
|
|
const data = new FormData();
|
|
|
|
|
|
|
|
data.append("file", file);
|
|
|
|
|
|
|
|
const res = await uploadStudentTrainingFile(data);
|
|
|
|
|
|
|
|
const url = res?.url || res?.fileName || res?.data?.url || res?.data?.fileName || "";
|
|
|
|
|
|
|
|
if (!url) throw new Error("图片上传失败,请重试");
|
|
|
|
|
|
|
|
step2Form.value.images.push({ name: res?.originalFilename || file.name, url });
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
proxy?.$modal?.msgSuccess("产品图片上传成功");
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
proxy?.$modal?.msgError(error?.message || "图片上传失败,请稍后重试");
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
uploadingImages.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function removeStepTwoImage(index) {
|
|
|
|
|
|
|
|
step2Form.value.images.splice(index, 1);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function previewStepTwoImage(url) {
|
|
|
|
|
|
|
|
imagePreviewUrl.value = url;
|
|
|
|
|
|
|
|
imagePreviewOpen.value = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function applyStep2Answer(value) {
|
|
|
|
function applyStep2Answer(value) {
|
|
|
|
const parsed = safeParse(value);
|
|
|
|
const parsed = safeParse(value);
|
|
|
|
const rows = Array.isArray(parsed) ? parsed : parsed?.step2Rows || parsed?.rows || parsed?.platformRows;
|
|
|
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return false;
|
|
|
|
if (!Array.isArray(rows) || !rows.length) return false;
|
|
|
|
const source = parsed.step2Form || parsed;
|
|
|
|
step2Rows.value = normalizeRows(rows, createStep2Rows());
|
|
|
|
const fields = ["platform", "scene", "customScene", "keyword", "productName"];
|
|
|
|
|
|
|
|
const images = Array.isArray(source.images)
|
|
|
|
|
|
|
|
? source.images.filter((image) => image?.url).slice(0, MAX_STEP_TWO_IMAGES).map((image) => ({ name: image.name || "产品图片", url: image.url }))
|
|
|
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
const hasValue = fields.some((field) => String(source[field] || "").trim()) || images.length;
|
|
|
|
|
|
|
|
if (!hasValue) return false;
|
|
|
|
|
|
|
|
step2Form.value = { ...createStep2Form(), ...pickFields(source, fields), images };
|
|
|
|
return true;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ -583,7 +673,7 @@ function buildAnswerPayload(status) {
|
|
|
|
saveAction,
|
|
|
|
saveAction,
|
|
|
|
currentStep: currentStep.value,
|
|
|
|
currentStep: currentStep.value,
|
|
|
|
step1Answer: JSON.stringify({ classifications: classifications.value, checkResult: checkResult.value }),
|
|
|
|
step1Answer: JSON.stringify({ classifications: classifications.value, checkResult: checkResult.value }),
|
|
|
|
step2Answer: JSON.stringify(step2Rows.value),
|
|
|
|
step2Answer: JSON.stringify(step2Form.value),
|
|
|
|
step3Answer: JSON.stringify(step3Rows.value),
|
|
|
|
step3Answer: JSON.stringify(step3Rows.value),
|
|
|
|
step4Answer: JSON.stringify(step4Form.value),
|
|
|
|
step4Answer: JSON.stringify(step4Form.value),
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -610,6 +700,20 @@ function submitTask() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function nextStep() {
|
|
|
|
async function nextStep() {
|
|
|
|
|
|
|
|
if (currentStep.value === 2) {
|
|
|
|
|
|
|
|
checking.value = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
await checkNewProductSurveyStepTwo(step2Form.value);
|
|
|
|
|
|
|
|
await saveAnswer("IN_PROGRESS", true);
|
|
|
|
|
|
|
|
proxy?.$modal?.msgSuccess("第二步信息已校验并保存");
|
|
|
|
|
|
|
|
advanceStep();
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
|
|
proxy?.$modal?.msgWarning(error?.message || "请完善第二步调研信息后再进入下一步");
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
|
|
checking.value = false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
}
|
|
|
|
if (currentStep.value !== 1) {
|
|
|
|
if (currentStep.value !== 1) {
|
|
|
|
advanceStep();
|
|
|
|
advanceStep();
|
|
|
|
return;
|
|
|
|
return;
|
|
|
|
@ -651,7 +755,9 @@ function prevStep() {
|
|
|
|
function resetExample() {
|
|
|
|
function resetExample() {
|
|
|
|
classifications.value = {};
|
|
|
|
classifications.value = {};
|
|
|
|
checkResult.value = null;
|
|
|
|
checkResult.value = null;
|
|
|
|
step2Rows.value = createStep2Rows();
|
|
|
|
step2Form.value = createStep2Form();
|
|
|
|
|
|
|
|
imagePreviewUrl.value = "";
|
|
|
|
|
|
|
|
imagePreviewOpen.value = false;
|
|
|
|
step3Rows.value = createStep3Rows();
|
|
|
|
step3Rows.value = createStep3Rows();
|
|
|
|
step4Form.value = createStep4Form();
|
|
|
|
step4Form.value = createStep4Form();
|
|
|
|
currentStep.value = 1;
|
|
|
|
currentStep.value = 1;
|
|
|
|
@ -667,7 +773,7 @@ function buildOutcome() {
|
|
|
|
steps: steps.value.map((item) => item.name),
|
|
|
|
steps: steps.value.map((item) => item.name),
|
|
|
|
classifications: classifications.value,
|
|
|
|
classifications: classifications.value,
|
|
|
|
checkResult: checkResult.value,
|
|
|
|
checkResult: checkResult.value,
|
|
|
|
step2Rows: step2Rows.value,
|
|
|
|
step2Form: step2Form.value,
|
|
|
|
step3Rows: step3Rows.value,
|
|
|
|
step3Rows: step3Rows.value,
|
|
|
|
step4Form: step4Form.value,
|
|
|
|
step4Form: step4Form.value,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
@ -1165,6 +1271,202 @@ function exportOutcome() {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.survey-form {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
gap: 18px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.survey-field-group {
|
|
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(74, 186, 234, 0.24);
|
|
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
|
|
background: linear-gradient(135deg, rgba(8, 51, 77, 0.48), rgba(1, 21, 36, 0.5));
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.survey-field-title {
|
|
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
|
|
margin: 0 0 12px;
|
|
|
|
|
|
|
|
color: #f0fbff;
|
|
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
|
|
font-weight: 900;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
b {
|
|
|
|
|
|
|
|
display: inline-grid;
|
|
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
|
|
color: #062236;
|
|
|
|
|
|
|
|
background: #66dcff;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.survey-field-hint {
|
|
|
|
|
|
|
|
margin: -4px 0 13px 33px;
|
|
|
|
|
|
|
|
color: #a9cee2;
|
|
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
|
|
line-height: 1.65;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.survey-options {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
gap: 9px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.survey-option.el-radio) {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: auto 1fr;
|
|
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
|
|
margin-right: 0;
|
|
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(67, 140, 181, 0.38);
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
color: #c9e8f7;
|
|
|
|
|
|
|
|
background: rgba(5, 30, 48, 0.52);
|
|
|
|
|
|
|
|
transition: border-color 0.18s ease, background 0.18s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.el-radio__label {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
|
|
padding-left: 9px;
|
|
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
strong {
|
|
|
|
|
|
|
|
color: #f2fbff;
|
|
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
span {
|
|
|
|
|
|
|
|
color: #9fc6da;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
line-height: 1.55;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.is-checked {
|
|
|
|
|
|
|
|
border-color: rgba(100, 226, 255, 0.82);
|
|
|
|
|
|
|
|
background: rgba(8, 109, 151, 0.38);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.custom-scene-input {
|
|
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.product-name-field {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: 76px minmax(0, 1fr);
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
|
|
color: #dff5ff;
|
|
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.image-upload-trigger {
|
|
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
|
|
min-height: 42px;
|
|
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
|
|
padding: 0 18px;
|
|
|
|
|
|
|
|
border: 1px dashed rgba(104, 224, 255, 0.75);
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
color: #e8fbff;
|
|
|
|
|
|
|
|
background: rgba(13, 107, 151, 0.34);
|
|
|
|
|
|
|
|
font-weight: 800;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
transition: background 0.18s ease, transform 0.18s ease;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
input {
|
|
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
clip: rect(0 0 0 0);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:hover:not(.disabled) {
|
|
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
|
|
background: rgba(21, 144, 194, 0.48);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&.disabled {
|
|
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.product-image-list {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
|
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
|
|
margin-top: 14px;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.product-image-card {
|
|
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
|
|
gap: 7px;
|
|
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
|
|
padding: 8px;
|
|
|
|
|
|
|
|
border: 1px solid rgba(79, 169, 217, 0.35);
|
|
|
|
|
|
|
|
border-radius: 12px;
|
|
|
|
|
|
|
|
background: rgba(1, 18, 31, 0.62);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
figcaption {
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
color: #b9d9e8;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.product-image-preview {
|
|
|
|
|
|
|
|
aspect-ratio: 1.2;
|
|
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
|
|
background: #06131d;
|
|
|
|
|
|
|
|
cursor: zoom-in;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
&:hover img {
|
|
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.remove-image-button {
|
|
|
|
|
|
|
|
justify-self: start;
|
|
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
|
|
color: #ff9eaf;
|
|
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.product-image-full {
|
|
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
max-height: 72vh;
|
|
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.training-table-wrap {
|
|
|
|
.training-table-wrap {
|
|
|
|
overflow-x: auto;
|
|
|
|
overflow-x: auto;
|
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
|
border: 1px solid rgba(0, 180, 255, 0.22);
|
|
|
|
|