门店端优化

This commit is contained in:
wong
2025-11-06 10:34:13 +08:00
parent 46c67e3bfd
commit 8daa0a5d5c
15 changed files with 1786 additions and 1933 deletions

View File

@@ -12,6 +12,8 @@
</view>
</view>
<!-- 上方区域数据概览 -->
<view class="top-section" style="position: relative;">
<!-- 概览标题和时间选择 -->
<view class="overview-header">
<view class="overview-title">数据概览</view>
@@ -34,15 +36,13 @@
<view class="overview-item">
<view class="overview-item-content">
<view class="item-header">
<text class="item-label">总客户数</text>
<text class="item-label">账号价值估值</text>
<view class="item-icon blue">
<text class="iconfont icon-yonghuqun" style="color: #0080ff; font-size: 20px;"></text>
<text class="iconfont icon-shuju1" style="color: #0080ff; font-size: 20px;"></text>
</view>
</view>
<view class="item-value">{{ overviewData.totalCustomers.toLocaleString() }}</view>
<view class="item-change" :class="overviewData.totalCustomersChange >= 0 ? 'up' : 'down'">
{{ (overviewData.totalCustomersChange >= 0 ? '+' : '') + overviewData.totalCustomersChange.toFixed(1) }}% 较上期
</view>
<view class="item-value">{{ overviewData.accountValue.toFixed(1) }}</view>
<view class="item-desc">RFM平均评分(满分10分)</view>
</view>
</view>
<view class="overview-item">
@@ -89,367 +89,110 @@
</view>
</view>
<!-- 分段器 -->
<view class="subsection-container">
<u-subsection
:list="subsectionList"
:current="currentSubsection"
@change="changeSubsection"
mode="button"
:activeColor="'#2979ff'"
bgColor="#f5f7fa"
fontSize="14"
itemStyle="padding-left: 15px; padding-right: 15px;"
></u-subsection>
</view>
<!-- 客户分析区域 -->
<view v-if="currentSubsection === 0" class="analysis-content">
<view class="analysis-grid">
<!-- 客户增长趋势卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">客户增长趋势</text>
<text class="card-subtitle">近期客户增长数据</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="trend-icon">
<text class="iconfont icon-shuju1" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">客户增长趋势图表</text>
</view>
<!-- 客户统计数据 -->
<view class="customer-stats">
<view class="customer-item">
<view class="customer-dot" style="background-color: #0080ff;"></view>
<text class="customer-label">总客户</text>
<text class="customer-value">{{ customerAnalysis.trend.total.toLocaleString() }}</text>
</view>
<view class="customer-item">
<view class="customer-dot" style="background-color: #19be6b;"></view>
<text class="customer-label">新增客户</text>
<text class="customer-value">{{ customerAnalysis.trend.new.toLocaleString() }}</text>
</view>
<view class="customer-item">
<view class="customer-dot" style="background-color: #fa3534;"></view>
<text class="customer-label">流失客户</text>
<text class="customer-value">{{ customerAnalysis.trend.lost.toLocaleString() }}</text>
</view>
</view>
</view>
</view>
<!-- 客户来源分布卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">客户来源分布</text>
<text class="card-subtitle">不同渠道客户占比</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="pie-icon">
<text class="iconfont icon-bingtu" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">客户来源分布图表</text>
</view>
<!-- 来源分布数据 -->
<view class="source-distribution">
<view v-for="(source, index) in customerAnalysis.sourceDistribution"
:key="index"
class="source-item"
>
<view class="source-dot" :style="{ backgroundColor: getSourceColor(index) }"></view>
<text class="source-name">{{ source.name }}</text>
<text class="source-value">{{ source.value }}</text>
</view>
</view>
</view>
</view>
<!-- 数据概览区域遮罩层 -->
<view v-if="isLoadingOverview" class="section-loading-mask">
<view class="section-loading-content">
<view class="section-spinner"></view>
<text class="section-loading-text">加载中...</text>
</view>
</view>
<!-- 其他分析区域 -->
<view v-if="currentSubsection > 0" class="analysis-content">
<!-- 互动分析区域 -->
<view v-if="currentSubsection === 1" class="analysis-grid">
<!-- 互动频率分析卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">互动频率分析</text>
<text class="card-subtitle">客户互动频次统计</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-shujucanmou" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">互动频率分析图表</text>
</view>
<!-- 互动频率统计 -->
<view class="interaction-stats">
<view class="interaction-row">
<view class="interaction-item">
<text class="interaction-label">高频互动</text>
<text class="interaction-value">{{ interactionAnalysis.frequencyAnalysis.highFrequency.toLocaleString() }}</text>
</view>
<view class="interaction-item">
<text class="interaction-label">中频互动</text>
<text class="interaction-value">{{ interactionAnalysis.frequencyAnalysis.midFrequency.toLocaleString() }}</text>
</view>
<view class="interaction-item">
<text class="interaction-label">低频互动</text>
<text class="interaction-value">{{ interactionAnalysis.frequencyAnalysis.lowFrequency.toLocaleString() }}</text>
</view>
</view>
<view class="interaction-row">
<view class="interaction-item">
<text class="interaction-label-small">每周多次互动</text>
</view>
<view class="interaction-item">
<text class="interaction-label-small">每月多次互动</text>
</view>
<view class="interaction-item">
<text class="interaction-label-small">偶尔互动</text>
</view>
</view>
</view>
</view>
</view>
<!-- 互动内容分析卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">互动内容分析</text>
<text class="card-subtitle">客户互动内容类型占比</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-bingtu" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">互动内容分析图表</text>
</view>
<!-- 互动内容类型分布 -->
<view class="content-distribution">
<view class="content-item">
<view class="content-icon blue">
<text class="iconfont icon-xiaoxi" style="color: #2979ff; font-size: 15px;"></text>
</view>
<text class="content-name">文字互动</text>
<text class="content-value">{{ interactionAnalysis.contentAnalysis.textMessages.toLocaleString() }}</text>
</view>
<view class="content-item">
<view class="content-icon green">
<text class="iconfont icon-tupian" style="color: #19be6b; font-size: 15px;"></text>
</view>
<text class="content-name">图片互动</text>
<text class="content-value">{{ interactionAnalysis.contentAnalysis.imgInteractions.toLocaleString() }}</text>
</view>
<view class="content-item">
<view class="content-icon purple">
<text class="iconfont icon-yonghuqun" style="color: #9c26b0; font-size: 15px;"></text>
</view>
<text class="content-name">群聊互动</text>
<text class="content-value">{{ interactionAnalysis.contentAnalysis.groupInteractions.toLocaleString() }}</text>
</view>
<view class="content-item">
<view class="content-icon orange">
<text class="iconfont icon-shujucanmou" style="color: #ff9900; font-size: 15px;"></text>
</view>
<text class="content-name">产品咨询</text>
<text class="content-value">{{ interactionAnalysis.contentAnalysis.productInquiries.toLocaleString() }}</text>
</view>
</view>
</view>
</view>
</view>
<!-- 转化分析区域 -->
<view v-if="currentSubsection === 2" class="analysis-grid">
<!-- 转化漏斗卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">转化漏斗</text>
<text class="card-subtitle">客户转化路径分析</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-shujucanmou" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">转化漏斗图表</text>
</view>
<!-- 转化漏斗数据 -->
<view class="funnel-stats">
<view class="funnel-item">
<view class="funnel-label">互动</view>
<view class="funnel-value">3,256</view>
<view class="funnel-percent">100%</view>
</view>
<view class="funnel-item">
<view class="funnel-label">咨询</view>
<view class="funnel-value">1,856</view>
<view class="funnel-percent">57%</view>
</view>
<view class="funnel-item">
<view class="funnel-label">意向</view>
<view class="funnel-value">845</view>
<view class="funnel-percent">26%</view>
</view>
<view class="funnel-item">
<view class="funnel-label">成交</view>
<view class="funnel-value">386</view>
<view class="funnel-percent">12%</view>
</view>
</view>
</view>
</view>
<!-- 转化效率卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">转化效率</text>
<text class="card-subtitle">各阶段转化率分析</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-bingtu" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">转化效率图表</text>
</view>
<!-- 转化效率数据 -->
<view class="efficiency-stats">
<view class="efficiency-item">
<view class="efficiency-row">
<view class="efficiency-label">互动咨询</view>
<view class="efficiency-value">57%</view>
</view>
<view class="efficiency-percent">
<text class="percent-change up">+5.2% 较上期</text>
</view>
</view>
<view class="efficiency-item">
<view class="efficiency-row">
<view class="efficiency-label">咨询意向</view>
<view class="efficiency-value">45.5%</view>
</view>
<view class="efficiency-percent">
<text class="percent-change up">+3.8% 较上期</text>
</view>
</view>
<view class="efficiency-item">
<view class="efficiency-row">
<view class="efficiency-label">意向成交</view>
<view class="efficiency-value">45.7%</view>
</view>
<view class="efficiency-percent">
<text class="percent-change up">+4.2% 较上期</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 收入分析区域 -->
<view v-if="currentSubsection === 3" class="analysis-grid">
<!-- 收入趋势卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">收入趋势</text>
<text class="card-subtitle">近期销售额和趋势</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-shuju1" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">收入趋势图表</text>
</view>
<!-- 收入统计数据 -->
<view class="income-stats">
<view class="income-stat-item">
<view class="income-label">总收入</view>
<view class="income-main-value">¥258,386</view>
<view class="income-change up">+22.5% 较上期</view>
</view>
<view class="income-stat-item" style="margin-top: 15px;">
<view class="income-label">客单价</view>
<view class="income-main-value">¥843</view>
<view class="income-change up">+5.8% 较上期</view>
</view>
</view>
</view>
</view>
<!-- 产品销售分布卡片 -->
<view class="analysis-card">
<view class="card-header">
<text class="card-title">产品销售分布</text>
<text class="card-subtitle">各产品系列销售占比</text>
</view>
<view class="card-content">
<view class="chart-placeholder">
<view class="chart-icon">
<text class="iconfont icon-bingtu" style="color: #999; font-size: 50px;"></text>
</view>
<text class="chart-text">产品销售分布图表</text>
</view>
<!-- 产品销售分布数据 -->
<view class="product-distribution">
<view class="product-item">
<view class="product-dot" style="background-color: #2979ff;"></view>
<text class="product-name">法儿曼胶原修复系列</text>
<text class="product-percent">42%</text>
</view>
<view class="product-value">¥108,551</view>
<view class="product-item" style="margin-top: 12px;">
<view class="product-dot" style="background-color: #19be6b;"></view>
<text class="product-name">安格安睛眼部系列</text>
<text class="product-percent">23%</text>
</view>
<view class="product-value">¥59,444</view>
<view class="product-item" style="margin-top: 12px;">
<view class="product-dot" style="background-color: #9c26b0;"></view>
<text class="product-name">色仕莱诺胸部系列</text>
<text class="product-percent">18%</text>
</view>
<view class="product-value">¥46,522</view>
<view class="product-item" style="margin-top: 12px;">
<view class="product-dot" style="background-color: #ff9900;"></view>
<text class="product-name">头部疗愈SPA系列</text>
<text class="product-percent">17%</text>
</view>
<view class="product-value">¥43,939</view>
</view>
</view>
</view>
</view>
<!-- 其他暂无数据区域 -->
<view v-else-if="currentSubsection > 3" class="empty-data">
<text class="iconfont icon-kong" style="color: #c0c4cc; font-size: 50px;"></text>
<text class="empty-text">{{ subsectionList[currentSubsection] }}暂无数据</text>
</view>
</view>
</view>
<!-- 下方区域综合分析 -->
<view class="bottom-section" style="position: relative;">
<view class="comprehensive-analysis-card">
<!-- 标题 -->
<view class="analysis-title">综合分析</view>
<!-- 客户平均转化金额 -->
<view class="avg-conversion-card">
<text class="avg-conversion-label">客户平均转化金额</text>
<text class="avg-conversion-value">¥{{ comprehensiveData.avgConversionAmount.toFixed(2) }}</text>
</view>
<!-- 价值指标和增长趋势 -->
<view class="metrics-grid">
<!-- 价值指标 -->
<view class="metrics-column">
<view class="metrics-header">
<text class="iconfont icon-shuju1" style="color: #999; font-size: 14px; margin-right: 4px;"></text>
<text class="metrics-title">价值指标</text>
</view>
<view class="metrics-item">
<text class="metrics-label">销售总额</text>
<text class="metrics-value">¥{{ comprehensiveData.totalSales.toLocaleString() }}</text>
</view>
<view class="metrics-item">
<text class="metrics-label">平均订单金额</text>
<text class="metrics-value">¥{{ comprehensiveData.avgOrderAmount.toFixed(2) }}</text>
</view>
<view class="metrics-item">
<text class="metrics-label">高价值客户</text>
<text class="metrics-value">{{ comprehensiveData.highValueCustomers.toFixed(1) }}%</text>
</view>
</view>
<!-- 增长趋势 -->
<view class="metrics-column">
<view class="metrics-header">
<text class="iconfont icon-shuju1" style="color: #999; font-size: 14px; margin-right: 4px;"></text>
<text class="metrics-title">增长趋势</text>
</view>
<view class="metrics-item">
<text class="metrics-label">周收益增长</text>
<text class="metrics-value up">{{ comprehensiveData.weeklyRevenueGrowth > 0 ? '+' : '' }}¥{{ comprehensiveData.weeklyRevenueGrowth.toLocaleString() }}</text>
</view>
<view class="metrics-item">
<text class="metrics-label">新客转化</text>
<text class="metrics-value up">{{ comprehensiveData.newCustomerConversion > 0 ? '+' : '' }}{{ comprehensiveData.newCustomerConversion }}</text>
</view>
<view class="metrics-item">
<text class="metrics-label">活跃客户增长</text>
<text class="metrics-value up">{{ comprehensiveData.activeCustomerGrowth > 0 ? '+' : '' }}{{ comprehensiveData.activeCustomerGrowth }}</text>
</view>
</view>
</view>
<!-- 客户活跃度和转化客户来源 -->
<view class="metrics-grid bottom-section">
<!-- 客户活跃度 -->
<view class="metrics-column">
<view class="metrics-header">
<text class="iconfont icon-shujucanmou" style="color: #666; font-size: 16px; margin-right: 4px;"></text>
<text class="metrics-title">客户活跃度</text>
</view>
<view class="activity-item" v-for="(item, index) in comprehensiveData.customerActivity" :key="index">
<view class="activity-dot" :class="getActivityDotClass(item.name)"></view>
<text class="activity-label">{{ item.name }}</text>
<text class="activity-value">{{ item.value }}</text>
</view>
</view>
<!-- 转化客户来源 -->
<view class="metrics-column">
<view class="metrics-header">
<text class="iconfont icon-shuju1" style="color: #666; font-size: 16px; margin-right: 4px;"></text>
<text class="metrics-title">转化客户来源</text>
</view>
<view class="source-item-new" v-for="(item, index) in comprehensiveData.conversionSource" :key="index">
<text class="iconfont" :class="getSourceIconClass(item.name)" style="color: #666; font-size: 14px; margin-right: 6px;"></text>
<text class="source-label">{{ item.name }}</text>
<text class="source-value">{{ item.count.toLocaleString() }}</text>
</view>
</view>
</view>
</view>
<!-- 日期选择弹窗 -->
<!-- 综合分析区域遮罩层 -->
<view v-if="isLoadingComprehensive" class="section-loading-mask">
<view class="section-loading-content">
<view class="section-spinner"></view>
<text class="section-loading-text">加载中...</text>
</view>
</view>
</view>
<!-- 日期选择弹窗 -->
<u-popup :show="showDatePopup" mode="bottom" @close="showDatePopup = false">
<view class="date-selector-popup">
<view class="date-selector-header">
@@ -527,6 +270,7 @@
@confirm="confirmEndDate"
@cancel="showEndDatePicker = false"
></u-datetime-picker>
</view>
</template>
@@ -545,6 +289,8 @@
data() {
const today = new Date();
return {
isLoadingOverview: false, // 数据概览区域加载状态
isLoadingComprehensive: false, // 综合分析区域加载状态
dateRange: '本周',
timeType: 'this_week',
showDatePopup: false,
@@ -560,14 +306,24 @@
subsectionList: ['客户分析', '互动分析'/* , '转化分析', '收入分析' */],
currentSubsection: 0,
overviewData: {
totalCustomers: 0,
accountValue: 0,
newCustomers: 0,
totalCustomersChange: 0,
newCustomersChange: 0,
interactions: 0,
interactionsChange: 0,
conversionRate: 28.6,
conversionRateChange: 3.2
conversionRate: 0,
conversionRateChange: 0
},
comprehensiveData: {
avgConversionAmount: 0,
totalSales: 0,
avgOrderAmount: 0,
highValueCustomers: 0,
weeklyRevenueGrowth: 0,
newCustomerConversion: 0,
activeCustomerGrowth: 0,
customerActivity: [], // 改为数组存储API返回的原始数据
conversionSource: [] // 改为数组存储API返回的原始数据
},
dateOptions: [
{ label: '今日', value: 'today' },
@@ -604,8 +360,12 @@
}
},
mounted() {
this.fetchOverviewData();
this.fetchCustomerAnalysis();
this.isLoadingOverview = true;
this.isLoadingComprehensive = true;
Promise.all([
this.fetchOverviewData(),
this.fetchCustomerAnalysis()
]);
},
methods: {
async fetchOverviewData() {
@@ -620,13 +380,13 @@
if (res.code === 200 && res.data) {
this.overviewData = {
...this.overviewData,
totalCustomers: res.data.total_customers.value || 0,
totalCustomersChange: res.data.total_customers.growth || 0,
newCustomers: res.data.new_customers.value || 0,
newCustomersChange: res.data.new_customers.growth || 0,
interactions: res.data.interaction_count.value || 0,
interactionsChange: res.data.interaction_count.growth || 0
accountValue: res.data.account_value?.avg_rfm || this.overviewData.accountValue,
newCustomers: res.data.new_customers?.value || this.overviewData.newCustomers,
newCustomersChange: res.data.new_customers?.growth || this.overviewData.newCustomersChange,
interactions: res.data.interaction_count?.value || this.overviewData.interactions,
interactionsChange: res.data.interaction_count?.growth || this.overviewData.interactionsChange,
conversionRate: res.data.conversion_rate?.value || this.overviewData.conversionRate,
conversionRateChange: res.data.conversion_rate?.growth || this.overviewData.conversionRateChange
};
} else {
uni.showToast({
@@ -640,28 +400,42 @@
title: '网络异常,请稍后重试',
icon: 'none'
});
} finally {
this.isLoadingOverview = false;
}
},
async fetchCustomerAnalysis() {
try {
const res = await request({
url: '/v1/store/statistics/customer-analysis',
url: '/v1/store/statistics/comprehensive-analysis',
method: 'GET',
data: {
time_type: this.timeType
}
});
if (res.code === 200 && res.data) {
// 更新趋势数据
this.customerAnalysis.trend = {
total: res.data.trend.total || 0,
new: res.data.trend.new || 0,
lost: res.data.trend.lost || 0
};
// 处理高价值客户百分比字符串(如"0.0%"转为0.0
let highValueCustomers = 0;
if (res.data.value_indicators?.high_value_customers) {
const highValueStr = res.data.value_indicators.high_value_customers;
highValueCustomers = parseFloat(highValueStr.replace('%', '')) || 0;
}
// 更新来源分布数据
this.customerAnalysis.sourceDistribution = res.data.source_distribution || [];
// 更新综合分析数据,直接存储数组数据
this.comprehensiveData = {
...this.comprehensiveData,
avgConversionAmount: res.data.avg_conversion_amount ?? this.comprehensiveData.avgConversionAmount,
totalSales: res.data.value_indicators?.total_sales ?? this.comprehensiveData.totalSales,
avgOrderAmount: res.data.value_indicators?.avg_order_amount ?? this.comprehensiveData.avgOrderAmount,
highValueCustomers: highValueCustomers ?? this.comprehensiveData.highValueCustomers,
weeklyRevenueGrowth: res.data.growth_trend?.weekly_revenue_growth ?? this.comprehensiveData.weeklyRevenueGrowth,
newCustomerConversion: res.data.growth_trend?.new_customer_conversion ?? this.comprehensiveData.newCustomerConversion,
activeCustomerGrowth: res.data.growth_trend?.active_customer_growth ?? this.comprehensiveData.activeCustomerGrowth,
customerActivity: res.data.frequency_analysis || [],
conversionSource: res.data.source_distribution || []
};
} else {
uni.showToast({
title: res.msg || '获取客户分析数据失败',
@@ -674,6 +448,8 @@
title: '网络异常,请稍后重试',
icon: 'none'
});
} finally {
this.isLoadingComprehensive = false;
}
},
async fetchInteractionAnalysis() {
@@ -715,21 +491,54 @@
title: '网络异常,请稍后重试',
icon: 'none'
});
} finally {
this.isLoadingComprehensive = false;
}
},
async changeSubsection(index) {
this.currentSubsection = index;
// 根据不同的分段加载不同的数据
if (index === 0) {
await this.fetchCustomerAnalysis();
} else if (index === 1) {
await this.fetchInteractionAnalysis();
this.isLoadingComprehensive = true;
try {
if (index === 0) {
await this.fetchCustomerAnalysis();
} else if (index === 1) {
await this.fetchInteractionAnalysis();
}
} finally {
this.isLoadingComprehensive = false;
}
},
closePage() {
this.$emit('close');
},
// 根据客户活跃度名称返回对应的dot颜色class
getActivityDotClass(name) {
if (!name) return 'gray';
// 优先精确匹配
if (name === '高频') return 'red';
if (name === '中频') return 'orange';
if (name === '低频') return 'gray';
// 模糊匹配
if (name.includes('高频')) return 'red';
if (name.includes('中频')) return 'orange';
if (name.includes('低频')) return 'gray';
return 'gray'; // 默认灰色
},
// 根据转化客户来源名称返回对应的图标class
getSourceIconClass(name) {
if (!name) return 'icon-yonghu';
// 优先精确匹配
if (name === '朋友推荐') return 'icon-yonghu';
if (name === '微信搜索') return 'icon-sousuo';
if (name === '微信群') return 'icon-yonghuqun';
// 模糊匹配
if (name.includes('推荐')) return 'icon-yonghu';
if (name.includes('搜索')) return 'icon-sousuo';
if (name.includes('群')) return 'icon-yonghuqun';
return 'icon-yonghu'; // 默认图标
},
showDateSelector() {
this.showDatePopup = true;
},
@@ -741,13 +550,19 @@
this.showDatePopup = false;
// 重新获取数据
await this.fetchOverviewData();
// 根据当前选中的分段重新加载对应数据
if (this.currentSubsection === 0) {
await this.fetchCustomerAnalysis();
} else if (this.currentSubsection === 1) {
await this.fetchInteractionAnalysis();
this.isLoadingOverview = true;
this.isLoadingComprehensive = true;
try {
await this.fetchOverviewData();
// 根据当前选中的分段重新加载对应数据
if (this.currentSubsection === 0) {
await this.fetchCustomerAnalysis();
} else if (this.currentSubsection === 1) {
await this.fetchInteractionAnalysis();
}
} finally {
// 加载状态在各自的 fetch 方法中控制
}
}
},
@@ -962,6 +777,21 @@
padding-right: 10px;
}
/* 上方区域:数据概览 */
.top-section {
background-color: #fff;
padding-bottom: 15px;
margin-bottom: 15px;
border-bottom: 8px solid #f5f7fa;
}
/* 下方区域:综合分析 */
.bottom-section {
background-color: #f5f7fa;
padding-top: 0;
padding-bottom: 20px;
}
.overview-header {
display: flex;
justify-content: space-between;
@@ -1064,6 +894,12 @@
font-size: 12px;
}
.item-desc {
font-size: 12px;
color: #999;
margin-top: 4px;
}
.up {
color: #18b566;
}
@@ -1280,7 +1116,8 @@
}
.ranking-list {
display: flex;
flex-direction: column;
}
.ranking-item {
@@ -1737,4 +1574,226 @@
margin-top: 4px;
margin-bottom: 8px;
}
/* 综合分析区域样式 - 整体卡片 */
.comprehensive-analysis-card {
background-color: #fff;
border-radius: 10px;
padding: 15px;
margin: 15px;
margin-top: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.analysis-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 15px;
}
/* 客户平均转化金额卡片 */
.avg-conversion-card {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border-radius: 8px;
padding: 15px 20px;
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.avg-conversion-label {
font-size: 14px;
color: #333;
margin-bottom: 8px;
}
.avg-conversion-value {
font-size: 28px;
font-weight: bold;
color: #2e7d32;
}
/* 指标网格 */
.metrics-grid {
display: flex;
justify-content: space-between;
gap: 20px;
}
/* 底部部分增加上边距 */
.metrics-grid.bottom-section {
margin-top: 30px;
padding-top: 25px;
border-top: 1px solid #f0f0f0;
}
.metrics-column {
flex: 1;
background-color: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
}
.metrics-column:first-child {
padding-right: 20px;
}
.metrics-column:last-child {
padding-left: 20px;
}
.metrics-header {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.metrics-title {
font-size: 14px;
font-weight: 500;
color: #333;
}
.metrics-item {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-bottom: 20px;
}
.metrics-item:last-child {
margin-bottom: 0;
}
.metrics-label {
font-size: 14px;
color: #666;
margin-bottom: 6px;
}
.metrics-value {
font-size: 22px;
font-weight: bold;
color: #333;
line-height: 1.3;
}
.metrics-value.up {
color: #18b566;
}
/* 客户活跃度样式 */
.activity-item {
display: flex;
align-items: center;
margin-bottom: 14px;
padding: 2px 0;
}
.activity-item:last-child {
margin-bottom: 0;
}
.activity-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 8px;
}
.activity-dot.red {
background-color: #fa3534;
}
.activity-dot.orange {
background-color: #ff9900;
}
.activity-dot.gray {
background-color: #c0c4cc;
}
.activity-label {
font-size: 14px;
color: #666;
flex: 1;
}
.activity-value {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 转化客户来源样式 */
.source-item-new {
display: flex;
align-items: center;
margin-bottom: 14px;
padding: 2px 0;
}
.source-item-new:last-child {
margin-bottom: 0;
}
.source-label {
font-size: 14px;
color: #666;
flex: 1;
}
.source-value {
font-size: 14px;
font-weight: 500;
color: #333;
}
/* 区域遮罩层样式 */
.section-loading-mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
border-radius: 8px;
}
.section-loading-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.section-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 128, 255, 0.2);
border-top-color: #0080ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 12px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.section-loading-text {
font-size: 14px;
color: #666;
}
</style>

View File

@@ -1,7 +1,61 @@
<template>
<view v-if="show" class="side-menu-container">
<view class="side-menu-mask" @tap="closeSideMenu"></view>
<view class="side-menu">
<view>
<!-- 更新弹窗 - 放在组件外层确保即使侧边栏关闭也能显示 -->
<!-- #ifdef APP-PLUS -->
<view v-if="showUpdateDialog" class="update-dialog-mask" @tap="closeUpdateDialog">
<view class="update-dialog" @tap.stop>
<!-- 火箭图标 -->
<view class="update-rocket">
<text class="iconfont" style="font-size: 80px; color: #5096ff;">🚀</text>
</view>
<!-- 版本信息 -->
<view class="update-version-info">
<text class="update-version-text">发现新版本 {{ updateInfo.version }}</text>
</view>
<!-- 更新内容列表 -->
<view class="update-content-list">
<view
class="update-content-item"
v-for="(item, index) in updateInfo.updateContent"
:key="index"
>
<text class="update-item-number">{{ index + 1 }}.</text>
<text class="update-item-text">{{ item }}</text>
</view>
</view>
<!-- 下载进度条 -->
<view v-if="downloading" class="download-progress-wrapper">
<view class="download-progress-bar">
<view class="download-progress-fill" :style="{ width: downloadProgress + '%' }"></view>
</view>
<text class="download-progress-text">{{ downloadProgress }}%</text>
</view>
<!-- 升级按钮 -->
<view class="update-button-wrapper">
<view
class="update-button"
:class="{ 'update-button-disabled': downloading }"
@tap="startDownload"
>
<text class="update-button-text">{{ downloading ? '下载中...' : '即刻升级' }}</text>
</view>
</view>
<!-- 关闭按钮 -->
<view v-if="!updateInfo.forceUpdate && !downloading" class="update-close-btn" @tap="closeUpdateDialog">
<text class="update-close-icon"></text>
</view>
</view>
</view>
<!-- #endif -->
<view v-if="show" class="side-menu-container">
<view class="side-menu-mask" @tap="closeSideMenu"></view>
<view class="side-menu">
<view class="side-menu-header">
<text class="side-menu-title">AI数智员工</text>
<!-- <text class="close-icon" @tap="closeSideMenu">
@@ -117,7 +171,7 @@
</view>
</view>
<!-- #ifdef APP-PLUS -->
<view class="module-item" @tap="handleCheckUpdate">
<view class="module-item" @tap="() => handleCheckUpdate(false)">
<view class="module-left">
<view class="module-icon green">
<text class="iconfont icon-shezhi" style="color: #33cc99; font-size: 24px;"></text>
@@ -131,6 +185,7 @@
</view>
</view>
<!-- #endif -->
<view class="module-item" @tap="showSettings" v-if='hide'>
<view class="module-left">
<view class="module-icon gray">
@@ -174,15 +229,7 @@
@login-success="handleLoginSuccess"
></login-register>
<!-- 更新弹窗 -->
<update-dialog
:show="showUpdateDialog"
:version="updateInfo.version"
:updateContent="updateInfo.updateContent"
:downloadUrl="updateInfo.downloadUrl"
:forceUpdate="updateInfo.forceUpdate"
@close="closeUpdateDialog"
></update-dialog>
</view>
</view>
</template>
@@ -193,10 +240,8 @@
import LoginRegister from './LoginRegister.vue';
import DataStatistics from './DataStatistics.vue';
import CustomerManagement from './CustomerManagement.vue';
import UpdateDialog from './UpdateDialog.vue';
import { hasValidToken, clearToken, redirectToLogin } from '../api/utils/auth';
import { request, APP_CONFIG } from '../api/config';
import { appApi } from '../api/modules/app';
export default {
name: "SideMenu",
@@ -206,8 +251,7 @@
SystemSettings,
LoginRegister,
DataStatistics,
CustomerManagement,
UpdateDialog
CustomerManagement
},
props: {
show: {
@@ -233,19 +277,22 @@
showLoginPageFlag: false,
isLoggedIn: false, // 用户登录状态
userInfo: null, // 用户信息
// 版本信息
currentVersion: APP_CONFIG.version, // 当前版本
// 版本更新相关
currentVersion: '', // 当前版本
latestVersion: '', // 最新版本
hasNewVersion: false, // 是否有新版本
checkingUpdate: false, // 是否正在检查更新
// 更新弹窗
showUpdateDialog: false,
showUpdateDialog: false, // 是否显示更新弹窗
updateInfo: {
version: '',
updateContent: '',
downloadUrl: '',
forceUpdate: false
}
version: '', // 新版本号
updateContent: [], // 更新内容列表
downloadUrl: '', // 下载地址
forceUpdate: false // 是否强制更新
},
// 下载相关
downloading: false, // 是否正在下载
downloadProgress: 0, // 下载进度 0-100
downloadTask: null // 下载任务对象
}
},
watch: {
@@ -267,6 +314,8 @@
this.checkLoginStatus();
// 获取功能开关状态
this.getFunctionStatus();
// 获取当前版本号并自动检查更新
this.getCurrentVersionAndCheckUpdate();
},
methods: {
// 检查登录状态
@@ -497,36 +546,75 @@
this.showSystemSettingsPage = true;
},
// 版本号比较函数
// 返回值: 1表示v1>v2, -1表示v1<v2, 0表示相等
compareVersion(v1, v2) {
const arr1 = v1.split('.').map(Number);
const arr2 = v2.split('.').map(Number);
const maxLen = Math.max(arr1.length, arr2.length);
// 获取当前版本号
getCurrentVersion() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
this.currentVersion = info.version || '1.0.0';
});
// #endif
for (let i = 0; i < maxLen; i++) {
const num1 = arr1[i] || 0;
const num2 = arr2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
// #ifndef APP-PLUS
this.currentVersion = '1.0.0';
// #endif
},
// 获取当前版本号并自动检查更新
getCurrentVersionAndCheckUpdate() {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
this.currentVersion = info.version || '1.0.0';
console.log('获取到当前版本号:', this.currentVersion);
// 版本号获取完成后自动检查更新延迟500ms确保应用已完全启动
setTimeout(() => {
this.handleCheckUpdate(true); // 传入 true 表示自动检查,不显示"已是最新版本"提示
}, 500);
});
// #endif
return 0;
// #ifndef APP-PLUS
this.currentVersion = '1.0.0';
// #endif
},
// 检查更新
async handleCheckUpdate() {
// autoCheck: true 表示自动检查(应用启动时),不显示"已是最新版本"提示
// autoCheck: false 表示手动检查(用户点击按钮),显示所有提示
async handleCheckUpdate(autoCheck = false) {
// #ifdef APP-PLUS
if (this.checkingUpdate) {
return; // 正在检查中,避免重复请求
}
// 如果版本号还没获取到,先获取版本号
if (!this.currentVersion) {
// #ifdef APP-PLUS
plus.runtime.getProperty(plus.runtime.appid, (info) => {
this.currentVersion = info.version || '1.0.0';
// 版本号获取完成后,继续检查更新
setTimeout(() => {
this.handleCheckUpdate(autoCheck);
}, 100);
});
// #endif
return;
}
this.checkingUpdate = true;
try {
console.log('开始检查更新,当前版本:', this.currentVersion);
const res = await appApi.checkUpdate();
// 调用检查更新接口
const res = await request({
url: '/v1/app/update',
method: 'GET',
data: {
version: this.currentVersion,
type: 'aiStore'
}
});
console.log('更新检测结果:', res);
if (res.code === 200 && res.data) {
@@ -537,42 +625,286 @@
const compareResult = this.compareVersion(this.latestVersion, this.currentVersion);
if (compareResult > 0) {
// 线上版本大于本地版本
// 线上版本大于本地版本,有新版本
this.hasNewVersion = true;
// 设置更新信息并显示自定义弹窗
// 设置更新信息
this.updateInfo = {
version: data.version || '',
updateContent: data.updateContent || '',
downloadUrl: data.downloadUrl ? data.downloadUrl.trim() : '',
updateContent: this.parseUpdateContent(data.updateContent || data.content || ''),
downloadUrl: data.downloadUrl || data.url || '',
forceUpdate: data.forceUpdate || false
};
this.showUpdateDialog = true;
// 根据检查类型决定是否显示弹窗
// autoCheck === false 表示手动检查autoCheck === true 表示自动检查
if (autoCheck === false) {
// 手动检查:有新版本时总是显示弹窗(不受每日限制)
console.log('手动检查更新,直接显示弹窗');
this.showUpdateDialog = true;
} else {
// 自动检查:每天只能弹出一次
console.log('自动检查更新,检查今日是否已显示过弹窗');
if (this.shouldShowUpdateDialog()) {
this.showUpdateDialog = true;
this.recordUpdateDialogShown();
} else {
console.log('今天已显示过更新弹窗,不再自动弹出');
}
}
} else {
// 已是最新版本
this.hasNewVersion = false;
// 只有手动检查时才显示"已是最新版本"提示
if (!autoCheck) {
uni.showToast({
title: '已是最新版本',
icon: 'success',
duration: 2000
});
}
}
} else {
// 只有手动检查时才显示错误提示
if (!autoCheck) {
uni.showToast({
title: '已是最新版本',
icon: 'success'
title: res.msg || '检查更新失败',
icon: 'none',
duration: 2000
});
}
}
} catch (error) {
console.error('检查更新失败:', error);
uni.showToast({
title: '检查更新失败',
icon: 'none'
});
// 只有手动检查时才显示错误提示
if (!autoCheck) {
uni.showToast({
title: '检查更新失败,请稍后重试',
icon: 'none',
duration: 2000
});
}
} finally {
this.checkingUpdate = false;
}
// #endif
// #ifndef APP-PLUS
if (!autoCheck) {
uni.showToast({
title: '此功能仅在APP中可用',
icon: 'none',
duration: 2000
});
}
// #endif
},
// 比较版本号,返回 1 表示 version1 > version2返回 -1 表示 version1 < version2返回 0 表示相等
compareVersion(version1, version2) {
if (!version1 || !version2) return 0;
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;
if (v1Part > v2Part) return 1;
if (v1Part < v2Part) return -1;
}
return 0;
},
// 解析更新内容,将字符串转换为数组
parseUpdateContent(content) {
if (!content) return [];
// 如果已经是数组,直接返回
if (Array.isArray(content)) {
return content;
}
// 如果是字符串,尝试按换行符或分号分割
if (typeof content === 'string') {
// 先尝试按换行符分割
let items = content.split(/\n+/).filter(item => item.trim());
// 如果没有换行,尝试按分号分割
if (items.length === 1) {
items = content.split(/[;]/).filter(item => item.trim());
}
// 清理每个项目,移除可能的编号前缀(如 "1. ", "1、", "- " 等)
return items.map(item => {
return item.replace(/^[\d一二三四五六七八九十]+[\.、\s\-]*/, '').trim();
}).filter(item => item);
}
return [];
},
// 检查今天是否应该显示更新弹窗(用于自动检查)
shouldShowUpdateDialog() {
try {
const lastShownDate = uni.getStorageSync('updateDialogLastShownDate');
if (!lastShownDate) {
return true; // 从未显示过,可以显示
}
// 获取今天的日期字符串格式YYYY-MM-DD
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
// 如果今天已经显示过,则不显示
return lastShownDate !== todayStr;
} catch (e) {
console.error('检查更新弹窗显示状态失败:', e);
return true; // 出错时默认允许显示
}
},
// 记录今天已显示更新弹窗
recordUpdateDialogShown() {
try {
const today = new Date();
const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
uni.setStorageSync('updateDialogLastShownDate', todayStr);
console.log('已记录更新弹窗显示日期:', todayStr);
} catch (e) {
console.error('记录更新弹窗显示日期失败:', e);
}
},
// 关闭更新弹窗
closeUpdateDialog() {
this.showUpdateDialog = false;
if (!this.updateInfo.forceUpdate) {
this.showUpdateDialog = false;
} else {
uni.showToast({
title: '此版本为重要更新,请升级后使用',
icon: 'none',
duration: 2000
});
}
},
// 开始下载更新
startDownload() {
// #ifdef APP-PLUS
if (!this.updateInfo.downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none',
duration: 2000
});
return;
}
if (this.downloading) {
return; // 已经在下载中
}
this.downloading = true;
this.downloadProgress = 0;
// 创建下载任务
const downloadPath = '_downloads/update_' + Date.now() + '.apk';
this.downloadTask = plus.downloader.createDownload(this.updateInfo.downloadUrl, {
filename: downloadPath // 下载文件名
}, (download, status) => {
if (status === 200) {
// 下载成功
console.log('下载成功:', download.filename);
this.downloading = false;
this.downloadProgress = 100;
// 安装APK
setTimeout(() => {
this.installAPK(download.filename);
}, 500);
} else {
// 下载失败
console.error('下载失败:', status);
this.downloading = false;
this.downloadProgress = 0;
uni.showToast({
title: '下载失败,请稍后重试',
icon: 'none',
duration: 2000
});
}
});
// 监听下载进度
this.downloadTask.addEventListener('statechanged', (download, status) => {
switch (download.state) {
case 1: // 开始下载
console.log('开始下载...');
break;
case 2: // 连接到服务器
console.log('连接到服务器...');
break;
case 3: // 下载中
if (download.totalSize > 0) {
const progress = Math.floor((download.downloadedSize / download.totalSize) * 100);
this.downloadProgress = Math.min(progress, 99); // 最大99完成时再设为100
console.log('下载进度:', this.downloadProgress + '%', '已下载:', download.downloadedSize, '总大小:', download.totalSize);
}
break;
case 4: // 下载完成
console.log('下载完成');
this.downloadProgress = 100;
break;
}
});
// 开始下载
this.downloadTask.start();
// #endif
},
// 安装APK
installAPK(filePath) {
// #ifdef APP-PLUS
try {
// 获取文件的完整路径
const fullPath = plus.io.convertLocalFileSystemURL(filePath);
console.log('准备安装APK:', fullPath);
plus.runtime.install(fullPath, {}, () => {
console.log('安装成功');
uni.showToast({
title: '安装成功',
icon: 'success',
duration: 1500
});
// 关闭弹窗
setTimeout(() => {
this.showUpdateDialog = false;
}, 1500);
}, (error) => {
console.error('安装失败:', error);
uni.showToast({
title: '安装失败,请到下载文件夹手动安装',
icon: 'none',
duration: 3000
});
});
} catch (error) {
console.error('安装异常:', error);
uni.showToast({
title: '安装异常,请到下载文件夹手动安装',
icon: 'none',
duration: 3000
});
}
// #endif
}
}
}
</script>
@@ -858,4 +1190,178 @@
font-size: 12px;
color: #999;
}
/* 更新弹窗样式 */
.update-dialog-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 10000;
display: flex;
justify-content: center;
align-items: center;
}
.update-dialog {
position: relative;
width: 85%;
max-width: 600px;
background-color: #fff;
border-radius: 20px;
padding: 40px 30px 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
}
/* 手机上最大宽度80% */
@media screen and (max-width: 768px) {
.update-dialog {
width: 80%;
max-width: 80%;
}
}
.update-rocket {
position: absolute;
top: -40px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 80px;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(135deg, #5096ff 0%, #6b7fff 100%);
border-radius: 50%;
box-shadow: 0 4px 15px rgba(80, 150, 255, 0.3);
}
.update-rocket text {
font-size: 50px;
line-height: 1;
}
.update-version-info {
text-align: center;
margin-top: 20px;
margin-bottom: 25px;
}
.update-version-text {
font-size: 18px;
font-weight: 600;
color: #333;
}
.update-content-list {
margin-bottom: 30px;
max-height: 300px;
overflow-y: auto;
}
.update-content-item {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 0 5px;
}
.update-item-number {
font-size: 15px;
color: #5096ff;
font-weight: 600;
margin-right: 8px;
min-width: 20px;
}
.update-item-text {
font-size: 15px;
color: #666;
line-height: 1.6;
flex: 1;
}
.download-progress-wrapper {
margin-bottom: 20px;
padding: 0 5px;
}
.download-progress-bar {
width: 100%;
height: 8px;
background-color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
margin-bottom: 8px;
}
.download-progress-fill {
height: 100%;
background: linear-gradient(90deg, #5096ff 0%, #6b7fff 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
.download-progress-text {
display: block;
text-align: center;
font-size: 13px;
color: #5096ff;
font-weight: 500;
}
.update-button-wrapper {
margin-top: 10px;
}
.update-button {
width: 100%;
height: 50px;
background: linear-gradient(135deg, #5096ff 0%, #6b7fff 100%);
border-radius: 25px;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 4px 15px rgba(80, 150, 255, 0.3);
transition: all 0.3s ease;
}
.update-button:active {
transform: scale(0.98);
box-shadow: 0 2px 8px rgba(80, 150, 255, 0.2);
}
.update-button-disabled {
opacity: 0.7;
}
.update-button-text {
font-size: 17px;
font-weight: 600;
color: #fff;
}
.update-close-btn {
position: absolute;
bottom: -50px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 40px;
background-color: rgba(255, 255, 255, 0.9);
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.update-close-icon {
font-size: 20px;
color: #666;
font-weight: 300;
line-height: 1;
}
</style>

View File

@@ -1,556 +0,0 @@
<template>
<view class="update-dialog-mask" v-if="show" @tap.stop="handleMaskClick">
<view class="update-dialog-container" @tap.stop>
<!-- 火箭图标 -->
<view class="rocket-container">
<view class="rocket-wrapper">
<!-- 火箭 SVG 图片 -->
<image class="rocket-svg" :src="rocketBase64" mode="aspectFit"></image>
<!-- 火焰效果 -->
<view class="flame-container">
<view class="flame flame-1"></view>
<view class="flame flame-2"></view>
<view class="flame flame-3"></view>
</view>
<!-- 星星装饰 -->
<view class="star star-1"></view>
<view class="star star-2"></view>
<view class="star star-3"></view>
<view class="star star-4"></view>
</view>
</view>
<!-- 内容区域 -->
<view class="dialog-content">
<!-- 强制更新提示 -->
<view class="force-notice" v-if="forceUpdate">
<text class="force-notice-icon"></text>
<text class="force-notice-text">本次为重要更新需要立即升级</text>
</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="update-item" v-for="(item, index) in updateList" :key="index">{{ index + 1 }}.{{ item }}</text>
</view>
<!-- 下载进度 -->
<view class="progress-container" v-if="downloading">
<view class="progress-bar">
<view class="progress-fill" :style="{width: downloadProgress + '%'}"></view>
</view>
<text class="progress-text">{{ downloadProgress }}%</text>
</view>
<!-- 按钮 -->
<view class="button-container" v-if="!downloading">
<button class="update-button" @tap="handleUpdate">即刻升级</button>
</view>
<!-- 下载中按钮 -->
<view class="button-container" v-else>
<button class="update-button downloading">下载中...</button>
</view>
</view>
<!-- 关闭按钮 -->
<view class="close-button" @tap="handleClose" v-if="!forceUpdate && !downloading">
<text class="close-icon">×</text>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateDialog',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
data() {
return {
downloading: false,
downloadProgress: 0,
downloadTask: null,
// 火箭 SVG 的 base64 图片
rocketBase64: 'data:image/svg+xml;base64,PHN2ZyB0PSIxNzYxODA0ODYyMzIwIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIwNDAiIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIj48cGF0aCBkPSJNMzk1LjI2NCAzMDQuMTI4Yy03MC42NTYgOTIuMTYtMTQ1LjQwOCAxOTQuNTYtMTg5LjQ0IDMwNC4xMjgtNS4xMiAxMi4yODggNi4xNDQgMjMuNTUyIDE4LjQzMiAyMC40OEwzNTguNCA1OTAuODQ4TTYyOC43MzYgMzA0LjEyOGM3MC42NTYgOTIuMTYgMTQ1LjQwOCAxOTQuNTYgMTg5LjQ0IDMwNC4xMjggNS4xMiAxMi4yODgtNi4xNDQgMjMuNTUyLTE4LjQzMiAyMC40OEw2NjUuNiA1OTAuODQ4IiBmaWxsPSIjRjc5ODM5IiBwLWlkPSIyMDQxIj48L3BhdGg+PHBhdGggZD0iTTY3Ni44NjQgNzExLjY4SDM0NS4wODhDMzE4LjQ2NCA2MjQuNjQgMzEyLjMyIDUzMi40OCAzMzEuNzc2IDQ0My4zOTJjMjIuNTI4LTEwMS4zNzYgNzAuNjU2LTE5Ny42MzIgMTQwLjI4OC0yNzcuNTA0bDcuMTY4LTguMTkyYzE2LjM4NC0xOS40NTYgNDYuMDgtMTkuNDU2IDYyLjQ2NCAwbDcuMTY4IDguMTkyYzcwLjY1NiA3OS44NzIgMTE3Ljc2IDE3Ni4xMjggMTQwLjI4OCAyNzcuNTA0IDIwLjQ4IDg5LjA4OCAxNC4zMzYgMTgxLjI0OC0xMi4yODggMjY4LjI4OHoiIGZpbGw9IiMwMDRGRkYiIHAtaWQ9IjIwNDIiPjwvcGF0aD48cGF0aCBkPSJNNDY3Ljk2OCA2NzUuODRjLTUxLjIgMC05NS4yMzItMzcuODg4LTEwMi40LTg4LjA2NC04LjE5Mi02MC40MTYtNi4xNDQtMTIwLjgzMiA2LjE0NC0xODAuMjI0IDIxLjUwNC05NS4yMzIgNjQuNTEyLTE4NS4zNDQgMTI2Ljk3Ni0yNjIuMTQ0LTguMTkyIDIuMDQ4LTE1LjM2IDYuMTQ0LTIwLjQ4IDEyLjI4OGwtNy4xNjggOC4xOTJDNDAyLjQzMiAyNDUuNzYgMzU0LjMwNCAzNDAuOTkyIDMzMS43NzYgNDQzLjM5MiAzMTIuMzIgNTMyLjQ4IDMxOC40NjQgNjI0LjY0IDM0NS4wODggNzExLjY4aDMzMS43NzZjNC4wOTYtMTIuMjg4IDcuMTY4LTIzLjU1MiAxMC4yNC0zNS44NEg0NjcuOTY4eiIgZmlsbD0iIzFENkZGRiIgcC1pZD0iMjA0MyI+PC9wYXRoPjxwYXRoIGQ9Ik0zODEuOTUyIDcyMS45MmgyMzYuNTQ0Vjc3OC4yNEgzODEuOTUyeiIgZmlsbD0iIzAwNEZGRiIgcC1pZD0iMjA0NCI+PC9wYXRoPjxwYXRoIGQ9Ik01MTQuNjk5Mjc2MjUgNDc0LjA2MzEyNjMxSDUwOC42MTAyMTEyM2wzLjA0NDUzMjUxIDIuODg3NTk3ODV6IiBmaWxsPSIjZmZmZmZmIiBwLWlkPSIyMDQ1Ij48L3BhdGg+PHBhdGggZD0iTTQzMC4wOCA0MjcuMDA4YTgwLjg5NiA3OS44NzIgMCAxIDAgMTYxLjc5MiAwIDgwLjg5NiA3OS44NzIgMCAxIDAgLTE2MS43OTIgMFoiIGZpbGw9IiNFOUYzRkIiIHAtaWQ9IjIwNDYiPjwvcGF0aD48L3N2Zz4='
}
},
computed: {
updateList() {
if (!this.updateContent) {
return ['修复已知问题', '优化用户体验', '提升系统稳定性'];
}
// 将更新内容按换行或分号分割
return this.updateContent.split(/[\n;]/).filter(item => item.trim());
}
},
methods: {
handleMaskClick() {
if (!this.forceUpdate && !this.downloading) {
this.handleClose();
}
},
handleClose() {
if (this.forceUpdate || this.downloading) {
return;
}
this.$emit('close');
},
handleUpdate() {
// #ifdef APP-PLUS
if (this.downloading) {
return;
}
if (!this.downloadUrl) {
uni.showToast({
title: '下载地址无效',
icon: 'none'
});
return;
}
this.downloading = true;
this.downloadProgress = 0;
// 创建下载任务
const downloadTask = uni.downloadFile({
url: this.downloadUrl.trim(),
success: (res) => {
if (res.statusCode === 200) {
console.log('下载成功,文件路径:', res.tempFilePath);
// 下载完成,安装应用
this.installApp(res.tempFilePath);
} else {
console.error('下载失败,状态码:', res.statusCode);
uni.showToast({
title: '下载失败',
icon: 'none'
});
this.downloading = false;
}
},
fail: (err) => {
console.error('下载失败:', err);
uni.showToast({
title: '下载失败,请稍后重试',
icon: 'none'
});
this.downloading = false;
}
});
// 监听下载进度
downloadTask.onProgressUpdate((res) => {
this.downloadProgress = res.progress;
console.log('下载进度:', res.progress + '%');
});
this.downloadTask = downloadTask;
// #endif
},
installApp(filePath) {
// #ifdef APP-PLUS
console.log('开始安装应用:', filePath);
plus.runtime.install(
filePath,
{
force: false
},
() => {
console.log('安装成功');
uni.showToast({
title: '安装成功,请重启应用',
icon: 'success',
duration: 2000
});
// 安装成功后关闭弹窗
setTimeout(() => {
this.downloading = false;
this.downloadProgress = 0;
this.$emit('close');
// 如果是强制更新,重启应用
if (this.forceUpdate) {
plus.runtime.restart();
}
}, 2000);
},
(error) => {
console.error('安装失败:', error);
uni.showToast({
title: '安装失败',
icon: 'none'
});
this.downloading = false;
}
);
// #endif
}
},
beforeDestroy() {
// 组件销毁时,取消下载任务
if (this.downloadTask) {
this.downloadTask.abort();
}
}
}
</script>
<style lang="scss" scoped>
.update-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 99999;
}
.update-dialog-container {
width: 580rpx;
background: linear-gradient(180deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 32rpx;
position: relative;
overflow: visible;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.3);
}
.rocket-container {
position: absolute;
top: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 200rpx;
height: 200rpx;
display: flex;
align-items: center;
justify-content: center;
}
.rocket-wrapper {
position: relative;
width: 100%;
height: 100%;
animation: rocketFloat 2s ease-in-out infinite;
}
/* 火箭 SVG 图片 */
.rocket-svg {
position: absolute;
top: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 160rpx;
height: 160rpx;
z-index: 10;
}
/* 火焰容器 */
.flame-container {
position: absolute;
bottom: 10rpx;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 80rpx;
display: flex;
flex-direction: column;
align-items: center;
z-index: 5;
}
/* 火焰效果 */
.flame {
position: absolute;
border-radius: 50%;
animation: flameFlicker 0.3s ease-in-out infinite alternate;
}
.flame-1 {
width: 40rpx;
height: 30rpx;
background: radial-gradient(ellipse at center, #FCD34D 0%, #FBBF24 50%, transparent 80%);
top: 0;
animation-delay: 0s;
}
.flame-2 {
width: 30rpx;
height: 40rpx;
background: radial-gradient(ellipse at center, #FBBF24 0%, #F59E0B 50%, transparent 80%);
top: 15rpx;
animation-delay: 0.1s;
}
.flame-3 {
width: 20rpx;
height: 35rpx;
background: radial-gradient(ellipse at center, #F59E0B 0%, #EF4444 50%, transparent 80%);
top: 30rpx;
animation-delay: 0.2s;
}
/* 星星装饰 */
.star {
position: absolute;
width: 8rpx;
height: 8rpx;
background: #FCD34D;
border-radius: 50%;
box-shadow: 0 0 10rpx #FCD34D;
}
.star-1 {
top: 30rpx;
left: 20rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0s;
}
.star-2 {
top: 50rpx;
right: 15rpx;
animation: starTwinkle 2s ease-in-out infinite;
animation-delay: 0.5s;
}
.star-3 {
top: 80rpx;
left: 10rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1s;
}
.star-4 {
top: 100rpx;
right: 20rpx;
animation: starTwinkle 1.5s ease-in-out infinite;
animation-delay: 1.5s;
}
/* 动画定义 */
@keyframes rocketFloat {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-15rpx);
}
}
@keyframes flameFlicker {
0% {
opacity: 0.8;
transform: scaleY(1);
}
100% {
opacity: 1;
transform: scaleY(1.2);
}
}
@keyframes starTwinkle {
0%, 100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.5);
}
}
.dialog-content {
padding: 50rpx 40rpx 40rpx;
background: #FFFFFF;
border-radius: 32rpx;
margin-top: 80rpx;
}
.dialog-title {
font-size: 36rpx;
font-weight: bold;
color: #333333;
text-align: center;
margin-bottom: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.force-tag {
display: inline-block;
margin-top: 10rpx;
padding: 4rpx 16rpx;
background: linear-gradient(135deg, #FF6B6B 0%, #FF4757 100%);
color: #FFFFFF;
font-size: 20rpx;
border-radius: 20rpx;
font-weight: normal;
animation: tagPulse 1.5s ease-in-out infinite;
}
@keyframes tagPulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7);
}
50% {
transform: scale(1.05);
box-shadow: 0 0 0 8rpx rgba(255, 71, 87, 0);
}
}
.force-notice {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx;
margin-bottom: 24rpx;
background: linear-gradient(135deg, #FFF5F5 0%, #FFE5E5 100%);
border-radius: 16rpx;
border: 2rpx solid #FFB8B8;
}
.force-notice-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.force-notice-text {
font-size: 26rpx;
color: #FF4757;
font-weight: 500;
}
.update-content {
background: #F8F9FA;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 40rpx;
max-height: 300rpx;
overflow-y: auto;
}
.update-item {
display: block;
font-size: 28rpx;
color: #666666;
line-height: 44rpx;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
}
.progress-container {
margin-bottom: 40rpx;
}
.progress-bar {
width: 100%;
height: 12rpx;
background: #E5E7EB;
border-radius: 6rpx;
overflow: hidden;
margin-bottom: 16rpx;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 6rpx;
transition: width 0.3s ease;
}
.progress-text {
display: block;
text-align: center;
font-size: 24rpx;
color: #4A9FF5;
font-weight: bold;
}
.button-container {
width: 100%;
}
.update-button {
width: 100%;
height: 88rpx;
background: linear-gradient(135deg, #4A9FF5 0%, #2E7FD9 100%);
border-radius: 44rpx;
border: none;
color: #FFFFFF;
font-size: 32rpx;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(46, 127, 217, 0.4);
&.downloading {
background: #CCCCCC;
box-shadow: none;
}
&::after {
border: none;
}
}
.close-button {
position: absolute;
bottom: -120rpx;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 80rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx solid rgba(255, 255, 255, 0.6);
}
.close-icon {
font-size: 60rpx;
color: #FFFFFF;
line-height: 1;
font-weight: 300;
}
</style>

View File

@@ -1,131 +0,0 @@
<template>
<view class="update-modal" v-if="show" @tap.stop="handleMaskClick">
<view class="modal-content" @tap.stop>
<!-- 版本号标题 -->
<view class="version-title">发现新版本 {{ version }}</view>
<!-- 更新内容 -->
<view class="update-content">
<text class="content-text">{{ updateContent }}</text>
</view>
<!-- 按钮区域 -->
<view class="button-area">
<view class="cancel-btn" v-if="!forceUpdate" @tap="handleCancel">稍后再说</view>
<view class="confirm-btn" @tap="handleConfirm">立即更新</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'UpdateModal',
props: {
show: {
type: Boolean,
default: false
},
version: {
type: String,
default: ''
},
updateContent: {
type: String,
default: ''
},
downloadUrl: {
type: String,
default: ''
},
forceUpdate: {
type: Boolean,
default: false
}
},
methods: {
handleMaskClick() {
// 强制更新时,点击遮罩不关闭
if (!this.forceUpdate) {
this.handleCancel();
}
},
handleCancel() {
this.$emit('cancel');
},
handleConfirm() {
this.$emit('confirm', this.downloadUrl);
}
}
}
</script>
<style lang="scss" scoped>
.update-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
.modal-content {
width: 600rpx;
background-color: #ffffff;
border-radius: 24rpx;
overflow: hidden;
.version-title {
text-align: center;
font-size: 36rpx;
font-weight: 600;
color: #333333;
padding: 60rpx 40rpx 40rpx;
}
.update-content {
max-height: 600rpx;
padding: 0 40rpx 40rpx;
overflow-y: auto;
.content-text {
font-size: 28rpx;
line-height: 44rpx;
color: #666666;
white-space: pre-wrap;
word-break: break-all;
}
}
.button-area {
display: flex;
border-top: 1px solid #eeeeee;
.cancel-btn,
.confirm-btn {
flex: 1;
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 500;
}
.cancel-btn {
color: #999999;
border-right: 1px solid #eeeeee;
}
.confirm-btn {
color: #007aff;
}
}
}
}
</style>