Browse Source

苏青壳阅读题,及语音播报

master
zhangminghao 2 months ago
parent
commit
7cdae82357
  1. 187
      components/SwipeToNext.vue
  2. 293
      components/SwipeToNext使用文档.md
  3. 27
      package-lock.json
  4. 60
      pages.json
  5. 1
      pages/index/readingBody.vue
  6. 25
      project.config.json
  7. 14
      project.private.config.json
  8. 218
      xqk/chapter1/index.vue
  9. 216
      xqk/chapter2/index.vue
  10. 244
      xqk/chapter3/index.vue
  11. 132
      xqk/chapter4/index.vue
  12. 188
      xqk/chapter5/index.vue
  13. 216
      xqk/chapter6/index.vue
  14. 143
      xqk/chapter7/index.vue
  15. 75
      xqk/chapter8/index.vue
  16. 248
      xqk/components/NavMenu.vue
  17. 95
      xqk/components/SinglePlayGif.vue
  18. 171
      xqk/home/home.vue

187
components/SwipeToNext.vue

@ -0,0 +1,187 @@
<template>
<view
class="swipe-to-next"
@touchstart="handleTouchStart"
@touchend="handleTouchEnd"
>
<slot></slot>
<!-- 提示文字 -->
<view v-if="showTip && shouldShowTip" class="bottom-tip">
<text>{{ tipText }}</text>
</view>
</view>
</template>
<script>
export default {
name: 'SwipeToNext',
props: {
//
isLastSlide: {
type: Boolean,
default: false
},
//
targetPath: {
type: String,
required: true
},
//
showTip: {
type: Boolean,
default: true
},
//
tipText: {
type: String,
default: '上滑动进入下一章节'
},
// px
swipeThreshold: {
type: Number,
default: 100
},
// ms
delayTime: {
type: Number,
default: 500
},
//
enableDelay: {
type: Boolean,
default: true
},
// isLastSlide
alwaysEnable: {
type: Boolean,
default: false
}
},
data() {
return {
touchStartY: 0, // Y
touchEndY: 0, // Y
canJump: false, //
delayTimer: null //
}
},
computed: {
//
shouldShowTip() {
return this.alwaysEnable || this.isLastSlide;
},
//
shouldEnableTouch() {
return this.alwaysEnable || this.isLastSlide;
}
},
watch: {
isLastSlide(newVal) {
this.handleSlideChange(newVal);
},
alwaysEnable: {
handler(newVal) {
if (newVal) {
//
this.handleSlideChange(true);
}
},
immediate: true
}
},
beforeDestroy() {
//
if (this.delayTimer) {
clearTimeout(this.delayTimer);
}
},
methods: {
//
handleSlideChange(isActive) {
console.log('ppsls');
if (isActive || this.alwaysEnable) {
//
this.canJump = false;
if (this.enableDelay && !this.alwaysEnable) {
//
this.delayTimer = setTimeout(() => {
if (this.shouldEnableTouch) {
this.canJump = true;
}
}, this.delayTime);
} else {
//
this.canJump = true;
}
} else {
//
this.canJump = false;
if (this.delayTimer) {
clearTimeout(this.delayTimer);
this.delayTimer = null;
}
}
},
//
handleTouchStart(e) {
console.log('。。。。。。。。。。///////////');
//
if (this.shouldEnableTouch && (this.canJump || !this.enableDelay || this.alwaysEnable)) {
this.touchStartY = e.touches[0].clientY;
}
},
//
handleTouchEnd(e) {
//
if (!this.shouldEnableTouch || !this.touchStartY) {
return;
}
//
if (this.enableDelay && !this.canJump && !this.alwaysEnable) {
return;
}
this.touchEndY = e.changedTouches[0].clientY;
const deltaY = this.touchStartY - this.touchEndY;
//
if (deltaY > this.swipeThreshold) {
console.log('向上滑动触发跳转,目标路径:', this.targetPath);
this.$emit('swipe-to-next', this.targetPath);
uni.navigateTo({
url:this.targetPath
});
}
//
this.touchStartY = 0;
this.touchEndY = 0;
},
}
}
</script>
<style lang="scss" scoped>
.swipe-to-next {
width: 100%;
height: 100%;
position: relative;
}
.bottom-tip {
position: absolute;
bottom: 50rpx;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.6);
padding: 10rpx 20rpx;
border-radius: 50rpx;
z-index: 999;
text {
color: #fff;
font-size: 24rpx;
}
}
</style>

293
components/SwipeToNext使用文档.md

@ -0,0 +1,293 @@
# 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; // 改变字体大小
}
}
```
这个组件极大地简化了触底跳转功能的实现,让你可以专注于业务逻辑而不用重复编写相同的手势检测代码。

27
package-lock.json

@ -1,8 +1,33 @@
{
"name": "cgc_wechat",
"version": "1.0.0",
"lockfileVersion": 1,
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cgc_wechat",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"moment": "^2.30.1",
"ydui-district": "^1.1.0"
},
"devDependencies": {}
},
"node_modules/moment": {
"version": "2.30.1",
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
"engines": {
"node": "*"
}
},
"node_modules/ydui-district": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/ydui-district/-/ydui-district-1.1.0.tgz",
"integrity": "sha512-MBhvfaR5Gkn6MUmEnrH1A7IFB5igALuDgtIF+gz3dRwNwW9+KOmih7z+xZFfGluMsEbWaT7C3lWOckYsLZQnFg=="
}
},
"dependencies": {
"moment": {
"version": "2.30.1",

60
pages.json

@ -553,6 +553,66 @@
}
}
]
},
{
"root": "xqk",
"pages": [{
"path": "home/home",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter1/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter2/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter3/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter4/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter5/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter6/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path": "chapter7/index",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
}
]
}
],
"tabBar": {

1
pages/index/readingBody.vue

@ -60,6 +60,7 @@
</view>
<view class="reading-box">
<image v-for="(item,index) in readingList" :key="index" :src="showImg(item.image)" @click="gotoUrlNew(item)"></image>
<image :src="showImg('/uploads/20250903/1aadd1b9a4c94ff0aa6b3f880a725b50.png')" @click="gotoUrlNew({jump_type:2,front_model:{mini:'/xqk/home/home'}})"></image>
</view>
<CustomTabBar :currentTab="1" />

25
project.config.json

@ -0,0 +1,25 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false,
"minifyWXML": true
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wx8954209bb3ad489e",
"editorSetting": {}
}

14
project.private.config.json

@ -0,0 +1,14 @@
{
"libVersion": "3.10.0",
"projectname": "EpicSoul",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}

218
xqk/chapter1/index.vue

@ -0,0 +1,218 @@
<template>
<view style="width: 100vw;">
<SwipeToNext :is-last-slide="isLastSlide" :target-path="'/xqk/chapter2/index'">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<!-- <template v-if="index === 1">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter1/img2-dian.png"
v-for="i in 3" :key="i" mode="widthFix" :class="['module'+(i+1)]"
@click="openPopup(i+1)"></image>
</template> -->
<template v-if="index == 2">
<video :src="showImg('/uploads/20250903/7af32cc4f824b724560f78c597c85864.mp4')"
style="width: 100vw;height: 30vh;" objectFit="cover"></video>
</template>
</view>
</swiper-item>
</swiper>
</SwipeToNext>
<!-- 第二页气泡弹框 -->
<!-- <uni-popup ref="chapterPopup">
<view style="width: 100vw;height: 100vh;" @click="$refs.chapterPopup.close()">
<image :src="getImageUrl(`img2-${popupIndex}.png`)" mode="widthFix" :class="[`img2-${popupIndex}`]">
</image>
</view>
</uni-popup> -->
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
import SwipeToNext from '@/components/SwipeToNext.vue';
export default {
components: {
MusicControl,
NavMenu,
SwipeToNext
},
data() {
return {
currentIndex: 0,
navIndex: 1,
swiperImages: [
this.showImg('/uploads/20250903/24303e4b7218eaf3d857c846417eb490.png'),
this.showImg('/uploads/20250903/17495ef65648c64c31920d312301e991.png'),
this.showImg('/uploads/20250903/92d6f1c6f8f7de040f3c31c8faf98927.png'),
],
animationConfig: {
delay: 0.5,
duration: 3,
keyframes: {
start: 1,
first: 0.8,
second: 1.2,
third: 0.9,
end: 1.1
}
},
popupIndex: 1,
isLastSlide:false,
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 2;
this.isLastSlide = true;
} else {
this.navIndex = 1
this.isLastSlide = false;
}
},
//
openPopup(i) {
this.popupIndex = i
this.$refs.chapterPopup.open();
},
// URL
getImageUrl(path) {
if (typeof path === 'object') {
path = path.url;
}
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter1/${path}`;
}
}
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.module1 {
position: absolute;
width: 52.1rpx;
top: 1020rpx;
left: 235rpx;
animation: breath1 3s ease-in-out infinite;
}
.module2 {
position: absolute;
width: 52.1rpx;
top: 760rpx;
left: 317rpx;
animation: breath2 4s ease-in-out infinite;
}
.module3 {
position: absolute;
width: 52.1rpx;
top: 700rpx;
left: 498rpx;
animation: breath3 5s ease-in-out infinite;
}
// -
@keyframes breath1 {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
@keyframes breath2 {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
@keyframes breath3 {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
}
.img4-text {
width: 428.43rpx;
position: absolute;
top: 170rpx;
left: 100rpx;
}
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 100rpx;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
.img2-1 {
width: 267.37rpx;
position: fixed;
top: 395rpx;
left: 70rpx;
}
.img2-2 {
width: 332.24rpx;
position: fixed;
top: 210rpx;
left: 360rpx;
}
.img2-3 {
width: 600.59rpx;
position: fixed;
bottom: 215rpx;
right: 40rpx;
}
</style>

216
xqk/chapter2/index.vue

@ -0,0 +1,216 @@
<template>
<view style="width: 100vw;">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<template v-if="index == 1" >
<image @click="hanldGifPage" :src="showImg('/uploads/20250903/dcfd8b8a708f4f2d43edf35a906f75ba.png')" mode="widthFix" class="img1-text"></image>
</template>
</view>
</swiper-item>
</swiper>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 2,
swiperImages: [
this.showImg('/uploads/20250903/3bd4fe43f2a6a8806799f06a548f9477.png'),
this.showImg('/uploads/20250903/8fe8d66210edd96a9f322a661b4d9ba4.png'),
],
//
showImg7_1: false,
showImg7_2: false,
showImg7_3: false,
//
timers: [],
popupIndex: 1
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
},
onUnload() {
//
this.timers.forEach(timer => clearTimeout(timer))
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if(idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
hanldGifPage(){
uni.navigateTo({
url:'/xqk/chapter5/index'
})
},
handleSwiperChange(e) {
//
this.timers.forEach(timer => clearTimeout(timer))
this.timers = []
this.currentIndex = e.detail.current;
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 3;
}else {
this.navIndex = 2
}
if (this.currentIndex === 6) {
//
this.showImg7_1 = false
this.showImg7_2 = false
this.showImg7_3 = false
// 0.5
const timer1 = setTimeout(() => {
this.showImg7_1 = true
}, 500)
// 1
const timer2 = setTimeout(() => {
this.showImg7_2 = true
}, 1000)
// 2
const timer3 = setTimeout(() => {
this.showImg7_3 = true
}, 2000)
this.timers.push(timer1, timer2, timer3)
} else {
//
this.showImg7_1 = false
this.showImg7_2 = false
this.showImg7_3 = false
}
},
//
openPopup(i) {
this.popupIndex = i
this.$refs.chapterPopup.open();
},
getImageUrl(path) {
if (typeof path === 'object') {
path = path.url;
}
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter2/${path}`;
}
}
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.img1-text{
width: 484rpx;
position: absolute;
bottom: 100rpx;
left: 250rpx;
}
.img10-text {
width: 484rpx;
position: absolute;
top: 170rpx;
left: 100rpx;
}
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 100rpx;
}
.module-img {
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
width: 564.25rpx;
}
.module1 {
top: 630rpx;
}
.module2 {
top: 780rpx;
}
.module3 {
top: 930rpx;
}
.module4 {
top: 1080rpx;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
/* 渐入动画样式 */
.fade-in-image {
/* 初始状态:透明 */
opacity: 0;
/* 添加过渡动画:1秒内透明度从0到1 */
animation: fadeIn 1s ease-out forwards;
/* 根据需要调整图片的定位 */
position: absolute;
}
/* 渐入动画关键帧 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx); /* 可选:添加轻微上移动画 */
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in-image:nth-child(1) {
width: 22.97rpx;
top: 825rpx;
right: 191rpx;
}
.fade-in-image:nth-child(2) {
width: 34.95rpx;
top: 790rpx;
right: 170rpx;
}
.fade-in-image:nth-child(3) {
width: 144.81rpx;
top: 680rpx;
right: 71rpx;
}
</style>

244
xqk/chapter3/index.vue

@ -0,0 +1,244 @@
<template>
<view style="width: 100vw; position: relative;">
<!-- 滑动拦截遮罩仅在第5页且未输入内容时显示 -->
<view
v-if="currentIndex === 5 && !inputValue.trim()"
class="swipe-blocker"
@click="$refs.customPopup.open()"
></view>
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<!-- 第5页内容 -->
<template v-if="index === 5">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img6-text.png"
mode="widthFix" class="img6-text"></image>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img6-btn.png"
mode="widthFix" class="img6-btn" @click="$refs.customPopup.open()"></image>
</template>
<!-- 第6页内容 -->
<template v-if="index === 6">
<view class="img7-box">
<view class="img7-textBg">
<view>你的</view>
<view style="margin: 10rpx 0;">{{inputValue}}</view>
<view>已备好</view>
</view>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img7-text.png"
mode="widthFix" class="img7-text"></image>
</view>
</template>
<template v-if="index === 7">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img8-text.png" mode="widthFix" class="img8-text"></image>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/btn-img.png" mode="widthFix"
class="btn-img" @click="gotoPath('/xrcc/chapter4/index')"></image>
</template>
</view>
</swiper-item>
</swiper>
<!-- 输入弹框 -->
<uni-popup ref="customPopup" type="center">
<view class="popup-content">
<textarea v-model="inputValue" class="input-area" placeholder="填写你的“物件” (可以是一本书、一首歌、一段回忆、一个困惑...)"
maxlength="20"></textarea>
<view class="word-count">
{{ inputValue.length }}/20
</view>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img6-btns.png" mode="widthFix" @click="submit" class="confirm-btn"></image>
</view>
</uni-popup>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
//
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 3,
swiperImages: [
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img1.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img2s.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img3s.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img4s.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img5s.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img6.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img7.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img8.gif',
],
inputValue: '',
swipeDirection: ''
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleTouchStart(e) {
this.startY = e.touches[0].clientY;
},
handleTouchMove(e) {
const moveY = e.touches[0].clientY;
this.swipeDirection = moveY < this.startY ? 'down' : 'up';
},
handleSwiperChange(e) {
const newIndex = e.detail.current;
if (this.currentIndex === 5 && newIndex === 6 && !this.inputValue.trim()) {
this.currentIndex = 5;
uni.showToast({
title: '请先填写内容才能继续',
icon: 'none',
duration: 2000
});
return;
}
this.currentIndex = newIndex;
if (this.currentIndex === this.swiperImages.length - 1) {
this.navIndex = 4;
} else {
this.navIndex = 3;
}
},
submit() {
if (!this.inputValue.trim()) return;
this.$refs.customPopup.close()
this.currentIndex = 6;
}
}
}
</script>
<style lang="scss" scoped>
/* 原有样式保持不变 */
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.img6-text {
width: 442.41rpx;
position: absolute;
top: 170rpx;
left: 0;
right: 0;
margin: 0 auto;
}
.img6-btn {
position: absolute;
width: 520.31rpx;
bottom: 210rpx;
left: 0;
right: 0;
margin: 0 auto;
}
/* 其他样式保持不变 */
.img7-box {
position: relative;
top: 170rpx;
margin: 0 auto;
}
.img7-textBg {
background-image: url('https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter3/img7-textBg.png');
background-size: 100% 100%;
padding: 30rpx 50rpx;
font-size: 50rpx;
text-align: center;
color: #fff;
width: 520.31rpx;
margin: 0 auto;
}
.img7-text {
display: block;
margin: 60rpx auto 0;
width: 447.4rpx;
}
.img8-text {
width: 379.49rpx;
position: absolute;
top: 170rpx;
left: 100rpx;
}
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 100rpx;
}
}
.swipe-blocker {
position: absolute;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9;
}
.popup-content {
width: 85vw;
background-color: #fff;
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-sizing: border-box;
.input-area {
width: 100%;
min-height: 180rpx;
padding: 20rpx;
border: 2rpx solid #eee;
border-radius: 8rpx;
font-size: 28rpx;
resize: none;
box-sizing: border-box;
margin-bottom: 15rpx;
}
.word-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-bottom: 35rpx;
}
.confirm-btn {
width: 100%;
}
}
</style>

132
xqk/chapter4/index.vue

@ -0,0 +1,132 @@
<template>
<view style="width: 100vw;">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<template v-if="index === 3">
<view class="module-box">
<image :src="`https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img4-${i+1}.png`"
v-for="i in 5" :key="i" mode="widthFix" :class="['module-img', 'module'+(i+1)]"
@click="openPopup(i+1)"></image>
</view>
</template>
<template v-if="index === 5">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img5-text.png" mode="widthFix" class="img5-text"></image>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/btn-img.png" mode="widthFix"
class="btn-img" @click="gotoPath('/xrcc/chapter5/index')"></image>
</template>
</view>
</swiper-item>
</swiper>
<uni-popup ref="chapterPopup">
<image :src="getImageUrl(`img4-${popupIndex}s.png`)" mode="widthFix" style="width: 600rpx;">
</image>
</uni-popup>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 4,
swiperImages: [
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img1.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img2.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img3.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img4.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img5s.png',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/img6.gif',
],
popupIndex: 1
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if(idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 5;
}else {
this.navIndex = 4
}
},
openPopup(i) {
this.popupIndex = i
this.$refs.chapterPopup.open();
},
getImageUrl(path) {
if (typeof path === 'object') {
path = path.url;
}
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter4/${path}`;
}
},
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.img5-text {
width: 576.23rpx;
position: absolute;
top: 170rpx;
left: 100rpx;
}
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 100rpx;
}
.module-box {
position: absolute;
top: 460rpx;
text-align: center;
image {
width: 650rpx;
margin-bottom: 80rpx;
}
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
</style>

188
xqk/chapter5/index.vue

@ -0,0 +1,188 @@
<template>
<view style="width: 100vw;">
<SwipeToNext
:is-last-slide="isLastSlide"
:always-enable="swiperImages.length === 1"
:target-path="'/xqk/chapter3/index'"
:enable-delay="swiperImages.length > 1"
>
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
</view>
</swiper-item>
</swiper>
</SwipeToNext>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
import SwipeToNext from '@/components/SwipeToNext.vue';
export default {
components: {
MusicControl,
NavMenu,
SwipeToNext
},
data() {
return {
currentIndex: 0,
navIndex: 5,
swiperImages: [
this.showImg('/uploads/20250903/dd5b260002da55d4c3d56b338451bc11.gif'),
],
animateShow: false,
isLastSlide: false //
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
//
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 1;
this.isLastSlide = true;
}
//
if (this.swiperImages.length === 1) {
this.isLastSlide = true;
}
},
// onLoad(option) {
// this.currentIndex = option.currentIndex || 0
// if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
// },
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
if(this.currentIndex == 1) {
this.animateShow = true
}else {
this.animateShow = false
}
if (this.currentIndex == this.swiperImages.length - 1) {
//
this.isLastSlide = true;
this.navIndex = 6;
} else {
//
this.isLastSlide = false;
this.navIndex = 5
}
},
//
goHome() {
uni.switchTab({
url: '/pages/index/index'
})
}
},
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.img2s {
position: absolute;
top: 380rpx;
right: 25rpx;
width: 358rpx;
transform: translateX(100%);
opacity: 0;
}
.img2-btn {
width: 558rpx;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
bottom: 70rpx;
}
.img3-btn {
width: 558rpx;
line-height: 72rpx;
text-align: center;
border-radius: 20rpx;
border: 2rpx solid;
font-size: 30rpx;
color: #fff;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
bottom: 180rpx;
}
.flex-column {
position: absolute;
bottom: 280rpx;
width: 100%;
align-items: center;
.img5-text {
width: 100%;
}
.img5-btn {
width: 230rpx;
margin-top: 99rpx;
}
}
.img6-text {
width: 408.46rpx;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
bottom: 365rpx
}
}
/* 从右往左进入的动画 */
.animate-enter-from-right {
animation: enterFromRight 2s ease-out forwards;
}
@keyframes enterFromRight {
0% {
/* 起始位置:右侧外部 */
transform: translateX(100%);
opacity: 0;
}
100% {
/* 结束位置:正常位置 */
transform: translateX(0);
opacity: 1;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
</style>

216
xqk/chapter6/index.vue

@ -0,0 +1,216 @@
<template>
<view style="width: 100vw;">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<template v-if="index === 0">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/img7-1.png" mode="widthFix"
class="img7-1" @click="$refs.customPopup.open()"></image>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/img7-2.png" mode="widthFix"
class="img7-2" style="top: 620rpx;" @click="$refs.customPopups.open()"></image>
<view class="bgm-box flex-between">
<view :class="{'bgm-active': index == bgmIndex}" v-for="(item,index) in bgmList" :key="index" @click="bgmIndex = index">{{item}}</view>
</view>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/img7-btn.png" mode="widthFix"
class="img7-btn" @click="confirm"></image>
</template>
</view>
</swiper-item>
</swiper>
<!-- 输入弹框 -->
<uni-popup ref="customPopup" type="center">
<view class="popup-content">
<textarea v-model="inputValue" class="input-area" placeholder="我的星槎"
maxlength="20"></textarea>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/confirm-btn.png" mode="widthFix" @click="submit" class="confirm-btn"></image>
</view>
</uni-popup>
<!-- 输入弹框 -->
<uni-popup ref="customPopups" type="center">
<view class="popup-content">
<textarea v-model="inputValues" class="input-area" placeholder="我的目的地"
maxlength="20"></textarea>
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/confirm-btn.png" mode="widthFix" @click="submit" class="confirm-btn"></image>
</view>
</uni-popup>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 5,
swiperImages: [
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter5/img7.png',
],
inputValue: '',
inputValues: '',
bgmList: [
'古典',
'民谣',
'电子',
'自然白噪音'
],
bgmIndex: null
}
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if(idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
},
submit() {
this.$refs.customPopup.close()
this.$refs.customPopups.close()
},
getImageUrl(path) {
if (typeof path === 'object') {
path = path.url;
}
return `https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter6/${path}`;
},
confirm() {
if (!this.inputValue.trim() || !this.inputValues.trim() || this.bgmIndex == null) {
uni.showToast({
title: '请先填写或选择您的日志信息',
icon: 'none'
});
}else {
let data = {
text1: this.inputValue.trim(),
text2: this.inputValues.trim(),
imgSrc: this.getImageUrl(`img${this.bgmIndex + 1}s.png`),
imgTitle: this.bgmList[this.bgmIndex]
}
this.gotoPath('/xrcc/chapter7/index?data=' + JSON.stringify(data))
}
}
},
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 84rpx;
}
.img7-1, .img7-2 {
width: 437.67rpx;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
top: 365rpx;
}
.bgm-box {
width: 437.67rpx;
position: absolute;
left: 0;
right: 0;
bottom: 650rpx;
margin: 0 auto;
flex-wrap: wrap;
view {
width: 207.89rpx;
line-height: 42.77rpx;
border-radius: 2rpx;
text-align: center;
color: #fff;
font-size: 20rpx;
border: 1rpx solid #fff;
}
view:nth-child(n+3) {
margin-top: 17rpx;
}
.bgm-active {
border-color: #00C48C;
color: #00C48C;
}
}
.img7-btn {
width: 439.66rpx;
position: absolute;
left: 0;
right: 0;
bottom: 244rpx;
margin: 0 auto;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
.popup-content {
width: 85vw;
background-color: #fff;
border-radius: 16rpx;
padding: 40rpx 30rpx;
box-sizing: border-box;
.input-area {
width: 100%;
min-height: 180rpx;
padding: 20rpx;
border: 2rpx solid #747c8e;
border-radius: 8rpx;
font-size: 28rpx;
resize: none;
box-sizing: border-box;
margin-bottom: 15rpx;
}
.word-count {
text-align: right;
font-size: 24rpx;
color: #999;
margin-bottom: 35rpx;
}
.confirm-btn {
width: 100%;
}
}
</style>

143
xqk/chapter7/index.vue

@ -0,0 +1,143 @@
<template>
<view style="width: 100vw;">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
<template v-if="index === 0">
<view class="box">
<!-- <view class="title">我的星槎</view> -->
<view class="subtitle subtitle1">{{info.text1}}</view>
<!-- <view class="title">我的目的地</view> -->
<view class="subtitle subtitle2">{{info.text2}}</view>
<!-- <view class="title">我的航行BGM</view> -->
<view class="subtitle subtitle3">{{info.imgTitle}}</view>
</view>
</template>
<template v-if="index === 1">
<image src="https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter6/img5-text.png" mode="widthFix" class="img5-text.png"></image>
</template>
<!-- 二维码 -->
<template v-if="index === 2">
<image src="https://static.ticket.sz-trip.com/epicSoul/bmzm/qrcode.png" mode="widthFix" class="qrcode" :show-menu-by-longpress="true"></image>
</template>
</view>
</swiper-item>
</swiper>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 5,
swiperImages: [
'',
'https://static.ticket.sz-trip.com/epicSoul/xrcc/chapter6/img5.png',
'https://static.ticket.sz-trip.com/epicSoul/bmzm/chapter5/img7.png'
],
info: {}
}
},
onLoad(option) {
if(option) {
let data = JSON.parse(option.data)
this.info = data
console.log(data)
this.swiperImages[0] = data.imgSrc
}
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
},
},
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% 100%;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.box {
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
top: 300rpx;
text-align: center;
width: 100%;
color: rgba(220, 221, 221, 1);
.title {
font-size: 25rpx;
margin-top: 50rpx;
}
.subtitle {
position: absolute;
font-size: 40rpx;
left: 0;
right: 0;
margin: 0 auto;
}
.subtitle1 {
top: 150rpx;
}
.subtitle2 {
top: 280rpx;
}
.subtitle3 {
top: 410rpx;
}
}
.img5-text.png {
width: 312.58rpx;
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
bottom: 492rpx;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
.qrcode {
position: absolute;
left: 0;
right: 0;
margin: 0 auto;
width: 25vw;
bottom: 28vh;
}
</style>

75
xqk/chapter8/index.vue

@ -0,0 +1,75 @@
<template>
<view style="width: 100vw;">
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
</view>
</swiper-item>
</swiper>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import NavMenu from '../components/NavMenu.vue';
export default {
components: {
MusicControl,
NavMenu
},
data() {
return {
currentIndex: 0,
navIndex: 3,
swiperImages: [
'https://static.ticket.sz-trip.com/epicSoul/xrcc/home/img1.gif',
]
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
if (this.currentIndex == this.swiperImages.length - 1) this.navIndex = 1;
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
},
handleSwiperChange(e) {
this.currentIndex = e.detail.current;
},
},
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
width: 100vw;
height: 100vh;
background-size: 100% auto;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 84rpx;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
</style>

248
xqk/components/NavMenu.vue

@ -0,0 +1,248 @@
<template>
<view>
<view class="overlay" v-if="showMenu" @click="onCloseMenu"></view>
<view class="fixed-nav" :class="{'hidden': showMenu}" @click="onShowMenu">
<image class="nav-icon" :class="{'rotated': iconRotated, 'bounce-back': iconBounceBack}" :src="navIconSrc"
mode="aspectFill"></image>
</view>
<view class="nav-menu" :class="{'show': showMenu}">
<view class="nav-item" :class="{'item-active': isItemActive(item)}" v-for="item in menuItems"
:key="item.targetIndex" @click="() => onJumpToPage(item)">
<view v-if="item.text.includes('#Chapter')" class="chapter-text">
<text class="chapter-title">#Chapter</text>
<text :class="{'active': isItemActive(item)}" class="chapter-number">
{{ item.text.replace('#Chapter', '') }}
</text>
</view>
<text v-else :class="{'active': isItemActive(item)}">{{ item.text }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
//
navIndex: {
type: Number,
required: true
},
//
navIconSrc: {
type: String,
default: 'https://static.ticket.sz-trip.com/epicSoul/taozi/nav-icon.png'
}
},
data() {
return {
showMenu: false,
iconRotated: false,
iconBounceBack: false,
menuItems: [{
text: 'INTRO序曲',
targetIndex: 0,
path: "/xqk/home/home"
},
{
text: '01 青壳初生',
targetIndex: 1,
path: "/xqk/chapter1/index"
},
{
text: '02 负海志 向湖生',
targetIndex: 2,
path: "/xqk/chapter2/index"
},
{
text: '03 名曰江湖',
targetIndex: 3,
path: "/xqk/chapter3/index"
},
{
text: '04 风味人间',
targetIndex: 4,
path: "/xqk/chapter4/index"
},
{
text: '05 共济',
targetIndex: 5,
path: "/xqk/chapter6/index"
},
{
text: '有感商品',
targetIndex: 6,
path: "/subPackages/techan/detail?id=39"
},
{
text: '购物车',
targetIndex: 7,
path: "/subPackages/user/gwc"
}
],
};
},
watch: {
navIndex(newVal) {
console.log(newVal)
if(newVal) this.navIndex = newVal
}
},
methods: {
onShowMenu() {
this.iconRotated = true;
setTimeout(() => {
this.showMenu = true;
this.$emit('menu-show');
}, 300);
},
onCloseMenu() {
this.showMenu = false;
setTimeout(() => {
this.iconBounceBack = true;
this.iconRotated = false;
setTimeout(() => {
this.iconBounceBack = false;
}, 500);
}, 300);
this.$emit('menu-hide');
},
onJumpToPage(item) {
console.log(this.navIndex)
if(item.path && item.targetIndex != this.navIndex) this.gotoPath(item.path)
this.onCloseMenu();
},
isItemActive(item) {
return this.navIndex === item.targetIndex;
}
}
};
</script>
<style lang="scss" scoped>
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.3);
z-index: 100;
}
.fixed-nav {
width: 80rpx;
height: 80rpx;
background-color: rgb(0 0 0 / 0.7);
border-radius: 10rpx 0 0 10rpx;
position: fixed;
right: 0;
top: 0;
bottom: 0;
margin: auto 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 9;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.fixed-nav.hidden {
transform: translateX(100%);
opacity: 0;
pointer-events: none;
}
.nav-icon {
width: 35rpx;
height: 35rpx;
transition: transform 0.3s ease;
}
.nav-icon.rotated {
transform: rotate(180deg);
}
.nav-icon.bounce-back {
animation: bounceRotation 0.5s ease;
}
@keyframes bounceRotation {
0% {
transform: rotate(180deg);
}
50% {
transform: rotate(-20deg);
}
75% {
transform: rotate(10deg);
}
100% {
transform: rotate(0deg);
}
}
.nav-menu {
position: fixed;
top: 50%;
right: 0;
transform: translate(100%, -50%);
z-index: 999;
background-color: rgba(255, 255, 255, 0.95);
border-radius: 16rpx 0 0 16rpx;
box-shadow: -4px 0 15px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
.nav-menu.show {
transform: translate(0, -50%);
}
.nav-item {
padding: 20rpx;
text-align: center;
text {
color: #333;
opacity: 0.7;
font-size: 28rpx;
}
}
.item-active {
background-color: rgba(0, 0, 0, 0.1);
}
.nav-item .active {
color: #333;
opacity: 1;
}
.chapter-text {
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.3;
}
.chapter-title {
color: #fff;
opacity: 0.7;
font-size: 24rpx;
}
.chapter-number {
color: #fff;
opacity: 0.7;
font-size: 28rpx;
margin-top: 8rpx;
}
.item-active .chapter-title,
.item-active .chapter-number.active {
opacity: 1;
}
</style>

95
xqk/components/SinglePlayGif.vue

@ -0,0 +1,95 @@
<template>
<view class="gif-container">
<view class="dynamic-container">
<image
:src="gifSrc"
mode="widthFix"
class="gif-image"
:style="{ display: isPlaying ? 'block' : 'none' }"
@load="startGifPlay"
></image>
<image
:src="staticCover"
mode="widthFix"
class="gif-image"
:style="{ display: isPlaying ? 'none' : 'block' }"
></image>
</view>
</view>
</template>
<script>
export default {
props: {
// GIF
gifSrc: {
type: String,
required: true
},
//
staticCover: {
type: String,
default: ''
},
// GIF()
duration: {
type: Number,
default: 2000
}
},
data() {
return {
isPlaying: false,
playTimer: null
}
},
methods: {
// GIF
startGifPlay() {
this.isPlaying = true;
//
if (this.playTimer) {
clearTimeout(this.playTimer);
}
//
this.playTimer = setTimeout(() => {
this.isPlaying = false;
}, this.duration);
}
},
onUnload() {
//
if (this.playTimer) {
clearTimeout(this.playTimer);
}
}
}
</script>
<style scoped>
.gif-container {
width: 100vw;
display: flex;
justify-content: center;
}
.gif-image {
width: 100vw;
height: auto;
}
.dynamic-container {
position: relative;
width: 100vw;
display: flex;
justify-content: center;
}
.dynamic-container image {
position: absolute;
top: 0;
left: 0;
}
</style>

171
xqk/home/home.vue

@ -0,0 +1,171 @@
<template>
<view style="width: 100vw;">
<!-- <SinglePlayGif
gifSrc="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/img1.gif"
staticCover="https://static.ticket.sz-trip.com/epicSoul/xrcc/home/img1.png"
duration="5000"
/> -->
<!-- 触底方法跳转页面组件 -->
<SwipeToNext
:is-last-slide="isLastSlide"
:always-enable="swiperImages.length === 1"
:target-path="'/xqk/chapter1/index'"
:enable-delay="swiperImages.length > 1"
@swipe-to-next="handleSwipeToNext"
>
<swiper class="swiper" :current="currentIndex" :vertical="true" @change="handleSwiperChange">
<swiper-item v-for="(image, index) in swiperImages" :key="index">
<view class="swiper-item" :style="{ backgroundImage: `url(${image})` }">
</view>
</swiper-item>
</swiper>
</SwipeToNext>
<NavMenu :nav-index="navIndex" @jump-to-page="handleJumpToPage" />
<MusicControl />
</view>
</template>
<script>
import MusicControl from '@/components/MusicControl.vue';
import SinglePlayGif from '../components/SinglePlayGif.vue';
import NavMenu from '../components/NavMenu.vue';
import SwipeToNext from '@/components/SwipeToNext.vue';
export default {
components: {
MusicControl,
SinglePlayGif,
NavMenu,
SwipeToNext
},
data() {
return {
isPlaying: false,
playTimer: null,
duration: 5000,
currentIndex: 0,
navIndex: 0,
swiperImages: [
this.showImg('/uploads/20250903/b4f601dee7b4ad1b42c878fd54693c92.png'),
// this.showImg('/uploads/20250903/24303e4b7218eaf3d857c846417eb490.png'),
// this.showImg('/uploads/20250903/17495ef65648c64c31920d312301e991.png'),
// this.showImg('/uploads/20250903/92d6f1c6f8f7de040f3c31c8faf98927.png'),
],
isLastSlide: false //
}
},
onLoad(option) {
this.currentIndex = option.currentIndex || 0
//
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 1;
this.isLastSlide = true;
}
//
if (this.swiperImages.length === 1) {
this.isLastSlide = true;
}
},
onShow() {
const app = getApp();
app.updateMusicSrc('https://static.ticket.sz-trip.com/epicSoul/xrcc/bgm.mp3');
app.initBackgroundMusic(); //
uni.$bgMusic.play(); //
},
methods: {
handleJumpToPage(idx) {
this.navIndex = idx
if (idx == this.swiperImages.length - 1) this.navIndex = idx + 1
},
handleSwiperChange(e) {
console.log(e);
this.currentIndex = e.detail.current;
if (this.currentIndex == this.swiperImages.length - 1) {
this.navIndex = 1;
//
this.isLastSlide = true;
} else {
this.navIndex = 0;
this.isLastSlide = false;
}
},
//
handleSwipeToNext(targetPath) {
console.log('收到滑动跳转事件,目标路径:', targetPath);
//
},
// <!---- >
// #ifdef MP-WEIXIN
onShareAppMessage() {
return {
title: '今夜,我们都有一艘秘密飞船|「Epic Soul」阅读体 issue05',
mpId: 'wx8954209bb3ad489e',
path: '/xrcc/home/home',
imageUrl: this.showImg('/uploads/20250903/66ff1f3cd63ea776a0203e8e0dd92dda.jpg')
};
},
onShareTimeline() {
return {
title: '今夜,我们都有一艘秘密飞船|「Epic Soul」阅读体 issue05',
query: '',
imageUrl: this.showImg('/uploads/20250903/66ff1f3cd63ea776a0203e8e0dd92dda.jpg')
};
}
// #endif
}
}
</script>
<style lang="scss" scoped>
.swiper {
width: 100vw;
height: 100vh;
}
.swiper-item {
/* 新增安全区域适配 */
padding-top: env(safe-area-inset-top);
/* 顶部安全距离 */
padding-bottom: env(safe-area-inset-bottom);
/* 底部安全距离 */
box-sizing: border-box;
/* 修改背景尺寸为覆盖模式 */
background-size: cover;
width: 100vw;
height: 100vh;
// background-size: 100% auto;
background-position: center center;
background-color: #000;
background-repeat: no-repeat;
position: relative;
.img1-text {
position: absolute;
width: 632.16rpx;
top: 170rpx;
left: 0;
right: 0;
margin: 0 auto;
}
.img4-text {
position: absolute;
width: 476.36rpx;
top: 170rpx;
left: 84rpx;
}
.btn-img {
position: absolute;
width: 149.8rpx;
bottom: 290rpx;
left: 84rpx;
}
}
.swiper-img {
width: 100vw;
height: 100vh;
}
</style>
Loading…
Cancel
Save