@ -50,60 +50,38 @@
< p class = "section-eyebrow" > Step 01 < / p >
< p class = "section-eyebrow" > Step 01 < / p >
< h3 > { { getStepName ( 1 ) } } < / h3 >
< h3 > { { getStepName ( 1 ) } } < / h3 >
< / div >
< / div >
< span class = "step-chip" > 案例信息提取 < / span >
< span class = "step-chip" > 卖点分类练习 < / span >
< / div >
< / div >
< p class = "step-desc" > 请根据实训要求中的案例一信息, 分别从核心层 、 有形层 、 延伸层提取信息 , 并填写对应信息 ; < / p >
< p class = "step-desc" > 查看案例产品详情, 仔细阅读主图及详情页信息 , 对下列产品卖点进行三层次分类 。 < / p >
< label class = "field-block" >
< div class = "training-table-wrap classification-table-wrap" >
< span > 该款智能产品平台类目是 : < / span >
< table class = "training-data-table classification-table" >
< input v -model = " step1Form.category " class = "input-field input-single" / >
< thead >
< / label >
< tr >
< th > 序号 < / th >
< label class = "field-block" >
< th > 卖点 < / th >
< span > 核心层 : 从上图可以提炼出该款智能产品解决的用户痛点为 : < / span >
< th > 三层次分类 < / th >
< textarea v -model = " step1Form.corePain " class = "input-field" rows = "4" / >
< / tr >
< / label >
< / thead >
< tbody >
< label class = "field-block" >
< tr v-for ="(point, index) in STEP_ONE_SELLING_POINTS" :key="point.id" :class="feedbackClass(point.id)" >
< span > 有形层 : 从产品主图可以看出 , 关于产品有形层的主要卖点为 : < / span >
< td class = "classification-number" > { { index + 1 } } < / td >
< textarea v -model = " step1Form.tangibleSellingPoint " class = "input-field" rows = "4" / >
< td class = "classification-selling-point" > { { point . text } } < / td >
< / label >
< td class = "classification-select-cell" >
< el -select v-model ="classifications[point.id]" placeholder="请选择分类" :disabled="checking" @change="clearFeedback(point.id)" >
< label class = "field-block" >
< el -option v -for = " category in LAYER_OPTIONS " :key ="category" :label ="category" :value ="category" / >
< span > 价格带为 : < / span >
< / e l - s e l e c t >
< div class = "price-options" >
< p v-if ="checkResult?.itemsById?.[point.id]" class="classification-feedback" >
< button
{ { checkResult . itemsById [ point . id ] . message } }
v - for = "option in priceOptions"
< / p >
: key = "option.value"
< / td >
type = "button"
< / tr >
: class = "['price-option', { active: step1Form.priceBand === option.value }]"
< / tbody >
@ click = "step1Form.priceBand = option.value"
< / table >
>
{ { step1Form . priceBand === option . value ? "√" : "" } } { { option . label } }
< / button >
< / div >
< / label >
< p class = "step-desc" > 延伸层 : 从产品主图可以看出 , 关于产品品牌保证发货等延伸层相关描述为 : < / p >
< div class = "form-grid three" >
< label class = "field-block" >
< span > 店铺名称 : < / span >
< input v -model = " step1Form.storeName " class = "input-field input-single" / >
< / label >
< label class = "field-block" >
< span > 店铺星级 : < / span >
< input v -model = " step1Form.storeRating " class = "input-field input-single" / >
< / label >
< label class = "field-block" >
< span > 发货优势 : < / span >
< input v -model = " step1Form.shippingAdvantage " class = "input-field input-single" / >
< / label >
< / div >
< / div >
< p v-if ="checkResult" class="classification-summary" :class="{ 'is-success': checkResult.allCorrect }" >
< label class = "field-block" >
本次判题 : { { checkResult . correctCount } } / { { checkResult . totalCount } } 项分类正确
< span > 根据该产品主图 、 详情 、 评价等信息 , 在产品介绍 、 外观设计和功能上您有何改进建议 ? < / span >
< / p >
< textarea v -model = " step1Form.improvement " class = "input-field" rows = "4" / >
< / label >
< / div >
< / div >
< / section >
< / section >
@ -114,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 >
< / e l - r a d i o >
< / e l - r a d i o - g r o u p >
< / 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 >
< / e l - r a d i o >
< th > 搜索关键词 ( 电商热词 ) < / th >
< / e l - r a d i o - g r o u p >
< 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 >
@ -216,12 +218,12 @@
< el -icon > < ArrowLeft / > < / e l - i c o n >
< el -icon > < ArrowLeft / > < / e l - i c o n >
上一步
上一步
< / button >
< / button >
< button type = "button" class = "btn-nav btn-save" :disabled ="saving " @click ="saveCurrentProgress" >
< button type = "button" class = "btn-nav btn-save" : disabled = "saving || checking || uploadingImages " @click ="saveCurrentProgress" >
< el -icon > < CircleCheck / > < / e l - i c o n >
< el -icon > < CircleCheck / > < / e l - i c o n >
保存当前进度
保存当前进度
< / button >
< / button >
< button type = "button" class = "btn-nav btn-primary" : disabled = "currentStep === steps.length " @click ="nextStep" >
< button type = "button" class = "btn-nav btn-primary" : disabled = "currentStep === steps.length || saving || checking || uploadingImages " @click ="nextStep" >
下一步
{ { checking ? "正在判题" : " 下一步" } }
< el -icon > < ArrowRight / > < / e l - i c o n >
< el -icon > < ArrowRight / > < / e l - i c o n >
< / button >
< / button >
< / div >
< / div >
@ -239,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" / >
< / e l - d i a l o g >
< TrainingAiSidebar
< TrainingAiSidebar
: study - tip = "currentStudyTip"
: study - tip = "currentStudyTip"
reference - title = "产品三层次分析参考"
reference - title = "产品三层次分析参考"
@ -263,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 { 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" ;
@ -276,14 +282,18 @@ const userStore = useUserStore();
const taskConfig = ref ( null ) ;
const taskConfig = ref ( null ) ;
const saving = ref ( false ) ;
const saving = 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 ) ;
const defaultSteps = [
const defaultSteps = [
{ no : 1 , name : "产品 三层次理论应用 ", icon : Search } ,
{ no : 1 , name : "产品 卖点的三层次分类 ", icon : Search } ,
{ no : 2 , name : "选定调查平台与产品" , icon : DataAnalysis } ,
{ no : 2 , name : "选定调查平台与产品" , icon : DataAnalysis } ,
{ no : 3 , name : "AI 新 产品三层次分析", icon : TrendCharts } ,
{ no : 3 , name : "AI 产品三层次分析", icon : TrendCharts } ,
{ no : 4 , name : "AI 技术 价值分析", icon : Operation } ,
{ no : 4 , name : "AI 应用 价值分析", icon : Operation } ,
] ;
] ;
const defaultGoals = [
const defaultGoals = [
@ -313,29 +323,40 @@ const steps = computed(() => {
} ) ) ;
} ) ) ;
} ) ;
} ) ;
const priceOptions = [
const LAYER _OPTIONS = [ "核心层" , "有形层" , "延伸层" ] ;
{ label : "1000元以下的基础款" , value : "basic" } ,
const MAX _STEP _TWO _IMAGES = 5 ;
{ label : "1000-3000元的进阶全能款" , value : "advanced" } ,
{ label : "3000元以上的旗舰高端款" , value : "flagship" } ,
const STEP _TWO _PLATFORM _OPTIONS = [
{ value : "京东" , label : "京东" , description : "3C/智能硬件类目数据全、评价真实" } ,
{ value : "天猫" , label : "天猫" , description : "品牌 AI 产品丰富,详情页设计成熟" } ,
{ value : "抖音电商" , label : "抖音电商" , description : "爆款 AI 玩具/家居新品多,价格带清晰" } ,
] ;
] ;
const createStep1Form = ( ) => ( {
const STEP _TWO _SCENE _OPTIONS = [
category : "" ,
{ value : "AI智能家居" , label : "AI智能家居" , description : "如 AI 音箱、AI 台灯、AI 门锁" } ,
corePain : "" ,
{ value : "AI智能办公" , label : "AI智能办公" , description : "如 AI 鼠标、AI 录音笔、AI 翻译机" } ,
tangibleSellingPoint : "" ,
{ value : "AI儿童玩具" , label : "AI儿童玩具" , description : "如 AI 早教机器人、AI 对话玩偶" } ,
priceBand : "basic" ,
{ value : "AI学习设备" , label : "AI学习设备" , description : "如 AI 学习机、AI 点读笔" } ,
storeName : "" ,
{ value : "AI美妆辅助" , label : "AI美妆辅助" , description : "如 AI 皮肤检测仪、AI 试妆镜" } ,
storeRating : "" ,
{ value : "其他场景" , label : "其他场景" , description : "填写你发现的其他高流量 AI 应用场景" } ,
shippingAdvantage : "" ,
] ;
improvement : "" ,
} ) ;
const createStep2Rows = ( ) => [
const STEP _ONE _SELLING _POINTS = [
{ scene : "" , platform : "" , keyword : "" , category : "" , sales : "" } ,
{ id : "selling-point-1" , text : "生日礼物女孩子毕业儿童3到6岁10小女童" } ,
{ scene : "" , platform : "" , keyword : "" , category : "" , sales : "" } ,
{ id : "selling-point-2" , text : "AI智能机器人毛绒玩具" } ,
{ scene : "" , platform : "" , keyword : "" , category : "" , sales : "" } ,
{ id : "selling-point-3" , text : "豆包+DeepSeek六大混合模型, 多语种AI对话" } ,
{ id : "selling-point-4" , text : "覆盖百科、算术、语文、诗词、AI翻译等, 早教能力强, 还具备情绪感知安抚功能" } ,
{ id : "selling-point-5" , text : "安全亲肤,零甲醛零荧光剂,母婴级不掉毛" } ,
{ id : "selling-point-6" , text : "柔软手感, 舒心无忧, 内赠USB充电线" } ,
{ id : "selling-point-7" , text : "嘴巴会动,翅膀煽动,脖子扭动,触摸感应触发,说话跳舞同步" } ,
{ id : "selling-point-8" , text : "可自由定制角色/专属音色,打造宝宝专属小伙伴,适配哄睡、早教、陪伴多场景" } ,
{ id : "selling-point-9" , text : "快递免运费, 预计7小时内发货" } ,
{ id : "selling-point-10" , text : "大促价保、假一赔四、极速退款、7天无理由退换" } ,
{ id : "selling-point-11" , text : "店铺优惠后169元起" } ,
] ;
] ;
const createStep2Form = ( ) => ( { platform : "" , scene : "" , customScene : "" , keyword : "" , productName : "" , images : [ ] } ) ;
const createStep3Rows = ( ) => [
const createStep3Rows = ( ) => [
{
{
dimension : "核心层" ,
dimension : "核心层" ,
@ -369,8 +390,9 @@ const createStep4Form = () => ({
extendedValue : "" ,
extendedValue : "" ,
} ) ;
} ) ;
const step1Form = ref ( createStep1Form ( ) ) ;
const classifications = ref ( { } ) ;
const step2Rows = ref ( createStep2Rows ( ) ) ;
const checkResult = ref ( null ) ;
const step2Form = ref ( createStep2Form ( ) ) ;
const step3Rows = ref ( createStep3Rows ( ) ) ;
const step3Rows = ref ( createStep3Rows ( ) ) ;
const step4Form = ref ( createStep4Form ( ) ) ;
const step4Form = ref ( createStep4Form ( ) ) ;
@ -383,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 : "当前步骤建议:把前面三步的发现收束为产品开发启发,说明哪些价值值得继续强化。" ,
} ;
} ;
@ -405,7 +427,7 @@ onMounted(async () => {
} ) ;
} ) ;
watch (
watch (
[ step1Form, step2Rows , step3Rows , step4Form , currentStep ] ,
[ classifications, checkResult , step2Form , step3Rows , step4Form , currentStep ] ,
( ) => persistDraft ( ) ,
( ) => persistDraft ( ) ,
{ deep : true }
{ deep : true }
) ;
) ;
@ -471,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 ? . step2 Rows || step1Parsed ? . step3Rows || step1Parsed ? . step4Form ) {
if ( step1Parsed ? . step1Form || step1Parsed ? . step2 Form || step1Parsed ? . step2 Rows || 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 ;
@ -485,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 . step2 Rows || parsed . rows2 || parsed . platformRows ) || loaded ;
loaded = applyStep2Answer ( parsed . step2 Form || parsed . step2Answer || parsed . step2 Rows || 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 ) ;
@ -495,19 +517,105 @@ function applySavedAnswer(value) {
function applyStep1Answer ( value ) {
function applyStep1Answer ( value ) {
const parsed = safeParse ( value ) ;
const parsed = safeParse ( value ) ;
if ( ! parsed || typeof parsed !== "object" ) return false ;
if ( ! parsed || typeof parsed !== "object" ) return false ;
const source = parsed . step1Form || parsed ;
const source = parsed . classifications || parsed . step1Form || parsed ;
const fields = Object . keys ( createStep1Form ( ) ) ;
const restored = STEP _ONE _SELLING _POINTS . reduce ( ( result , point ) => {
const hasValue = fields . some ( ( key ) => source [ key ] !== undefined && source [ key ] !== null && source [ key ] !== "" ) ;
if ( LAYER _OPTIONS . includes ( source ? . [ point . id ] ) ) {
if ( ! hasValue ) return false ;
result [ point . id ] = source [ point . id ] ;
step1Form . value = { ... createStep1Form ( ) , ... pickFields ( source , fields ) } ;
}
return true ;
return result ;
} , { } ) ;
classifications . value = restored ;
checkResult . value = normalizeCheckResult ( parsed . checkResult ) ;
return Object . keys ( restored ) . length > 0 ;
}
function normalizeCheckResult ( value ) {
if ( ! value || typeof value !== "object" || ! Array . isArray ( value . items ) ) return null ;
const itemsById = value . items . reduce ( ( result , item ) => {
if ( item ? . id ) result [ item . id ] = item ;
return result ;
} , { } ) ;
return {
items : value . items ,
itemsById ,
correctCount : Number ( value . correctCount || 0 ) ,
totalCount : Number ( value . totalCount || value . items . length ) ,
allCorrect : Boolean ( value . allCorrect ) ,
} ;
}
function feedbackClass ( id ) {
const item = checkResult . value ? . itemsById ? . [ id ] ;
if ( ! item ) return "" ;
return item . correct ? "classification-row--correct" : "classification-row--incorrect" ;
}
function clearFeedback ( id ) {
if ( checkResult . value ? . itemsById ? . [ id ] ) {
checkResult . value = null ;
}
}
function hasCompletedClassifications ( ) {
return STEP _ONE _SELLING _POINTS . every ( ( point ) => LAYER _OPTIONS . includes ( classifications . value [ point . id ] ) ) ;
}
function buildCheckItems ( ) {
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 ;
}
}
@ -564,8 +672,8 @@ function buildAnswerPayload(status) {
return {
return {
saveAction ,
saveAction ,
currentStep : currentStep . value ,
currentStep : currentStep . value ,
step1Answer : JSON . stringify ( step1Form . value ) ,
step1Answer : JSON . stringify ( { classifications : classifications . value , checkResult : checkResult . value } ) ,
step2Answer : JSON . stringify ( step2 Rows . value ) ,
step2Answer : JSON . stringify ( step2 Form . value ) ,
step3Answer : JSON . stringify ( step3Rows . value ) ,
step3Answer : JSON . stringify ( step3Rows . value ) ,
step4Answer : JSON . stringify ( step4Form . value ) ,
step4Answer : JSON . stringify ( step4Form . value ) ,
} ;
} ;
@ -591,7 +699,48 @@ function submitTask() {
return saveAnswer ( "SUBMITTED" ) ;
return saveAnswer ( "SUBMITTED" ) ;
}
}
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 ) {
advanceStep ( ) ;
return ;
}
if ( ! hasCompletedClassifications ( ) ) {
proxy ? . $modal ? . msgWarning ( "请完成全部卖点分类后再进入下一步" ) ;
return ;
}
checking . value = true ;
try {
const res = await checkNewProductSurveyStepOne ( buildCheckItems ( ) ) ;
checkResult . value = normalizeCheckResult ( res ? . data ) ;
await saveAnswer ( "IN_PROGRESS" , true ) ;
if ( checkResult . value ? . allCorrect ) {
proxy ? . $modal ? . msgSuccess ( "分类全部正确,已进入下一步" ) ;
advanceStep ( ) ;
} else {
proxy ? . $modal ? . msgWarning ( "存在分类错误,请根据提示修改后重新判题" ) ;
}
} catch ( error ) {
proxy ? . $modal ? . msgError ( error ? . message || "判题失败,请稍后重试" ) ;
} finally {
checking . value = false ;
}
}
function advanceStep ( ) {
if ( currentStep . value < steps . value . length ) {
if ( currentStep . value < steps . value . length ) {
currentStep . value += 1 ;
currentStep . value += 1 ;
}
}
@ -604,8 +753,11 @@ function prevStep() {
}
}
function resetExample ( ) {
function resetExample ( ) {
step1Form . value = createStep1Form ( ) ;
classifications . value = { } ;
step2Rows . value = createStep2Rows ( ) ;
checkResult . value = null ;
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 ;
@ -619,8 +771,9 @@ function buildOutcome() {
taskId : taskConfig . value ? . id || "" ,
taskId : taskConfig . value ? . id || "" ,
currentStep : currentStep . value ,
currentStep : currentStep . value ,
steps : steps . value . map ( ( item ) => item . name ) ,
steps : steps . value . map ( ( item ) => item . name ) ,
step1Form : step1Form . value ,
classifications : classifications . value ,
step2Rows : step2Rows . value ,
checkResult : checkResult . value ,
step2Form : step2Form . value ,
step3Rows : step3Rows . value ,
step3Rows : step3Rows . value ,
step4Form : step4Form . value ,
step4Form : step4Form . value ,
} ;
} ;
@ -1118,6 +1271,202 @@ function exportOutcome() {
}
}
}
}
. survey - form {
display : grid ;
gap : 18 px ;
}
. survey - field - group {
padding : 16 px ;
border : 1 px solid rgba ( 74 , 186 , 234 , 0.24 ) ;
border - radius : 18 px ;
background : linear - gradient ( 135 deg , rgba ( 8 , 51 , 77 , 0.48 ) , rgba ( 1 , 21 , 36 , 0.5 ) ) ;
}
. survey - field - title {
display : flex ;
align - items : center ;
gap : 9 px ;
margin : 0 0 12 px ;
color : # f0fbff ;
font - size : 16 px ;
font - weight : 900 ;
b {
display : inline - grid ;
width : 24 px ;
height : 24 px ;
place - items : center ;
border - radius : 50 % ;
color : # 062236 ;
background : # 66 dcff ;
font - size : 12 px ;
}
}
. survey - field - hint {
margin : - 4 px 0 13 px 33 px ;
color : # a9cee2 ;
font - size : 13 px ;
line - height : 1.65 ;
}
. survey - options {
display : grid ;
gap : 9 px ;
}
: deep ( . survey - option . el - radio ) {
display : grid ;
grid - template - columns : auto 1 fr ;
align - items : start ;
height : auto ;
margin - right : 0 ;
padding : 10 px 12 px ;
border : 1 px solid rgba ( 67 , 140 , 181 , 0.38 ) ;
border - radius : 12 px ;
color : # c9e8f7 ;
background : rgba ( 5 , 30 , 48 , 0.52 ) ;
transition : border - color 0.18 s ease , background 0.18 s ease ;
. el - radio _ _label {
display : grid ;
gap : 4 px ;
padding - left : 9 px ;
white - space : normal ;
}
strong {
color : # f2fbff ;
font - weight : 800 ;
}
span {
color : # 9 fc6da ;
font - size : 12 px ;
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 : 10 px ;
}
. product - name - field {
display : grid ;
grid - template - columns : 76 px minmax ( 0 , 1 fr ) ;
align - items : center ;
gap : 10 px ;
margin - bottom : 14 px ;
color : # dff5ff ;
font - size : 14 px ;
font - weight : 800 ;
}
. image - upload - trigger {
display : inline - flex ;
min - height : 42 px ;
align - items : center ;
justify - content : center ;
padding : 0 18 px ;
border : 1 px dashed rgba ( 104 , 224 , 255 , 0.75 ) ;
border - radius : 12 px ;
color : # e8fbff ;
background : rgba ( 13 , 107 , 151 , 0.34 ) ;
font - weight : 800 ;
cursor : pointer ;
transition : background 0.18 s ease , transform 0.18 s ease ;
input {
position : absolute ;
width : 1 px ;
height : 1 px ;
overflow : hidden ;
clip : rect ( 0 0 0 0 ) ;
}
& : hover : not ( . disabled ) {
transform : translateY ( - 1 px ) ;
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 ( 132 px , 1 fr ) ) ;
gap : 12 px ;
margin - top : 14 px ;
}
. product - image - card {
display : grid ;
gap : 7 px ;
min - width : 0 ;
margin : 0 ;
padding : 8 px ;
border : 1 px solid rgba ( 79 , 169 , 217 , 0.35 ) ;
border - radius : 12 px ;
background : rgba ( 1 , 18 , 31 , 0.62 ) ;
figcaption {
overflow : hidden ;
color : # b9d9e8 ;
font - size : 12 px ;
text - overflow : ellipsis ;
white - space : nowrap ;
}
}
. product - image - preview {
aspect - ratio : 1.2 ;
overflow : hidden ;
padding : 0 ;
border : 0 ;
border - radius : 8 px ;
background : # 06131 d ;
cursor : zoom - in ;
img {
display : block ;
width : 100 % ;
height : 100 % ;
object - fit : cover ;
transition : transform 0.2 s 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 : 12 px ;
cursor : pointer ;
}
. product - image - full {
display : block ;
width : 100 % ;
max - height : 72 vh ;
object - fit : contain ;
}
. training - table - wrap {
. training - table - wrap {
overflow - x : auto ;
overflow - x : auto ;
border : 1 px solid rgba ( 0 , 180 , 255 , 0.22 ) ;
border : 1 px solid rgba ( 0 , 180 , 255 , 0.22 ) ;
@ -1249,6 +1598,61 @@ function exportOutcome() {
background : rgba ( 13 , 64 , 88 , 0.62 ) ;
background : rgba ( 13 , 64 , 88 , 0.62 ) ;
}
}
. classification - table - wrap {
margin - top : 20 px ;
}
. classification - table th : first - child ,
. classification - table td : first - child {
width : 84 px ;
text - align : center ;
}
. classification - table th : last - child ,
. classification - table td : last - child {
width : 240 px ;
}
. classification - number {
color : # 9 bdcff ;
font - weight : 800 ;
}
. classification - selling - point {
line - height : 1.65 ;
}
. classification - select - cell : deep ( . el - select ) {
width : 100 % ;
}
. classification - row -- correct td {
background : rgba ( 23 , 181 , 121 , 0.12 ) ;
}
. classification - row -- incorrect td {
background : rgba ( 239 , 91 , 91 , 0.13 ) ;
}
. classification - feedback ,
. classification - summary {
margin : 8 px 0 0 ;
color : # ffb1b1 ;
font - size : 12 px ;
line - height : 1.45 ;
}
. classification - row -- correct . classification - feedback ,
. classification - summary . is - success {
color : # 86 e7bd ;
}
. classification - summary {
margin - top : 16 px ;
font - size : 14 px ;
font - weight : 700 ;
}
@ media ( max - width : 1100 px ) {
@ media ( max - width : 1100 px ) {
. new - product - value - page {
. new - product - value - page {
grid - template - columns : 1 fr ;
grid - template - columns : 1 fr ;
@ -1312,6 +1716,10 @@ function exportOutcome() {
. price - options {
. price - options {
grid - template - columns : 1 fr ;
grid - template - columns : 1 fr ;
}
}
. classification - table {
min - width : 720 px ;
}
}
}
< / style >
< / style >