You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
dianshang-qianduan/src/views/test/promotion.vue

781 lines
31 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!--
* @Author: qinzhenpen qzp1807@126.com
* @Date: 2025-04-30 16:34:04
* @LastEditors: qinzhenpen qzp1807@126.com
* @LastEditTime: 2025-05-16 17:32:57
* @FilePath: \e-commerce-internet\src\views\test\promotion.vue
* @Description: A/B testing component with ECharts boxplots using rawData1 directly
-->
<template>
<ProductStandardTrainingPage class="promotion" task-key="promotion-effect-optimization" title="产品推广测试" badge-text="核心实训 · 产品上线与运营推广" background="电商产品推广测试通过对比不同广告版本的市场表现,帮助商家定位推广问题、优化投放策略并提升用户体验。" requirement="使用 A/B 测试法,根据曝光量、点击量、转化量、广告费用和订单金额,评估 A、B 版广告优劣并制定相应对策。" :goals="taskGoals" @save="saveTask" @submit="submitTask" @reset="resetTask">
<template #step-content="{ activeStep, hasConfiguredSteps }">
<section v-show="!hasConfiguredSteps || activeStep === 1" class="promotion-phase">
<section class="intro">
<p>在电商产品推广测试中,A/B测试(也称为分裂测试)是一种科学的方法,用于比较两个或多个版本的广告、网页或其他营销元素,以确定哪个版本在给定目标方面表现更好。</p>
<p>
<strong>A/B测试的步骤:</strong>
<br />
- 明确目标:确定A/B测试的目标,例如提升点击率、转化率或投资回报率。
<br />
- 选择变量:选择需要测试的变量,例如广告文案、图片、按钮颜色等。
<br />
- 创建实验组和对照组:将用户随机分配到实验组和对照组,确保样本量足够大。
<br />
- 施加影响:对实验组施加影响(如不同的广告创意),而对照组保持不变。
<br />
- 收集数据:收集实验组和对照组的数据,包括点击率、转化率、停留时间等指标。
<br />
- 分析数据:对收集的数据进行分析,以确定新运营策略是否优于旧运营策略。
<br />
- 优化策略:根据分析结果,优化运营策略,以提升运营效果。
</p>
</section>
<section class="case-study">
<div class="case-backtitle"><span>实验实训</span></div>
<p>假设京东某电子产品店铺进行新产品推广测试,设计以下A、B两个版本广告,5个月时间两个版本广告的点击量、曝光量、转化量、订单金额和广告费用数据如下所示:</p>
<el-table :data="rawData" border class="data-table">
<el-table-column prop="version" label="广告版本" align="center" />
<el-table-column prop="clicks" label="点击量" align="center" />
<el-table-column prop="exposure" label="曝光量" align="center" />
<el-table-column prop="conversions" label="转化量" align="center" />
<el-table-column prop="orderAmount" label="订单金额" align="center" />
<el-table-column prop="adCost" label="广告费用" align="center" />
</el-table>
</section>
</section>
<section v-show="!hasConfiguredSteps || activeStep === 2" class="promotion-phase">
<section class="analysis">
<h3>数据分析与可视化</h3>
<p class="formulas">
<span>点击率 = 点击量 / 曝光量</span>
<span>转化率 = 转化量 / 点击量</span>
<span>投资回报率 = (订单金额 - 广告费用) / 广告费用</span>
</p>
<p>请根据上述公式计算A、B两个版本的点击率、转化率、投资回报率指标数据(计算结果保留4位小数),根据计算结果绘制点击率对比、转化率对比、投资回报率对比、订单金额对比箱线图</p>
<el-table :data="rawData1" border class="data-table">
<el-table-column prop="version" label="广告版本" align="center" />
<el-table-column prop="clicks" label="点击率" align="center">
<template #default="{ row }">
<el-input v-model.number="row.clicks" placeholder="请输入点击率" />
</template>
</el-table-column>
<el-table-column prop="exposure" label="转化率" align="center">
<template #default="{ row }">
<el-input v-model.number="row.exposure" placeholder="请输入转化率" />
</template>
</el-table-column>
<el-table-column prop="conversions" label="投资回报率" align="center">
<template #default="{ row }">
<el-input v-model.number="row.conversions" placeholder="请输入投资回报率" />
</template>
</el-table-column>
<el-table-column prop="orderAmount" label="订单金额" align="center" />
</el-table>
<el-button @click="initializeChart">点击绘制箱线图</el-button>
<el-button @click="runResultShow = true">查看代码</el-button>
<el-button @click="go">跳转到Python在线编程环境</el-button>
<div ref="chartContainer" class="chart-container"></div>
</section>
</section>
<section v-show="!hasConfiguredSteps || activeStep === 3" class="promotion-phase">
<section class="statistical-test">
<h3>统计显著性检验(保留4位小数):</h3>
<el-table :data="testResults" border class="data-table" style="width: 50%">
<el-table-column prop="testName" label="t检验结果" align="center" />
<el-table-column prop="statistic" label="t统计量" align="center">
<template #default="{ row }">
<el-input v-model="row.statistic" placeholder="请输入t统计量" />
</template>
</el-table-column>
<el-table-column prop="pValue" label="p值" align="center">
<template #default="{ row }">
<el-input v-model="row.pValue" placeholder="请输入p值" />
</template>
</el-table-column>
</el-table>
</section>
</section>
<section v-show="!hasConfiguredSteps || activeStep >= 4" class="promotion-phase">
<section class="results">
<h3>A/B测试结果分析</h3>
<el-input v-model="analysisText" :autosize="{ minRows: 11, maxRows: 11 }" type="textarea" placeholder="请输入A/B测试结果分析" />
</section>
</section>
</template>
</ProductStandardTrainingPage>
<pop-model :showModel="runResultShow" title="设置代码" @closePop="closeCode">
<template v-slot:content>
<div>
<codemirror :code="code"></codemirror>
</div>
</template>
</pop-model>
</template>
<script setup >
import ProductStandardTrainingPage from "@/views/product/components/ProductStandardTrainingPage.vue";
import * as analysisAPI from "@/api/market-demand-mining.js";
import * as echarts from "echarts";
import codemirror from "@/components/codemirror/index.vue";
import popModel from "@/views/components/popModal.vue";
import { getCurrentInstance, onMounted, onUnmounted, ref } from "vue";
const { proxy } = getCurrentInstance();
import useUserStore from "@/store/modules/user";
const userStore = useUserStore();
import jStat from "jstat";
const taskGoals = ["理解 A/B 测试的实验设计与关键指标", "能够计算并可视化点击率、转化率和投资回报率", "通过统计显著性检验形成可执行的投放优化策略"];
const runResultShow = ref(false);
// 图表显示隐藏
const code = ref(
`
import pandas as pd
import numpy as np
import scipy.stats as stats
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
import seaborn as sns
# 模拟数据
data = {
'广告版本': ['A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B', 'B'],
'点击量': [120, 150, 130, 140, 160, 180, 200, 190, 210, 220],
'曝光量': [1000, 1200, 1100, 1300, 1400, 1500, 1600, 1700, 1800, 2000],
'转化量': [20, 25, 22, 24, 28, 30, 35, 32, 38, 40],
'订单金额': [2000, 2500, 2200, 2400, 2800, 3000, 3500, 3200, 3800, 4000],
'广告费用': [500, 600, 550, 650, 700, 750, 800, 780, 850, 900]
}
df = pd.DataFrame(data)
# 计算关键指标
df['点击率'] = df['点击量'] / df['曝光量']
df['转化率'] = df['转化量'] / df['点击量']
df['投资回报率'] = (df['订单金额'] - df['广告费用']) / df['广告费用']
# 数据可视化
plt.figure(figsize=(12, 8))
# 点击率
plt.subplot(2, 2, 1)
sns.boxplot(x='广告版本', y='点击率', data=df)
plt.title('点击率对比')
# 转化率
plt.subplot(2, 2, 2)
sns.boxplot(x='广告版本', y='转化率', data=df)
plt.title('转化率对比')
# 投资回报率
plt.subplot(2, 2, 3)
sns.boxplot(x='广告版本', y='投资回报率', data=df)
plt.title('投资回报率对比')
# 订单金额
plt.subplot(2, 2, 4)
sns.boxplot(x='广告版本', y='订单金额', data=df)
plt.title('订单金额对比')
plt.tight_layout()
plt.show()
# 提取A组和B组数据
a_group = df[df['广告版本'] == 'A']
b_group = df[df['广告版本'] == 'B']
# 点击率的t检验
t_stat_click, p_val_click = stats.ttest_ind(a_group['点击率'], b_group['点击率'])
print(f"点击率t检验结果: t统计量={t_stat_click}, p值={p_val_click}")
# 转化率的t检验
t_stat_conversion, p_val_conversion = stats.ttest_ind(a_group['转化率'], b_group['转化率'])
print(f"转化率t检验结果: t统计量={t_stat_conversion}, p值={p_val_conversion}")
# 投资回报率的t检验
t_stat_roi, p_val_roi = stats.ttest_ind(a_group['投资回报率'], b_group['投资回报率'])
print(f"投资回报率t检验结果: t统计量={t_stat_roi}, p值={p_val_roi}")
# 订单金额的t检验
t_stat_revenue, p_val_revenue = stats.ttest_ind(a_group['订单金额'], b_group['订单金额'])
print(f"订单金额t检验结果: t统计量={t_stat_revenue}, p值={p_val_revenue}")
# 设置显著性水平
alpha = 0.05
# 评估结果
def evaluate_result(p_value, metric):
if p_value < alpha:
print(f"{metric}差异显著,B组效果优于A组。")
else:
print(f"{metric}差异不显著,无法确定B组效果优于A组。")
evaluate_result(p_val_click, "点击率")
evaluate_result(p_val_conversion, "转化率")
evaluate_result(p_val_roi, "投资回报率")
evaluate_result(p_val_revenue, "订单金额")
# 基于结果优化策略
if p_val_click < alpha and p_val_conversion < alpha and p_val_roi < alpha:
print("B组在点击率、转化率和投资回报率方面均优于A组,建议全面推广B组广告版本。")
elif p_val_click < alpha:
print("B组在点击率方面优于A组,建议优化广告文案或图片以提高点击率。")
elif p_val_conversion < alpha:
print("B组在转化率方面优于A组,建议优化产品页面或购买流程以提高转化率。")
elif p_val_roi < alpha:
print("B组在投资回报率方面优于A组,建议调整广告预算分配以提高投资回报率。")
else:
print("B组效果不显著,建议重新设计测试或优化测试变量。")`
);
// Constants
const TABLE_HEADER_STYLE = {
color: "#fff",
fontSize: "14px",
backgroundColor: "#6A7DE9 !important",
};
const CHART_COLOR = "#4682B4";
const ALPHA = 0.05;
// Reactive State
const chartContainer = ref(null);
const chartLoading = ref(false);
const analysisText = ref("");
const rawData = ref([
{ version: "A (1月)", clicks: 123, exposure: 1042, conversions: 21, orderAmount: 2059, adCost: 512 },
{ version: "A (2月)", clicks: 159, exposure: 1208, conversions: 25, orderAmount: 2515, adCost: 632 },
{ version: "A (3月)", clicks: 131, exposure: 1137, conversions: 22, orderAmount: 2207, adCost: 552 },
{ version: "A (4月)", clicks: 144, exposure: 1321, conversions: 24, orderAmount: 2436, adCost: 653 },
{ version: "A (5月)", clicks: 167, exposure: 1423, conversions: 28, orderAmount: 2853, adCost: 709 },
{ version: "B (1月)", clicks: 183, exposure: 1505, conversions: 30, orderAmount: 3079, adCost: 751 },
{ version: "B (2月)", clicks: 209, exposure: 1609, conversions: 35, orderAmount: 3507, adCost: 819 },
{ version: "B (3月)", clicks: 194, exposure: 1719, conversions: 32, orderAmount: 3215, adCost: 784 },
{ version: "B (4月)", clicks: 218, exposure: 1851, conversions: 38, orderAmount: 3831, adCost: 853 },
{ version: "B (5月)", clicks: 220, exposure: 2032, conversions: 40, orderAmount: 4023, adCost: 921 },
]);
const rawData1 = ref([
{ version: "A (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 2059 },
{ version: "A (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 2515 },
{ version: "A (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 2207 },
{ version: "A (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 2436 },
{ version: "A (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 2853 },
{ version: "B (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 3079 },
{ version: "B (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 3507 },
{ version: "B (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 3215 },
{ version: "B (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 3831 },
{ version: "B (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 4023 },
]);
const testResults = ref([
{ testName: "点击率", statistic: "", pValue: "" },
{ testName: "转化率", statistic: "", pValue: "" },
{ testName: "投资回报率", statistic: "", pValue: "" },
{ testName: "订单金额", statistic: "", pValue: "" },
]);
// Computed Metrics for rawData (to display CTR, CVR, ROI in the first table)
function calculateMetrics(data) {
return data.map((item) => ({
...item,
ctr: item.exposure ? item.clicks / item.exposure : 0,
cvr: item.clicks ? item.conversions / item.clicks : 0,
roi: item.adCost ? (item.orderAmount - item.adCost) / item.adCost : 0,
}));
}
// Chart Logic
function initializeChart() {
if (!chartContainer.value) {
return;
}
chartLoading.value = true;
try {
const chart = echarts.init(chartContainer.value);
// Group data by version (A or B)
const groupedData = {
A: { clicks: [], exposure: [], conversions: [], orderAmount: [] },
B: { clicks: [], exposure: [], conversions: [], orderAmount: [] },
};
rawData1.value.forEach((item) => {
const version = item.version.startsWith("A") ? "A" : "B";
// Convert user input to numbers, default to 0 if empty or invalid
groupedData[version].clicks.push(Number(item.clicks) || 0);
groupedData[version].exposure.push(Number(item.exposure) || 0);
groupedData[version].conversions.push(Number(item.conversions) || 0);
groupedData[version].orderAmount.push(Number(item.orderAmount) || 0);
});
// Prepare boxplot data and calculate outliers
const boxData = {
clicks: [prepareBoxPlotData(groupedData.A.clicks), prepareBoxPlotData(groupedData.B.clicks)],
exposure: [prepareBoxPlotData(groupedData.A.exposure), prepareBoxPlotData(groupedData.B.exposure)],
conversions: [prepareBoxPlotData(groupedData.A.conversions), prepareBoxPlotData(groupedData.B.conversions)],
orderAmount: [prepareBoxPlotData(groupedData.A.orderAmount), prepareBoxPlotData(groupedData.B.orderAmount)],
};
const outlierData = {
clicks: [calcOutliers(groupedData.A.clicks), calcOutliers(groupedData.B.clicks)],
exposure: [calcOutliers(groupedData.A.exposure), calcOutliers(groupedData.B.exposure)],
conversions: [calcOutliers(groupedData.A.conversions), calcOutliers(groupedData.B.conversions)],
orderAmount: [calcOutliers(groupedData.A.orderAmount), calcOutliers(groupedData.B.orderAmount)],
};
// ECharts configuration with adjusted sizes
const option = {
backgroundColor: "transparent",
textStyle: { color: "#d7ecff" },
title: [
{ text: "点击量对比", left: "25%", top: "0%", textStyle: { fontSize: 16, color: "#d7ecff" } },
{ text: "曝光量对比", left: "70%", top: "0%", textStyle: { fontSize: 16, color: "#d7ecff" } },
{ text: "转化量对比", left: "25%", top: "55%", textStyle: { fontSize: 16, color: "#d7ecff" } },
{ text: "订单金额对比", left: "70%", top: "55%", textStyle: { fontSize: 16, color: "#d7ecff" } },
],
grid: [
{ left: "10%", top: "10%", width: "35%", height: "35%" },
{ left: "55%", top: "10%", width: "35%", height: "35%" },
{ left: "10%", top: "60%", width: "35%", height: "35%" },
{ left: "55%", top: "60%", width: "35%", height: "35%" },
],
xAxis: [
{ gridIndex: 0, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14, color: "#d7ecff" }, axisLabel: { color: "#d7ecff" }, axisLine: { lineStyle: { color: "#2c83aa" } } },
{ gridIndex: 1, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14, color: "#d7ecff" }, axisLabel: { color: "#d7ecff" }, axisLine: { lineStyle: { color: "#2c83aa" } } },
{ gridIndex: 2, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14, color: "#d7ecff" }, axisLabel: { color: "#d7ecff" }, axisLine: { lineStyle: { color: "#2c83aa" } } },
{ gridIndex: 3, type: "category", data: ["A", "B"], name: "广告版本", nameLocation: "middle", nameGap: 30, nameTextStyle: { fontSize: 14, color: "#d7ecff" }, axisLabel: { color: "#d7ecff" }, axisLine: { lineStyle: { color: "#2c83aa" } } },
],
yAxis: [
{ gridIndex: 0, type: "value", name: "点击量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
{ gridIndex: 1, type: "value", name: "曝光量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
{ gridIndex: 2, type: "value", name: "转化量", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
{ gridIndex: 3, type: "value", name: "订单金额", nameLocation: "middle", nameGap: 50, nameTextStyle: { fontSize: 14 } },
],
series: [
// Clicks boxplot
{
name: "点击量",
type: "boxplot",
xAxisIndex: 0,
yAxisIndex: 0,
data: boxData.clicks,
itemStyle: {
borderColor: CHART_COLOR,
color: CHART_COLOR,
},
boxWidth: 150, // Increased box width
tooltip: {
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
},
},
// Clicks outliers
{
name: "outlier",
type: "scatter",
xAxisIndex: 0,
yAxisIndex: 0,
data: outlierData.clicks.map((outliers, i) => outliers.map((val) => [i, val])),
itemStyle: {
color: "#fff",
borderColor: "#000",
borderWidth: 2,
},
symbolSize: 12, // Increased outlier size
},
// Exposure boxplot
{
name: "曝光量",
type: "boxplot",
xAxisIndex: 1,
yAxisIndex: 1,
data: boxData.exposure,
itemStyle: {
borderColor: CHART_COLOR,
color: CHART_COLOR,
},
boxWidth: 150, // Increased box width
tooltip: {
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
},
},
// Exposure outliers
{
name: "outlier",
type: "scatter",
xAxisIndex: 1,
yAxisIndex: 1,
data: outlierData.exposure.map((outliers, i) => outliers.map((val) => [i, val])),
itemStyle: {
color: "#fff",
borderColor: "#000",
borderWidth: 2,
},
symbolSize: 12, // Increased outlier size
},
// Conversions boxplot
{
name: "转化量",
type: "boxplot",
xAxisIndex: 2,
yAxisIndex: 2,
data: boxData.conversions,
itemStyle: {
borderColor: CHART_COLOR,
color: CHART_COLOR,
},
boxWidth: 150, // Increased box width
tooltip: {
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
},
},
// Conversions outliers
{
name: "outlier",
type: "scatter",
xAxisIndex: 2,
yAxisIndex: 2,
data: outlierData.conversions.map((outliers, i) => outliers.map((val) => [i, val])),
itemStyle: {
color: "#fff",
borderColor: "#000",
borderWidth: 2,
},
symbolSize: 12, // Increased outlier size
},
// Order Amount boxplot
{
name: "订单金额",
type: "boxplot",
xAxisIndex: 3,
yAxisIndex: 3,
data: boxData.orderAmount,
itemStyle: {
borderColor: CHART_COLOR,
color: CHART_COLOR,
},
boxWidth: 150, // Increased box width
tooltip: {
formatter: (param) => [`${param.name}:`, `Upper: ${param.data[4].toFixed(2)}`, `Q3: ${param.data[3].toFixed(2)}`, `Median: ${param.data[2].toFixed(2)}`, `Q1: ${param.data[1].toFixed(2)}`, `Lower: ${param.data[0].toFixed(2)}`].join("<br/>"),
},
},
// Order Amount outliers
{
name: "outlier",
type: "scatter",
xAxisIndex: 3,
yAxisIndex: 3,
data: outlierData.orderAmount.map((outliers, i) => outliers.map((val) => [i, val])),
itemStyle: {
color: "#fff",
borderColor: "#000",
borderWidth: 2,
},
symbolSize: 12, // Increased outlier size
},
],
};
chart.setOption(option);
// Handle window resize
const resizeHandler = () => chart.resize();
window.addEventListener("resize", resizeHandler);
// Cleanup
onUnmounted(() => {
window.removeEventListener("resize", resizeHandler);
chart.dispose();
});
} catch (error) {
ElMessage.error("图表渲染失败,请稍后重试");
console.error("Chart initialization error:", error);
} finally {
chartLoading.value = false;
}
}
// Helper Functions
function prepareBoxPlotData(data) {
const sorted = data.slice().sort((a, b) => a - b);
const len = sorted.length;
const q1 = sorted[Math.floor(len / 4)];
const median = sorted[Math.floor(len / 2)];
const q3 = sorted[Math.floor((len * 3) / 4)];
const iqr = q3 - q1;
const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;
const lower = Math.max(sorted[0], lowerBound);
const upper = Math.min(sorted[len - 1], upperBound);
return [lower, q1, median, q3, upper];
}
function calcOutliers(data) {
const sorted = data.slice().sort((a, b) => a - b);
const len = sorted.length;
const q1 = sorted[Math.floor(len / 4)];
const q3 = sorted[Math.floor((len * 3) / 4)];
const iqr = q3 - q1;
const lowerBound = q1 - 1.5 * iqr;
const upperBound = q3 + 1.5 * iqr;
return data.filter((val) => val < lowerBound || val > upperBound);
}
function performTTest(a, b) {
const result = jStat.ttest(a, b, 2); // Two-sample t-test
return {
statistic: result.tstat,
pValue: result.p,
};
}
// Reset Task
function resetTask() {
analysisAPI.productReleaseReTrain({ userId: userStore.userInfo.userId, module: "产品推广测试" }).then((res) => {
proxy.$modal.msgSuccess("重新实训成功");
rawData1.value = [
{ version: "A (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 2059 },
{ version: "A (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 2515 },
{ version: "A (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 2207 },
{ version: "A (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 2436 },
{ version: "A (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 2853 },
{ version: "B (1月)", clicks: "", exposure: "", conversions: "", orderAmount: 3079 },
{ version: "B (2月)", clicks: "", exposure: "", conversions: "", orderAmount: 3507 },
{ version: "B (3月)", clicks: "", exposure: "", conversions: "", orderAmount: 3215 },
{ version: "B (4月)", clicks: "", exposure: "", conversions: "", orderAmount: 3831 },
{ version: "B (5月)", clicks: "", exposure: "", conversions: "", orderAmount: 4023 },
];
testResults.value = [
{ testName: "点击量", statistic: "", pValue: "" },
{ testName: "曝光量", statistic: "", pValue: "" },
{ testName: "转化量", statistic: "", pValue: "" },
{ testName: "订单金额", statistic: "", pValue: "" },
];
analysisText.value = "";
// 图表重置
chartLoading.value = true;
setTimeout(() => {
chartLoading.value = false;
initializeChart();
}, 1000);
});
}
function buildTaskParams() {
return {
userId: userStore.userInfo.userId,
clickRate: rawData1.value.map((item) => item.clicks).join(","),
conversionRate: rawData1.value.map((item) => item.exposure).join(","),
roiData: rawData1.value.map((item) => item.conversions).join(","),
pValue: testResults.value.map((item) => item.pValue).join(","),
orderAmount: testResults.value.map((item) => item.statistic).join(","),
context: analysisText.value,
};
}
function saveTask() {
return analysisAPI.productPromotionTestSubmit(buildTaskParams()).then(() => {
proxy.$modal.msgSuccess("当前进度已保存");
});
}
function submitTask() {
if (rawData1.value.some((item) => !item.clicks || !item.exposure || !item.conversions)) {
return proxy.$modal.msgWarning("请先指标数据计算填写!");
}
if (testResults.value.some((item) => !item.statistic || !item.pValue)) {
return proxy.$modal.msgWarning("请先完成统计显著性检验!");
}
analysisAPI.productPromotionTestSubmit(buildTaskParams()).then(() => proxy.$modal.msgSuccess("提交成功"));
}
function closeCode() {
runResultShow.value = false;
}
// 回显数据
function loadTaskInfo() {
analysisAPI.productPromotionTestEcho({ userId: userStore.userInfo.userId }).then((res) => {
if (!res.data) return;
const clickRate = res.data.clickRate.split(",");
const conversionRate = res.data.conversionRate.split(",");
const roiData = res.data.roiData.split(",");
const pValue = res.data.pValue.split(",");
const orderAmount = res.data.orderAmount.split(",");
rawData1.value.forEach((item, index) => {
item.clicks = clickRate[index] || "";
item.exposure = conversionRate[index] || "";
item.conversions = roiData[index] || "";
});
// Update testResults with echoed data
testResults.value.forEach((item, index) => {
item.pValue = pValue[index];
item.statistic = orderAmount[index];
});
analysisText.value = res.data.context || "";
chartLoading.value = false;
initializeChart();
});
}
const go=()=>{
window.open('https://jrdsj.sztzjy.com:8000/user/zy84cf311/lab')
}
// Lifecycle
onMounted(() => {
rawData.value = calculateMetrics(rawData.value);
const observer = new MutationObserver(() => {
if (chartContainer.value) {
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
loadTaskInfo();
});
</script>
<style lang="scss" scoped>
.promotion {
color: #eef5ff;
background: transparent;
.intro,
.case-study,
.analysis,
.statistical-test,
.results {
margin-bottom: 30px;
}
h3 {
font-size: 22px;
color: #ffffff;
font-weight: 900;
margin-bottom: 15px;
}
.case-backtitle {
display: inline-flex;
align-items: center;
min-height: 28px;
margin-bottom: 6px;
padding: 4px 12px;
border: 1px solid rgba(106, 226, 255, 0.45);
border-radius: 999px;
background: rgba(0, 127, 184, 0.25);
span {
font-size: 12px;
font-weight: 800;
color: #71dfff;
}
}
.data-table {
width: 100% !important;
max-width: 1000px;
margin: 20px 0;
overflow: hidden;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.formulas {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 20px;
span {
padding: 10px 14px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 999px;
color: #d7ecff;
font-size: 14px;
font-weight: 700;
background: rgba(1, 14, 26, 0.72);
}
}
.chart-container {
width: 100%;
max-width: 1500px;
height: 600px; /* Increased height for better visibility */
margin: 20px 0;
display: block;
overflow: hidden;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 18px;
background: rgba(1, 14, 26, 0.72);
}
.promotion-phase {
display: grid;
gap: 14px;
}
.promotion-phase > .intro,
.promotion-phase > .case-study,
.promotion-phase > .analysis,
.promotion-phase > .statistical-test,
.promotion-phase > .results {
display: grid;
gap: 14px;
margin: 0;
}
.promotion-phase p {
margin: 0;
color: #d7ecff;
font-size: 15px;
line-height: 1.8;
}
.promotion-phase .intro {
padding: 16px;
border: 1px solid rgba(0, 180, 255, 0.22);
border-radius: 16px;
background: rgba(1, 14, 26, 0.72);
}
.promotion-phase :deep(.el-table) {
--el-table-border-color: rgba(77, 167, 220, 0.2);
--el-table-header-bg-color: rgba(8, 55, 82, 0.98);
--el-table-header-text-color: #eaffff;
--el-table-row-hover-bg-color: rgba(11, 84, 122, 0.45);
--el-table-bg-color: rgba(1, 14, 26, 0.72);
--el-table-tr-bg-color: rgba(1, 14, 26, 0.48);
--el-table-text-color: #d7f1ff;
}
.promotion-phase :deep(.el-table th.el-table__cell) {
color: #eaffff !important;
background: rgba(8, 55, 82, 0.98) !important;
font-size: 14px;
font-weight: 900;
}
.promotion-phase :deep(.el-table tr),
.promotion-phase :deep(.el-table td.el-table__cell) {
color: #d7f1ff !important;
background: rgba(1, 14, 26, 0.48) !important;
}
.promotion-phase :deep(.el-input__wrapper),
.promotion-phase :deep(.el-textarea__inner) {
border: 1px solid rgba(45, 95, 126, 0.95);
border-radius: 14px;
box-shadow: none !important;
background: #0f1a24;
}
.promotion-phase :deep(.el-input__wrapper) { min-height: 42px; }
.promotion-phase :deep(.el-input__inner),
.promotion-phase :deep(.el-textarea__inner) { color: #eef5ff; }
.promotion-phase :deep(.el-textarea__inner) { min-height: 110px !important; padding: 12px 14px; line-height: 1.65; }
.promotion-phase :deep(.el-button) {
width: fit-content;
min-height: 40px;
padding: 0 18px;
border: 1px solid rgba(92, 224, 255, 0.7) !important;
border-radius: 999px;
color: #ffffff !important;
font-size: 14px;
font-weight: 800;
background: linear-gradient(95deg, #0b84bd, #096491) !important;
}
.task-footer {
display: flex;
gap: 10px;
justify-content: flex-end;
}
@media (max-width: 768px) {
padding: 10px;
.data-table,
.chart-container {
max-width: 100%;
}
.chart-container {
height: 400px; /* Adjusted for smaller screens */
}
.formulas {
flex-direction: column;
gap: 10px;
}
.promotion-phase :deep(.el-button) { width: 100%; }
}
}
</style>