Browse Source

删除

dev_des
1054425342@qq.com 1 month ago
parent
commit
40b2f2bd55
  1. 155
      components/AudioControl使用文档.md
  2. 795
      components/DynamicIsland - 副本.vue
  3. 293
      components/SwipeToNext使用文档.md
  4. 225
      components/跨页面音频控制解决方案.md
  5. 144
      components/音频背景音乐交互说明.md

155
components/AudioControl使用文档.md

@ -1,155 +0,0 @@
# AudioControl 音频控制组件使用文档
## 组件功能
- 在父组件右上角显示音频控制图标
- 点击图标播放指定音频,同时暂停背景音乐
- 再次点击暂停音频,恢复背景音乐
- 音频播放结束后自动恢复背景音乐
## 组件属性 (Props)
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| audioSrc | String | 是 | - | 音频文件路径 |
| visible | Boolean | 否 | true | 是否显示组件 |
## 使用方法
### 1. 在父组件中引入和注册组件
```vue
<template>
<view class="parent-container">
<!-- 父组件内容 -->
<view class="content">
<!-- 你的页面内容 -->
</view>
<!-- 音频控制组件 -->
<AudioControl
:audioSrc="audioUrl"
:visible="showAudio"
/>
</view>
</template>
<script>
import AudioControl from '@/components/AudioControl.vue';
export default {
components: {
AudioControl
},
data() {
return {
audioUrl: 'https://your-domain.com/audio/sample.mp3', // 替换为你的音频URL
showAudio: true
}
}
}
</script>
<style>
.parent-container {
position: relative; /* 重要:确保AudioControl能正确定位 */
width: 100vw;
height: 100vh;
}
</style>
```
### 2. 使用项目中的showImg方法(推荐)
如果你的音频文件也存储在项目服务器上,可以使用项目的showImg方法:
```vue
<script>
export default {
data() {
return {
audioUrl: this.showImg('/uploads/audio/your-audio-file.mp3'),
showAudio: true
}
}
}
</script>
```
### 3. 动态控制音频源
你可以根据不同的页面或条件播放不同的音频:
```vue
<script>
export default {
data() {
return {
audioUrl: '',
showAudio: true
}
},
mounted() {
// 根据页面设置不同的音频
this.setAudioForCurrentPage();
},
methods: {
setAudioForCurrentPage() {
const currentRoute = this.$route.path; // 假设使用vue-router
switch(currentRoute) {
case '/chapter1':
this.audioUrl = this.showImg('/uploads/audio/chapter1.mp3');
break;
case '/chapter2':
this.audioUrl = this.showImg('/uploads/audio/chapter2.mp3');
break;
default:
this.audioUrl = this.showImg('/uploads/audio/default.mp3');
}
}
}
}
</script>
```
## 样式说明
组件默认定位在父组件的右上角(top: 30rpx, right: 30rpx),如果需要调整位置,可以在父组件中覆盖样式:
```vue
<style>
/* 调整音频控制组件位置 */
.parent-container ::v-deep .audio-control {
top: 50rpx !important;
right: 50rpx !important;
}
</style>
```
## 注意事项
1. **父组件样式**:确保父组件设置了 `position: relative`,这样AudioControl组件才能正确定位
2. **音频格式**:建议使用 mp3 格式的音频文件,兼容性最好
3. **音频路径**:确保音频文件路径正确且可访问
4. **背景音乐**:组件会自动处理与MusicControl组件的交互,无需额外配置
## 图标说明
- 🔊:音频未播放状态
- 🎧:音频播放中状态,带有脉动动画效果
## 事件处理
组件内部已处理所有音频播放逻辑,包括:
- 播放音频时自动暂停背景音乐
- 暂停音频时自动恢复背景音乐
- 音频播放结束时自动恢复背景音乐
- 组件销毁时自动清理资源
## 示例场景
适用于以下场景:
- 章节页面播放对应的音频解说
- 展示页面播放介绍音频
- 互动页面播放提示音频
- 任何需要临时播放音频并暂停背景音乐的场景

795
components/DynamicIsland - 副本.vue

@ -1,795 +0,0 @@
<template>
<!-- 灵动岛占位区域 - 始终存在但控制可见性 -->
<view class="dynamic-island-placeholder" :class="{ visible: isScrolled }"
:style="{ height: 216 + 'rpx' }">
<view class="dynamic-island" :class="{
compact: actualCompactState,
fixed: isFixed,
}" :style="{ top: isFixed ? fixedTopPosition + 'px' : 0 }" @click="handleToggle">
<!-- 展开状态 -->
<view v-if="!actualCompactState" class="expanded-content">
<template>
<template v-if="styleType != 'timeShop'">
<!-- 三栏布局 -->
<view class="three-column-layout">
<!-- 右侧头像和链接 -->
<view class="right-section">
<view class="avatar-container" @click="toWebView">
<image class="avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"></image>
</view>
<view class="" style="display: flex;flex-direction: column;justify-content: space-between;height: 100%;">
<view class="profile-info">
<text class="profile-title">数字领航员</text>
<text class="profile-name">EVITA</text>
</view>
<view class="platform-link">
<view class="link-text" style="margin-bottom: 15rpx;">DES介绍 >>
</view>
<view class="link-text">DES广播 >></view>
</view>
</view>
</view>
<!-- 左侧分隔线 -->
<view class="column-divider"></view>
<!-- 左侧欢迎信息 -->
<view class="left-section">
<view class="welcome-message">
<view class="welcome-text">Hi!{{
userInfo && userInfo.token ? userInfo.nickname : "用户"
}}欢迎回来~</view>
</view>
<view class="" style="font-size: 24rpx;font-weight: bold;color: #000000;">
查看您的DES数据资产行
</view>
<view class="" style="display: flex;align-items: flex-end;justify-content: space-between;">
<view class="">
<view class="stats-info" @click="toOrder">
<text class="stats-number">2</text>
<text class="stats-unit"></text>
</view>
<view class="stats-label">数字权益行</view>
</view>
<div>
<view class="stats-info" @click="toOrder">
<text class="stats-number">2</text>
<text class="stats-unit"></text>
</view>
<view class="stats-label">数字资产行</view>
</div>
<view class="middle-section">
<view class="time-reward-container">
<text class="time-reward-title" style="margin-bottom: 5rpx;">时间奖励</text>
<view class="time-reward-stats">
<text class="time-reward-number">120</text>
<text class="time-reward-unit"></text>
</view>
<text class="time-reward-label" style="font-weight: bold;">旅行时间行</text>
</view>
</view>
</view>
</view>
</view>
</template>
<template v-if="styleType == 'timeShop'">
<view class="bottom-section">
<view class="" style="flex:1;">
<view class="" style="display: flex;">
<view class="time-reward-container" style="width: 200rpx;">
<text class="time-reward-title">时间奖励</text>
<view class="time-reward-stats">
<text class="time-reward-number">120</text>
<text class="time-reward-unit"></text>
</view>
<text class="time-reward-label"
style="font-size: 24rpx;font-weight: bold;">旅行时间行</text>
</view>
<view class="">
<view class="time-reward-number" style="font-size: 34rpx;">
{{
userInfo && userInfo.token ? userInfo.nickname : "用户"
}}
</view>
<view class="time-reward-label"
style="margin-top: 15rpx;font-weight: bold;font-size: 26rpx;">
积分:999<text
style="color: #999999;font-size: 22rpx;margin-left: 10rpx;">积分获取规则</text>
</view>
</view>
</view>
<view class=""
style="display: flex;align-items: center;font-size: 26rpx;font-weight: bold;display: flex;align-items: center;margin-top: 20rpx;">
<view class="" style="width: 200rpx;">
时长:{{
userInfo && userInfo.token ? userInfo.hour+'h' : "-"
}}
</view>
<view class="">
<image style="width: 22rpx;height: 22rpx;margin-right: 15rpx;"
:src="showImg('/uploads/20250822/c8ee7615823a1ffaba400a4d5746de9a.png')">
</image>
点赞:120
</view>
<view class="">
<image style="width: 22rpx;height: 22rpx;margin: 0 15rpx;"
:src="showImg('/uploads/20250822/84c49f78f1c86b7340aaaa391bd4b7cf.png')">
</image>
留言:120
</view>
</view>
</view>
<image class="avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"></image>
</view>
</template>
</template>
</view>
<!-- 紧凑状态 -->
<view v-else class="compact-content">
<text class="compact-name">{{ getCompactName() }}</text>
<image class="compact-avatar"
src="https://epic.js-dyyj.com/uploads/20250728/7d9ba1fe109643681396cb03f60f3218.png"
mode="aspectFill"></image>
</view>
</view>
</view>
</template>
<script>
export default {
name: "DynamicIsland",
props: {
isCompact: {
type: Boolean,
default: false,
},
styleType: {
type: String,
default: "",
},
isFixed: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "用户",
},
subtitle: {
type: String,
default: "周杰伦 - 青花瓷",
},
avatarUrl: {
type: String,
default: "https://picsum.photos/80/80",
},
actionText: {
type: String,
default: "暂停",
},
//
pageId: {
type: String,
default: "default_page",
},
},
data() {
return {
isExpanded: false,
statusBarHeight: 0,
isScrolled: false,
scrollThreshold: 160, // 160rpx
// props
currentTitle: "Hi!用户,欢迎回来~",
currentSubtitle: "2个权益 | 120时间银行",
currentAvatar: "https://picsum.photos/80/80",
currentAction: "激活你的Agent",
userInfo: {},
JDSU_IMG_URL: "https://epic.js-dyyj.com",
};
},
computed: {
// 使
actualCompactState() {
if (this.isScrolled) {
return !this.isExpanded;
}
return false; //
},
// top
fixedTopPosition() {
// + (40px) + (20px)
return this.statusBarHeight + 40 + 20;
},
// -
placeholderHeight() {
//
// 32rpx() + 160rpx() + 24rpx() = 216rpx
const islandHeightRpx = 160; //
const topMarginRpx = 32; //
const bottomMarginRpx = 24; //
return topMarginRpx + islandHeightRpx + bottomMarginRpx;
},
// 使props
title() {
return this.currentTitle;
},
subtitle() {
return this.currentSubtitle;
},
avatarUrl() {
return this.currentAvatar;
},
actionText() {
return this.currentAction;
},
//
isFixed() {
return this.isScrolled;
},
},
mounted() {
// uni-app
this.setStatusBarHeight();
//
this.addScrollListener();
//
this.getUserInfo();
},
beforeDestroy() {
//
this.removeScrollListener();
},
methods: {
toLogin() {
uni.navigateTo({
url: "/pages/login/login",
});
},
handleToggle() {
if (this.isScrolled) {
//
this.isExpanded = !this.isExpanded;
this.$emit("toggle", this.isExpanded);
} else {
//
this.$emit("toggle");
}
},
handleAction() {
this.$emit("action");
},
//
collapseIsland() {
if (this.isScrolled && this.isExpanded) {
this.isExpanded = false;
this.$emit("toggle", this.isExpanded);
}
},
//
addScrollListener() {
// ID
const eventName = `pageScroll_${this.pageId}`;
console.log("DynamicIsland 添加滚动监听:", eventName);
uni.$on(eventName, this.handlePageScroll);
},
//
removeScrollListener() {
// ID
const eventName = `pageScroll_${this.pageId}`;
uni.$off(eventName, this.handlePageScroll);
},
//
handlePageScroll(e) {
const scrollTop = e.scrollTop || e;
const shouldScroll = scrollTop > this.scrollThreshold;
if (this.isScrolled !== shouldScroll) {
this.isScrolled = shouldScroll;
//
}
//
if (this.isScrolled) {
this.collapseIsland();
}
},
toOrder() {
uni.switchTab({
url: '/pages/index/iSoul'
})
},
getCompactName() {
//
if (this.userInfo && this.userInfo.nickname) {
return this.userInfo.nickname;
}
return "用户";
},
getStatNumber(type) {
//
if (this.subtitle) {
if (type === "权益") {
const match = this.subtitle.match(/(\d+)个权益/);
return match ? match[1] : "0";
} else if (type === "时间银行") {
const match = this.subtitle.match(/(\d+)时间银行/);
return match ? match[1] : "0";
}
}
return "0";
},
//
setStatusBarHeight() {
try {
const systemInfo = uni.getSystemInfoSync();
this.statusBarHeight = systemInfo.statusBarHeight || 0;
} catch (e) {
console.warn("获取系统信息失败:", e);
this.statusBarHeight = 0;
}
},
//
getUserInfo() {
try {
this.userInfo =
(uni.getStorageSync("userInfo") &&
JSON.parse(uni.getStorageSync("userInfo"))) ||
this.$store.state.user.userInfo || {};
console.log(this.userInfo, "this.userInfo");
//
if (this.userInfo && this.userInfo.nickname) {
this.currentTitle = `Hi!${this.userInfo.nickname},欢迎回来~`;
}
} catch (e) {
console.warn("获取用户信息失败:", e);
this.userInfo = {};
}
},
toWebView() {
uni.navigateTo({
url: "/subPackages/webPage/webPage?url=" +
"https://des.js-dyyj.com/dist/#/",
});
},
//
showImg(img) {
if (!img) return;
if (img.indexOf("https://") != -1 || img.indexOf("http://") != -1) {
return img;
} else {
return this.JDSU_IMG_URL + img;
}
},
},
};
</script>
<style scoped lang="scss">
/* 灵动岛占位区域样式 - 始终存在但控制可见性 */
.dynamic-island-placeholder {
width: 100%;
background: transparent;
position: relative;
opacity: 1;
transition: opacity 0.3s ease;
padding: 24rpx 0;
}
.dynamic-island-placeholder.visible {
opacity: 1;
}
/* 当灵动岛不是固定状态时,确保它在占位符内正常显示 */
.dynamic-island-placeholder .dynamic-island:not(.fixed) {
position: relative;
z-index: 100;
}
.dynamic-island {
// margin: 24rpx auto 24rpx;
margin: 0 auto;
z-index: 100;
background: linear-gradient(180deg, #fffdb7 0%, #97fffa 100%);
backdrop-filter: blur(20rpx);
-webkit-backdrop-filter: blur(20rpx);
border-radius: 40rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.1),
0 0 0 1rpx rgba(255, 255, 255, 0.2) inset;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
//
width: 710rpx;
height: 216rpx;
//
&.compact {
width: 300rpx;
height: 80rpx;
border-radius: 40rpx;
.expanded-content {
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease-out, visibility 0s linear 0.15s;
}
}
&:not(.compact) {
.compact-content {
opacity: 0;
visibility: hidden;
transition: opacity 0.15s ease-out, visibility 0s linear 0.15s;
}
}
//
&.fixed {
position: fixed;
left: 50%;
transform: translateX(-50%);
z-index: 998;
margin: 0;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
animation: slideInFromTop 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
}
.expanded-content {
display: flex;
flex-direction: column;
height: 100%;
padding: 24rpx 25rpx;
opacity: 1;
visibility: visible;
transition: opacity 0.2s ease-in 0.1s, visibility 0s linear 0s;
}
/* 三栏布局样式 */
.three-column-layout {
display: flex;
align-items: flex-end;
height: 100%;
width: 100%;
}
/* 列分隔线 */
.column-divider {
width: 2rpx;
height: 160rpx;
background: rgba(0, 0, 0, 0.1);
margin: 0rpx 25rpx;
flex-shrink: 0;
}
/* 左侧区域 */
.left-section {
color: #333;
display: flex;
flex-direction: column;
justify-content: space-between;
flex: 1;
height: 100%;
}
.welcome-message {
display: flex;
flex-direction: column;
}
.welcome-text {
font-size: 22rpx;
color: #000000;
line-height: 1.2;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.welcome-subtitle {
font-size: 24rpx;
color: #000000;
margin-top: 4rpx;
font-weight: bold;
}
.stats-info {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
}
.stats-number {
font-size: 40rpx;
color: #333;
font-weight: bold;
line-height: 1;
}
.stats-unit {
font-size: 24rpx;
color: #000000;
margin-left: 4rpx;
}
.stats-label {
font-size: 22rpx;
color: #000000;
font-weight: bold;
}
/* 中间区域 */
.middle-section {
display: flex;
justify-content: flex-start;
align-items: center;
}
.time-reward-container {
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-start;
}
.time-reward-title {
font-size: 20rpx;
color: #000000;
font-weight: 500;
margin-bottom: 12rpx;
}
.time-reward-stats {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
}
.time-reward-number {
font-size: 40rpx;
color: #000000;
font-weight: bold;
line-height: 1;
}
.time-reward-unit {
font-size: 24rpx;
color: #000000;
margin-left: 4rpx;
}
.time-reward-label {
font-size: 22rpx;
color: #000000;
}
/* 右侧区域 */
.right-section {
display: flex;
align-items: flex-end;
height: 100%;
}
.avatar-container {
position: relative;
margin-right: 10rpx;
width: 140rpx;
height: 140rpx;
}
.avatar {
width: 140rpx;
height: 140rpx;
border-radius: 50%;
}
.profile-info {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
}
.profile-title {
font-size: 26rpx;
color: #000000;
margin-bottom: 4rpx;
font-weight: bold;
}
.profile-name {
font-size: 26rpx;
color: #000000;
font-weight: bold;
}
.platform-link {
cursor: pointer;
}
.link-text {
font-size: 22rpx;
color: #000000;
text-decoration: underline;
font-weight: 500;
}
/* 保留原有的底部区域样式用于timeShop模式 */
.bottom-section {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1;
color: #000000;
}
.stats-section {
display: flex;
gap: 32rpx;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
font-size: 28rpx;
font-weight: bold;
}
.stat-number {
font-size: 32rpx;
color: #000000;
font-weight: bold;
line-height: 1;
margin-bottom: 4rpx;
}
.stat-label {
font-size: 28rpx;
color: #000000;
line-height: 1;
font-weight: bold;
margin-top: 20rpx;
}
.divider {
width: 2rpx;
height: 60rpx;
background: rgba(255, 255, 255, 0.3);
margin: 0 24rpx;
}
.action-section {
display: flex;
align-items: center;
flex: 1;
justify-content: space-between;
}
.action-text {
font-size: 26rpx;
color: #ffffff;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
//
.dynamic-island:active {
transform: scale(0.98);
&.fixed {
transform: translateX(-50%) scale(0.98);
}
}
//
@keyframes slideInFromTop {
0% {
transform: translateX(-50%) translateY(-100%);
opacity: 0;
}
100% {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
.compact-content {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 24rpx;
opacity: 1;
visibility: visible;
transition: opacity 0.2s ease-in 0.1s, visibility 0s linear 0s;
}
.compact-name {
font-size: 27rpx;
color: #333;
font-weight: bold;
flex: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200rpx;
}
.compact-avatar {
width: 48rpx;
height: 48rpx;
border-radius: 50%;
border: 2rpx solid rgba(0, 0, 0, 0.2);
flex-shrink: 0;
}
//
@keyframes pulse {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
100% {
opacity: 1;
transform: scale(1);
}
}
//
// @media (max-width: 750rpx) {
// .dynamic-island {
// &.is-expanded {
// width: 100vw;
// max-width: 750rpx;
// }
// &.is-compact {
// width: 280rpx;
// }
// }
// }
.action-text-box {
color: #000000;
.action-text-box-des {
font-size: 20rpx;
}
.action-text-box-msg {
font-size: 24rpx;
display: flex;
align-items: center;
margin-top: 20rpx;
}
.action-text-box-img {
width: 57rpx;
height: 46rpx;
margin-right: 10rpx;
}
}
</style>

293
components/SwipeToNext使用文档.md

@ -1,293 +0,0 @@
# SwipeToNext 组件使用文档
## 组件介绍
`SwipeToNext` 是一个通用的触底跳转组件,封装了手势检测、延迟防抖、提示文字等功能,可以在任何需要滑动跳转的页面中使用。
## 组件特性
- ✅ 手势滑动检测
- ✅ 防误触发机制(延迟允许跳转)
- ✅ 可配置的滑动阈值
- ✅ 自定义提示文字
- ✅ 支持事件监听
- ✅ 完全可配置的参数
## 使用方法
### 1. 引入组件
```vue
<script>
import SwipeToNext from '@/components/SwipeToNext.vue';
export default {
components: {
SwipeToNext
}
}
</script>
```
### 2. 基础使用
```vue
<template>
<SwipeToNext
:is-last-slide="isLastSlide"
:target-path="'/next/page'"
@swipe-to-next="handleSwipeToNext"
>
<!-- 你的页面内容 -->
<view class="content">
<!-- 轮播图或其他内容 -->
</view>
</SwipeToNext>
</template>
<script>
export default {
data() {
return {
isLastSlide: false
}
},
methods: {
// 处理swiper切换或其他逻辑
handlePageChange() {
// 根据你的逻辑设置 isLastSlide
this.isLastSlide = true; // 当到达最后一页时
},
handleSwipeToNext(targetPath) {
console.log('即将跳转到:', targetPath);
// 可以在这里添加额外的逻辑,如数据统计
}
}
}
</script>
```
### 3. 完整配置使用
```vue
<template>
<SwipeToNext
:is-last-slide="isLastSlide"
:target-path="'/next/page'"
:show-tip="true"
tip-text="向上滑动查看更多内容"
:swipe-threshold="100"
:delay-time="800"
:enable-delay="true"
@swipe-to-next="handleSwipeToNext"
>
<!-- 你的页面内容 -->
<view class="content">
<!-- 内容区域 -->
</view>
</SwipeToNext>
</template>
```
## Props 参数
| 参数名 | 类型 | 默认值 | 必填 | 说明 |
|--------|------|--------|------|------|
| `isLastSlide` | Boolean | `false` | ✅ | 是否在最后一页/最后一个状态 |
| `targetPath` | String | - | ✅ | 跳转的目标路径 |
| `showTip` | Boolean | `true` | ❌ | 是否显示提示文字 |
| `tipText` | String | `'继续向上滑动进入下一章节'` | ❌ | 提示文字内容 |
| `swipeThreshold` | Number | `80` | ❌ | 滑动阈值(像素) |
| `delayTime` | Number | `500` | ❌ | 延迟允许跳转的时间(毫秒) |
| `enableDelay` | Boolean | `true` | ❌ | 是否启用延迟机制 |
| `alwaysEnable` | Boolean | `false` | ❌ | 是否总是启用跳转(忽略isLastSlide状态) |
## Events 事件
| 事件名 | 参数 | 说明 |
|--------|------|------|
| `swipe-to-next` | `targetPath` | 触发跳转时的回调事件 |
## 使用场景示例
### 场景1:图片轮播页面
```vue
<template>
<SwipeToNext
:is-last-slide="currentIndex === images.length - 1"
:target-path="'/gallery/next'"
>
<swiper @change="handleSwiperChange">
<swiper-item v-for="(img, index) in images" :key="index">
<image :src="img" mode="aspectFit" />
</swiper-item>
</swiper>
</SwipeToNext>
</template>
<script>
export default {
data() {
return {
currentIndex: 0,
images: ['img1.jpg', 'img2.jpg', 'img3.jpg']
}
},
methods: {
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
}
}
}
</script>
```
### 场景2:文章阅读页面
```vue
<template>
<SwipeToNext
:is-last-slide="isReadComplete"
:target-path="'/article/next'"
tip-text="继续滑动阅读下一篇文章"
:swipe-threshold="60"
>
<scroll-view @scrolltolower="handleScrollToBottom">
<view class="article-content">
<!-- 文章内容 -->
</view>
</scroll-view>
</SwipeToNext>
</template>
<script>
export default {
data() {
return {
isReadComplete: false
}
},
methods: {
handleScrollToBottom() {
// 滚动到底部时认为阅读完成
this.isReadComplete = true;
}
}
}
</script>
```
### 场景4:单张图片或总是启用触底跳转
```vue
<template>
<SwipeToNext
:always-enable="true"
:target-path="'/next/chapter'"
tip-text="向上滑动查看下一内容"
:enable-delay="false"
>
<view class="single-image">
<image :src="imageUrl" mode="aspectFit" />
</view>
</SwipeToNext>
</template>
<script>
export default {
data() {
return {
imageUrl: 'single-image.jpg'
}
}
}
</script>
```
```vue
<template>
<SwipeToNext
:is-last-slide="currentStep === totalSteps - 1"
:target-path="'/guide/complete'"
tip-text="向上滑动完成引导"
>
<view class="guide-step">
<view class="step-content">
步骤 {{ currentStep + 1 }} / {{ totalSteps }}
</view>
<button @click="nextStep">下一步</button>
</view>
</SwipeToNext>
</template>
<script>
export default {
data() {
return {
currentStep: 0,
totalSteps: 5
}
},
methods: {
nextStep() {
if (this.currentStep < this.totalSteps - 1) {
this.currentStep++;
}
}
}
}
</script>
```
## 特殊情况处理
### 单张图片问题
当只有一张图片时,传统的 `isLastSlide` 逻辑不适用。这时可以使用 `alwaysEnable` 参数:
```vue
<!-- 单张图片的解决方案 -->
<SwipeToNext
:always-enable="true"
:target-path="'/next/page'"
:enable-delay="false"
>
<image src="single-image.jpg" />
</SwipeToNext>
```
### 参数优先级
`alwaysEnable="true"` 时:
- 忽略 `isLastSlide` 的值
- 总是显示提示文字
- 总是允许触底跳转
- 建议设置 `enableDelay="false"` 以获得更好的响应速度
1. **确保正确设置 `isLastSlide`**:这是控制是否允许跳转的关键属性
2. **路径格式**:`targetPath` 需要是有效的 uni-app 路由路径
3. **性能考虑**:如果不需要延迟机制,可以设置 `enableDelay: false` 来提高响应速度
4. **样式覆盖**:组件内的提示文字样式可以通过全局样式覆盖
5. **事件监听**:建议监听 `swipe-to-next` 事件进行数据统计或其他操作
## 自定义样式
如果需要自定义提示文字的样式,可以在页面中添加:
```scss
// 覆盖组件样式
.swipe-to-next .bottom-tip {
bottom: 200rpx !important; // 调整位置
background: rgba(255, 255, 255, 0.9) !important; // 改变背景色
text {
color: #333 !important; // 改变文字颜色
font-size: 32rpx !important; // 改变字体大小
}
}
```
这个组件极大地简化了触底跳转功能的实现,让你可以专注于业务逻辑而不用重复编写相同的手势检测代码。

225
components/跨页面音频控制解决方案.md

@ -1,225 +0,0 @@
# 跨页面音频控制解决方案
## 进一步优化:解决跨页面状态同步问题
### 问题描述
在跨页面场景下,当音频正在播放时跳转到新页面,新页面的MusicControl组件不知道有音频在播放,点击背景音乐按钮时会直接播放背景音乐,导致音频和背景音乐同时播放。
### 解决方案
#### 1. MusicControl组件增强检测
```javascript
// 在mounted生命周期中添加全局音频状态检测
mounted() {
this.syncMusicState();
this.checkGlobalAudioState(); // 新增:检查全局音频状态
// 定时器也要检查全局音频状态
this.timer = setInterval(() => {
this.syncMusicState();
this.checkGlobalAudioState();
}, 1000);
}
// 新增方法:检查全局音频状态
checkGlobalAudioState() {
const app = getApp();
if (app && app.globalData && app.globalData.currentAudio) {
const globalAudio = app.globalData.currentAudio;
this.isAudioPlaying = !globalAudio.paused;
} else {
this.isAudioPlaying = false;
}
}
```
#### 2. 全局音频管理工具优化
```javascript
// 在音频状态变化时发送全局事件
pauseCurrentAudio() {
const audio = this.getCurrentAudio();
if (audio && !audio.paused) {
audio.pause();
this.notifyAudioStateChange(false); // 通知状态变化
return true;
}
return false;
}
// 新增:通知音频状态变化
notifyAudioStateChange(isPlaying) {
if (typeof uni !== 'undefined') {
uni.$emit('audioPlaying', isPlaying);
}
}
```
### 修复后的交互流程
```mermaid
graph TD
A[跨页面跳转] --> B[MusicControl组件加载]
B --> C[检查全局音频状态]
C --> D{有音频在播放?}
D -->|是| E[设置isAudioPlaying=true]
D -->|否| F[设置isAudioPlaying=false]
E --> G[点击背景音乐按钮]
F --> G
G --> H{检查isAudioPlaying}
H -->|有音频| I[先暂停音频再播放背景音乐]
H -->|无音频| J[直接播放背景音乐]
```
## 问题描述
AudioControl组件在页面跳转时会出现以下问题:
1. 组件状态重置,图标显示不正确
2. 音频实例丢失连接,但音频可能仍在播放
3. 无法在其他页面控制正在播放的音频
## 解决方案
### 1. 全局音频实例管理
在`App.vue`的`globalData`中添加`currentAudio`属性,用于保存当前的音频实例:
```javascript
globalData: {
// ... 其他属性
currentAudio: null // 全局音频实例
}
```
### 2. AudioControl组件优化
#### 状态同步机制
- 组件挂载时检查全局音频状态
- 复用已存在的音频实例(如果URL匹配)
- 组件销毁时不销毁全局音频实例
#### 核心方法改进
```javascript
// 检查全局音频状态
checkGlobalAudioState() {
const app = getApp();
if (app && app.globalData && app.globalData.currentAudio) {
const globalAudio = app.globalData.currentAudio;
if (globalAudio.src === this.audioSrc) {
this.isAudioPlaying = !globalAudio.paused;
}
}
}
// 初始化音频时复用全局实例
initAudio() {
const app = getApp();
if (app && app.globalData && app.globalData.currentAudio) {
if (app.globalData.currentAudio.src === this.audioSrc) {
// 复用现有实例
this.audioContext = app.globalData.currentAudio;
this.isAudioPlaying = !this.audioContext.paused;
return;
}
}
// 创建新实例...
}
```
### 3. 全局音频管理工具
创建了`utils/globalAudioManager.js`工具类,提供统一的音频控制接口:
```javascript
// 在任何页面或组件中使用
uni.$globalAudio.pauseCurrentAudio(); // 暂停当前音频
uni.$globalAudio.playCurrentAudio(); // 播放当前音频
uni.$globalAudio.isAudioPlaying(); // 检查播放状态
uni.$globalAudio.getCurrentAudioSrc(); // 获取当前音频源
```
## 使用示例
### 在页面中控制音频
```vue
<template>
<view>
<!-- 音频控制组件 -->
<AudioControl :audioSrc="audioUrl" />
<!-- 手动控制按钮 -->
<button @click="toggleAudio">切换音频</button>
<button @click="stopAudio">停止音频</button>
</view>
</template>
<script>
import AudioControl from '@/components/AudioControl.vue';
export default {
components: { AudioControl },
data() {
return {
audioUrl: this.showImg('/uploads/audio/chapter1.mp3')
}
},
methods: {
toggleAudio() {
if (uni.$globalAudio.isAudioPlaying()) {
uni.$globalAudio.pauseCurrentAudio();
} else {
uni.$globalAudio.playCurrentAudio();
}
},
stopAudio() {
uni.$globalAudio.stopCurrentAudio();
}
}
}
</script>
```
### 在其他页面检查音频状态
```javascript
// 在任何页面的onShow生命周期中
onShow() {
// 检查是否有音频在播放
if (uni.$globalAudio.isAudioPlaying()) {
console.log('有音频正在播放:', uni.$globalAudio.getCurrentAudioSrc());
}
}
```
## 技术优势
### ✅ **状态持久化**
- 音频实例在页面跳转时不会丢失
- 组件状态能够正确同步全局音频状态
### ✅ **跨页面控制**
- 在任何页面都可以控制当前播放的音频
- 提供统一的音频管理接口
### ✅ **资源优化**
- 避免创建多个音频实例
- 自动清理无用的音频资源
### ✅ **用户体验**
- 页面跳转时音频播放不中断
- 图标状态显示正确
- 音频控制逻辑一致
## 注意事项
1. **页面生命周期**:音频实例与页面生命周期解耦,需要手动管理
2. **内存管理**:确保在应用退出时正确清理音频资源
3. **状态同步**:多个AudioControl组件需要监听相同的全局状态
4. **错误处理**:增强错误处理机制,确保音频异常时的状态恢复
## 兼容性
- ✅ uni-app
- ✅ 小程序环境
- ✅ H5环境
- ✅ APP环境

144
components/音频背景音乐交互说明.md

@ -1,144 +0,0 @@
# 音频与背景音乐交互功能说明
## Bug修复记录
### 问题描述
当音频播放时点击背景音乐按钮,背景音乐暂停音频并开始播放。此时再点击关闭背景音乐,图标显示关闭状态但背景音乐仍在播放。
### 问题原因
1. 点击背景音乐按钮时会发送事件暂停音频
2. 音频暂停时会调用restoreBackgroundMusic恢复背景音乐
3. 然后MusicControl再执行自己的切换逻辑
4. 导致背景音乐被恢复后又被操作,状态混乱
### 解决方案
1. **MusicControl组件优化**
- 检测是否有音频在播放
- 如有音频,先暂停音频,延迟执行背景音乐切换
- 如无音频,直接切换背景音乐状态
2. **AudioControl组件优化**
- 在handleBackgroundMusicToggle中只暂停音频
- 不自动恢复背景音乐,让MusicControl自己控制
### 修复后的交互流程
```mermaid
graph TD
A[点击背景音乐按钮] --> B{是否有音频播放?}
B -->|是| C[发送暂停音频事件]
C --> D[AudioControl暂停音频\n不恢复背景音乐]
D --> E[延迟100ms后切换背景音乐]
B -->|否| F[直接切换背景音乐状态]
```
## 实现的功能
### 🎵 **背景音乐控制音频**
当点击背景音乐控制按钮时:
- 如果有音频正在播放,会自动暂停音频
- 然后正常切换背景音乐的播放/暂停状态
### 🎧 **音频控制背景音乐**
当点击音频控制按钮时:
- 播放音频时自动暂停背景音乐
- 暂停音频时自动恢复背景音乐
- 音频播放结束时自动恢复背景音乐
## 技术实现
### 事件通信机制
使用uni-app的全局事件机制实现组件间通信:
```javascript
// AudioControl组件发送事件
uni.$emit('audioPlaying', true/false);
// MusicControl组件发送事件
uni.$emit('backgroundMusicToggle');
// 组件监听事件
uni.$on('eventName', this.handlerFunction);
```
### 交互流程
#### 点击背景音乐按钮:
```mermaid
graph TD
A[点击背景音乐按钮] --> B[发送backgroundMusicToggle事件]
B --> C[AudioControl收到事件]
C --> D{音频是否在播放?}
D -->|是| E[暂停音频]
D -->|否| F[继续背景音乐操作]
E --> G[恢复背景音乐]
F --> H[切换背景音乐状态]
```
#### 点击音频按钮:
```mermaid
graph TD
A[点击音频按钮] --> B{当前音频状态?}
B -->|未播放| C[暂停背景音乐]
C --> D[播放音频]
D --> E[发送audioPlaying:true事件]
B -->|正在播放| F[暂停音频]
F --> G[恢复背景音乐]
G --> H[发送audioPlaying:false事件]
```
## 使用方法
在页面中同时使用两个组件:
```vue
<template>
<view class="page-container">
<!-- 页面内容 -->
<!-- 音频控制组件 -->
<AudioControl :audioSrc="audioUrl" />
<!-- 背景音乐控制组件 -->
<MusicControl />
</view>
</template>
<script>
import AudioControl from '@/components/AudioControl.vue';
import MusicControl from '@/components/MusicControl.vue';
export default {
components: {
AudioControl,
MusicControl
},
data() {
return {
audioUrl: this.showImg('/uploads/audio/your-audio.mp3')
}
}
}
</script>
```
## 优势特点
### ✅ **智能交互**
- 两个组件能够智能感知对方的状态
- 避免同时播放音频和背景音乐造成的冲突
- 提供良好的用户体验
### ✅ **解耦设计**
- 组件间通过事件通信,保持松耦合
- 每个组件都能独立工作
- 易于维护和扩展
### ✅ **状态同步**
- 实时同步音频和背景音乐的播放状态
- 确保状态的一致性和准确性
## 注意事项
1. **事件监听清理**:组件销毁时会自动清理事件监听,避免内存泄漏
2. **状态管理**:两个组件都维护各自的状态,通过事件保持同步
3. **错误处理**:包含完善的错误处理机制,确保功能稳定运行
Loading…
Cancel
Save