@ -18,7 +18,7 @@
< p class = "intro" > 第 1 步分类结果已自动带入 。 请填写四类一级指标权重和各维度内二级指标权重 , 系统将自动计算对应总权重 。 < / p >
< p v-if ="!stepOnePassed" class="warning" > 请 先 完 成 并 保 存 第 1 步 的 指 标 分 类 , 再 填 写 本 步 骤 。 < / p >
< div class = "weight-summary" > < span > 一级指标权重合计 < / span > < strong : class = "{ valid: primaryWeightTotal === 100 }" > { { primaryWeightTotal } } % < / strong > < span > 对应总权重合计 < / span > < strong : class = "{ valid: correspondingWeightTotal === 100 }" > { { correspondingWeightTotal } } % < / strong > < small > 一级权重合计 、 每类维度内权重合计 、 对应总权重均须为 100 % < / small > < / div >
< div class = "table-wrap" > < table class = "weight-table" > < thead > < tr > < th > 一级指标 < / th > < th > 权重占比 ( % ) < / th > < th > 二级指标 < / th > < th > 维度内权重 ( % ) < / th > < th > 对应总权重 < / th > < / tr > < / thead > < tbody > < template v-for ="group in indicatorGroups" :key="group.name"><tr v-for="(item, index) in group.items" :key="item.id"><td v-if="index === 0" :rowspan="group.items.length" class="first-level">{{ group.name }}</td><td v-if="index === 0" :rowspan="group.items.length" class="primary-weight"><input v-model.number="primaryWeights[group.name]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${group.name} 的一级指标权重`" /><span class="percent">%</span><small>建议 {{ group.suggestion }}</small>< /td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的维度内权重`" /><span class="percent">%</span><small>建议 {{ item.suggestion }}</small></td><td class="corresponding-weight"> {{ formatWeight ( correspondingWeight ( item ) ) }} % < / td > < / tr > < / template > < / tbody > < / table > < / div >
< div class = "table-wrap" > < table class = "weight-table" > < thead > < tr > < th > 一级指标 < / th > < th > 权重占比 ( % ) < / th > < th > 二级指标 < / th > < th > 维度内权重 ( % ) < / th > < th > 对应总权重 < / th > < / tr > < / thead > < tbody > < template v-for ="group in indicatorGroups" :key="group.name"><tr v-for="(item, index) in group.items" :key="item.id"><td v-if="index === 0" :rowspan="group.items.length" class="first-level">{{ group.name }}</td><td v-if="index === 0" :rowspan="group.items.length" class="primary-weight"><input v-model.number="primaryWeights[group.name]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${group.name} 的一级指标权重`" /><span class="percent">%</span><small>建议 {{ group.suggestion }}</small>< small>维度内权重合计 <strong :class="{ valid: dimensionWeightTotal(group.name) === 100 }">{{ dimensionWeightTotal(group.name) }}%</strong></small>< /td><td class="metric">{{ item.name }}</td><td><input v-model.number="weights[item.id]" :disabled="!stepOnePassed" type="number" min="1" max="100" step="1" :aria-label="`${item.name} 的维度内权重`" /><span class="percent">%</span><small>建议 {{ item.suggestion }}</small></td><td class="corresponding-weight"> {{ formatWeight ( correspondingWeight ( item ) ) }} % < / td > < / tr > < / template > < / tbody > < / table > < / div >
< div class = "step-actions" > < button class = "btn primary" type = "button" : disabled = "saving || !stepOnePassed" @click ="saveAndGoToStepThree" > 保 存 并 进 入 第 3 步 < / button > < button class = "btn" type = "button" : disabled = "saving || !stepOnePassed" @click ="resetStepTwo" > < el -icon > < RefreshLeft / > < / e l - i c o n > 清 空 填 写 < / b u t t o n > < / d i v >
< / template >
< template v -else -if = " currentStep = = = 3 " >
@ -79,8 +79,8 @@ const TASK_KEY = "market-opportunity-selection";
const DEFAULT _STEPS = [ "指标分类" , "指标权重设置" , "综合得分计算" , "数据可视化" , "用户需求洞察" , "选品结论" ] ;
const categories = [ "市场规模" , "市场潜力" , "竞争强度" , "运营难度" ] ;
const indicators = [
{ id : "search-count" , name : "搜索次数(万次)" , group : "市场规模" , suggestion : "25– 35" } , { id : " demand-supply-ratio", name : "需供比" , group : "竞争强度" , suggestion : "25– 35" } , { id : " transaction-amount", name : "成交金额(万元)" , group : "市场规模" , suggestion : "65– 75" } ,
{ id : "transaction-growth" , name : "成交金额增速(%) " , group : "市场潜力" , suggestion : "55– 65" } , { id : "category-transaction-growth" , name : "类目成交增速(%) " , group : "市场潜力" , suggestion : "35– 45" } , { id : " online-products", name : "在线商品数" , group : "竞争强度" , suggestion : "35– 45" } ,
{ id : "search-count" , name : "搜索次数(万次)" , group : "市场规模" , suggestion : "25– 35" } , { id : " transaction-amount", name : "成交金额(万元)" , group : "市场规模" , suggestion : "65– 75" } ,
{ id : "transaction-growth" , name : "成交金额增速(%) " , group : "市场潜力" , suggestion : "55– 65" } , { id : "category-transaction-growth" , name : "类目成交增速(%) " , group : "市场潜力" , suggestion : "35– 45" } , { id : " demand-supply-ratio", name : "需供比" , group : "竞争强度" , suggestion : "25– 35" } , { id : " online-products", name : "在线商品数" , group : "竞争强度" , suggestion : "35– 45" } ,
{ id : "online-merchants" , name : "在线商家数" , group : "竞争强度" , suggestion : "25– 35" } , { id : "organic-traffic-ratio" , name : "自然流占比(%) " , group : "运营难度" , suggestion : "55– 65" } , { id : "return-rate" , name : "退货率(%) " , group : "运营难度" , suggestion : "35– 45" } ,
] ;
const groupSuggestions = { "市场规模" : "25– 35" , "市场潜力" : "25– 35" , "竞争强度" : "20– 30" , "运营难度" : "10– 20" } ;
@ -90,7 +90,7 @@ const { proxy } = getCurrentInstance();
const taskConfig = ref ( null ) ; const currentStep = ref ( 1 ) ; const saving = ref ( false ) ; const uploading = ref ( false ) ; const visualizationInput = ref ( null ) ; const visualization = ref ( { url : "" , name : "" } ) ; const stepOnePassed = ref ( false ) ; const stepTwoPassed = ref ( false ) ; const stepThreePassed = ref ( false ) ; const stepFourPassed = ref ( false ) ; const stepFivePassed = ref ( false ) ; const answers = ref ( createAnswers ( ) ) ; const primaryWeights = ref ( createPrimaryWeights ( ) ) ; const weights = ref ( createWeights ( ) ) ; const scores = ref ( createScores ( ) ) ; const reviewInsights = ref ( createReviewInsights ( ) ) ; const developmentConclusion = ref ( "" ) ; const dynamicStepAnswers = ref ( { } ) ; const stepOneCheckResults = ref ( { } ) ;
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 ) ) ; const taskRequirement = computed ( ( ) => taskConfig . value ? . requirements || defaultTask . requirement ) ; const trainingSteps = computed ( ( ) => { const configured = parseTrainingArray ( taskConfig . value ? . steps , DEFAULT _STEPS ) ; return DEFAULT _STEPS . map ( ( fallback , index ) => configured [ index ] || fallback ) ; } ) ;
const progressItems = computed ( ( ) => indicators . map ( ( item ) => ( { text : ` ${ answers . value [ item . id ] ? "已分类" : "待分类" } : ${ item . name } ` , status : answers . value [ item . id ] ? "done" : "" } ) ) ) ;
const indicatorGroups = computed ( ( ) => categories . map ( ( name ) => ( { name , suggestion : groupSuggestions [ name ] , items : indicators . filter ( ( item ) => answers. value [ item . id ] === name ) } ) ) ) ;
const indicatorGroups = computed ( ( ) => categories . map ( ( name ) => ( { name , suggestion : groupSuggestions [ name ] , items : indicators . filter ( ( item ) => item. group === name ) } ) ) ) ;
const primaryWeightTotal = computed ( ( ) => categories . reduce ( ( total , group ) => total + ( Number ( primaryWeights . value [ group ] ) || 0 ) , 0 ) ) ;
const correspondingWeightTotal = computed ( ( ) => roundWeight ( indicators . reduce ( ( total , item ) => total + correspondingWeight ( item ) , 0 ) ) ) ;
onMounted ( async ( ) => { await loadTaskConfig ( ) ; await loadSavedAnswer ( ) ; } ) ;
@ -103,7 +103,7 @@ function isValidRate(value) { return Number.isFinite(Number(value)) && Number(va
function hasCompleteReviewInsights ( ) { return products . every ( ( product ) => { const insight = reviewInsights . value [ product . id ] || { } ; return isValidRate ( insight . favorableRate ) && String ( insight . painPoint || "" ) . trim ( ) && String ( insight . unmetNeed || "" ) . trim ( ) ; } ) ; }
function parseArray ( value , fallback ) { if ( Array . isArray ( value ) ) return value . length ? value : fallback ; if ( ! value ) return fallback ; try { const parsed = JSON . parse ( value ) ; return Array . isArray ( parsed ) && parsed . length ? parsed . map ( ( item ) => String ( item ? . name ? ? item ) . trim ( ) ) . filter ( Boolean ) : fallback ; } catch ( error ) { const list = String ( value ) . split ( /[; ;|]/ ) . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ; return list . length ? list : fallback ; } }
async function loadTaskConfig ( ) { try { taskConfig . value = ( await getTrainingTaskByKey ( TASK _KEY ) ) ? . data || null ; } catch ( error ) { taskConfig . value = null ; } }
async function loadSavedAnswer ( ) { try { const answer = ( await getStudentTrainingAnswer ( TASK _KEY ) ) ? . data ; if ( answer ? . step1Answer ) { const parsed = JSON . parse ( answer . step1Answer ) ; answers . value = { ... createAnswers ( ) , ... ( parsed ? . answers || parsed || { } ) } ; stepOnePassed . value = indicators . every ( ( item ) => Boolean ( answers . value [ item . id ] ) ) ; } if ( answer ? . step2Answer ) { const parsed = JSON . parse ( answer . step2Answer ) ; primaryWeights . value = { ... createPrimaryWeights ( ) , ... ( parsed ? . primaryWeights || { } ) } ; weights . value = { ... createWeights ( ) , ... ( parsed ? . dimensionWeights || parsed ? . weights || { } ) } ; stepTwoPassed . value = stepOnePassed . value && categories . every ( ( group ) => isIntegerWeight ( primaryWeights . value [ group ] ) ) && indicators . every ( ( item ) => isIntegerWeight ( weights . value [ item . id ] ) ) && primaryWeightTotal . value === 100 && categories . every ( ( group ) => dimensionWeightTotal ( group ) === 100 ) ; } if ( answer ? . step3Answer ) { const parsed = JSON . parse ( answer . step3Answer ) ; scores . value = { ... createScores ( ) , ... ( parsed ? . scores || parsed || { } ) } ; stepThreePassed . value = stepTwoPassed . value ; } if ( answer ? . step4Answer ) { const parsed = JSON . parse ( answer . step4Answer ) ; visualization . value = { url : parsed ? . visualization ? . url || "" , name : parsed ? . visualization ? . name || "" } ; stepFourPassed . value = stepThreePassed . value && Boolean ( visualization . value . url ) ; } if ( answer ? . dynamicStepAnswers ) { const parsed = JSON . parse ( answer . dynamicStepAnswers ) ; dynamicStepAnswers . value = parsed && typeof parsed === "object" ? parsed : { } ; reviewInsights . value = { ... createReviewInsights ( ) , ... ( dynamicStepAnswers . value ? . step5 ? . reviewInsights || { } ) } ; developmentConclusion . value = String ( dynamicStepAnswers . value ? . step6 ? . developmentConclusion || "" ) ; stepFivePassed . value = stepFourPassed . value && hasCompleteReviewInsights ( ) ; } const savedStep = Number ( answer ? . currentStep ) || 1 ; currentStep . value = stepFivePassed . value ? Math . min ( savedStep , 6 ) : stepFourPassed . value ? Math . min ( savedStep , 5 ) : stepThreePassed . value ? Math . min ( savedStep , 4 ) : stepTwoPassed . value ? Math . min ( savedStep , 3 ) : stepOnePassed . value ? Math . min ( savedStep , 2 ) : 1 ; } catch ( error ) { /* Keep the page available when history cannot be restored. */ } }
async function loadSavedAnswer ( ) { try { const answer = ( await getStudentTrainingAnswer ( TASK _KEY ) ) ? . data ; if ( answer ? . step1Answer ) { const parsed = JSON . parse ( answer . step1Answer ) ; answers . value = { ... createAnswers ( ) , ... ( parsed ? . answers || parsed || { } ) } ; stepOnePassed . value = indicators . every ( ( item ) => answers . value [ item . id ] === item . group ) ; } if ( answer ? . step2Answer ) { const parsed = JSON . parse ( answer . step2Answer ) ; primaryWeights . value = { ... createPrimaryWeights ( ) , ... ( parsed ? . primaryWeights || { } ) } ; weights . value = { ... createWeights ( ) , ... ( parsed ? . dimensionWeights || parsed ? . weights || { } ) } ; stepTwoPassed . value = stepOnePassed . value && categories . every ( ( group ) => isIntegerWeight ( primaryWeights . value [ group ] ) ) && indicators . every ( ( item ) => isIntegerWeight ( weights . value [ item . id ] ) ) && primaryWeightTotal . value === 100 && categories . every ( ( group ) => dimensionWeightTotal ( group ) === 100 ) ; } if ( answer ? . step3Answer ) { const parsed = JSON . parse ( answer . step3Answer ) ; scores . value = { ... createScores ( ) , ... ( parsed ? . scores || parsed || { } ) } ; stepThreePassed . value = stepTwoPassed . value ; } if ( answer ? . step4Answer ) { const parsed = JSON . parse ( answer . step4Answer ) ; visualization . value = { url : parsed ? . visualization ? . url || "" , name : parsed ? . visualization ? . name || "" } ; stepFourPassed . value = stepThreePassed . value && Boolean ( visualization . value . url ) ; } if ( answer ? . dynamicStepAnswers ) { const parsed = JSON . parse ( answer . dynamicStepAnswers ) ; dynamicStepAnswers . value = parsed && typeof parsed === "object" ? parsed : { } ; reviewInsights . value = { ... createReviewInsights ( ) , ... ( dynamicStepAnswers . value ? . step5 ? . reviewInsights || { } ) } ; developmentConclusion . value = String ( dynamicStepAnswers . value ? . step6 ? . developmentConclusion || "" ) ; stepFivePassed . value = stepFourPassed . value && hasCompleteReviewInsights ( ) ; } const savedStep = Number ( answer ? . currentStep ) || 1 ; currentStep . value = stepFivePassed . value ? Math . min ( savedStep , 6 ) : stepFourPassed . value ? Math . min ( savedStep , 5 ) : stepThreePassed . value ? Math . min ( savedStep , 4 ) : stepTwoPassed . value ? Math . min ( savedStep , 3 ) : stepOnePassed . value ? Math . min ( savedStep , 2 ) : 1 ; } catch ( error ) { /* Keep the page available when history cannot be restored. */ } }
function buildStepOneOutcome ( ) { return { title : taskTitle . value , answers : { ... answers . value } } ; }
function buildStepTwoOutcome ( ) { return { title : taskTitle . value , primaryWeights : { ... primaryWeights . value } , dimensionWeights : { ... weights . value } , correspondingWeights : Object . fromEntries ( indicators . map ( ( item ) => [ item . id , correspondingWeight ( item ) ] ) ) } ; }
function buildStepThreeOutcome ( ) { return { title : taskTitle . value , scores : { ... scores . value } } ; }
@ -113,12 +113,13 @@ function buildStepSixOutcome() { return { title: taskTitle.value, developmentCon
function buildDynamicStepAnswers ( overrides ) { return { ... dynamicStepAnswers . value , ... overrides } ; }
async function checkAndContinue ( ) { if ( saving . value ) return ; saving . value = true ; try { const result = await checkMarketOpportunitySelectionStepOne ( indicators . map ( ( item ) => ( { id : item . id , category : answers . value [ item . id ] } ) ) ) ; stepOneCheckResults . value = Object . fromEntries ( ( result ? . data ? . items || [ ] ) . map ( ( item ) => [ item . id , item ] ) ) ; if ( ! result ? . data ? . allCorrect ) throw new Error ( ` 当前答对 ${ result ? . data ? . correctCount || 0 } / ${ indicators . length } 项,请根据表内提示修改 ` ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step1Answer : JSON . stringify ( buildStepOneOutcome ( ) ) , currentStep : 2 , submitted : false , saveAction : "SAVE" } ) ; stepOnePassed . value = true ; currentStep . value = 2 ; proxy ? . $modal ? . msgSuccess ( "分类正确,已进入第 2 步" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请完成指标分类并根据提示修改" ) ; } finally { saving . value = false ; } }
function isIntegerWeight ( value ) { return value !== null && value !== "" && Number . isInteger ( Number ( value ) ) && Number ( value ) >= 1 && Number ( value ) <= 100 ; }
function dimensionWeightTotal ( group ) { return indicators . filter ( ( item ) => answers. value [ item . id ] === group ) . reduce ( ( total , item ) => total + ( Number ( weights . value [ item . id ] ) || 0 ) , 0 ) ; }
function correspondingWeight ( item ) { return roundWeight ( ( ( Number ( primaryWeights . value [ answers. value [ item . id ] ] ) || 0 ) * ( Number ( weights . value [ item . id ] ) || 0 ) ) / 100 ) ; }
function dimensionWeightTotal ( group ) { return indicators . filter ( ( item ) => item. group === group ) . reduce ( ( total , item ) => total + ( Number ( weights . value [ item . id ] ) || 0 ) , 0 ) ; }
function correspondingWeight ( item ) { return roundWeight ( ( ( Number ( primaryWeights . value [ item. group ] ) || 0 ) * ( Number ( weights . value [ item . id ] ) || 0 ) ) / 100 ) ; }
function roundWeight ( value ) { return Math . round ( ( value + Number . EPSILON ) * 100 ) / 100 ; }
function formatWeight ( value ) { return Number . isInteger ( value ) ? value : value . toFixed ( 2 ) ; }
function isStepTwoReady ( ) { return categories . every ( ( group ) => isIntegerWeight ( primaryWeights . value [ group ] ) && dimensionWeightTotal ( group ) === 100 ) && indicators . every ( ( item ) => isIntegerWeight ( weights . value [ item . id ] ) ) && primaryWeightTotal . value === 100 && correspondingWeightTotal . value === 100 ; }
function switchStep ( step ) { if ( step > 1 && ! stepOnePassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成第 1 步指标分类" ) ; return ; } if ( step > 2 && ! stepTwoPassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成第 2 步指标权重设定" ) ; return ; } if ( step > 3 && ! stepThreePassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成第 3 步综合得分计算" ) ; return ; } if ( step > 4 && ! stepFourPassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成第 4 步数据可视化" ) ; return ; } if ( step > 5 && ! stepFivePassed . value ) { proxy ? . $modal ? . msgWarning ( "请先完成第 5 步用户需求洞察" ) ; return ; } currentStep . value = step ; }
async function saveAndGoToStepThree ( ) { if ( saving . value || ! stepOnePassed . value ) return ; saving . value = true ; try { await checkMarketOpportunitySelectionStepTwo ( { primaryWeights : categories . map ( ( group ) => ( { group , weight : primaryWeights . value [ group ] } ) ) , dimensionWeights : indicators . map ( ( item ) => ( { id : item . id , weight : weights . value [ item . id ] } ) ) } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step2Answer : JSON . stringify ( buildStepTwoOutcome ( ) ) , currentStep : 3 , submitted : false , saveAction : "SAVE" } ) ; stepTwoPassed . value = true ; currentStep . value = 3 ; proxy ? . $modal ? . msgSuccess ( "权重校验通过,已进入第 3 步" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请确认一级指标、维度内权重和对应总权重均满足规则" ) ; } finally { saving . value = false ; } }
async function saveAndGoToStepThree ( ) { if ( saving . value || ! stepOnePassed . value ) return ; if ( ! isStepTwoReady ( ) ) { proxy ? . $modal ? . msgError ? . ( "请确认一级指标、维度内权重和对应总权重均为 100%" ) ; return ; } saving . value = true ; try { await checkMarketOpportunitySelectionStepTwo ( { primaryWeights : categories . map ( ( group ) => ( { group , weight : primaryWeights . value [ group ] } ) ) , dimensionWeights : indicators . map ( ( item ) => ( { id : item . id , weight : weights . value [ item . id ] } ) ) } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step2Answer : JSON . stringify ( buildStepTwoOutcome ( ) ) , currentStep : 3 , submitted : false , saveAction : "SAVE" } ) ; stepTwoPassed . value = true ; currentStep . value = 3 ; proxy ? . $modal ? . msgSuccess ( "权重校验通过,已进入第 3 步" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请确认一级指标、维度内权重和对应总权重均满足规则" ) ; } finally { saving . value = false ; } }
async function saveAndGoToStepFour ( ) { if ( saving . value || ! stepTwoPassed . value ) return ; saving . value = true ; try { await checkMarketOpportunitySelectionStepThree ( { scores : products . map ( ( product ) => ( { id : product . id , score : scores . value [ product . id ] } ) ) } ) ; await saveStudentTrainingAnswer ( TASK _KEY , { step3Answer : JSON . stringify ( buildStepThreeOutcome ( ) ) , currentStep : 4 , submitted : false , saveAction : "SAVE" } ) ; stepThreePassed . value = true ; currentStep . value = 4 ; proxy ? . $modal ? . msgSuccess ( "综合得分校验通过,已进入第 4 步" ) ; } catch ( error ) { proxy ? . $modal ? . msgError ? . ( error ? . message || "请填写 0 至 100 分、最多保留两位小数的综合得分" ) ; } finally { saving . value = false ; } }
function clearStepOneCheck ( id ) { if ( stepOneCheckResults . value [ id ] ) { const next = { ... stepOneCheckResults . value } ; delete next [ id ] ; stepOneCheckResults . value = next ; } }
function resetStepOne ( ) { answers . value = createAnswers ( ) ; stepOneCheckResults . value = { } ; proxy ? . $modal ? . msgSuccess ( "已清空第 1 步填写内容" ) ; }