Browse Source

feat: 更新静态资源路径并优化页面样式

refactor: 统一图片资源命名规范
style: 优化书籍卡片和底部导航栏样式
fix: 修复音频控制组件加载逻辑
hfll
hflllll 2 weeks ago
parent
commit
0b036adddc
72 changed files with 1044 additions and 639 deletions
  1. +1
    -1
      manifest.json
  2. +11
    -11
      pages.json
  3. +1
    -1
      pages/index/desk.vue
  4. +61
    -31
      pages/index/home.vue
  5. +1
    -1
      pages/index/maintain.vue
  6. +14
    -13
      pages/index/member.vue
  7. +1
    -1
      pages/index/test.vue
  8. +15
    -16
      pages/index/user.vue
  9. +0
    -0
      static/alarm-icon.png
  10. +0
    -0
      static/contact-icon.png
  11. +0
    -0
      static/content-icon.png
  12. +0
    -0
      static/coupon-icon.png
  13. +0
    -0
      static/course-icon.png
  14. +0
    -0
      static/decorative-arrow.png
  15. +0
    -0
      static/default-avatar.png
  16. +0
    -0
      static/default-image.png
  17. +0
    -0
      static/desk-icon-active.png
  18. +0
    -0
      static/desk-icon.png
  19. +0
    -0
      static/edit-info-icon.png
  20. +0
    -0
      static/expired-stamp.png
  21. +0
    -0
      static/highlight-icon.png
  22. +0
    -0
      static/highlight-image.png
  23. +0
    -0
      static/home-background.png
  24. +0
    -0
      static/home-icon-active.png
  25. +0
    -0
      static/home-icon.png
  26. +0
    -0
      static/intro-icon.png
  27. +0
    -0
      static/knowledge-icon.png
  28. +0
    -0
      static/logout-icon.png
  29. +0
    -0
      static/member-1.png
  30. +0
    -0
      static/member-2.png
  31. +0
    -0
      static/member-3.png
  32. +0
    -0
      static/member-background.png
  33. +0
    -0
      static/member-diamond.png
  34. +0
    -0
      static/member-icon-active.png
  35. +0
    -0
      static/member-icon.png
  36. +0
    -0
      static/member-image-1.png
  37. +0
    -0
      static/member-image-2.png
  38. +0
    -0
      static/member-image-3.png
  39. +0
    -0
      static/miniprogram-title.png
  40. +0
    -0
      static/no-favorites.png
  41. +0
    -0
      static/no-search-results.png
  42. +0
    -0
      static/phone-icon.png
  43. +0
    -0
      static/play-icon-highlight.png
  44. +0
    -0
      static/play-icon.png
  45. +0
    -0
      static/privacy-icon.png
  46. +0
    -0
      static/profile-icon-active.png
  47. +0
    -0
      static/profile-icon.png
  48. +0
    -0
      static/promoter.png
  49. +0
    -0
      static/promotion-icon.png
  50. +0
    -0
      static/promotion-slogan.png
  51. +0
    -0
      static/qrcode-icon.png
  52. +0
    -0
      static/team-icon.png
  53. +0
    -0
      static/upload-avatar-placeholder.png
  54. +0
    -0
      static/used-stamp.png
  55. +0
    -0
      static/voice-switch-icon.png
  56. +0
    -0
      static/withdraw-icon.png
  57. +29
    -25
      subPages/home/AudioControls.vue
  58. +253
    -507
      subPages/home/book.vue
  59. +168
    -0
      subPages/home/components/CoursePopup.vue
  60. +229
    -0
      subPages/home/components/CustomTabbar.vue
  61. +225
    -0
      subPages/home/components/MeaningPopup.vue
  62. +2
    -2
      subPages/home/directory.vue
  63. +2
    -2
      subPages/home/plan.vue
  64. +1
    -1
      subPages/home/search.vue
  65. +1
    -1
      subPages/login/otherDemo.vue
  66. +1
    -1
      subPages/login/userInfo.vue
  67. +4
    -4
      subPages/member/recharge.vue
  68. +3
    -0
      subPages/user/cash.vue
  69. +1
    -1
      subPages/user/discount.vue
  70. +1
    -1
      subPages/user/profile.vue
  71. +6
    -6
      subPages/user/promote.vue
  72. +13
    -13
      subPages/user/team.vue

+ 1
- 1
manifest.json View File

@ -52,7 +52,7 @@
"h5" : {
"devServer" : {
"port" : 8002,
"disableHostCheck": true
"disableHostCheck" : true
}
},
/* */


+ 11
- 11
pages.json View File

@ -7,17 +7,17 @@
"navigationBarTitleText": "主页"
}
},
// #ifdef H5
{
"path": "pages/index/desk",
"style": {
// #ifdef H5
"navigationStyle": "custom",
// #endif
"navigationBarTitleText": "书桌",
"enablePullDownRefresh": true
}
},
// #endif
{
"path": "pages/index/member",
"style": {
@ -213,26 +213,26 @@
{
"pagePath": "pages/index/home",
"text": "主页",
"iconPath": "/static/主页图标.png",
"selectedIconPath": "/static/主页图标-点击.png"
"iconPath": "/static/home-icon.png",
"selectedIconPath": "/static/home-icon-active.png"
},
{
"pagePath": "pages/index/desk",
"text": "书桌",
"iconPath": "/static/书桌图标.png",
"selectedIconPath": "/static/书桌图标-点击.png"
"iconPath": "/static/desk-icon.png",
"selectedIconPath": "/static/desk-icon-active.png"
},
{
"pagePath": "pages/index/member",
"text": "会员",
"iconPath": "/static/会员图标.png",
"selectedIconPath": "/static/会员图标-点击.png"
"iconPath": "/static/member-icon.png",
"selectedIconPath": "/static/member-icon-active.png"
},
{
"pagePath": "pages/index/user",
"text": "我的",
"iconPath": "/static/我的图标.png",
"selectedIconPath": "/static/我的图标-点击.png"
"iconPath": "/static/profile-icon.png",
"selectedIconPath": "/static/profile-icon-active.png"
}
]
}


+ 1
- 1
pages/index/desk.vue View File

@ -43,7 +43,7 @@
<text class="book-grid-title">{{ book.book.booksName }}</text>
<view class="book-grid-meta">
<text class="book-grid-grade">{{ book.book.categoryName }}/</text>
<image src="/static/播放图标.png" class="book-grid-duration-icon" />
<image src="/static/play-icon.png" class="book-grid-duration-icon" />
<text class="book-grid-duration">{{ book.book.duration }}</text>
</view>
</view>


+ 61
- 31
pages/index/home.vue View File

@ -85,13 +85,13 @@
@click="goBook(item)"
>
<view class="item-cover">
<image :src="item.booksImg || '/static/默认图片.png'" mode="aspectFill"></image>
<image :src="item.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
</view>
<view class="item-info">
<text class="item-title">{{ item.booksName }}</text>
<text class="item-author">{{ item.booksAuthor }}</text>
<view class="item-duration">
<image src="/static/播放图标.png" class="item-icon" />
<image src="/static/play-icon.png" class="item-icon" />
<text>{{ item.duration }}</text>
</view>
</view>
@ -114,10 +114,10 @@
@click="goBook(book)"
>
<view class="book-cover">
<image :src="book.booksImg || '/static/默认图片.png'" mode="aspectFill"></image>
<image :src="book.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
<view class="book-overlay">
<view class="book-duration">
<image src="/static/闹钟图标.png" class="book-duration-icon" />
<view class="book-duration" v-if="book.duration">
<image src="/static/alarm-icon.png" class="book-duration-icon" />
<text class="book-duration-text">{{ book.duration }}</text>
</view>
<view class="book-title">{{ book.booksName }}</view>
@ -136,16 +136,16 @@
@click="goBook(book)"
>
<view class="book-grid-cover">
<image :src="book.booksImg || '/static/默认图片.png'" mode="aspectFill"></image>
<image :src="book.booksImg || '/static/default-image.png'" mode="aspectFill"></image>
</view>
<view class="book-grid-info">
<!-- <view class="book-grid-info">
<text class="book-grid-title">{{ book.booksName }}</text>
<view class="book-grid-meta">
<text class="book-grid-grade">{{ book.categoryName }}/</text>
<image src="/static/播放图标.png" class="book-grid-duration-icon" />
<image src="/static/play-icon.png" class="book-grid-duration-icon" />
<text class="book-grid-duration">{{ book.duration }}</text>
</view>
</view>
</view> -->
</view>
</view>
</view>
@ -523,17 +523,30 @@ export default {
.book-item {
flex-shrink: 0;
width: 270rpx;
// border-radius: 16rpx;
transition: transform 0.3s ease, box-shadow 0.3s ease;
&:active {
transform: scale(0.98);
}
.book-cover {
width: 100%;
height: 360rpx;
border-radius: 16rpx;
overflow: hidden;
position: relative;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
transition: box-shadow 0.3s ease, transform 0.3s ease;
&:active {
box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.25);
transform: translateY(-2rpx);
}
image {
width: 100%;
height: 100%;
transition: transform 0.3s ease;
}
@ -544,46 +557,63 @@ export default {
right: 0;
width: 100%;
height: 140rpx;
padding-top: 4rpx;
padding-right: 16rpx;
padding-bottom: 8rpx;
padding-left: 16rpx;
backdrop-filter: blur(5px);
padding: 20rpx 16rpx 12rpx;
box-sizing: border-box;
background: #00000066;
padding: 20rpx 16rpx 8rpx;
// gap: 26rpx;
/* 优化的渐变遮罩效果 */
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0) 0%,
rgba(0, 0, 0, 0.3) 30%,
rgba(0, 0, 0, 0.7) 70%,
rgba(0, 0, 0, 0.85) 100%
);
/* 增强的毛玻璃效果 */
backdrop-filter: blur(8px) saturate(1.2);
-webkit-backdrop-filter: blur(8px) saturate(1.2);
/* 添加微妙的边框 */
border-top: 1px solid rgba(255, 255, 255, 0.1);
/* 平滑过渡效果 */
transition: all 0.3s ease;
.book-duration{
display: flex;
gap: 8rpx;
align-items: center;
gap: 6rpx;
margin-bottom: 8rpx;
&-icon{
width: 24rpx;
height: 24rpx;
width: 22rpx;
height: 22rpx;
opacity: 0.9;
}
&-text{
font-size: 20rpx;
color: #DCDCDC;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
}
}
.book-title {
margin-top: 10rpx;
max-width: 220rpx;
font-size: 24rpx;
line-height: 1.4;
color: #fff;
// max-height: 68rpx; /* = line-height * 234rpx * 2 */
font-weight: 600;
line-height: 1.3;
color: #ffffff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
/* 文本截断优化 */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 关键:显示两行,超过两行才省略 */
-webkit-line-clamp: 2;
overflow: hidden;
word-break: break-word; /* 长单词也能断行 */
white-space: normal; /* 允许换行 */
word-break: break-word;
white-space: normal;
}
}
}


+ 1
- 1
pages/index/maintain.vue View File

@ -80,7 +80,7 @@
</view>
<!-- 空状态 -->
</view>
<uv-empty v-if="!list.length" icon="/static/暂无搜索结果.png" />
<uv-empty v-if="!list.length" icon="/static/no-search-results.png" />
<!-- 紧急程度选择器 -->
<uv-picker


+ 14
- 13
pages/index/member.vue View File

@ -4,7 +4,7 @@
<!-- #ifndef H5 -->
<view class="header-bg">
<image
src="/static/会员背景.png"
src="/static/member-background.png"
class="header-img"
mode="scaleToFill"
/>
@ -15,14 +15,14 @@
<view class="header-content">
<view class="zuanshi">
<image
src="/static/会员钻石.png"
src="/static/member-diamond.png"
mode="scaleToFill"
class="zuanshi-img"
/>
</view>
<view v-if="!isMember" class="noVip-container">
<image
src="/static/VIP.png"
src="/static/vip.png"
mode="aspectFit"
class="VIP-img"
/>
@ -82,9 +82,10 @@
<view class="benefits-list">
<!-- 碎片学习 系统掌握 -->
<view class="benefit-item" v-for="(item, index) in memberBenefits" :key="index">
<uv-parse :content="item"></uv-parse>
</view>
<!-- <view class="benefit-item" v-for="(item, index) in memberBenefits" :key="index"> -->
<!-- <view class="benefit-item"> -->
<uv-parse :content="memberBenefits[0]"></uv-parse>
<!-- </view> -->
<!-- 匹配水平 -->
<!-- <view class="benefit-item">
@ -93,7 +94,7 @@
<view class="benefit-desc">依据水平精准推课不做无用功快速提升</view>
</view>
<view class="benefit-icon">
<image src="/static/会员图片2.png" mode="aspectFit"></image>
<image src="/static/member-image-2.png" mode="aspectFit"></image>
</view>
</view> -->
@ -104,7 +105,7 @@
<view class="benefit-desc">精心设计科学的学习流程 测试-讲解-练习-检验知识掌握更牢固</view>
</view>
<view class="benefit-icon">
<image src="/static/会员图片3.png" mode="aspectFit"></image>
<image src="/static/member-image-3.png" mode="aspectFit"></image>
</view>
</view> -->
</view>
@ -137,8 +138,8 @@
<text class="plan-book-title" :class="{ 'highlight-title': index === 1 }">{{ book.book.booksName || '暂无课程' }}</text>
<view class="plan-book-meta" >
<text class="plan-book-grade" :class="{ 'highlight-title': index === 1 }">{{ book.book.categoryName || '--' }}/</text>
<image v-if="index !== 1" src="/static/播放图标.png" class="plan-book-duration-icon" />
<image v-else src="/static/播放图标高亮.png" class="plan-book-duration-icon" />
<image v-if="index !== 1" src="/static/play-icon.png" class="plan-book-duration-icon" />
<image v-else src="/static/play-icon-highlight.png" class="plan-book-duration-icon" />
<text class="plan-book-duration" :class="{ 'highlight-title': index === 1 }">{{ book.book.duration }}</text>
</view>
</view>
@ -170,7 +171,7 @@
<text class="recommend-grid-title">{{ book.booksName }}</text>
<view class="recommend-grid-meta">
<text class="recommend-grid-grade">{{ book.categoryName }}/</text>
<image src="/static/播放图标.png" class="recommend-grid-duration-icon" />
<image src="/static/play-icon.png" class="recommend-grid-duration-icon" />
<text class="recommend-grid-duration">{{ book.duration }}</text>
</view>
</view>
@ -190,7 +191,7 @@ export default{
memberBenefits: [],
userInfo: {
name: '战斗世界',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
//
@ -292,7 +293,7 @@ export default{
this.isLogin = false
this.userInfo = {
name: '登录后查看会员情况',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
}
}
}


+ 1
- 1
pages/index/test.vue View File

@ -62,7 +62,7 @@ export default {
},
{
name: 'img',
attrs: { src: '/static/默认图片.png' }
attrs: { src: '/static/default-image.png' }
}
],
],


+ 15
- 16
pages/index/user.vue View File

@ -24,7 +24,7 @@
<!-- 会员中心 -->
<view class="menu-item special-item" @click="goToMember">
<view class="menu-left">
<image src="/static/会员钻石.png" class="menu-icon"></image>
<image src="/static/member-diamond.png" class="menu-icon"></image>
<text class="menu-title">会员中心</text>
</view>
<view class="menu-right">
@ -47,7 +47,7 @@
<!-- 推广中心 -->
<view class="menu-item" @click="goToPromotion" v-if="userInfo.isPromote === 'Y'">
<view class="menu-left">
<image src="/static/推广图标.png" class="menu-icon"></image>
<image src="/static/promotion-icon.png" class="menu-icon"></image>
<text class="menu-title">推广中心</text>
</view>
<view class="menu-right">
@ -58,7 +58,7 @@
<!-- 我的优惠券 -->
<view class="menu-item" @click="goToCoupons">
<view class="menu-left">
<image src="/static/优惠卷图标.png" class="menu-icon"></image>
<image src="/static/coupon-icon.png" class="menu-icon"></image>
<text class="menu-title">我的优惠券</text>
</view>
<view class="menu-right">
@ -69,7 +69,7 @@
<!-- 产品介绍 -->
<view class="menu-item" @click="goToProduct">
<view class="menu-left">
<image src="/static/介绍图标.png" class="menu-icon"></image>
<image src="/static/intro-icon.png" class="menu-icon"></image>
<text class="menu-title">产品介绍</text>
</view>
<view class="menu-right">
@ -83,7 +83,7 @@
<!-- 联系我们 -->
<view class="menu-item" @click="goToContact">
<view class="menu-left">
<image src="/static/联系我们图标.png" class="menu-icon"></image>
<image src="/static/contact-icon.png" class="menu-icon"></image>
<text class="menu-title">联系我们</text>
</view>
<view class="menu-right">
@ -94,18 +94,17 @@
<!-- 服务协议与隐私政策 -->
<view class="menu-item" @click="goToPolicy">
<view class="menu-left">
<image src="/static/服务隐私图标.png" class="menu-icon"></image>
<text class="menu-title">服务协议与隐私政策</text>
<image src="/static/privacy-icon.png" class="menu-icon"></image>
<text class="menu-title">服务与隐私</text>
</view>
<view class="menu-right">
<uv-icon name="arrow-right" color="#999" size="16"></uv-icon>
<text class="menu-arrow">></text>
</view>
</view>
<!-- 修改信息 -->
<view class="menu-item" @click="goToBasicInfo">
<view class="menu-item" @click="goToProfile">
<view class="menu-left">
<image src="/static/修改信息图标.png" class="menu-icon"></image>
<image src="/static/edit-info-icon.png" class="menu-icon"></image>
<text class="menu-title">修改信息</text>
</view>
<view class="menu-right">
@ -115,7 +114,7 @@
<view class="menu-item" @click="logout" v-if="isLogin">
<view class="menu-left">
<image src="/static/退出登录图标.png" class="menu-icon"></image>
<image src="/static/logout-icon.png" class="menu-icon"></image>
<text class="menu-title">退出登录</text>
</view>
<view class="menu-right">
@ -130,7 +129,7 @@
<view class="title">联系我们</view>
<view class="contact-item" @click="callPhone">
<text class="contact-phone">{{ configParamContent('contact_us') }}</text>
<image src="/static/拨号图标.png" class="contact-icon"></image>
<image src="/static/phone-icon.png" class="contact-icon"></image>
</view>
</view>
</uv-popup>
@ -144,7 +143,7 @@ export default {
data() {
return {
userInfo: {
avatar: '/static/默认头像.png',
avatar: '/static/default-avatar.png',
name: '请先登录',
id: 'XXXXX'
},
@ -153,7 +152,7 @@ export default {
displayInfo: {
name: '点击登录',
phone: 'xxxxxxxxx',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
}
}
},
@ -278,7 +277,7 @@ export default {
//
this.userInfo = {
avatar: '/static/默认头像.png',
avatar: '/static/default-avatar.png',
name: '请先登录',
id: 'XXXXX'
}


static/闹钟图标.png → static/alarm-icon.png View File


static/联系我们图标.png → static/contact-icon.png View File


static/内容图标.png → static/content-icon.png View File


static/优惠卷图标.png → static/coupon-icon.png View File


static/课程图标.png → static/course-icon.png View File


static/修饰箭头.png → static/decorative-arrow.png View File


static/默认头像.png → static/default-avatar.png View File


static/默认图片.png → static/default-image.png View File


static/书桌图标-点击.png → static/desk-icon-active.png View File


static/书桌图标.png → static/desk-icon.png View File


static/修改信息图标.png → static/edit-info-icon.png View File


static/已过期盖章.png → static/expired-stamp.png View File


static/划重点图标.png → static/highlight-icon.png View File


static/划重点图片.png → static/highlight-image.png View File


static/首页背景图.png → static/home-background.png View File


static/主页图标-点击.png → static/home-icon-active.png View File


static/主页图标.png → static/home-icon.png View File


static/介绍图标.png → static/intro-icon.png View File


static/知识收获图标.png → static/knowledge-icon.png View File


static/退出登录图标.png → static/logout-icon.png View File


static/会员1.png → static/member-1.png View File


static/会员2.png → static/member-2.png View File


static/会员3.png → static/member-3.png View File


static/会员背景.png → static/member-background.png View File


static/会员钻石.png → static/member-diamond.png View File


static/会员图标-点击.png → static/member-icon-active.png View File


static/会员图标.png → static/member-icon.png View File


static/会员图片1.png → static/member-image-1.png View File


static/会员图片2.png → static/member-image-2.png View File


static/会员图片3.png → static/member-image-3.png View File


static/小程序标题.png → static/miniprogram-title.png View File


static/暂无收藏.png → static/no-favorites.png View File


static/暂无搜索结果.png → static/no-search-results.png View File


static/拨号图标.png → static/phone-icon.png View File


static/播放图标高亮.png → static/play-icon-highlight.png View File


static/播放图标.png → static/play-icon.png View File


static/服务隐私图标.png → static/privacy-icon.png View File


static/我的图标-点击.png → static/profile-icon-active.png View File


static/我的图标.png → static/profile-icon.png View File


static/推广官.png → static/promoter.png View File


static/推广图标.png → static/promotion-icon.png View File


static/推广标语.png → static/promotion-slogan.png View File


static/二维码图标.png → static/qrcode-icon.png View File


static/团队图标.png → static/team-icon.png View File


static/待上传头像.png → static/upload-avatar-placeholder.png View File


static/已使用盖章.png → static/used-stamp.png View File


static/音色切换图标.png → static/voice-switch-icon.png View File


static/提现图标.png → static/withdraw-icon.png View File


+ 29
- 25
subPages/home/AudioControls.vue View File

@ -95,6 +95,10 @@ export default {
type: Boolean,
default: false
},
shouldLoadAudio: {
type: Boolean,
default: false
},
isMember: {
type: Boolean,
default: false
@ -250,8 +254,8 @@ export default {
methods: {
//
checkAndLoadPreloadedAudio() {
//
if (!this.isTextPage) {
//
if (!this.shouldLoadAudio) {
return;
}
@ -452,9 +456,9 @@ export default {
return;
}
//
if (!this.isTextPage) {
console.log('当前为卡片页面不加载音频');
//
if (!this.shouldLoadAudio) {
console.log('当前页面不需要加载音频');
//
this.currentPageAudios = [];
this.hasAudioData = false;
@ -662,7 +666,7 @@ export default {
//
this.hasAudioData = this.currentPageAudios.length > 0;
this.audioLoadFailed = !this.hasAudioData && this.isTextPage; //
this.audioLoadFailed = !this.hasAudioData && this.shouldLoadAudio; //
//
this.$emit('audio-state-change', {
@ -702,9 +706,9 @@ export default {
retryGetAudio() {
console.log('用户点击重新获取音频');
//
if (!this.isTextPage) {
console.log('卡片页面不支持音频功能');
//
if (!this.shouldLoadAudio) {
console.log('当前页面不支持音频功能');
return;
}
@ -748,9 +752,9 @@ export default {
this.currentHighlightIndex = -1;
this.playSpeed = 1.0;
//
if (!this.isTextPage) {
console.log('卡片页面:重置音频状态');
//
if (!this.shouldLoadAudio) {
console.log('不需要音频的页面:重置音频状态');
this.currentPageAudios = [];
this.totalTime = 0;
this.hasAudioData = false;
@ -857,10 +861,10 @@ export default {
return;
}
//
if (!this.isTextPage) {
//
if (!this.shouldLoadAudio) {
uni.showToast({
title: '当前页面没有文本内容',
title: '当前页面不支持音频播放',
icon: 'none'
});
return;
@ -1652,9 +1656,9 @@ export default {
try {
console.log('開始自動加載第一頁音頻');
//
if (this.currentPage === 1 && this.isTextPage) {
console.log('當前是第一頁文字頁面,開始加載音頻');
//
if (this.currentPage === 1 && this.shouldLoadAudio) {
console.log('當前是第一頁且需要音頻,開始加載音頻');
//
await this.getCurrentPageAudio();
@ -1746,9 +1750,9 @@ export default {
async autoLoadAndPlayAudio() {
console.log('开始自动加载音频');
//
if (!this.isTextPage) {
console.log('当前不是文本页面,跳过音频加载');
//
if (!this.shouldLoadAudio) {
console.log('当前页面不需要加载音频,跳过音频加载');
return;
}
@ -1829,15 +1833,15 @@ export default {
currentHighlightIndex: -1
});
// 8.
if (this.isTextPage && this.courseId && this.currentPage) {
// 8.
if (this.shouldLoadAudio && this.courseId && this.currentPage) {
console.log('🎵 AudioControls: 优先获取当前页面音频,使用新音色:', newVoiceId);
await this.getCurrentPageAudio();
console.log('🎵 AudioControls: 当前页面音频获取完成');
} else {
//
//
this.isAudioLoading = false;
console.log('🎵 AudioControls: 非文本页面,直接清除加载状态');
console.log('🎵 AudioControls: 不需要音频的页面,直接清除加载状态');
}
// 9.


+ 253
- 507
subPages/home/book.vue View File

@ -52,12 +52,18 @@
v-for="(token, tokenIndex) in splitEnglishSentence(item.content)"
:key="tokenIndex"
:class="['english-token', { 'clickable-word': token.isWord && findWordDefinition(token.text) }]"
@tap="token.isWord && findWordDefinition(token.text) ? handleWordClick(token.text) : null"
@click.stop="token.isWord && findWordDefinition(token.text) ? handleWordClick(token.text) : null"
user-select
>{{ token.text }}</text>
</view>
<view v-else-if="item && item.type === 'text' && item.language === 'zh' && item.content" @click.stop="handleTextClick(item.content, item, index)">
<text class="chinese-text clickable-text" user-select>{{ item.content }}</text>
<text
v-for="(segment, segmentIndex) in processChineseText(item.content)"
:key="segmentIndex"
:class="['chinese-segment', { 'clickable-keyword': segment.isKeyword }]"
@click.stop="segment.isKeyword ? handleChineseKeywordClick(segment.keywordData) : null"
user-select
>{{ segment.text }}</text>
</view>
</view>
</view>
@ -68,11 +74,14 @@
<view v-if="item && item.type === 'text' && item.content" class="text-content" >
<view :class="{ 'text-highlight': isTextHighlighted(page, itemIndex) }" @click.stop="handleTextClick(item.content, item, index)">
<text
v-if="!item.isLead"
class="content-text clickable-text"
:style="item.style"
user-select
>
{{ item.content }}
</text>
<text v-else-if="item.isLead" class="content-text clickable-text lead-text" :style="item.style" user-select>{{ item.content }}</text>
</view>
</view>
@ -97,7 +106,6 @@
:poster="item.coverUrl"
@loadstart="onVideoLoadStart"
@loadeddata="onVideoLoadStart"
@error="onVideoError"
></video>
</view>
@ -109,155 +117,64 @@
</swiper>
<!-- 自定义底部控制栏 -->
<view class="custom-tabbar" :class="{ 'tabbar-hidden': !showNavbar }">
<!-- 音频控制栏组件 -->
<AudioControls
:current-page="currentPage"
:course-id="courseId"
:voice-id="voiceId"
:book-pages="bookPages"
:is-text-page="isTextPage"
:is-member="isMember"
:current-page-requires-member="currentPageRequiresMember"
:page-pay="pagePay"
@previous-page="previousPage"
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
ref="audioControls"
/>
<view style="background-color: #fff;position: relative;z-index: 100" >
<view class="tabbar-content">
<view class="tabbar-left">
<view class="tab-button" @click="toggleCoursePopup">
<image src="/static/课程图标.png" class="tab-icon" />
<text class="tab-text">课程</text>
</view>
<view class="tab-button" @click="toggleSound">
<image src="/static/音色切换图标.png" class="tab-icon" />
<text class="tab-text">音色切换</text>
</view>
</view>
<view class="tabbar-right">
<view class="page-controls">
<view class="page-numbers">
<view
v-for="(page, index) in bookPages"
:key="index"
class="page-number"
:class="{ 'active': (index + 1) === currentPage }"
@click="goToPage(index + 1)"
>
{{ index + 1 }}
</view>
</view>
</view>
</view>
</view>
<uv-safe-bottom></uv-safe-bottom>
</view>
</view>
<CustomTabbar
:show-navbar="showNavbar"
:current-page="currentPage"
:course-id="courseId"
:voice-id="voiceId"
:book-pages="bookPages"
:is-text-page="isTextPage"
:should-load-audio="shouldLoadAudio"
:is-member="isMember"
:current-page-requires-member="currentPageRequiresMember"
:page-pay="pagePay"
@toggle-course-popup="toggleCoursePopup"
@toggle-sound="toggleSound"
@go-to-page="goToPage"
@previous-page="previousPage"
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
ref="customTabbar"
/>
<!-- 课程选择弹出窗 -->
<uv-popup
mode="bottom"
<CoursePopup
:style="{zIndex: 10000}"
:course-list="courseList"
:current-course="currentCourse"
:is-reversed="isReversed"
@toggle-sort="toggleSort"
@select-course="selectCourse"
ref="coursePopup"
round="32rpx"
bg-color="#f8f8f8"
>
<view class="course-popup">
<view class="popup-header">
<view>
<uv-icon name="arrow-down" color="black" size="20"></uv-icon>
</view>
<view class="popup-title">课程</view>
<view class="popup-title" @click="toggleSort">
倒序
</view>
</view>
<view class="course-list">
<view
v-for="(course, index) in displayCourseList"
:key="course.id"
class="course-item"
:class="{ 'active': course.id === currentCourse }"
@click="selectCourse(course.id)"
>
<view class="course-number " :class="{ 'highlight': course.id === currentCourse }">{{ String(course.index).padStart(2, '0') }}</view>
<view class="course-content">
<view class="course-english" :class="{ 'highlight': course.id === currentCourse }">{{ course.english }}</view>
<view class="course-chinese" :class="{ 'highlight': course.id === currentCourse }">{{ course.chinese }}</view>
</view>
</view>
</view>
</view>
</uv-popup>
/>
<!-- 释义弹出窗 -->
<uv-popup
mode="bottom"
<MeaningPopup
:style="{zIndex: 10000}"
:current-word-meaning="currentWordMeaning"
@close-meaning-popup="closeMeaningPopup"
@repeat-word-audio="repeatWordAudio"
ref="meaningPopup"
round="32rpx"
bg-color="#FFFFFF"
:overlay="true"
>
<view class="meaning-popup" v-if="currentWordMeaning">
<view class="meaning-header">
<view class="close-btn" @click="closeMeaningPopup">
<text class="close-text">关闭</text>
</view>
<view class="meaning-title">释义</view>
<view style="width: 80rpx;"></view>
</view>
<view class="meaning-content">
<image v-if="currentWordMeaning.imag" class="meaning-image" :src="currentWordMeaning.image" mode="aspectFill"></image>
<view class="word-info">
<view class="word-main">
<text class="word-text">{{ currentWordMeaning.word }}</text>
</view>
<view class="phonetic-container">
<uv-icon
name="volume-fill"
size="16"
color="#007AFF"
class="speaker-icon"
@click="repeatWordAudio"
></uv-icon>
<text class="phonetic-text">{{ currentWordMeaning.phonetic }}</text>
</view>
<view class="word-meaning">
<text class="part-of-speech">{{ currentWordMeaning.partOfSpeech }}</text>
<text class="meaning-text">{{ currentWordMeaning.meaning }}</text>
</view>
</view>
<view class="knowledge-gain">
<view class="knowledge-header">
<image src="/static/知识收获图标.png" class="knowledge-icon" mode="aspectFill" />
<text class="knowledge-title">知识收获</text>
</view>
<text class="knowledge-content">{{ currentWordMeaning.knowledgeGain }}</text>
</view>
</view>
</view>
</uv-popup>
/>
</view>
</template>
<script>
import AudioControls from './AudioControls.vue'
import CustomTabbar from './components/CustomTabbar.vue'
import CoursePopup from './components/CoursePopup.vue'
import MeaningPopup from './components/MeaningPopup.vue'
export default {
components: {
AudioControls
AudioControls,
CustomTabbar,
CoursePopup,
MeaningPopup
},
data() {
return {
@ -318,6 +235,21 @@ export default {
return currentPageData && currentPageData.some(item => item.type === 'text');
},
//
shouldLoadAudio() {
//
if (this.isTextPage) {
return true;
}
// type'1'
if (this.currentPageType === '1') {
return true;
}
return false;
},
//
currentPageTitle() {
@ -429,15 +361,15 @@ export default {
//
handleTextClick(textContent, item, pageIndex) {
console.log('点击文本:', textContent);
console.log('textContent类型:', typeof textContent);
console.log('textContent是否为undefined:', textContent === undefined);
console.log('完整item对象:', item);
console.log('item.content:', item ? item.content : 'item为空');
console.log('当前页面索引:', this.currentPage);
console.log('点击的页面索引:', pageIndex);
console.log('当前页面数据:', this.bookPages[this.currentPage - 1]);
console.log('页面数据长度:', this.bookPages[this.currentPage - 1] ? this.bookPages[this.currentPage - 1].length : '页面不存在');
// console.log(':', textContent);
// console.log('textContent:', typeof textContent);
// console.log('textContentundefined:', textContent === undefined);
// console.log('item:', item);
// console.log('item.content:', item ? item.content : 'item');
// console.log(':', this.currentPage);
// console.log(':', pageIndex);
// console.log(':', this.bookPages[this.currentPage - 1]);
// console.log(':', this.bookPages[this.currentPage - 1] ? this.bookPages[this.currentPage - 1].length : '');
//
if (pageIndex !== undefined && pageIndex !== this.currentPage - 1) {
@ -472,7 +404,7 @@ export default {
}
//
if (!this.$refs.audioControls) {
if (!this.$refs.customTabbar || !this.$refs.customTabbar.$refs.audioControls) {
console.log('音频控制组件未找到');
uni.showToast({
title: '音频控制组件未准备好',
@ -481,16 +413,10 @@ export default {
return;
}
//
// type'1'
if (!this.isTextPage) {
//
if (this.currentPageType === '1') {
console.log('卡片页面的文本点击,单词播放由handleWordClick处理');
return;
}
console.log('当前页面不是文本页面');
//
// type'1'
if (!this.isTextPage && this.currentPageType !== '1') {
console.log('当前页面不是文本页面或卡片页面');
uni.showToast({
title: '当前页面不支持音频播放',
icon: 'none'
@ -499,7 +425,7 @@ export default {
}
// AudioControls
const success = this.$refs.audioControls.playSpecificAudio(textContent);
const success = this.$refs.customTabbar.$refs.audioControls.playSpecificAudio(textContent);
if (success) {
console.log('成功播放指定音频段落');
@ -562,9 +488,6 @@ export default {
},
selectCourse(courseId) {
this.currentCourse = courseId
if (this.$refs.coursePopup) {
this.$refs.coursePopup.close()
}
//
// console.log(':', courseId)
this.getCourseList(courseId)
@ -575,9 +498,6 @@ export default {
}
},
closeMeaningPopup() {
if (this.$refs.meaningPopup) {
this.$refs.meaningPopup.close()
}
this.currentWordMeaning = null
},
@ -602,6 +522,82 @@ export default {
);
},
//
processChineseText(text) {
const currentPageWords = this.pageWords[this.currentPage - 1] || [];
if (!text || currentPageWords.length === 0) {
return [{ text: text, isKeyword: false, keywordData: null }];
}
//
const segments = [];
let currentIndex = 0;
//
const sortedWords = [...currentPageWords].sort((a, b) => b.word.length - a.word.length);
while (currentIndex < text.length) {
let matched = false;
//
for (const wordData of sortedWords) {
const keyword = wordData.word;
if (text.substr(currentIndex, keyword.length) === keyword) {
//
segments.push({
text: keyword,
isKeyword: true,
keywordData: wordData
});
currentIndex += keyword.length;
matched = true;
break;
}
}
if (!matched) {
//
segments.push({
text: text[currentIndex],
isKeyword: false,
keywordData: null
});
currentIndex++;
}
}
//
const mergedSegments = [];
let currentSegment = null;
for (const segment of segments) {
if (segment.isKeyword) {
//
if (currentSegment) {
mergedSegments.push(currentSegment);
currentSegment = null;
}
//
mergedSegments.push(segment);
} else {
//
if (currentSegment) {
currentSegment.text += segment.text;
} else {
currentSegment = { ...segment };
}
}
}
//
if (currentSegment) {
mergedSegments.push(currentSegment);
}
return mergedSegments;
},
async playWordAudio(word) {
try {
console.log('開始播放單詞語音:', word);
@ -678,8 +674,10 @@ export default {
//
repeatWordAudio() {
if (this.currentWordMeaning && this.currentWordMeaning.word) {
console.log('重複播放單詞語音:', this.currentWordMeaning.word);
this.playWordAudio(this.currentWordMeaning.word);
//
const combinedText = `${this.currentWordMeaning.word}${this.currentWordMeaning.meaning || ''}`;
console.log('重複播放合併文本:', combinedText);
this.playWordAudio(combinedText);
} else {
console.warn('沒有當前單詞可以播放');
}
@ -690,11 +688,6 @@ export default {
const definition = this.findWordDefinition(word);
console.log('查找单词:', word, '释义:', definition);
//
if (word) {
this.playWordAudio(word);
}
if (definition) {
this.currentWordMeaning = {
word: definition.word,
@ -703,9 +696,43 @@ export default {
meaning: definition.paraphrase || '',
knowledgeGain: definition.knowledge || ''
};
//
const combinedText = `${word}${definition.paraphrase || ''}`;
console.log('播放合并文本:', combinedText);
this.playWordAudio(combinedText);
this.showWordMeaning();
} else {
console.log('未找到单词释义:', word);
//
if (word) {
this.playWordAudio(word);
}
}
},
//
handleChineseKeywordClick(keywordData) {
console.log('点击中文重点词汇:', keywordData.word, '释义:', keywordData);
if (keywordData) {
this.currentWordMeaning = {
word: keywordData.word,
phonetic: keywordData.soundmark || '',
partOfSpeech: '', //
meaning: keywordData.paraphrase || '',
knowledgeGain: keywordData.knowledge || ''
};
//
const combinedText = `${keywordData.word}${keywordData.paraphrase || ''}`;
console.log('播放中文合并文本:', combinedText);
this.playWordAudio(combinedText);
this.showWordMeaning();
} else {
console.log('未找到中文词汇释义');
}
},
@ -870,6 +897,8 @@ export default {
createAudioInstance() {
//
if (this.currentAudio) {
console.log('销毁原来的音频');
this.currentAudio.destroy();
}
@ -1397,7 +1426,7 @@ export default {
}
// AudioControls
if (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading) {
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls && this.$refs.customTabbar.$refs.audioControls.isAudioLoading) {
uni.showToast({
title: '音频加载中,请稍后再试',
icon: 'none',
@ -1439,8 +1468,8 @@ export default {
})
if (res.code === 200) {
//
if (this.$refs.audioControls) {
this.$refs.audioControls.resetForCourseChange();
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
this.$refs.customTabbar.$refs.audioControls.resetForCourseChange();
}
//
@ -1475,11 +1504,11 @@ export default {
// 使$nextTickDOM
this.$nextTick(async () => {
if (this.$refs.audioControls) {
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
console.log('开始自动加载课程音频');
try {
// getCurrentPageAudio
await this.$refs.audioControls.getCurrentPageAudio();
await this.$refs.customTabbar.$refs.audioControls.getCurrentPageAudio();
console.log('课程切换后音频加载完成');
} catch (error) {
console.error('课程切换后音频加载失败:', error);
@ -1591,8 +1620,8 @@ export default {
// 1.5AudioControls
setTimeout(() => {
if (this.$refs.audioControls) {
this.$refs.audioControls.startPreloadAudio();
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
this.$refs.customTabbar.$refs.audioControls.startPreloadAudio();
}
}, 1500);
@ -1634,9 +1663,9 @@ export default {
try {
console.log('開始自動加載第一頁音頻');
//
if (this.currentPage === 1 && this.isTextPage) {
console.log('當前是第一頁文字頁面,開始加載音頻');
//
if (this.currentPage === 1 && this.shouldLoadAudio) {
console.log('當前是第一頁且需要音頻,開始加載音頻');
//
await this.getCurrentPageAudio();
@ -1665,7 +1694,7 @@ export default {
}
//
if (this.isAudioLoading || (this.$refs.audioControls && this.$refs.audioControls.isAudioLoading)) {
if (this.isAudioLoading || (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls && this.$refs.customTabbar.$refs.audioControls.isAudioLoading)) {
console.log('音频正在加载中,阻止音色切换');
uni.showToast({
title: '音频加载中,请稍后再试',
@ -1686,11 +1715,11 @@ export default {
}
// AudioControls
if (this.$refs.audioControls) {
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
try {
console.log('开始处理音色切换,清理所有音频缓存并重新获取所有页面音频');
// preloadAllPages: true
await this.$refs.audioControls.handleVoiceChange(voiceId, {
await this.$refs.customTabbar.$refs.audioControls.handleVoiceChange(voiceId, {
preloadAllPages: true
});
console.log('音色切换处理完成');
@ -1708,8 +1737,8 @@ export default {
// AudioControls
this.$nextTick(() => {
if (this.$refs.audioControls) {
this.$refs.audioControls.autoLoadAndPlayFirstPage();
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
this.$refs.customTabbar.$refs.audioControls.autoLoadAndPlayFirstPage();
}
});
@ -1726,16 +1755,16 @@ export default {
this.clearWordAudioCache();
// AudioControls
if (this.$refs.audioControls) {
this.$refs.audioControls.destroyAudio();
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
this.$refs.customTabbar.$refs.audioControls.destroyAudio();
}
},
//
onHide() {
// AudioControls
if (this.$refs.audioControls) {
this.$refs.audioControls.pauseOnHide();
if (this.$refs.customTabbar && this.$refs.customTabbar.$refs.audioControls) {
this.$refs.customTabbar.$refs.audioControls.pauseOnHide();
}
}
}
@ -1855,11 +1884,12 @@ export default {
display: flex;
flex-direction: column;
gap: 32rpx;
height: 1172rpx;
min-height: 1172rpx;
margin-top: 20rpx;
border-radius: 32rpx;
// height: 100%;
padding: 40rpx;
padding-bottom: 100rpx;
// margin: 0
border: 1px solid #FFFFFF;
box-sizing: border-box;
@ -1926,6 +1956,27 @@ export default {
background-color: rgba(0, 122, 255, 0.1);
border-radius: 4rpx;
}
.chinese-segment {
font-family: PingFang SC;
font-weight: 400;
font-size: 28rpx;
line-height: 48rpx;
color: #3B3D3D;
}
.clickable-keyword {
background: $primary-color;
text-decoration: underline;
cursor: pointer;
transition: all 0.2s ease;
border-radius: 4rpx;
padding: 2rpx 4rpx;
}
.clickable-keyword:hover {
background-color: rgba(0, 122, 255, 0.1);
}
.chinese-text {
display: block;
@ -2027,331 +2078,26 @@ export default {
border-radius: 4rpx;
}
}
.text-highlight {
background-color: $primary-color;
// color: #fff;
// padding: 8rpx 16rpx;
// border-radius: 8rpx;
// box-shadow: 0 2rpx 8rpx rgba(6, 218, 220, 0.3);
}
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
// background-color: #fff;
border-top: 1rpx solid #EEEEEE;
z-index: 1000;
transition: transform 0.3s ease;
&.tabbar-hidden {
transform: translateY(100%);
}
.lead-text {
background: #fffbe6; /* 柔和的提示背景 */
border: 1px solid #ffe58f;
border-radius: 8px;
}
.tabbar-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 62rpx;
// z-index: 100;
// position: relative;
// background-color: #fff;
// padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
height: 88rpx;
}
.tabbar-left {
display: flex;
align-items: center;
gap: 35rpx;
}
.tab-button {
display: flex;
align-items: center;
flex-direction: column;
gap: 8rpx;
}
.tab-icon {
width: 52rpx;
height: 52rpx;
}
.tab-text {
font-family: PingFang SC;
// font-weight: 400;
font-size: 22rpx;
color: #999;
line-height: 24rpx;
}
.tabbar-right {
flex: 1;
display: flex;
justify-content: flex-end;
}
.page-controls {
display: flex;
align-items: center;
}
.page-numbers {
display: flex;
align-items: center;
gap: 8rpx;
overflow-x: auto;
max-width: 400rpx;
&::-webkit-scrollbar {
display: none;
}
}
.page-number {
min-width: 84rpx;
height: 58rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100rpx;
font-family: PingFang SC;
// font-weight: 400;
font-size: 30rpx;
color: #3B3D3D;
background-color: transparent;
border: 1px solid #3B3D3D;
.text-highlight {
background-color: rgba(255, 248, 220, 0.8); /* 温暖的米黄色,对眼睛友好 */
border-left: 4rpx solid #ffd700; /* 左侧金色边框作为朗读指示 */
padding: 4rpx 8rpx;
border-radius: 6rpx;
transition: all 0.3s ease;
&.active {
border: 1px solid $primary-color;
color: $primary-color;
}
}
/* 课程弹出窗样式 */
.course-popup {
padding: 0 32rpx;
max-height: 80vh;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 2rpx solid #EEEEEE
// margin-bottom: 40rpx;
}
.popup-title {
font-family: PingFang SC;
font-weight: 500;
font-size: 34rpx;
color: #181818;
}
.course-list {
max-height: 60vh;
overflow-y: auto;
}
.course-item {
display: flex;
align-items: center;
gap: 24rpx;
padding-top: 24rpx;
padding-right: 8rpx;
padding-bottom: 24rpx;
padding-left: 8rpx;
border-bottom: 1px solid #EEEEEE;
cursor: pointer;
&:last-child {
border-bottom: none;
}
}
.course-number {
width: 80rpx;
font-family: PingFang SC;
// font-weight: 400;
font-size: 36rpx;
color: #999;
&.highlight {
color: $primary-color;
}
// margin-right: 24rpx;
}
.course-content {
flex: 1;
}
.course-english {
font-family: PingFang SC;
font-weight: 600;
font-size: 36rpx;
line-height: 44rpx;
color: #252545;
margin-bottom: 8rpx;
&.highlight {
color: $primary-color;
}
}
.course-chinese {
font-size: 28rpx;
line-height: 48rpx;
color: #3B3D3D;
&.highlight {
color: $primary-color;
}
}
/* 释义弹窗样式 */
.meaning-popup {
// width: 670rpx;
background-color: #FFFFFF;
// border-radius: 32rpx;
overflow: hidden;
}
.meaning-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #EEEEEE;
}
.close-btn {
width: 80rpx;
}
.close-text {
font-family: PingFang SC;
font-size: 32rpx;
color: #8b8b8b;
}
.meaning-title {
font-family: PingFang SC;
font-weight: 500;
font-size: 34rpx;
color: #181818;
box-shadow: 0 2rpx 6rpx rgba(255, 215, 0, 0.15); /* 柔和的阴影 */
}
.meaning-content {
padding: 32rpx;
}
.meaning-image {
width: 670rpx;
height: 268rpx;
border-radius: 24rpx;
margin-bottom: 32rpx;
}
.word-info {
margin-bottom: 32rpx;
}
.word-main {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
}
.word-text {
font-family: PingFang SC;
font-weight: 500;
font-size: 40rpx;
color: #181818;
}
.phonetic-container{
display: flex;
gap: 24rpx;
align-items: center;
.speaker-icon {
cursor: pointer;
transition: opacity 0.2s ease;
padding: 8rpx;
border-radius: 8rpx;
&:hover {
opacity: 0.7;
background-color: rgba(0, 122, 255, 0.1);
}
}
.phonetic-text {
font-family: PingFang SC;
font-size: 28rpx;
color: #262626;
margin-bottom: 16rpx;
}
}
.word-meaning {
display: flex;
gap: 16rpx;
}
.part-of-speech {
font-family: PingFang SC;
font-size: 28rpx;
color: #262626;
}
.meaning-text {
font-family: PingFang SC;
font-size: 28rpx;
color: #181818;
flex: 1;
}
.knowledge-gain {
background: linear-gradient(180deg, #DEFFFF 0%, #FBFEFF 22.65%, #F0FBFF 100%);
border-radius: 24rpx;
padding: 32rpx 40rpx;
}
.knowledge-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
}
.knowledge-icon {
width: 48rpx;
height: 48rpx;
}
.knowledge-title {
font-family: PingFang SC;
font-weight: 600;
font-size: 30rpx;
color: #3B3D3D;
}
.knowledge-content {
font-family: PingFang SC;
font-size: 28rpx;
color: #4f4f4f;
line-height: 48rpx;
}
</style>

+ 168
- 0
subPages/home/components/CoursePopup.vue View File

@ -0,0 +1,168 @@
<template>
<uv-popup
mode="bottom"
ref="coursePopup"
round="32rpx"
bg-color="#f8f8f8"
safeAreaInsetBottom
>
<view class="course-popup">
<view class="popup-header">
<view>
<uv-icon name="arrow-down" color="black" size="20"></uv-icon>
</view>
<view class="popup-title">课程</view>
<view class="popup-title" @click="toggleSort">
倒序
</view>
</view>
<view class="course-list">
<view
v-for="course in displayCourseList"
:key="course.id"
class="course-item"
:class="{ 'active': course.id === currentCourse }"
@click="selectCourse(course.id)"
>
<view class="course-number" :class="{ 'highlight': course.id === currentCourse }">
{{ String(course.index).padStart(2, '0') }}
</view>
<view class="course-content">
<view class="course-english" :class="{ 'highlight': course.id === currentCourse }">
{{ course.english }}
</view>
<view class="course-chinese" :class="{ 'highlight': course.id === currentCourse }">
{{ course.chinese }}
</view>
</view>
</view>
</view>
</view>
</uv-popup>
</template>
<script>
export default {
name: 'CoursePopup',
props: {
courseList: {
type: Array,
default: () => []
},
currentCourse: {
type: [String, Number],
default: 1
},
isReversed: {
type: Boolean,
default: false
}
},
computed: {
displayCourseList() {
return this.isReversed ? [...this.courseList].reverse() : this.courseList;
}
},
methods: {
open() {
if (this.$refs.coursePopup) {
this.$refs.coursePopup.open()
}
},
close() {
if (this.$refs.coursePopup) {
this.$refs.coursePopup.close()
}
},
toggleSort() {
this.$emit('toggle-sort')
},
selectCourse(courseId) {
this.$emit('select-course', courseId)
this.close()
}
}
}
</script>
<style lang="scss" scoped>
/* 课程弹出窗样式 */
.course-popup {
padding: 0 32rpx;
max-height: 80vh;
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
border-bottom: 2rpx solid #EEEEEE;
}
.popup-title {
font-family: PingFang SC;
font-weight: 500;
font-size: 34rpx;
color: #181818;
}
.course-list {
max-height: 60vh;
overflow-y: auto;
}
.course-item {
display: flex;
align-items: center;
gap: 24rpx;
padding-top: 24rpx;
padding-right: 8rpx;
padding-bottom: 24rpx;
padding-left: 8rpx;
border-bottom: 1px solid #EEEEEE;
cursor: pointer;
&:last-child {
border-bottom: none;
}
}
.course-number {
width: 80rpx;
font-family: PingFang SC;
font-size: 36rpx;
color: #999;
&.highlight {
color: #06DADC;
}
}
.course-content {
flex: 1;
}
.course-english {
font-family: PingFang SC;
font-weight: 600;
font-size: 36rpx;
line-height: 44rpx;
color: #252545;
margin-bottom: 8rpx;
&.highlight {
color: #06DADC;
}
}
.course-chinese {
font-size: 28rpx;
line-height: 48rpx;
color: #3B3D3D;
&.highlight {
color: #06DADC;
}
}
</style>

+ 229
- 0
subPages/home/components/CustomTabbar.vue View File

@ -0,0 +1,229 @@
<template>
<view class="custom-tabbar" :class="{ 'tabbar-hidden': !showNavbar }">
<!-- 音频控制栏组件 -->
<AudioControls
:current-page="currentPage"
:course-id="courseId"
:voice-id="voiceId"
:book-pages="bookPages"
:is-text-page="isTextPage"
:should-load-audio="shouldLoadAudio"
:is-member="isMember"
:current-page-requires-member="currentPageRequiresMember"
:page-pay="pagePay"
@previous-page="previousPage"
@next-page="nextPage"
@audio-state-change="onAudioStateChange"
@highlight-change="onHighlightChange"
@voice-change-complete="onVoiceChangeComplete"
@voice-change-error="onVoiceChangeError"
ref="audioControls"
/>
<view style="background-color: #fff;position: relative;z-index: 100">
<view class="tabbar-content">
<view class="tabbar-left">
<view class="tab-button" @click="toggleCoursePopup">
<image src="/static/course-icon.png" class="tab-icon" />
<text class="tab-text">课程</text>
</view>
<view class="tab-button" @click="toggleSound">
<image src="/static/voice-switch-icon.png" class="tab-icon" />
<text class="tab-text">音色切换</text>
</view>
</view>
<view class="tabbar-right">
<view class="page-controls">
<view class="page-numbers">
<view
v-for="(page, index) in bookPages"
:key="index"
class="page-number"
:class="{ 'active': (index + 1) === currentPage }"
@click="goToPage(index + 1)"
>
{{ index + 1 }}
</view>
</view>
</view>
</view>
</view>
<uv-safe-bottom></uv-safe-bottom>
</view>
</view>
</template>
<script>
import AudioControls from '../AudioControls.vue'
export default {
name: 'CustomTabbar',
components: {
AudioControls
},
props: {
showNavbar: {
type: Boolean,
default: true
},
currentPage: {
type: Number,
default: 1
},
courseId: {
type: String,
default: ''
},
voiceId: {
type: Number,
default: null
},
bookPages: {
type: Array,
default: () => []
},
isTextPage: {
type: Boolean,
default: false
},
shouldLoadAudio: {
type: Boolean,
default: false
},
isMember: {
type: Boolean,
default: false
},
currentPageRequiresMember: {
type: Boolean,
default: false
},
pagePay: {
type: Array,
default: () => []
}
},
methods: {
toggleCoursePopup() {
this.$emit('toggle-course-popup')
},
toggleSound() {
this.$emit('toggle-sound')
},
goToPage(pageNumber) {
this.$emit('go-to-page', pageNumber)
},
previousPage() {
this.$emit('previous-page')
},
nextPage() {
this.$emit('next-page')
},
onAudioStateChange(audioState) {
this.$emit('audio-state-change', audioState)
},
onHighlightChange(highlightData) {
this.$emit('highlight-change', highlightData)
},
onVoiceChangeComplete(data) {
this.$emit('voice-change-complete', data)
},
onVoiceChangeError(error) {
this.$emit('voice-change-error', error)
}
}
}
</script>
<style lang="scss" scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
border-top: 1rpx solid #EEEEEE;
z-index: 1000;
transition: transform 0.3s ease;
&.tabbar-hidden {
transform: translateY(100%);
}
}
.tabbar-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 62rpx;
height: 88rpx;
}
.tabbar-left {
display: flex;
align-items: center;
gap: 35rpx;
}
.tab-button {
display: flex;
align-items: center;
flex-direction: column;
gap: 8rpx;
}
.tab-icon {
width: 52rpx;
height: 52rpx;
}
.tab-text {
font-family: PingFang SC;
font-size: 22rpx;
color: #999;
line-height: 24rpx;
}
.tabbar-right {
flex: 1;
display: flex;
justify-content: flex-end;
}
.page-controls {
display: flex;
align-items: center;
}
.page-numbers {
display: flex;
align-items: center;
gap: 8rpx;
overflow-x: auto;
max-width: 400rpx;
&::-webkit-scrollbar {
display: none;
}
}
.page-number {
min-width: 84rpx;
height: 58rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 100rpx;
font-family: PingFang SC;
font-size: 30rpx;
color: #3B3D3D;
background-color: transparent;
border: 1px solid #3B3D3D;
transition: all 0.3s ease;
&.active {
border: 1px solid #06DADC;
color: #06DADC;
}
}
</style>

+ 225
- 0
subPages/home/components/MeaningPopup.vue View File

@ -0,0 +1,225 @@
<template>
<uv-popup
mode="bottom"
ref="meaningPopup"
round="32rpx"
bg-color="#FFFFFF"
:overlay="true"
safeAreaInsetBottom
>
<view class="meaning-popup" v-if="currentWordMeaning">
<view class="meaning-header">
<view class="close-btn" @click="closeMeaningPopup">
<text class="close-text">关闭</text>
</view>
<view class="meaning-title">释义</view>
<view style="width: 80rpx;"></view>
</view>
<view class="meaning-content">
<image
v-if="currentWordMeaning.image"
class="meaning-image"
:src="currentWordMeaning.image"
mode="aspectFill"
/>
<view class="word-info">
<view class="word-main">
<text class="word-text">{{ currentWordMeaning.word }}</text>
</view>
<view class="phonetic-container">
<uv-icon
name="volume-fill"
size="16"
color="#007AFF"
class="speaker-icon"
@click="repeatWordAudio"
/>
<text class="phonetic-text">{{ currentWordMeaning.phonetic }}</text>
</view>
<view class="word-meaning">
<text class="part-of-speech">{{ currentWordMeaning.partOfSpeech }}</text>
<text class="meaning-text">{{ currentWordMeaning.meaning }}</text>
</view>
</view>
<view class="knowledge-gain">
<view class="knowledge-header">
<image src="/static/knowledge-icon.png" class="knowledge-icon" mode="aspectFill" />
<text class="knowledge-title">知识收获</text>
</view>
<text class="knowledge-content">{{ currentWordMeaning.knowledgeGain }}</text>
</view>
</view>
</view>
</uv-popup>
</template>
<script>
export default {
name: 'MeaningPopup',
props: {
currentWordMeaning: {
type: Object,
default: null
}
},
methods: {
open() {
if (this.$refs.meaningPopup) {
this.$refs.meaningPopup.open()
}
},
close() {
if (this.$refs.meaningPopup) {
this.$refs.meaningPopup.close()
}
},
closeMeaningPopup() {
this.$emit('close-meaning-popup')
this.close()
},
repeatWordAudio() {
this.$emit('repeat-word-audio')
}
}
}
</script>
<style lang="scss" scoped>
/* 释义弹窗样式 */
.meaning-popup {
background-color: #FFFFFF;
overflow: hidden;
}
.meaning-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 2rpx solid #EEEEEE;
}
.close-btn {
width: 80rpx;
}
.close-text {
font-family: PingFang SC;
font-size: 32rpx;
color: #8b8b8b;
}
.meaning-title {
font-family: PingFang SC;
font-weight: 500;
font-size: 34rpx;
color: #181818;
}
.meaning-content {
padding: 32rpx;
}
.meaning-image {
width: 670rpx;
height: 268rpx;
border-radius: 24rpx;
margin-bottom: 32rpx;
}
.word-info {
margin-bottom: 32rpx;
}
.word-main {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 8rpx;
}
.word-text {
font-family: PingFang SC;
font-weight: 500;
font-size: 40rpx;
color: #181818;
}
.phonetic-container {
display: flex;
gap: 24rpx;
align-items: center;
.speaker-icon {
cursor: pointer;
transition: opacity 0.2s ease;
padding: 8rpx;
border-radius: 8rpx;
&:hover {
opacity: 0.7;
background-color: rgba(0, 122, 255, 0.1);
}
}
.phonetic-text {
font-family: PingFang SC;
font-size: 28rpx;
color: #262626;
margin-bottom: 16rpx;
}
}
.word-meaning {
display: flex;
gap: 16rpx;
}
.part-of-speech {
font-family: PingFang SC;
font-size: 28rpx;
color: #262626;
}
.meaning-text {
font-family: PingFang SC;
font-size: 28rpx;
color: #181818;
flex: 1;
}
.knowledge-gain {
background: linear-gradient(180deg, #DEFFFF 0%, #FBFEFF 22.65%, #F0FBFF 100%);
border-radius: 24rpx;
padding: 32rpx 40rpx;
}
.knowledge-header {
display: flex;
align-items: center;
gap: 16rpx;
margin-bottom: 20rpx;
}
.knowledge-icon {
width: 48rpx;
height: 48rpx;
}
.knowledge-title {
font-family: PingFang SC;
font-weight: 600;
font-size: 30rpx;
color: #3B3D3D;
}
.knowledge-content {
font-family: PingFang SC;
font-size: 28rpx;
color: #4f4f4f;
line-height: 48rpx;
}
</style>

+ 2
- 2
subPages/home/directory.vue View File

@ -89,11 +89,11 @@
<view class="bottom-action-bar">
<view class="bottom-action-container">
<view class="action-button secondary" @click="joinCourse">
<image src="/static/课程图标.png" class="button-icon" mode="aspectFill"></image>
<image src="/static/course-icon.png" class="button-icon" mode="aspectFill"></image>
<text>加入课程</text>
</view>
<view class="action-button primary" @click="startLearning(courseList.records[0].id)">
<image src="/static/内容图标.png" class="button-icon" ></image>
<image src="/static/content-icon.png" class="button-icon" ></image>
<text>内容朗读</text>
</view>
<uv-button @click="startLearning(courseList.records[0].id)" type="primary" :custom-style="{


+ 2
- 2
subPages/home/plan.vue View File

@ -47,14 +47,14 @@ export default {
<h3 style="font-size: 36rpx; font-weight: bold; color: #333; margin: 0 0 20rpx 0;">灵活设置</h3>
<p style="font-size: 28rpx; color: #666; line-height: 1.6; margin: 0 0 30rpx 0;">语音合成支持中文英文粤语四川话也可以合成中英混读语音</p>
<div style="width: 100%; height: 300rpx; overflow: hidden; background: #eee;">
<img src="/static/默认图片.png" style="width: 100%; height: 100%; object-fit: cover; " />
<img src="/static/default-image.png" style="width: 100%; height: 100%; object-fit: cover; " />
</div>
</div>
<div style="padding: 40rpx; background: #f8f9fa; ">
<h3 style="font-size: 36rpx; font-weight: bold; color: #333; margin: 0 0 20rpx 0;">高拟真度</h3>
<p style="font-size: 28rpx; color: #666; line-height: 1.6; margin: 0 0 30rpx 0;">基于业界领先技术构建的语音合成系统具备合成速度快合成语音自然流畅等特点合成语音拟真度高能够符合多样化的应用场景让设备和应用轻松发声人机语音交互效果更加逼真</p>
<div style="width: 100%; height: 300rpx; overflow: hidden; background: #eee;">
<img src="/static/默认图片.png" style="width: 100%; height: 100%; object-fit: cover; " />
<img src="/static/default-image.png" style="width: 100%; height: 100%; object-fit: cover; " />
</div>
</div>
</div>


+ 1
- 1
subPages/home/search.vue View File

@ -57,7 +57,7 @@
<view class="book-author">{{ book.booksAuthor }}</view>
<view class="book-meta">
<view class="book-duration">
<image src="/static/播放图标.png" mode="aspectFill" class="book-icon"></image>
<image src="/static/play-icon.png" mode="aspectFill" class="book-icon"></image>
<text>{{ book.duration }}</text>
</view>
<view class="book-membership" :class="classMap[book.vipInfo.title]">


+ 1
- 1
subPages/login/otherDemo.vue View File

@ -29,7 +29,7 @@
>
<image
class="avatar-image"
:src="userInfo.headImage || '/static/待上传头像.png'"
:src="userInfo.headImage || '/static/upload-avatar.png'"
mode="aspectFill"
></image>
</button>


+ 1
- 1
subPages/login/userInfo.vue View File

@ -20,7 +20,7 @@
<button class="avatar-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<image
class="avatar-image"
:src="userInfo.avatar || '/static/默认头像.png'"
:src="userInfo.avatar || '/static/default-avatar.png'"
mode="aspectFill"
></image>
</button>


+ 4
- 4
subPages/member/recharge.vue View File

@ -3,7 +3,7 @@
<view class="header">
<view class="header-bg">
<image
src="/static/会员背景.png"
src="/static/member-background.png"
class="header-img"
mode="scaleToFill"
/>
@ -36,7 +36,7 @@
</button>
<image
class="swiper-arrow"
src="/static/修饰箭头.png"
src="/static/decorative-arrow.png"
mode="aspectFill"
/>
</view>
@ -126,7 +126,7 @@
<view class="benefit-desc">依据水平精准推课不做无用功快速提升</view>
</view>
<view class="benefit-icon">
<image src="/static/会员图片2.png" mode="aspectFit"></image>
<image src="/static/member-image-2.png" mode="aspectFit"></image>
</view>
</view>
@ -137,7 +137,7 @@
<view class="benefit-desc">精心设计科学的学习流程 测试-讲解-练习-检验知识掌握更牢固</view>
</view>
<view class="benefit-icon">
<image src="/static/会员图片3.png" mode="aspectFit"></image>
<image src="/static/member-image-3.png" mode="aspectFit"></image>
</view>
</view> -->
</view>


+ 3
- 0
subPages/user/cash.vue View File

@ -1,6 +1,9 @@
<template>
<view class="cash-page">
<!-- 顶部标题栏 -->
<view class="header-icon">
<image class="cash-icon" src="/static/withdraw-icon.png" mode="aspectFit"></image>
</view>
<!-- 表单内容 -->
<view class="form-container">


+ 1
- 1
subPages/user/discount.vue View File

@ -33,7 +33,7 @@
<image
v-if="currentTab !== 0"
class="status-stamp"
:src="currentTab === 1 ? '/static/已使用盖章.png' : '/static/已过期盖章.png'"
:src="currentTab === 1 ? '/static/used-stamp.png' : '/static/expired-stamp.png'"
mode="aspectFit"
></image>
</view>


+ 1
- 1
subPages/user/profile.vue View File

@ -49,7 +49,7 @@
</view>
<image
v-else
:src="userInfo.avatar || '/static/默认头像.png'"
:src="userInfo.avatar || '/static/default-avatar.png'"
class="avatar-image"
mode="aspectFill"
></image>


+ 6
- 6
subPages/user/promote.vue View File

@ -10,7 +10,7 @@
<view :style="{flex: 1, justifyContent: 'center', display: 'flex'}">
<image
class="promote-image"
src="/static/推广官.png"
src="/static/promoter.png"
mode="widthFix"
></image>
</view>
@ -20,7 +20,7 @@
<view class="slogan-container">
<image
class="slogan-image"
src="/static/推广标语.png"
src="/static/promotion-slogan.png"
mode="widthFix"
></image>
</view>
@ -52,15 +52,15 @@
<!-- 功能按钮区域 -->
<view class="function-buttons">
<view class="function-item" @click="goTeam">
<image class="function-icon" src="/static/团队图标.png" mode="aspectFit"></image>
<image class="function-icon" src="/static/team-icon.png" mode="aspectFit"></image>
<text class="function-text">我的团队</text>
</view>
<view class="function-item" @click="goQrcode">
<image class="function-icon" src="/static/二维码图标.png" mode="aspectFit"></image>
<text class="function-text">我的二维码</text>
<image class="function-icon" src="/static/qrcode-icon.png" mode="aspectFit"></image>
<text class="function-text">推广二维码</text>
</view>
<view class="function-item" @click="goCash">
<image class="function-icon" src="/static/提现图标.png" mode="aspectFit"></image>
<image class="function-icon" src="/static/withdraw-icon.png" mode="aspectFit"></image>
<text class="function-text">提现</text>
</view>
</view>


+ 13
- 13
subPages/user/team.vue View File

@ -31,7 +31,7 @@
>
<image
class="member-avatar"
:src="member.avatar || '/static/默认头像.png'"
:src="member.avatar || '/static/default-avatar.png'"
mode="aspectFill"
></image>
<text class="member-name">{{ member.name }}</text>
@ -55,62 +55,62 @@ export default {
{
id: 1,
name: '李世海',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 2,
name: '周静',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 3,
name: '周海',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 4,
name: '冯启彬',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 5,
name: '李嫣',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 6,
name: '李书萍',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 7,
name: '赵吾光',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 8,
name: '冯云',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 9,
name: '周静',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 10,
name: '周海',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 11,
name: '冯启彬',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
},
{
id: 12,
name: '李嫣',
avatar: '/static/默认头像.png'
avatar: '/static/default-avatar.png'
}
]
}


Loading…
Cancel
Save