|
|
@@ -32,7 +32,7 @@
|
|
|
clearable
|
|
|
/>
|
|
|
<el-button type="primary" size="small" :loading="loading" @click="getList">刷新分析</el-button>
|
|
|
- <el-button type="success" size="small" :disabled="!hasResults" @click="exportResults">导出分析</el-button>
|
|
|
+ <el-button type="success" size="small" :disabled="!hasRawResults" @click="exportResults">导出分析</el-button>
|
|
|
</div>
|
|
|
<div class="toolbar-status" v-if="generatedAt">数据更新时间:{{ generatedAt }}</div>
|
|
|
<div class="toolbar-status muted" v-else>数据库实时统计</div>
|
|
|
@@ -48,6 +48,11 @@
|
|
|
<el-select v-model="selectedValue" filterable size="small" class="entity-select">
|
|
|
<el-option v-for="item in entityOptions" :key="item" :label="item" :value="item" />
|
|
|
</el-select>
|
|
|
+ <el-select v-model="lifecycleStatusFilter" size="small" class="status-select">
|
|
|
+ <el-option label="全部生命周期" value="all" />
|
|
|
+ <el-option label="完整生命周期" value="complete" />
|
|
|
+ <el-option label="不完整生命周期" value="incomplete" />
|
|
|
+ </el-select>
|
|
|
</div>
|
|
|
<el-tag :type="stageTagType(currentStage)" effect="plain">{{ currentStage }}</el-tag>
|
|
|
</div>
|
|
|
@@ -157,6 +162,39 @@
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
+ <div class="panel mb-20">
|
|
|
+ <div class="panel-header">
|
|
|
+ <div>
|
|
|
+ <h3>阶段建议</h3>
|
|
|
+ <p class="panel-subtitle">按当前{{ currentConfig.entityName }}的生命周期阶段生成运营动作,后续可接入 AI 输出个性化建议。</p>
|
|
|
+ </div>
|
|
|
+ <el-tag type="info" effect="plain">当前阶段:{{ currentStage }}</el-tag>
|
|
|
+ </div>
|
|
|
+ <div class="advice-grid">
|
|
|
+ <div
|
|
|
+ v-for="card in stageAdviceCards"
|
|
|
+ :key="card.stage"
|
|
|
+ class="advice-card"
|
|
|
+ :class="[card.className, { active: card.isCurrent }]"
|
|
|
+ >
|
|
|
+ <div class="advice-card-header">
|
|
|
+ <span class="advice-icon"><i :class="card.icon"></i></span>
|
|
|
+ <div>
|
|
|
+ <h4>{{ card.stage }}</h4>
|
|
|
+ <p>{{ card.summary }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="advice-metrics">
|
|
|
+ <span>销售额占比 {{ card.revenuePercentage }}</span>
|
|
|
+ <span>持续 {{ card.durationDays }} 天</span>
|
|
|
+ </div>
|
|
|
+ <ul>
|
|
|
+ <li v-for="item in card.suggestions" :key="item">{{ item }}</li>
|
|
|
+ </ul>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="panel">
|
|
|
<div class="panel-header">
|
|
|
<h3>完整性评估细项</h3>
|
|
|
@@ -176,7 +214,7 @@
|
|
|
|
|
|
<div v-else class="empty-state">
|
|
|
<i class="el-icon-data-analysis"></i>
|
|
|
- <p>{{ loading ? '正在加载数据库生命周期分析结果...' : `暂无${currentConfig.entityName}生命周期分析结果` }}</p>
|
|
|
+ <p>{{ emptyMessage }}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -202,6 +240,11 @@ const CACHE_CONFIG = {
|
|
|
}
|
|
|
const DATE_RANGE_KEY = 'lifecycle_database_analysis_date_range'
|
|
|
const stageOrder = ['引入期', '成长期', '成熟期', '衰退期']
|
|
|
+const lifecycleStatusOptions = {
|
|
|
+ all: '全部生命周期',
|
|
|
+ complete: '完整生命周期',
|
|
|
+ incomplete: '不完整生命周期'
|
|
|
+}
|
|
|
const breakdownMapping = [
|
|
|
{ key: 'sufficient_time', label: '时间长度≥120天' },
|
|
|
{ key: 'reasonable_peak_position', label: '峰值位置25%-75%' },
|
|
|
@@ -213,6 +256,32 @@ const breakdownMapping = [
|
|
|
{ key: 'cycle_completeness', label: '周期完整性' },
|
|
|
{ key: 'trend_consistency', label: '趋势一致性' }
|
|
|
]
|
|
|
+const stageAdviceMapping = {
|
|
|
+ 引入期: {
|
|
|
+ icon: 'el-icon-s-promotion',
|
|
|
+ className: 'intro',
|
|
|
+ summary: '验证市场接受度和基础转化效率。',
|
|
|
+ suggestions: ['聚焦首批核心渠道,避免过早铺量。', '跟踪点击、加购和退款反馈,快速修正标题、价格与主图。', '准备小批量补货阈值,防止试销转好后断货。']
|
|
|
+ },
|
|
|
+ 成长期: {
|
|
|
+ icon: 'el-icon-top-right',
|
|
|
+ className: 'growth',
|
|
|
+ summary: '放大有效流量并保障供给稳定。',
|
|
|
+ suggestions: ['提升投放和活动资源,优先放大高转化渠道。', '按销量增速校准安全库存和补货周期。', '监控毛利、评价和履约稳定性,避免增长质量下滑。']
|
|
|
+ },
|
|
|
+ 成熟期: {
|
|
|
+ icon: 'el-icon-medal',
|
|
|
+ className: 'maturity',
|
|
|
+ summary: '稳定利润表现并延长高质量销售窗口。',
|
|
|
+ suggestions: ['保持主推资源,控制折扣频率,守住毛利。', '通过组合、赠品或会员权益提升复购和客单价。', '观察竞品价格和搜索热度,提前准备迭代款。']
|
|
|
+ },
|
|
|
+ 衰退期: {
|
|
|
+ icon: 'el-icon-bottom-right',
|
|
|
+ className: 'decline',
|
|
|
+ summary: '控制投入,降低库存和资源占用。',
|
|
|
+ suggestions: ['减少低效投放,将预算迁移到成长或潜力商品。', '结合库存水位设置清仓、套装或尾货策略。', '复盘衰退原因,沉淀到下一代商品选品和定价。']
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
const ANALYSIS_CONFIG = {
|
|
|
sku: {
|
|
|
@@ -261,6 +330,7 @@ export default {
|
|
|
dateRange: loadDateRange(),
|
|
|
loading: false,
|
|
|
error: '',
|
|
|
+ lifecycleStatusFilter: 'all',
|
|
|
trendChart: null,
|
|
|
stageCompareChart: null,
|
|
|
cache: {
|
|
|
@@ -285,6 +355,9 @@ export default {
|
|
|
results() {
|
|
|
return this.currentCache.results || {}
|
|
|
},
|
|
|
+ hasRawResults() {
|
|
|
+ return this.allEntityOptions.length > 0
|
|
|
+ },
|
|
|
hasResults() {
|
|
|
return this.entityOptions.length > 0
|
|
|
},
|
|
|
@@ -296,9 +369,17 @@ export default {
|
|
|
this.setSelectedValue(value)
|
|
|
}
|
|
|
},
|
|
|
- entityOptions() {
|
|
|
+ allEntityOptions() {
|
|
|
return Object.keys(this.results || {}).filter(key => key !== '_analysis_summary_')
|
|
|
},
|
|
|
+ entityOptions() {
|
|
|
+ return this.allEntityOptions.filter(key => {
|
|
|
+ if (this.lifecycleStatusFilter === 'all') return true
|
|
|
+ const item = this.results[key]
|
|
|
+ const isComplete = !!(item && item.is_complete)
|
|
|
+ return this.lifecycleStatusFilter === 'complete' ? isComplete : !isComplete
|
|
|
+ })
|
|
|
+ },
|
|
|
detail() {
|
|
|
return (this.results && this.selectedValue && this.results[this.selectedValue]) || null
|
|
|
},
|
|
|
@@ -339,6 +420,29 @@ export default {
|
|
|
}
|
|
|
})
|
|
|
},
|
|
|
+ stageAdviceCards() {
|
|
|
+ return stageOrder.map(stage => {
|
|
|
+ const config = stageAdviceMapping[stage]
|
|
|
+ const stats = (this.displayStageStats && this.displayStageStats[stage]) || {}
|
|
|
+ return {
|
|
|
+ stage,
|
|
|
+ icon: config.icon,
|
|
|
+ className: config.className,
|
|
|
+ summary: config.summary,
|
|
|
+ suggestions: config.suggestions,
|
|
|
+ isCurrent: this.currentStage === stage,
|
|
|
+ revenuePercentage: this.formatPercent(stats.revenuePercentage),
|
|
|
+ durationDays: stats.durationDays != null ? stats.durationDays : 0
|
|
|
+ }
|
|
|
+ })
|
|
|
+ },
|
|
|
+ emptyMessage() {
|
|
|
+ if (this.loading) return '正在加载数据库生命周期分析结果...'
|
|
|
+ if (this.hasRawResults && !this.hasResults) {
|
|
|
+ return `暂无${lifecycleStatusOptions[this.lifecycleStatusFilter]}的${this.currentConfig.entityName}分析结果`
|
|
|
+ }
|
|
|
+ return `暂无${this.currentConfig.entityName}生命周期分析结果`
|
|
|
+ },
|
|
|
totalRevenue() {
|
|
|
if (this.detail && this.detail.total_revenue != null) return this.detail.total_revenue
|
|
|
return 0
|
|
|
@@ -358,6 +462,9 @@ export default {
|
|
|
localStorage.setItem(DATE_RANGE_KEY, JSON.stringify(this.dateRange || []))
|
|
|
} catch (e) {}
|
|
|
},
|
|
|
+ lifecycleStatusFilter() {
|
|
|
+ this.ensureCurrentSelection()
|
|
|
+ },
|
|
|
detail() {
|
|
|
this.$nextTick(() => {
|
|
|
this.renderTrend()
|
|
|
@@ -382,7 +489,7 @@ export default {
|
|
|
this.activeView = view
|
|
|
},
|
|
|
initCurrentView() {
|
|
|
- if (!this.hasResults) {
|
|
|
+ if (!this.hasRawResults) {
|
|
|
this.getList()
|
|
|
return
|
|
|
}
|
|
|
@@ -421,12 +528,17 @@ export default {
|
|
|
this.persistCurrentCache()
|
|
|
},
|
|
|
ensureCurrentSelection() {
|
|
|
+ if (!this.hasRawResults) {
|
|
|
+ this.selectedValue = ''
|
|
|
+ this.destroyCharts()
|
|
|
+ return
|
|
|
+ }
|
|
|
if (!this.hasResults) {
|
|
|
this.selectedValue = ''
|
|
|
this.destroyCharts()
|
|
|
return
|
|
|
}
|
|
|
- if (!this.selectedValue || !this.results[this.selectedValue]) {
|
|
|
+ if (!this.selectedValue || this.entityOptions.indexOf(this.selectedValue) === -1) {
|
|
|
this.selectedValue = this.pickFirstValue(this.results)
|
|
|
}
|
|
|
},
|
|
|
@@ -653,7 +765,7 @@ export default {
|
|
|
return `${year}-${month}-${day}`
|
|
|
},
|
|
|
exportResults() {
|
|
|
- if (!this.hasResults) {
|
|
|
+ if (!this.hasRawResults) {
|
|
|
this.$modal.msgError('暂无可导出的分析结果')
|
|
|
return
|
|
|
}
|
|
|
@@ -670,7 +782,13 @@ export default {
|
|
|
},
|
|
|
pickFirstValue(results) {
|
|
|
const keys = Object.keys(results || {})
|
|
|
- return keys.find(key => key !== '_analysis_summary_') || ''
|
|
|
+ return keys.find(key => {
|
|
|
+ if (key === '_analysis_summary_') return false
|
|
|
+ if (this.lifecycleStatusFilter === 'all') return true
|
|
|
+ const item = results[key]
|
|
|
+ const isComplete = !!(item && item.is_complete)
|
|
|
+ return this.lifecycleStatusFilter === 'complete' ? isComplete : !isComplete
|
|
|
+ }) || ''
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -788,16 +906,28 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+.panel-subtitle {
|
|
|
+ margin: -10px 0 0;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+
|
|
|
.selector-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
}
|
|
|
|
|
|
.entity-select {
|
|
|
width: 260px;
|
|
|
}
|
|
|
|
|
|
+.status-select {
|
|
|
+ width: 150px;
|
|
|
+}
|
|
|
+
|
|
|
.stats-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
|
@@ -954,6 +1084,116 @@ export default {
|
|
|
color: #b91c1c;
|
|
|
}
|
|
|
|
|
|
+.advice-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
|
+ gap: 16px;
|
|
|
+ margin-top: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card {
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #ffffff;
|
|
|
+ padding: 16px;
|
|
|
+ min-height: 260px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 14px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card.active {
|
|
|
+ border-color: #409eff;
|
|
|
+ box-shadow: 0 6px 16px rgba(64, 158, 255, 0.12);
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card::before {
|
|
|
+ content: '';
|
|
|
+ position: absolute;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ bottom: 0;
|
|
|
+ width: 4px;
|
|
|
+ border-radius: 8px 0 0 8px;
|
|
|
+ background: #94a3b8;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card.intro::before {
|
|
|
+ background: #3b82f6;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card.growth::before {
|
|
|
+ background: #10b981;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card.maturity::before {
|
|
|
+ background: #f59e0b;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card.decline::before {
|
|
|
+ background: #ef4444;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card-header {
|
|
|
+ display: flex;
|
|
|
+ gap: 12px;
|
|
|
+ align-items: flex-start;
|
|
|
+
|
|
|
+ h4 {
|
|
|
+ margin: 0 0 6px;
|
|
|
+ font-size: 16px;
|
|
|
+ color: #303133;
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 0;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.5;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.advice-icon {
|
|
|
+ width: 34px;
|
|
|
+ height: 34px;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f1f5f9;
|
|
|
+ color: #2563eb;
|
|
|
+ display: inline-flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ flex: 0 0 auto;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-metrics {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 8px;
|
|
|
+
|
|
|
+ span {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #475569;
|
|
|
+ background: #f8fafc;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 5px 8px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card ul {
|
|
|
+ margin: 0;
|
|
|
+ padding-left: 18px;
|
|
|
+ color: #475569;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.7;
|
|
|
+}
|
|
|
+
|
|
|
+.advice-card li + li {
|
|
|
+ margin-top: 6px;
|
|
|
+}
|
|
|
+
|
|
|
.breakdown-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
|
|
@@ -1008,5 +1248,9 @@ export default {
|
|
|
.entity-select {
|
|
|
width: 100%;
|
|
|
}
|
|
|
+
|
|
|
+ .status-select {
|
|
|
+ width: 100%;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|