Files
smartDriveEEUniApp/pages/ai_analysis/ai_analysis.vue
2026-02-07 19:51:03 +08:00

597 lines
20 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="page">
<!-- #ifdef APP -->
<statusBar></statusBar>
<!-- #endif -->
<!-- 导航栏 -->
<uni-nav-bar
:fixed="true"
:statusBar="true"
title="AI销冠系统"
leftIcon="left"
@clickLeft="goBack"
color="#333"
backgroundColor="#FFFFFF">
</uni-nav-bar>
<view class="content">
<!-- 功能分类展示 -->
<scroll-view class="content-scroll" scroll-y="true" :style="{ top: computedContentTop || contentTop, bottom: contentBottom || '96rpx' }">
<!-- 录音和接待统计 -->
<view class="category-section two-column">
<view class="category-header">
<text class="category-icon">📊</text>
<text class="category-title">录音和接待统计</text>
</view>
<view class="function-grid">
<view class="function-item" @click="navigateToPage('recording_duration')">
<view class="function-icon"></view>
<text class="function-name">录音时长</text>
<text class="function-desc">统计录音总时长</text>
</view>
<view class="function-item" @click="showCustomerCountPopup">
<view class="function-icon">👥</view>
<text class="function-name">接待客户数</text>
<text class="function-desc">统计接待客户数量</text>
</view>
</view>
</view>
<!-- 质量管控 -->
<view class="category-section two-column">
<view class="category-header">
<text class="category-icon"></text>
<text class="category-title">质量管控</text>
</view>
<view class="function-grid">
<view class="function-item" @click="showQualityCoveragePopup">
<view class="function-icon">📊</view>
<text class="function-name">质检覆盖率</text>
<text class="function-desc">质检录音覆盖统计</text>
</view>
<view class="function-item" @click="navigateToPage('speech_rating')">
<view class="function-icon">🗣</view>
<text class="function-name">员工话术评分</text>
<text class="function-desc">员工沟通质量评分</text>
</view>
</view>
</view>
<!-- 销售分析 -->
<view class="category-section sales-analysis">
<view class="category-header">
<text class="category-icon">📈</text>
<text class="category-title">销售分析</text>
</view>
<view class="function-grid">
<view class="function-item full-row-item" @click="navigateToPage('intent_level')">
<view class="function-icon">🎯</view>
<text class="function-name">客户意向分级</text>
<text class="function-desc">客户购买意向分析</text>
</view>
<view class="function-item full-row-item" @click="navigateToPage('deal_attribution')">
<view class="function-icon">💰</view>
<text class="function-name">成交归因</text>
<text class="function-desc">成交成功因素分析</text>
</view>
<view class="function-item full-row-item new-line-item" @click="navigateToPage('opening_rate')">
<view class="function-icon">🚪</view>
<text class="function-name">开口率</text>
<text class="function-desc">客户主动开口统计</text>
</view>
</view>
</view>
<!-- 风险监控 -->
<view class="category-section two-column">
<view class="category-header">
<text class="category-icon"></text>
<text class="category-title">风险监控</text>
</view>
<view class="function-grid">
<view class="function-item" @click="navigateToPage('over_commitment')">
<view class="function-icon">🚨</view>
<text class="function-name">过度承诺统计</text>
<text class="function-desc">监控过度承诺情况</text>
</view>
<view class="function-item" @click="navigateToPage('red_line_touch')">
<view class="function-icon">🔴</view>
<text class="function-name">触及红线统计</text>
<text class="function-desc">违规行为监控统计</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 接待客户数弹出框 -->
<CustomerCountPopup
v-if="showCustomerCount"
:open-trigger="openCustomerCountTrigger"
@close="onCustomerCountPopupClose" />
<!-- 质检覆盖率弹出框 -->
<QualityCoveragePopup
v-if="showQualityCoverage"
:open-trigger="openQualityCoverageTrigger"
@close="onQualityCoveragePopupClose" />
</view>
</template>
<script>
// #ifdef APP
import statusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar";
// #endif
// 注意:子包组件不使用 import 导入,而是通过 componentPlaceholder 配置
// 这样可以确保子包异步加载时组件能正确找到
// CustomerCountPopup, QualityCoveragePopup 通过 componentPlaceholder 自动注册
// 获取系统信息的辅助函数,优先使用新 API
function getSystemInfo() {
// #ifdef MP-WEIXIN
// 微信小程序:优先使用新的 API
if (typeof wx !== 'undefined' && wx.getWindowInfo) {
try {
const windowInfo = wx.getWindowInfo();
return {
statusBarHeight: windowInfo.statusBarHeight || 20,
windowWidth: windowInfo.windowWidth,
windowHeight: windowInfo.windowHeight
};
} catch (e) {
console.warn('[AIAnalysis] Failed to use wx.getWindowInfo, fallback to getSystemInfoSync:', e);
}
}
// #endif
// 回退到旧的 API 或使用 uni-app 的 API
try {
const systemInfo = uni.getSystemInfoSync();
return {
statusBarHeight: systemInfo.statusBarHeight || 20,
windowWidth: systemInfo.windowWidth,
windowHeight: systemInfo.windowHeight
};
} catch (e) {
console.error('[AIAnalysis] Failed to get system info:', e);
return {
statusBarHeight: 20,
windowWidth: 375,
windowHeight: 667
};
}
}
export default {
// #ifdef APP
components: {
statusBar
// 注意:子包组件不在这里注册,通过 componentPlaceholder 自动处理
// RecordingDurationPopup, CustomerCountPopup, QualityCoveragePopup 通过 componentPlaceholder 配置自动注册
},
// #endif
// #ifndef APP
// 注意:子包组件不在这里注册,通过 componentPlaceholder 自动处理
// CustomerCountPopup, QualityCoveragePopup 通过 componentPlaceholder 配置自动注册
// #endif
data() {
// 动态计算导航栏高度
const systemInfo = getSystemInfo();
const statusBarHeight = systemInfo.statusBarHeight; // 默认20px
const navbarHeight = 44; // navbar高度44px
// 转换为rpx1px = 2rpx在375px设计稿下
const statusBarHeightRpx = statusBarHeight * 2;
const navbarHeightRpx = navbarHeight * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
return {
contentTop: totalNavbarHeight + 'rpx', // 内容区域距离顶部的距离
contentBottom: '96rpx', // 内容区域距离底部的距离(默认值,会在 onReady 中更新)
showCustomerCount: false, // 是否显示接待客户数弹出框
showQualityCoverage: false, // 是否显示质检覆盖率弹出框
subpackageLoaded: false, // 子包是否已加载
popupsInitialized: false, // 弹出框组件是否已初始化
openCustomerCountTrigger: 0, // 打开接待客户数弹出框的触发器
openQualityCoverageTrigger: 0, // 打开质检覆盖率弹出框的触发器
}
},
computed: {
// 计算内容区域的位置(使用计算属性确保响应式)
computedContentTop() {
const systemInfo = getSystemInfo();
const statusBarHeight = systemInfo.statusBarHeight;
const navbarHeight = 44;
const statusBarHeightRpx = statusBarHeight * 2;
const navbarHeightRpx = navbarHeight * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
return totalNavbarHeight + 'rpx';
}
},
onLoad() {
// 更新导航栏位置
this.updateNavbarPosition();
// 在微信小程序中,子包会在组件使用时自动加载
// 但我们可以尝试预加载以提高响应速度
// #ifdef MP-WEIXIN
// 延迟预加载,避免阻塞页面初始化
setTimeout(() => {
this.preloadSubpackage();
}, 100);
// #endif
// 预先创建弹出框组件(使用 v-show组件会被创建但隐藏
// 这样可以避免在访问 $refs 时出现组件未初始化的问题
// 由于使用 v-show组件会在页面加载时就被创建只是通过 CSS 隐藏
this.$nextTick(() => {
// 等待一个渲染周期,确保组件已创建
setTimeout(() => {
this.popupsInitialized = true;
console.log('[AIAnalysis] Popup components ready (using v-show)');
}, 500);
});
},
onShow() {
// 每次页面显示时都重新计算导航栏位置
this.$nextTick(() => {
this.updateNavbarPosition();
setTimeout(() => {
this.updateNavbarPosition();
}, 50);
});
},
onReady() {
// 页面渲染完成后,查询实际导航栏高度并更新位置
this.$nextTick(() => {
// 查询导航栏实际高度
const query = uni.createSelectorQuery().in(this);
query.select('.uni-navbar__content').boundingClientRect((data) => {
if (data && data.height) {
// 使用实际高度(已经是包含状态栏的总高度)
const actualHeight = data.height; // 实际导航栏高度px
// 转换为rpx1px = 2rpx在375px设计稿下
const actualHeightRpx = actualHeight * 2;
// 稍微减小一点,确保紧贴
const adjustedHeight = Math.max(actualHeightRpx - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
console.log('[AIAnalysis] 使用实际导航栏高度:', actualHeight, 'px =', actualHeightRpx, 'rpx, 调整后:', adjustedHeight, 'rpx');
} else {
// 如果查询失败,使用默认计算并减小一点
const systemInfo = getSystemInfo();
const statusBarHeight = systemInfo.statusBarHeight;
const navbarHeight = 44;
const statusBarHeightRpx = statusBarHeight * 2;
const navbarHeightRpx = navbarHeight * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
const adjustedHeight = Math.max(totalNavbarHeight - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
}
}).exec();
// 查询底部 tabbar 实际高度
const queryTabbar = uni.createSelectorQuery().in(this);
queryTabbar.select('.tabbar').boundingClientRect((tabbarData) => {
if (tabbarData && tabbarData.height) {
// 使用实际高度
const tabbarHeight = tabbarData.height; // tabbar 实际高度px
const tabbarHeightRpx = tabbarHeight * 2;
// 稍微减小一点,确保紧贴
const adjustedBottom = Math.max(tabbarHeightRpx - 4, 0);
this.$set(this, 'contentBottom', adjustedBottom + 'rpx');
console.log('[AIAnalysis] 使用实际tabbar高度:', tabbarHeight, 'px =', tabbarHeightRpx, 'rpx, 调整后:', adjustedBottom, 'rpx');
} else {
// 如果查询失败,使用默认值
this.$set(this, 'contentBottom', '96rpx');
}
}).exec();
// 延迟再次更新确保DOM已渲染
setTimeout(() => {
// 再次查询并更新导航栏
const query2 = uni.createSelectorQuery().in(this);
query2.select('.uni-navbar__content').boundingClientRect((data) => {
if (data && data.height) {
const actualHeight = data.height;
const actualHeightRpx = actualHeight * 2;
// 稍微减小一点,确保紧贴
const adjustedHeight = Math.max(actualHeightRpx - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
console.log('[AIAnalysis] 延迟更新导航栏高度:', actualHeight, 'px =', actualHeightRpx, 'rpx, 调整后:', adjustedHeight, 'rpx');
}
}).exec();
// 再次查询 tabbar 高度
const queryTabbar2 = uni.createSelectorQuery().in(this);
queryTabbar2.select('.tabbar').boundingClientRect((tabbarData) => {
if (tabbarData && tabbarData.height) {
const tabbarHeight = tabbarData.height;
const tabbarHeightRpx = tabbarHeight * 2;
const adjustedBottom = Math.max(tabbarHeightRpx - 4, 0);
this.$set(this, 'contentBottom', adjustedBottom + 'rpx');
console.log('[AIAnalysis] 延迟更新tabbar高度:', tabbarHeight, 'px =', tabbarHeightRpx, 'rpx, 调整后:', adjustedBottom, 'rpx');
}
}).exec();
}, 200);
});
},
methods: {
// 预加载子包
// #ifdef MP-WEIXIN
preloadSubpackage() {
// 使用微信原生 API 预加载子包
if (typeof wx !== 'undefined' && wx.loadSubpackage) {
wx.loadSubpackage({
name: 'pages-subpackage',
success: () => {
console.log('[AIAnalysis] Subpackage loaded successfully');
this.subpackageLoaded = true;
},
fail: (err) => {
console.error('[AIAnalysis] Failed to load subpackage:', err);
// 即使加载失败,也尝试继续(可能已经加载过了)
this.subpackageLoaded = true;
}
});
} else {
// 如果 API 不可用,标记为已加载(可能已经自动加载)
console.log('[AIAnalysis] loadSubpackage API not available, assuming subpackage will load automatically');
this.subpackageLoaded = true;
}
},
// #endif
// 显示接待客户数弹出框
showCustomerCountPopup() {
// 显示组件(使用 v-show组件已存在只是显示
this.showCustomerCount = true;
// 使用触发器来通知子组件打开弹窗,避免直接访问 refs
this.$nextTick(() => {
this.openCustomerCountTrigger++;
});
},
// 接待客户数弹出框关闭事件
onCustomerCountPopupClose() {
// 弹出框关闭时隐藏组件
this.showCustomerCount = false;
},
// 显示质检覆盖率弹出框
showQualityCoveragePopup() {
// 显示组件(使用 v-show组件已存在只是显示
this.showQualityCoverage = true;
// 使用触发器来通知子组件打开弹窗,避免直接访问 refs
this.$nextTick(() => {
this.openQualityCoverageTrigger++;
});
},
// 质检覆盖率弹出框关闭事件
onQualityCoveragePopupClose() {
// 弹出框关闭时隐藏组件
this.showQualityCoverage = false;
},
// 计算并更新导航栏和内容区域的位置
updateNavbarPosition() {
const systemInfo = getSystemInfo();
const statusBarHeight = systemInfo.statusBarHeight;
const navbarHeight = 44;
// 转换为rpx1px = 2rpx在375px设计稿下
const statusBarHeightRpx = statusBarHeight * 2;
const navbarHeightRpx = navbarHeight * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
const contentTop = totalNavbarHeight + 'rpx';
this.$set(this, 'contentTop', contentTop);
// #ifdef MP-WEIXIN
this.$forceUpdate();
// #endif
},
goBack() {
// 检查页面栈,如果当前是第一个页面,则不能返回
const pages = getCurrentPages();
if (pages.length <= 1) {
console.log('[AIAnalysis][goBack] 当前是第一个页面,无法返回');
return;
}
uni.navigateBack({
fail: (err) => {
console.error('[AIAnalysis][goBack] 返回失败:', err);
}
});
},
navigateToPage(pageName) {
const pagePath = `pages-subpackage/ai_features/${pageName}/${pageName}`
uni.navigateTo({
url: `/${pagePath}`,
fail: (err) => {
console.error('导航失败:', err)
uni.showToast({
title: '页面跳转失败',
icon: 'none'
})
}
})
}
}
}
</script>
<style scoped>
page {
background-color: #F5F5F5;
}
.page {
min-height: 100vh;
background-color: #F5F5F5;
}
.content {
padding-top: 0 !important;
margin-top: 0 !important;
position: relative;
min-height: 100vh;
/* 确保内容从顶部开始,不受占位符影响 */
top: 0;
}
/* 隐藏导航栏占位区域,避免产生空白 */
::v-deep .uni-navbar__placeholder {
display: none !important;
height: 0 !important;
}
::v-deep .uni-navbar__placeholder-view {
display: none !important;
height: 0 !important;
}
/* 隐藏底部 tabBar 占位区域,避免产生空白(类似顶部导航栏的处理方式) */
/* 注意:只隐藏占位符,不隐藏实际的 tabbar */
::v-deep [class*="tabbar"][class*="placeholder"] {
display: none !important;
height: 0 !important;
}
/* 隐藏可能的页面底部安全区域占位符 */
::v-deep [class*="safe-area"][class*="bottom"],
::v-deep [class*="bottom"][class*="safe"] {
display: none !important;
height: 0 !important;
}
.content-scroll {
position: absolute;
left: 0;
right: 0;
top: 0; /* 先从顶部开始 */
bottom: 0; /* 先设为0通过查询实际高度动态设置 */
/* top值通过内联样式动态设置覆盖上面的 top: 0 */
padding: 0 !important;
padding-bottom: 0 !important;
margin: 0 !important;
box-sizing: border-box;
}
/* 确保scroll-view内部没有默认间距 */
.content-scroll > view {
margin-top: 0;
padding-top: 0;
margin-bottom: 0;
padding-bottom: 0;
}
.category-section {
margin: 20rpx 30rpx;
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.category-section:first-child {
margin-top: 0;
}
.category-section:last-child {
margin-bottom: 0;
}
.category-header {
display: flex;
align-items: center;
padding: 30rpx;
background: #FFFFFF;
border-bottom: 1rpx solid #F0F0F0;
}
.category-icon {
font-size: 32rpx;
margin-right: 20rpx;
}
.category-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.function-grid {
display: flex;
flex-wrap: wrap;
padding: 10rpx;
}
/* 两列布局的分类 */
.two-column .function-item {
flex: 1;
background: #FFFFFF;
border-radius: 12rpx;
padding: 24rpx 16rpx;
margin: 0 5rpx;
text-align: center;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
border: 1rpx solid #F0F0F0;
}
/* 三列布局的分类 */
.three-column .function-item {
width: 31%;
background: #FFFFFF;
border-radius: 12rpx;
padding: 24rpx 12rpx;
margin: 1%;
text-align: center;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
border: 1rpx solid #F0F0F0;
}
/* 销售分析所有项目统一宽度50% */
.sales-analysis .function-item.full-row-item {
flex: 1;
width: calc(50% - 10rpx);
background: #FFFFFF;
border-radius: 12rpx;
padding: 24rpx 16rpx;
margin: 0 5rpx;
text-align: center;
box-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.04);
transition: all 0.3s ease;
border: 1rpx solid #F0F0F0;
}
/* 销售分析开口率单独一行但保持50%宽度 */
.sales-analysis .function-item.new-line-item {
flex-basis: 100%;
width: calc(50% - 10rpx);
margin-top: 10rpx;
}
.function-item:active {
transform: scale(0.95);
box-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.1);
}
.function-icon {
font-size: 48rpx;
margin-bottom: 12rpx;
}
.function-name {
display: block;
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.function-desc {
display: block;
font-size: 24rpx;
color: #666;
line-height: 1.4;
}
</style>