完善录音时长功能

This commit is contained in:
zhonghua.li
2026-02-07 19:51:03 +08:00
parent 33950f41fd
commit 3944fa6969
52 changed files with 2013 additions and 886 deletions

View File

@@ -1,274 +0,0 @@
<template>
<uni-popup ref="popup" type="top" :mask-click="true" @change="onPopupChange">
<view class="recording-duration-popup">
<view class="popup-header">
<text class="popup-title">录音时长统计</text>
<view class="popup-close" @click="close">
<text class="close-icon">×</text>
</view>
</view>
<scroll-view class="popup-content" scroll-y="true">
<!-- 统计卡片 -->
<view class="popup-stats-card">
<view class="popup-stat-item">
<text class="popup-stat-value">{{ totalDuration }}</text>
<text class="popup-stat-label">总录音时长</text>
</view>
<view class="popup-stat-item">
<text class="popup-stat-value">{{ todayDuration }}</text>
<text class="popup-stat-label">今日录音时长</text>
</view>
</view>
<!-- 图表区域 -->
<view class="popup-chart-section">
<text class="popup-section-title">时长趋势图</text>
<view class="popup-chart-placeholder">
<text class="popup-placeholder-text">📊 图表区域</text>
<text class="popup-placeholder-desc">此处将显示录音时长趋势图表</text>
</view>
</view>
<!-- 数据列表 -->
<view class="popup-data-list">
<text class="popup-section-title">详细数据</text>
<view class="popup-list-item" v-for="(item, index) in dataList" :key="index">
<view class="popup-item-left">
<text class="popup-item-date">{{ item.date }}</text>
<text class="popup-item-desc">{{ item.description }}</text>
</view>
<view class="popup-item-right">
<text class="popup-item-duration">{{ item.duration }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</uni-popup>
</template>
<script>
export default {
name: 'RecordingDurationPopup',
data() {
return {
totalDuration: '125.5 小时',
todayDuration: '8.2 小时',
dataList: [
{ date: '2024-02-07', description: '客户咨询录音', duration: '2.5 小时' },
{ date: '2024-02-06', description: '销售洽谈录音', duration: '3.2 小时' },
{ date: '2024-02-05', description: '培训录音', duration: '1.8 小时' },
{ date: '2024-02-04', description: '会议录音', duration: '4.1 小时' },
{ date: '2024-02-03', description: '客户反馈录音', duration: '2.9 小时' }
]
}
},
methods: {
open() {
this.$refs.popup.open()
},
close() {
this.$refs.popup.close()
},
onPopupChange(e) {
if (!e.show) {
this.$emit('close')
}
}
}
}
</script>
<style scoped>
/* 确保弹出框在导航栏之上 */
::v-deep .uni-popup.top {
z-index: 9999 !important;
}
/* 确保弹出框 wrapper 正确显示 */
::v-deep .uni-popup.top .uni-popup__wrapper {
overflow: visible !important;
position: relative !important;
}
.recording-duration-popup {
background-color: #FFFFFF;
border-radius: 16rpx;
display: flex;
flex-direction: column;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
overflow: hidden;
position: relative;
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx;
border-bottom: 1rpx solid #E0E0E0;
flex-shrink: 0;
background-color: #FFFFFF;
box-sizing: border-box;
min-height: 112rpx;
position: relative;
z-index: 10;
overflow: visible;
width: 100%;
}
.popup-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.popup-close {
width: 64rpx;
height: 64rpx;
min-width: 64rpx;
min-height: 64rpx;
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 50%;
transition: background-color 0.2s;
position: relative;
z-index: 20;
overflow: visible;
visibility: visible !important;
opacity: 1 !important;
}
.popup-close:active {
background-color: rgba(0, 0, 0, 0.05);
}
.close-icon {
font-size: 48rpx;
color: #333;
line-height: 1;
display: block !important;
visibility: visible !important;
opacity: 1 !important;
}
.popup-content {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.popup-stats-card {
display: flex;
margin: 20rpx 30rpx;
background: #f8f9fa;
border-radius: 16rpx;
padding: 40rpx 30rpx;
}
.popup-stat-item {
flex: 1;
text-align: center;
}
.popup-stat-item:not(:last-child) {
border-right: 1rpx solid #e9ecef;
}
.popup-stat-value {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #007aff;
margin-bottom: 10rpx;
}
.popup-stat-label {
display: block;
font-size: 24rpx;
color: #666666;
}
.popup-chart-section,
.popup-data-list {
margin: 20rpx 30rpx;
background: #f8f9fa;
border-radius: 16rpx;
overflow: hidden;
}
.popup-section-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333333;
padding: 30rpx;
border-bottom: 1rpx solid #e9ecef;
background: #ffffff;
}
.popup-chart-placeholder {
padding: 80rpx 30rpx;
text-align: center;
background: #ffffff;
}
.popup-placeholder-text {
display: block;
font-size: 48rpx;
margin-bottom: 20rpx;
}
.popup-placeholder-desc {
display: block;
font-size: 28rpx;
color: #666666;
}
.popup-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #ffffff;
}
.popup-list-item:last-child {
border-bottom: none;
}
.popup-item-left {
flex: 1;
}
.popup-item-date {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.popup-item-desc {
display: block;
font-size: 24rpx;
color: #666666;
}
.popup-item-right {
text-align: right;
}
.popup-item-duration {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #007aff;
}
</style>

View File

@@ -0,0 +1,539 @@
<template>
<view class="feature-page">
<view class="page-header" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="nav-bar">
<view class="nav-left" @click="goBack">
<text class="back-icon"></text>
</view>
<text class="nav-title">录音时长统计</text>
</view>
</view>
<scroll-view class="content-scroll" scroll-y="true" :style="{ top: contentTop, bottom: contentBottom }">
<!-- 搜索区域 -->
<view class="search-toolbar">
<view class="toolbar-picker-wrapper" @click="toggleTimeRangeDropdown">
<view class="toolbar-picker-view">
<text class="toolbar-picker-text">{{ timeRangeOptions[timeRangeIndex] }}</text>
<uni-icons :type="showTimeRangeDropdown ? 'top' : 'bottom'" size="14" color="#999"></uni-icons>
</view>
<!-- 自定义下拉菜单 -->
<view class="time-range-dropdown" v-if="showTimeRangeDropdown">
<view
class="dropdown-item"
v-for="(option, index) in timeRangeOptions"
:key="index"
:class="{ 'dropdown-item-active': timeRangeIndex === index }"
@click.stop="selectTimeRange(index)">
<text>{{ option }}</text>
</view>
</view>
</view>
<view class="toolbar-actions">
<view class="toolbar-btn toolbar-btn--refresh" @click="onRefresh">
<uni-icons type="refresh" size="18" color="#2A68FF"></uni-icons>
<text>刷新</text>
</view>
</view>
</view>
<!-- 遮罩层点击关闭下拉菜单 -->
<view class="dropdown-mask" v-if="showTimeRangeDropdown" @click="closeTimeRangeDropdown"></view>
<!-- 统计卡片 -->
<view class="stats-card">
<view class="stat-item">
<text class="stat-value">{{ totalDuration }}</text>
<text class="stat-label">总录音时长</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ todayDuration }}</text>
<text class="stat-label">今日录音时长</text>
</view>
</view>
<!-- 图表区域 -->
<view class="chart-section">
<text class="section-title">时长趋势图</text>
<view class="chart-placeholder">
<text class="placeholder-text">📊 图表区域</text>
<text class="placeholder-desc">此处将显示录音时长趋势图表</text>
</view>
</view>
<!-- 数据列表 -->
<view class="data-list">
<text class="section-title">详细数据</text>
<view class="list-item" v-for="(item, index) in dataList" :key="index">
<view class="item-left">
<text class="item-date">{{ item.date }}</text>
<text class="item-desc">{{ item.description }}</text>
</view>
<view class="item-right">
<text class="item-duration">{{ item.duration }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
</template>
<script>
// 获取系统信息的辅助函数
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('[RecordingDuration] 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('[RecordingDuration] Failed to get system info:', e);
return {
statusBarHeight: 20,
windowWidth: 375,
windowHeight: 667
};
}
}
export default {
data() {
// 动态计算导航栏高度
const systemInfo = getSystemInfo();
const statusBarHeight = systemInfo.statusBarHeight;
const navbarHeight = 44; // navbar高度44px
// 转换为rpx1px = 2rpx在375px设计稿下
const statusBarHeightRpx = statusBarHeight * 2;
const navbarHeightRpx = navbarHeight * 2;
const totalNavbarHeight = statusBarHeightRpx + navbarHeightRpx;
return {
statusBarHeight: statusBarHeight, // 状态栏高度px
contentTop: totalNavbarHeight + 'rpx', // 内容区域距离顶部的距离
contentBottom: '0rpx', // 内容区域距离底部的距离(默认值,会在 onReady 中更新)
timeRangeOptions: ['1个月', '3个月', '6个月'],
timeRangeIndex: 0, // 默认选择1个月
showTimeRangeDropdown: false, // 控制下拉菜单显示
totalDuration: '125.5 小时',
todayDuration: '8.2 小时',
dataList: [
{ date: '2024-02-07', description: '客户咨询录音', duration: '2.5 小时' },
{ date: '2024-02-06', description: '销售洽谈录音', duration: '3.2 小时' },
{ date: '2024-02-05', description: '培训录音', duration: '1.8 小时' },
{ date: '2024-02-04', description: '会议录音', duration: '4.1 小时' },
{ date: '2024-02-03', description: '客户反馈录音', duration: '2.9 小时' }
]
}
},
onReady() {
// 页面渲染完成后,查询实际导航栏高度并更新位置
this.$nextTick(() => {
// 查询导航栏实际高度
const query = uni.createSelectorQuery().in(this);
query.select('.nav-bar').boundingClientRect((data) => {
if (data && data.height) {
// 使用实际高度
const actualHeight = data.height; // 实际导航栏高度px
// 转换为rpx1px = 2rpx在375px设计稿下
const actualHeightRpx = actualHeight * 2;
const statusBarHeightRpx = this.statusBarHeight * 2;
const totalHeight = statusBarHeightRpx + actualHeightRpx;
// 稍微减小一点,确保紧贴
const adjustedHeight = Math.max(totalHeight - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
console.log('[RecordingDuration] 使用实际导航栏高度:', actualHeight, 'px =', actualHeightRpx, 'rpx, 总高度:', totalHeight, '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('[RecordingDuration] 使用实际tabbar高度:', tabbarHeight, 'px =', tabbarHeightRpx, 'rpx, 调整后:', adjustedBottom, 'rpx');
} else {
// 如果查询失败使用默认值无tabbar
this.$set(this, 'contentBottom', '0rpx');
}
}).exec();
// 延迟再次更新确保DOM已渲染
setTimeout(() => {
// 再次查询并更新导航栏
const query2 = uni.createSelectorQuery().in(this);
query2.select('.nav-bar').boundingClientRect((data) => {
if (data && data.height) {
const actualHeight = data.height;
const actualHeightRpx = actualHeight * 2;
const statusBarHeightRpx = this.statusBarHeight * 2;
const totalHeight = statusBarHeightRpx + actualHeightRpx;
const adjustedHeight = Math.max(totalHeight - 4, 0);
this.$set(this, 'contentTop', adjustedHeight + 'rpx');
console.log('[RecordingDuration] 延迟更新导航栏高度:', actualHeight, 'px =', actualHeightRpx, 'rpx, 总高度:', totalHeight, '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('[RecordingDuration] 延迟更新tabbar高度:', tabbarHeight, 'px =', tabbarHeightRpx, 'rpx, 调整后:', adjustedBottom, 'rpx');
}
}).exec();
}, 200);
});
},
methods: {
goBack() {
uni.navigateBack()
},
toggleTimeRangeDropdown() {
this.showTimeRangeDropdown = !this.showTimeRangeDropdown;
},
closeTimeRangeDropdown() {
this.showTimeRangeDropdown = false;
},
selectTimeRange(index) {
this.timeRangeIndex = index;
this.showTimeRangeDropdown = false;
// 根据选择的时间范围重新加载数据
this.loadData();
},
onRefresh() {
// 刷新数据
this.loadData();
uni.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
},
loadData() {
// 根据 timeRangeIndex 加载对应时间范围的数据
// 0 -> 1个月, 1 -> 3个月, 2 -> 6个月
const months = this.timeRangeIndex === 0 ? 1 : (this.timeRangeIndex === 1 ? 3 : 6);
console.log(`加载最近${months}个月的数据`);
// TODO: 这里可以调用API加载数据
// 示例:调用接口获取数据
}
}
}
</script>
<style scoped>
.feature-page {
min-height: 100vh;
background: #f8f9fa;
}
.page-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
background: #ffffff;
border-bottom: 1rpx solid #e9ecef;
}
.nav-bar {
display: flex;
align-items: center;
height: 88rpx;
padding: 0 30rpx;
}
.nav-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.back-icon {
font-size: 36rpx;
color: #333333;
}
.nav-title {
flex: 1;
text-align: center;
font-size: 32rpx;
font-weight: bold;
color: #333333;
}
.content-scroll {
position: absolute;
left: 0;
right: 0;
top: 0; /* 先从顶部开始,通过内联样式动态设置 */
bottom: 0; /* 先从底部开始,通过内联样式动态设置 */
/* top和bottom值通过内联样式动态设置覆盖上面的默认值 */
padding: 0 !important;
margin: 0 !important;
box-sizing: border-box;
}
.search-toolbar {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 8rpx;
padding: 12rpx 30rpx;
margin: 20rpx 30rpx;
background-color: #F8F8FA;
border-radius: 8rpx;
border: 1rpx solid #EFEFF2;
}
.toolbar-picker-wrapper {
position: relative;
flex: 1;
min-width: 120rpx;
max-width: 200rpx;
flex-shrink: 1;
z-index: 100;
}
.toolbar-picker-view {
height: 56rpx;
line-height: 56rpx;
background-color: #F5F6FA;
border-radius: 8rpx;
padding: 0 12rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.toolbar-picker-text {
font-size: 24rpx;
color: #333;
flex: 1;
}
.time-range-dropdown {
position: absolute;
top: calc(100% + 8rpx);
left: 0;
right: 0;
background-color: #FFFFFF;
border-radius: 8rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.12);
z-index: 101;
overflow: hidden;
border: 1rpx solid #EFEFF2;
}
.dropdown-item {
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #F3F4F6;
transition: background-color 0.2s ease;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:active {
background-color: #F9FAFB;
}
.dropdown-item text {
font-size: 24rpx;
color: #333;
}
.dropdown-item-active {
background-color: #F0F7FF;
}
.dropdown-item-active text {
color: #2A68FF;
font-weight: 500;
}
.dropdown-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: transparent;
z-index: 99;
}
.toolbar-actions {
display: flex;
align-items: center;
margin-left: auto;
flex-shrink: 0;
}
.toolbar-btn {
padding: 0 12rpx;
height: 56rpx;
min-width: 56rpx;
color: #2A68FF;
display: flex;
align-items: center;
justify-content: center;
gap: 4rpx;
font-size: 24rpx;
font-weight: 400;
}
.toolbar-btn text {
color: #2A68FF;
}
.stats-card {
display: flex;
margin: 20rpx 30rpx;
background: #ffffff;
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-item:not(:last-child) {
border-right: 1rpx solid #e9ecef;
}
.stat-value {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #007aff;
margin-bottom: 10rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666666;
}
.chart-section, .data-list {
margin: 20rpx 30rpx;
background: #ffffff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.section-title {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333333;
padding: 30rpx;
border-bottom: 1rpx solid #e9ecef;
}
.chart-placeholder {
padding: 80rpx 30rpx;
text-align: center;
}
.placeholder-text {
display: block;
font-size: 48rpx;
margin-bottom: 20rpx;
}
.placeholder-desc {
display: block;
font-size: 28rpx;
color: #666666;
}
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.list-item:last-child {
border-bottom: none;
}
.item-left {
flex: 1;
}
.item-date {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
}
.item-desc {
display: block;
font-size: 24rpx;
color: #666666;
}
.item-right {
text-align: right;
}
.item-duration {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #007aff;
}
</style>