13 changed files with 9873 additions and 2 deletions
@ -0,0 +1,71 @@ |
|||
<template> |
|||
<view class="battery-container"> |
|||
<view class="battery-body"> |
|||
<view class="battery" :style="{width: `${level}%`}"></view> |
|||
<text class="iconfont charging" v-if="charging"></text> |
|||
</view> |
|||
<view class="battery-head"></view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props:{ |
|||
level: { |
|||
type: Number, |
|||
default: 0 |
|||
}, |
|||
charging: { |
|||
type: Boolean, |
|||
default: false |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
} |
|||
}, |
|||
mounted() { |
|||
}, |
|||
methods: { |
|||
|
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.battery-container{ |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 25px; |
|||
height: 10px; |
|||
.battery-body{ |
|||
position: relative; |
|||
padding: 1px; |
|||
width: 22px; |
|||
height: 100%; |
|||
border-radius: 1px; |
|||
border: $minor-text-color solid 1px; |
|||
.battery{ |
|||
height: 100%; |
|||
background-color: $minor-text-color; |
|||
} |
|||
.charging{ |
|||
position: absolute; |
|||
left: 50%; |
|||
top: 50%; |
|||
transform: translate(-50%, -50%); |
|||
height: 12px; |
|||
line-height: 12px; |
|||
font-size: 15px; |
|||
color: #333; |
|||
} |
|||
} |
|||
.battery-head{ |
|||
width: 2px; |
|||
height: 6px; |
|||
background-color: $minor-text-color; |
|||
} |
|||
} |
|||
</style> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,455 @@ |
|||
const chaptersList = [ |
|||
{ |
|||
index: 0, |
|||
chapterId: '1', |
|||
name: '第1章 考研失败是有原因的' |
|||
}, |
|||
{ |
|||
index: 1, |
|||
chapterId: '2', |
|||
name: '第2章 收服上古神兽' |
|||
}, |
|||
{ |
|||
index: 2, |
|||
chapterId: '3', |
|||
name: '第3章 宗门选拔' |
|||
}, |
|||
{ |
|||
index: 3, |
|||
chapterId: '4', |
|||
name: '第4章 成功登百阶' |
|||
}, |
|||
{ |
|||
index: 4, |
|||
chapterId: '5', |
|||
name: '第5章 你们求不得的我应有尽有' |
|||
}, |
|||
{ |
|||
index: 5, |
|||
chapterId: '6', |
|||
name: '第6章 月华果的主人居然是他?' |
|||
}, |
|||
{ |
|||
index: 6, |
|||
chapterId: '7', |
|||
name: '第7章 大师姐的黑暗料理' |
|||
}, |
|||
{ |
|||
index: 7, |
|||
chapterId: '8', |
|||
name: '第8章 有钱的无为宗' |
|||
}, |
|||
{ |
|||
index: 8, |
|||
chapterId: '9', |
|||
name: '第9章 天下武修第一人' |
|||
}, |
|||
{ |
|||
index: 9, |
|||
chapterId: '10', |
|||
name: '第10章 虽然没灵力,但我有外挂' |
|||
}, |
|||
{ |
|||
index: 10, |
|||
chapterId: '11', |
|||
name: '第11章 前尘往事难道破' |
|||
}, |
|||
{ |
|||
index: 11, |
|||
chapterId: '12', |
|||
name: '第12章 鬼艮的秘密' |
|||
}, |
|||
{ |
|||
index: 12, |
|||
chapterId: '13', |
|||
name: '第13章 屎中添花' |
|||
}, |
|||
{ |
|||
index: 13, |
|||
chapterId: '14', |
|||
name: '第14章 强盗一般的人物' |
|||
}, |
|||
{ |
|||
index: 14, |
|||
chapterId: '15', |
|||
name: '第15章 恶人先告状' |
|||
}, |
|||
{ |
|||
index: 15, |
|||
chapterId: '16', |
|||
name: '第16章 被关惩戒堂' |
|||
}, |
|||
{ |
|||
index: 16, |
|||
chapterId: '17', |
|||
name: '第17章 惩戒堂里的东西' |
|||
}, |
|||
{ |
|||
index: 17, |
|||
chapterId: '18', |
|||
name: '第18章 一举拿下魄' |
|||
}, |
|||
{ |
|||
index: 18, |
|||
chapterId: '19', |
|||
name: '第19章 掌门掐架' |
|||
}, |
|||
{ |
|||
index: 19, |
|||
chapterId: '20', |
|||
name: '第20章 无为道法' |
|||
}, |
|||
{ |
|||
index: 20, |
|||
chapterId: '21', |
|||
name: '第21章 他的精神力是最上乘' |
|||
}, |
|||
{ |
|||
index: 21, |
|||
chapterId: '22', |
|||
name: '第22章 时空回溯的能力' |
|||
}, |
|||
{ |
|||
index: 22, |
|||
chapterId: '23', |
|||
name: '第23章 碧波龙鳞扇' |
|||
}, |
|||
{ |
|||
index: 23, |
|||
chapterId: '24', |
|||
name: '第24章 萧玉林当他的贴身保镖' |
|||
}, |
|||
{ |
|||
index: 24, |
|||
chapterId: '25', |
|||
name: '第25章 大师姐的饭菜' |
|||
}, |
|||
{ |
|||
index: 25, |
|||
chapterId: '26', |
|||
name: '第26章 张景轩是登徒子' |
|||
}, |
|||
{ |
|||
index: 26, |
|||
chapterId: '27', |
|||
name: '第27章 言出法随,风动' |
|||
}, |
|||
{ |
|||
index: 27, |
|||
chapterId: '28', |
|||
name: '第28章 师父给的压力' |
|||
}, |
|||
{ |
|||
index: 28, |
|||
chapterId: '29', |
|||
name: '第29章 新徒比试大会(一)' |
|||
}, |
|||
{ |
|||
index: 29, |
|||
chapterId: '30', |
|||
name: '第30章 新徒比试大会(二)' |
|||
}, |
|||
{ |
|||
index: 30, |
|||
chapterId: '31', |
|||
name: '第31章 新徒比试大会(三)' |
|||
}, |
|||
{ |
|||
index: 31, |
|||
chapterId: '32', |
|||
name: '第32章 走后门' |
|||
}, |
|||
{ |
|||
index: 32, |
|||
chapterId: '33', |
|||
name: '第33章 把魄当狗遛' |
|||
}, |
|||
{ |
|||
index: 33, |
|||
chapterId: '34', |
|||
name: '第34章 万事不要逞强' |
|||
}, |
|||
{ |
|||
index: 34, |
|||
chapterId: '35', |
|||
name: '第35章 进入侯林' |
|||
}, |
|||
{ |
|||
index: 35, |
|||
chapterId: '36', |
|||
name: '第36章 侯林密林,第二境无觅处' |
|||
}, |
|||
{ |
|||
index: 36, |
|||
chapterId: '37', |
|||
name: '第37章 灵草收集完' |
|||
}, |
|||
{ |
|||
index: 37, |
|||
chapterId: '38', |
|||
name: '第38章 赛博武器' |
|||
}, |
|||
{ |
|||
index: 38, |
|||
chapterId: '39', |
|||
name: '第39章 起阵画符得心应手' |
|||
}, |
|||
{ |
|||
index: 39, |
|||
chapterId: '40', |
|||
name: '第40章 窥天镜异动' |
|||
}, |
|||
{ |
|||
index: 40, |
|||
chapterId: '41', |
|||
name: '第41章 姜思张锁仙对战' |
|||
}, |
|||
{ |
|||
index: 41, |
|||
chapterId: '42', |
|||
name: '第42章 有人跟踪他' |
|||
}, |
|||
{ |
|||
index: 42, |
|||
chapterId: '43', |
|||
name: '第43章 灵狼求救' |
|||
}, |
|||
{ |
|||
index: 43, |
|||
chapterId: '44', |
|||
name: '第44章 无奈当奶妈' |
|||
}, |
|||
{ |
|||
index: 44, |
|||
chapterId: '45', |
|||
name: '第45章 误入留园' |
|||
}, |
|||
{ |
|||
index: 45, |
|||
chapterId: '46', |
|||
name: '第46章 青芽决' |
|||
}, |
|||
{ |
|||
index: 46, |
|||
chapterId: '47', |
|||
name: '第47章 仙缘初现' |
|||
}, |
|||
{ |
|||
index: 47, |
|||
chapterId: '48', |
|||
name: '第48章 青木长生' |
|||
}, |
|||
{ |
|||
index: 48, |
|||
chapterId: '49', |
|||
name: '第49章 封印解密' |
|||
}, |
|||
{ |
|||
index: 49, |
|||
chapterId: '50', |
|||
name: '第50章 临时小队' |
|||
}, |
|||
{ |
|||
index: 50, |
|||
chapterId: '51', |
|||
name: '第51章 梦中警示' |
|||
}, |
|||
{ |
|||
index: 51, |
|||
chapterId: '52', |
|||
name: '第52章 封印松动' |
|||
}, |
|||
{ |
|||
index: 52, |
|||
chapterId: '53', |
|||
name: '第53章 血脉真相' |
|||
}, |
|||
{ |
|||
index: 53, |
|||
chapterId: '54', |
|||
name: '第54章 故人重逢' |
|||
}, |
|||
{ |
|||
index: 54, |
|||
chapterId: '55', |
|||
name: '第55章 青帝遗藏' |
|||
}, |
|||
{ |
|||
index: 55, |
|||
chapterId: '56', |
|||
name: '第56章 青帝归来' |
|||
}, |
|||
{ |
|||
index: 56, |
|||
chapterId: '57', |
|||
name: '第57章 继承' |
|||
}, |
|||
{ |
|||
index: 57, |
|||
chapterId: '58', |
|||
name: '第58章 月食' |
|||
}, |
|||
{ |
|||
index: 58, |
|||
chapterId: '59', |
|||
name: '第59章 异变' |
|||
}, |
|||
{ |
|||
index: 59, |
|||
chapterId: '60', |
|||
name: '第60章 躁动' |
|||
}, |
|||
{ |
|||
index: 60, |
|||
chapterId: '61', |
|||
name: '第61章 血池' |
|||
}, |
|||
{ |
|||
index: 61, |
|||
chapterId: '62', |
|||
name: '第62章 共鸣' |
|||
}, |
|||
{ |
|||
index: 62, |
|||
chapterId: '63', |
|||
name: '第63章 轮回' |
|||
}, |
|||
{ |
|||
index: 63, |
|||
chapterId: '64', |
|||
name: '第64章 囚徒' |
|||
}, |
|||
{ |
|||
index: 64, |
|||
chapterId: '65', |
|||
name: '第65章 分裂的同盟' |
|||
}, |
|||
{ |
|||
index: 65, |
|||
chapterId: '66', |
|||
name: '第66章 祭祀' |
|||
}, |
|||
{ |
|||
index: 66, |
|||
chapterId: '67', |
|||
name: '第67章 千毒窟救援' |
|||
}, |
|||
{ |
|||
index: 67, |
|||
chapterId: '68', |
|||
name: '第68章 白帝城' |
|||
}, |
|||
{ |
|||
index: 68, |
|||
chapterId: '69', |
|||
name: '第69章 白帝疑云' |
|||
}, |
|||
{ |
|||
index: 69, |
|||
chapterId: '70', |
|||
name: '第70章 白帝七卫' |
|||
}, |
|||
{ |
|||
index: 70, |
|||
chapterId: '71', |
|||
name: '第71章 核心' |
|||
}, |
|||
{ |
|||
index: 71, |
|||
chapterId: '72', |
|||
name: '第72章 平衡' |
|||
}, |
|||
{ |
|||
index: 72, |
|||
chapterId: '73', |
|||
name: '第73章 数据' |
|||
}, |
|||
{ |
|||
index: 73, |
|||
chapterId: '74', |
|||
name: '第74章 中枢密室' |
|||
}, |
|||
{ |
|||
index: 74, |
|||
chapterId: '75', |
|||
name: '第75章 崩塌' |
|||
}, |
|||
{ |
|||
index: 75, |
|||
chapterId: '76', |
|||
name: '第76章 异星迷途' |
|||
}, |
|||
{ |
|||
index: 76, |
|||
chapterId: '77', |
|||
name: '第77章 卫星避难所' |
|||
}, |
|||
{ |
|||
index: 77, |
|||
chapterId: '78', |
|||
name: '第78章 启动' |
|||
}, |
|||
{ |
|||
index: 78, |
|||
chapterId: '79', |
|||
name: '第79章 玄冰劫火' |
|||
}, |
|||
{ |
|||
index: 79, |
|||
chapterId: '80', |
|||
name: '第80章 晶劫之战' |
|||
}, |
|||
{ |
|||
index: 80, |
|||
chapterId: '81', |
|||
name: '第81章 归墟之秘' |
|||
}, |
|||
{ |
|||
index: 81, |
|||
chapterId: '82', |
|||
name: '第82章 两界之子' |
|||
}, |
|||
{ |
|||
index: 82, |
|||
chapterId: '83', |
|||
name: '第83章 北冥寒渊' |
|||
}, |
|||
{ |
|||
index: 83, |
|||
chapterId: '84', |
|||
name: '第84章 最后一块' |
|||
}, |
|||
{ |
|||
index: 84, |
|||
chapterId: '85', |
|||
name: '第85章 终局之战' |
|||
}, |
|||
{ |
|||
index: 85, |
|||
chapterId: '86', |
|||
name: '第86章 异常' |
|||
}, |
|||
{ |
|||
index: 86, |
|||
chapterId: '87', |
|||
name: '第87章 遗址' |
|||
}, |
|||
{ |
|||
index: 87, |
|||
chapterId: '88', |
|||
name: '第88章 灵晶潮汐' |
|||
}, |
|||
{ |
|||
index: 88, |
|||
chapterId: '89', |
|||
name: '第89章 晶界迷踪' |
|||
} |
|||
]; |
|||
|
|||
// 正确导出模块
|
|||
export default { |
|||
chaptersList |
|||
}; |
|||
|
|||
// 也支持默认导出
|
|||
export { chaptersList }; |
@ -0,0 +1,124 @@ |
|||
<template> |
|||
<view class="progress-container"> |
|||
<view class="progress-container2" id="progress" @touchstart="touchstart" @touchend="touchend" @touchmove="touchmove"> |
|||
<view class="progress-box"> |
|||
<progress :percent="percent" activeColor="#000" backgroundColor="#1c1c1c" stroke-width="3"/> |
|||
</view> |
|||
<view class="ball-box" :class="{bigger: isTouch, shadow: isTouch}" :style="{left: `${percent}%`}"></view> |
|||
</view> |
|||
|
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
props:{ |
|||
total: { |
|||
type: Number, |
|||
default: 1 |
|||
}, |
|||
index: { |
|||
type: Number, |
|||
default: 0 |
|||
} |
|||
}, |
|||
data() { |
|||
return { |
|||
left: 0, //进度条最左侧位置 |
|||
right: 0, //进度条最右侧位置 |
|||
isTouch: false, |
|||
// touchTimer: null, //用于触摸节流 |
|||
percent: 0, |
|||
} |
|||
}, |
|||
watch:{ |
|||
index() { |
|||
this.percent = this.index / this.total * 100 |
|||
} |
|||
}, |
|||
mounted() { |
|||
this.percent = this.index / this.total * 100 |
|||
this.getLocation() |
|||
}, |
|||
methods: { |
|||
|
|||
getLocation() { |
|||
const query = uni.createSelectorQuery().in(this); |
|||
query.select('#progress').boundingClientRect(data => { |
|||
this.left = data.left |
|||
this.right = data.right |
|||
}).exec(); |
|||
}, |
|||
|
|||
touchstart() { |
|||
this.isTouch = true |
|||
this.$emit('progressStart') |
|||
}, |
|||
|
|||
touchend(e) { |
|||
this.isTouch = false |
|||
let index = this.calcIndex(e.changedTouches[0].clientX) |
|||
this.$emit('progressEnd', index) |
|||
this.percent = index / this.total * 100 |
|||
}, |
|||
|
|||
touchmove(e) { |
|||
// if (!this.touchTimer) { |
|||
let index = this.calcIndex(e.touches[0].clientX) |
|||
this.$emit('indexChange', index) |
|||
this.percent = index / this.total * 100 |
|||
// this.touchTimer = setTimeout(() => { |
|||
// this.touchTimer = null; |
|||
// }, 100) |
|||
// } |
|||
}, |
|||
|
|||
/** |
|||
* 输入位置计算index |
|||
**/ |
|||
calcIndex(px) { |
|||
let single = (this.right - this.left) / this.total |
|||
let index = Math.round((px - this.left) / single) |
|||
index = index < 0 ? 0 : index |
|||
index = index > this.total ? this.total : index |
|||
return index |
|||
} |
|||
|
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style lang="scss" scoped> |
|||
.progress-container{ |
|||
padding: 0 10px; |
|||
width: 100%; |
|||
height: 100%; |
|||
.progress-container2{ |
|||
position: relative; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
width: 100%; |
|||
height: 100%; |
|||
.progress-box{ |
|||
width: 100%; |
|||
} |
|||
.ball-box{ |
|||
position: absolute; |
|||
width: 10px; |
|||
height: 10px; |
|||
border-radius: 50%; |
|||
background-color: #000; |
|||
transform: translateX(-50%); |
|||
} |
|||
.shadow{ |
|||
box-shadow: 0px 0px 1px 5px rgba(#888,.4); |
|||
} |
|||
.bigger{ |
|||
width: 20px; |
|||
height: 20px; |
|||
} |
|||
} |
|||
|
|||
} |
|||
</style> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,134 @@ |
|||
<template> |
|||
<view class="virtual-list" style="position: relative;"> |
|||
<movable-area style="position: absolute;right: 0;width: 30px;height: 100%;"> |
|||
<movable-view class="action-bar-box" direction="vertical" @change="change" :y="y" :animation="false"> |
|||
<view style="border-bottom: #000 solid 2px;width: 100%;"></view> |
|||
<view style="border-bottom: #000 solid 2px;width: 100%;"></view> |
|||
</movable-view> |
|||
</movable-area> |
|||
<scroll-view scroll-y="true" |
|||
:style="{ |
|||
'height': scrollHeight + 'px', |
|||
'position': 'relative', |
|||
'zIndex': 1 |
|||
}" |
|||
@scroll="handleScroll" :scroll-top="scrollTop" :show-scrollbar="false"> |
|||
|
|||
<view class="scroll-bar" |
|||
:style="{ |
|||
'height': localHeight + 'px' |
|||
}"></view> |
|||
<view class="list" |
|||
:style="{ |
|||
'transform': `translateY(${offset}px)` |
|||
}"> |
|||
<view class="item-wrap" |
|||
v-for="(item, index) in visibleData" |
|||
:key="index"> |
|||
<slot :item="item" :active="active"></slot> |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'VirtualList', |
|||
props: { |
|||
// 所有的items |
|||
items: Array, |
|||
// 可视区域的item数量 |
|||
remain: Number, |
|||
// item大小 |
|||
size: Number, |
|||
// 当前章节 |
|||
active: Number, |
|||
// 可使区域高度 |
|||
scrollHeight: Number |
|||
}, |
|||
data() { |
|||
return { |
|||
// 起始 |
|||
start: 0, |
|||
// 结束 |
|||
end: this.remain, |
|||
// list 偏移量 |
|||
offset: 0, |
|||
scrollTop: 0, |
|||
y: 0 |
|||
} |
|||
}, |
|||
created() { |
|||
//当前章节滚动至顶部 |
|||
this.scrollTop = this.size * this.active |
|||
}, |
|||
computed: { |
|||
// 预留项 |
|||
preCount() { |
|||
return Math.min(this.start, this.remain); |
|||
}, |
|||
nextCount() { |
|||
return Math.min(this.items.length - this.end, this.remain); |
|||
}, |
|||
// 可视区域的item |
|||
visibleData() { |
|||
const start = this.start - this.preCount; |
|||
const end = this.end + this.nextCount; |
|||
console.log(this.items,'this.items.slice(start, end)'); |
|||
return this.items.slice(start, end); |
|||
}, |
|||
localHeight() { |
|||
return this.items.length * this.size |
|||
} |
|||
}, |
|||
methods: { |
|||
change(e) { |
|||
if (e.detail.source !== 'touch') { |
|||
return |
|||
} |
|||
let y = e.detail.y; |
|||
let scroll = y/(this.scrollHeight-40)*(this.localHeight-this.scrollHeight); |
|||
scroll = scroll < 0 ? 0 : scroll; |
|||
this.scrollTop = scroll; |
|||
}, |
|||
handleScroll(ev) { |
|||
const scrollTop = ev.detail.scrollTop; |
|||
this.y = scrollTop/(this.localHeight-this.scrollHeight)*(this.scrollHeight-40) |
|||
// 开始位置 |
|||
const start = Math.floor(scrollTop / this.size) |
|||
this.start = start < 0 ? 0 : start; |
|||
// 结束位置 |
|||
this.end = this.start + this.remain; |
|||
// 计算偏移 |
|||
const offset = scrollTop - (scrollTop % this.size) - this.preCount * this.size |
|||
this.offset = offset < 0 ? 0 : offset; |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped> |
|||
|
|||
.list { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
} |
|||
.action-bar-box{ |
|||
padding: 3px; |
|||
display: flex; |
|||
flex-flow: column; |
|||
justify-content: space-around; |
|||
align-items: center; |
|||
position: absolute; |
|||
right: 0; |
|||
background-color: transparent; |
|||
border-radius: 10rpx; |
|||
box-shadow: 0 0 5px #000; |
|||
width: 20px; |
|||
height: 40px; |
|||
z-index:2; |
|||
} |
|||
</style> |
@ -0,0 +1,249 @@ |
|||
// ... existing code ... |
|||
<template> |
|||
<view class="content"> |
|||
<!-- 顶部导航 --> |
|||
<!-- <view class="top-nav"> |
|||
<view class="back-btn" @click="goBack">返回</view> |
|||
<view class="add-book-btn" @click="addToBookshelf">+ 加入书架</view> |
|||
</view> --> |
|||
<BackButton /> |
|||
<!-- 封面图 --> |
|||
<image class="cover-image" :src="showImg('/uploads/20250918/478322390dfe8befd6fb30643e1b5cb1.png')" |
|||
mode="aspectFill"></image> |
|||
|
|||
<!-- 小说信息 --> |
|||
<view class="book-info"> |
|||
<text class="title">{{ bookTitle }}</text> |
|||
<view class="info-row"> |
|||
<text class="info-text">{{ updateInfo }} | 字数: {{ wordCount }}万字</text> |
|||
</view> |
|||
|
|||
<!-- 更新进度 --> |
|||
<view class="progress-row"> |
|||
<text class="progress-label">更新进度:</text> |
|||
<text class="progress-text">{{ progressText }}</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 分类标签 --> |
|||
<view class="tags-container"> |
|||
<view class="tag-item" v-for="tag in tags" :key="tag">{{ tag }}</view> |
|||
</view> |
|||
<view class="line"> |
|||
|
|||
</view> |
|||
<!-- 简介 --> |
|||
<view class="intro-container"> |
|||
<text class="intro-text">{{ intro }}</text> |
|||
</view> |
|||
|
|||
<!-- 按钮组 --> |
|||
<view class="button-group"> |
|||
<view class="read-btn" @click="startReading"> |
|||
<text class="btn-icon">📖</text> |
|||
<text class="btn-text">开始阅读</text> |
|||
</view> |
|||
<view class="read-btn" @click="playAudio"> |
|||
<text class="btn-icon">🎧</text> |
|||
<text class="btn-text">点击播放</text> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
data() { |
|||
return { |
|||
bookTitle: '《园门修真传》', |
|||
updateInfo: '1天前更新', |
|||
wordCount: '20', |
|||
progressText: '第八十九章(未完结)', |
|||
tags: ['穿越玄幻', '修真进阶', '古典园林', '热血爽文'], |
|||
intro: '张锁仙意外穿越修仙世界,凭借着自己的扫地圣体本以为会在宗门大开杀戒,没想到竟被扔到山门做个杂役弟子?从此过上了钓鱼种田的不被卷生活。可偏偏宗门大会要拉他一个没有灵力的人上场看笑话?' |
|||
} |
|||
}, |
|||
onLoad() { |
|||
|
|||
}, |
|||
methods: { |
|||
goBack() { |
|||
uni.navigateBack(); |
|||
}, |
|||
addToBookshelf() { |
|||
uni.showToast({ |
|||
title: '已加入书架', |
|||
icon: 'success' |
|||
}); |
|||
}, |
|||
startReading() { |
|||
uni.navigateTo({ |
|||
url: '/subPackages/other/read' |
|||
}); |
|||
}, |
|||
playAudio() { |
|||
uni.navigateTo({ |
|||
url: '/subPackages/other/novelCatalog' |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style> |
|||
.content { |
|||
display: flex; |
|||
flex-direction: column; |
|||
align-items: center; |
|||
justify-content: center; |
|||
padding: 20rpx; |
|||
background-color: #F6EBD4; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
.top-nav { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
align-items: center; |
|||
padding: 20rpx 30rpx; |
|||
position: relative; |
|||
z-index: 10; |
|||
background-color: #ffffff; |
|||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.back-btn { |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
} |
|||
|
|||
.add-book-btn { |
|||
font-size: 28rpx; |
|||
color: #e64340; |
|||
background-color: #fff; |
|||
border: 1rpx solid #e64340; |
|||
border-radius: 20rpx; |
|||
padding: 8rpx 16rpx; |
|||
} |
|||
|
|||
.cover-image { |
|||
width: 300rpx; |
|||
height: 400rpx; |
|||
margin: 30rpx auto; |
|||
border-radius: 16rpx; |
|||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1); |
|||
} |
|||
|
|||
.book-info { |
|||
width: 100%; |
|||
text-align: center; |
|||
padding: 20rpx 0; |
|||
} |
|||
|
|||
.title { |
|||
font-size: 48rpx; |
|||
font-weight: bold; |
|||
color: #333; |
|||
line-height: 1.4; |
|||
} |
|||
|
|||
.info-row { |
|||
font-size: 28rpx; |
|||
color: #666; |
|||
margin: 10rpx 0; |
|||
} |
|||
|
|||
.progress-row { |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
font-size: 28rpx; |
|||
color: #999; |
|||
margin-top: 10rpx; |
|||
} |
|||
|
|||
.progress-label { |
|||
margin-right: 10rpx; |
|||
} |
|||
|
|||
.progress-text { |
|||
color: #333; |
|||
} |
|||
|
|||
.tags-container { |
|||
display: flex; |
|||
flex-wrap: wrap; |
|||
justify-content: center; |
|||
gap: 15rpx; |
|||
margin: 30rpx 0; |
|||
} |
|||
|
|||
.tag-item { |
|||
background-color: #76352D; |
|||
color: #fff; |
|||
padding: 6rpx 12rpx; |
|||
border-radius: 12rpx; |
|||
font-size: 24rpx; |
|||
} |
|||
|
|||
.line { |
|||
width: 100%; |
|||
height: 1rpx; |
|||
/* border: 1rpx solid #d3d3d3; */ |
|||
background: #d3d3d3; |
|||
margin: 20rpx 0; |
|||
} |
|||
|
|||
.intro-container { |
|||
width: 100%; |
|||
padding: 20rpx 40rpx; |
|||
/* background-color: #ffffff; */ |
|||
border-radius: 16rpx; |
|||
/* box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.1); */ |
|||
margin-bottom: 40rpx; |
|||
} |
|||
|
|||
.intro-text { |
|||
font-size: 22rpx; |
|||
color: #333; |
|||
line-height: 1.8; |
|||
text-align: left; |
|||
} |
|||
|
|||
.button-group { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: space-around; |
|||
gap: 30rpx; |
|||
} |
|||
|
|||
.read-btn, |
|||
.play-btn { |
|||
display: flex; |
|||
align-items: center; |
|||
justify-content: center; |
|||
width: 300rpx; |
|||
height: 80rpx; |
|||
border-radius: 40rpx; |
|||
font-size: 32rpx; |
|||
color: #333; |
|||
background-color: #ffffff; |
|||
border: 1rpx solid #ddd; |
|||
} |
|||
|
|||
.read-btn { |
|||
border-color: #e64340; |
|||
color: #e64340; |
|||
} |
|||
|
|||
.play-btn { |
|||
border-color: #888; |
|||
color: #888; |
|||
} |
|||
|
|||
.btn-icon { |
|||
font-size: 36rpx; |
|||
margin-right: 10rpx; |
|||
} |
|||
</style> |
@ -0,0 +1,48 @@ |
|||
<template> |
|||
<view class="content"> |
|||
<!-- <BackButton /> |
|||
<view class=""> |
|||
园门修真转 |
|||
</view> --> |
|||
|
|||
<scroll-view scroll-y="true" @scroll="handleScroll" :scroll-top="scrollTop" :show-scrollbar="false"> |
|||
<view class="chapter" v-for="item in directoryList" :key="item.chapterId" @click="changePlay"> |
|||
<view class=""> |
|||
{{item.name}} |
|||
</view> |
|||
</view> |
|||
</scroll-view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
import directoryModule from './components/directory.js' |
|||
// 正确解构 directoryList |
|||
const directoryListJs = directoryModule.chaptersList || directoryModule.default?.chaptersList || []; |
|||
export default { |
|||
data() { |
|||
return { |
|||
directoryList: directoryListJs, |
|||
} |
|||
}, |
|||
methods: { |
|||
changePlay(){ |
|||
uni.navigateTo({ |
|||
url:'/subPackages/other/playNovel' |
|||
}) |
|||
}, |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<style scoped lang="scss"> |
|||
.content{ |
|||
padding: 30rpx; |
|||
background: #595959; |
|||
color: #fff; |
|||
.chapter{ |
|||
padding-bottom: 30rpx; |
|||
font-size: 28rpx; |
|||
} |
|||
} |
|||
</style> |
@ -0,0 +1,448 @@ |
|||
<template> |
|||
<view class="novel-player-container"> |
|||
<!-- 1. 小说封面图区域(适配多端屏幕比例) --> |
|||
<view class="novel-cover-wrapper"> |
|||
<!-- 封面图:加载态+失败备用 --> |
|||
<image |
|||
class="novel-cover" |
|||
:class="{ 'cover-loading': isCoverLoading }" |
|||
:src="showImg('/uploads/20250918/478322390dfe8befd6fb30643e1b5cb1.png')" |
|||
mode="widthFix" |
|||
@load="isCoverLoading = false" |
|||
@error="handleCoverError" |
|||
lazy-load |
|||
></image> |
|||
<!-- 封面加载失败备用视图 --> |
|||
<view class="cover-fallback" v-if="isCoverError"> |
|||
<uni-icons type="image" size="40" color="#999"></uni-icons> |
|||
<text class="fallback-text">封面加载失败</text> |
|||
</view> |
|||
</view> |
|||
|
|||
<!-- 2. 小说信息区域 --> |
|||
<view class="novel-info"> |
|||
<text class="novel-title">园界修真传</text> |
|||
<text class="novel-chapter">第1章:考研失败是有原因的</text> |
|||
</view> |
|||
|
|||
<!-- 3. 音频核心控制区域 --> |
|||
<view class="audio-player"> |
|||
<!-- Uniapp 音频组件(替代原生audio,支持多端) --> |
|||
<uni-audio |
|||
ref="audioRef" |
|||
:src="audioUrl" |
|||
:preload="preloadMode" |
|||
:initial-time="0" |
|||
@play="updatePlayStatus(true)" |
|||
@pause="updatePlayStatus(false)" |
|||
@ended="handleAudioEnd" |
|||
@timeupdate="updateProgress" |
|||
@error="handleAudioError" |
|||
hidden |
|||
></uni-audio> |
|||
|
|||
<!-- 播放/暂停按钮 --> |
|||
<button |
|||
class="play-btn" |
|||
:disabled="isAudioLoading" |
|||
@click="togglePlayPause" |
|||
hover-class="play-btn-hover" |
|||
> |
|||
<uni-icons |
|||
:type="isPlaying ? 'pause' : 'play'" |
|||
size="24" |
|||
color="#fff" |
|||
></uni-icons> |
|||
</button> |
|||
|
|||
<!-- 进度条控制(支持点击+拖动) --> |
|||
<view class="progress-container" @click="handleProgressClick"> |
|||
<!-- 已播放进度 --> |
|||
<view |
|||
class="progress-played" |
|||
:style="{ width: `${progressPercent}%` }" |
|||
></view> |
|||
<!-- 进度滑块 --> |
|||
<view |
|||
class="progress-thumb" |
|||
:style="{ left: `${progressPercent}%` }" |
|||
@touchstart="startDragProgress" |
|||
@touchmove="onDragProgress" |
|||
@touchend="endDragProgress" |
|||
></view> |
|||
</view> |
|||
|
|||
<!-- 时间显示(已播放/总时长) --> |
|||
<view class="time-display"> |
|||
<text>{{ formatTime(currentTime) }}</text> |
|||
<text class="time-split">/</text> |
|||
<text>{{ formatTime(totalTime) }}</text> |
|||
</view> |
|||
|
|||
<!-- 音量控制 --> |
|||
<view class="volume-control"> |
|||
<uni-icons |
|||
:type="isMuted ? 'volume-off' : 'volume-up'" |
|||
size="18" |
|||
color="#666" |
|||
@click="toggleMute" |
|||
></uni-icons> |
|||
<slider |
|||
class="volume-slider" |
|||
min="0" |
|||
max="100" |
|||
:value="currentVolume" |
|||
@change="adjustVolume" |
|||
activeColor="#32c5ff" |
|||
></slider> |
|||
</view> |
|||
|
|||
<!-- 清晰度选择(模拟多音质切换) --> |
|||
<view class="quality-select"> |
|||
<text class="quality-label">清晰度:</text> |
|||
<picker |
|||
class="quality-picker" |
|||
:value="selectedQuality" |
|||
:range="qualityOptions" |
|||
:range-key="'label'" |
|||
@change="switchAudioQuality" |
|||
> |
|||
<text>{{ qualityOptions.find(item => item.value === selectedQuality).label }}</text> |
|||
<uni-icons type="down" size="14" color="#666" class="picker-icon"></uni-icons> |
|||
</picker> |
|||
</view> |
|||
</view> |
|||
</view> |
|||
</template> |
|||
|
|||
<script> |
|||
export default { |
|||
name: 'PlayNovel', |
|||
data() { |
|||
return { |
|||
// 图片配置 |
|||
coverUrl: '/static/image.png', // 封面图路径(Uniapp 静态资源放static目录) |
|||
isCoverLoading: true, // 封面加载状态 |
|||
isCoverError: false, // 封面加载失败状态 |
|||
|
|||
// 音频配置 |
|||
audioUrl: 'https://des.js-dyyj.com/data/2025/09/05/9875a62d-14ef-481e-b88f-19c061478ce6.MP3', // 音频文件路径(需替换为实际地址,支持本地/CDN) |
|||
preloadMode: 'auto', // 预加载策略:auto/metadata/none |
|||
isAudioLoading: false, // 音频加载状态 |
|||
isPlaying: false, // 播放状态 |
|||
currentTime: 0, // 当前播放时间(秒) |
|||
totalTime: 0, // 音频总时长(秒) |
|||
progressPercent: 0, // 播放进度百分比(0-100) |
|||
currentVolume: 80, // 当前音量(0-100) |
|||
isMuted: false, // 静音状态 |
|||
isDragging: false, // 进度条拖动状态 |
|||
|
|||
// 清晰度配置(需后端提供对应音质的音频地址) |
|||
qualityOptions: [ |
|||
{ label: '标准', value: 'low' }, |
|||
{ label: '高清', value: 'medium' }, |
|||
{ label: '无损', value: 'high' } |
|||
], |
|||
selectedQuality: 'medium' // 默认高清 |
|||
}; |
|||
}, |
|||
onReady() { |
|||
// Uniapp 组件就绪后获取音频实例 |
|||
this.audioRef = this.$refs.audioRef; |
|||
// 初始化音量 |
|||
this.audioRef.setVolume(this.currentVolume / 100); |
|||
// 初始化音频地址(可根据清晰度默认值设置) |
|||
this.switchAudioQuality({ detail: { value: 'medium' } }); |
|||
}, |
|||
onUnload() { |
|||
// 页面卸载时停止播放,释放资源 |
|||
if (this.audioRef) { |
|||
this.audioRef.pause(); |
|||
} |
|||
}, |
|||
methods: { |
|||
// ---------------------- 图片相关方法 ---------------------- |
|||
handleCoverError() { |
|||
this.isCoverLoading = false; |
|||
this.isCoverError = true; |
|||
}, |
|||
|
|||
// ---------------------- 音频核心控制 ---------------------- |
|||
// 切换播放/暂停 |
|||
togglePlayPause() { |
|||
if (this.isPlaying) { |
|||
this.audioRef.pause(); |
|||
} else { |
|||
this.isAudioLoading = true; |
|||
this.audioRef.play().catch(err => { |
|||
console.error('播放失败:', err); |
|||
this.isAudioLoading = false; |
|||
uni.showToast({ title: '音频播放失败', icon: 'none' }); |
|||
}); |
|||
} |
|||
}, |
|||
|
|||
// 更新播放状态 |
|||
updatePlayStatus(isPlaying) { |
|||
this.isPlaying = isPlaying; |
|||
this.isAudioLoading = false; |
|||
}, |
|||
|
|||
// 音频播放完毕(可扩展下一章逻辑) |
|||
handleAudioEnd() { |
|||
this.isPlaying = false; |
|||
this.currentTime = 0; |
|||
this.progressPercent = 0; |
|||
uni.showToast({ title: '本章播放完毕', icon: 'none' }); |
|||
// 如需自动下一章,可在此处调用章节切换逻辑 |
|||
}, |
|||
|
|||
// 音频加载失败 |
|||
handleAudioError() { |
|||
this.isAudioLoading = false; |
|||
uni.showToast({ title: '音频加载失败', icon: 'none' }); |
|||
}, |
|||
|
|||
// ---------------------- 进度条控制 ---------------------- |
|||
// 实时更新进度 |
|||
updateProgress() { |
|||
if (!this.isDragging) { |
|||
this.currentTime = this.audioRef.currentTime; |
|||
this.totalTime = this.audioRef.duration || 0; |
|||
this.progressPercent = (this.currentTime / this.totalTime) * 100 || 0; |
|||
} |
|||
}, |
|||
|
|||
// 点击进度条跳转 |
|||
handleProgressClick(e) { |
|||
const containerWidth = e.currentTarget.offsetWidth; |
|||
const clickLeft = e.touches[0].clientX - e.currentTarget.offsetLeft; |
|||
const percent = (clickLeft / containerWidth) * 100; |
|||
this.setProgress(percent); |
|||
}, |
|||
|
|||
// 开始拖动进度条 |
|||
startDragProgress() { |
|||
this.isDragging = true; |
|||
}, |
|||
|
|||
// 拖动进度条中 |
|||
onDragProgress(e) { |
|||
if (!this.isDragging) return; |
|||
const containerWidth = e.currentTarget.offsetWidth; |
|||
const dragLeft = e.touches[0].clientX - e.currentTarget.offsetLeft; |
|||
let percent = (dragLeft / containerWidth) * 100; |
|||
// 限制进度在0-100之间 |
|||
percent = Math.max(0, Math.min(100, percent)); |
|||
this.progressPercent = percent; |
|||
}, |
|||
|
|||
// 结束拖动进度条 |
|||
endDragProgress() { |
|||
this.isDragging = false; |
|||
this.setProgress(this.progressPercent); |
|||
}, |
|||
|
|||
// 设置进度(通用方法) |
|||
setProgress(percent) { |
|||
const targetTime = (percent / 100) * this.totalTime; |
|||
this.audioRef.seek(targetTime); |
|||
this.currentTime = targetTime; |
|||
this.progressPercent = percent; |
|||
}, |
|||
|
|||
// ---------------------- 音量控制 ---------------------- |
|||
// 切换静音 |
|||
toggleMute() { |
|||
this.isMuted = !this.isMuted; |
|||
this.audioRef.setMuted(this.isMuted); |
|||
}, |
|||
|
|||
// 调节音量 |
|||
adjustVolume(e) { |
|||
this.currentVolume = e.detail.value; |
|||
this.audioRef.setVolume(this.currentVolume / 100); |
|||
// 调节音量时自动取消静音 |
|||
if (this.isMuted && this.currentVolume > 0) { |
|||
this.isMuted = false; |
|||
this.audioRef.setMuted(false); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 清晰度切换 ---------------------- |
|||
switchAudioQuality(e) { |
|||
const quality = e.detail.value; |
|||
this.selectedQuality = quality; |
|||
// 此处需替换为实际音质对应的音频地址(示例格式) |
|||
const audioMap = { |
|||
low: '/static/audio/chapter1-low.mp3', |
|||
medium: '/static/audio/chapter1-medium.mp3', |
|||
high: '/static/audio/chapter1-high.mp3' |
|||
}; |
|||
this.audioUrl = audioMap[quality]; |
|||
// 切换音频后保持播放状态 |
|||
if (this.isPlaying) { |
|||
this.audioRef.play(); |
|||
} |
|||
}, |
|||
|
|||
// ---------------------- 工具方法 ---------------------- |
|||
// 格式化时间(秒 → 分:秒,如 8:16) |
|||
formatTime(seconds) { |
|||
if (!seconds) return '00:00'; |
|||
const min = Math.floor(seconds / 60); |
|||
const sec = Math.floor(seconds % 60); |
|||
// 补零处理(如 1 → 01) |
|||
return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; |
|||
} |
|||
} |
|||
}; |
|||
</script> |
|||
|
|||
<style scoped> |
|||
/* 容器整体样式 */ |
|||
.novel-player-container { |
|||
padding: 20rpx; |
|||
background: #595959; |
|||
min-height: 100vh; |
|||
} |
|||
|
|||
/* 封面图样式 */ |
|||
.novel-cover-wrapper { |
|||
width: 100%; |
|||
border-radius: 20rpx; |
|||
overflow: hidden; |
|||
margin-bottom: 30rpx; |
|||
position: relative; |
|||
} |
|||
.novel-cover { |
|||
width: 100%; |
|||
background-color: #f5f5f5; |
|||
} |
|||
.cover-loading { |
|||
opacity: 0.5; |
|||
} |
|||
.cover-fallback { |
|||
position: absolute; |
|||
top: 0; |
|||
left: 0; |
|||
width: 100%; |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: center; |
|||
align-items: center; |
|||
background-color: #f5f5f5; |
|||
} |
|||
.fallback-text { |
|||
margin-top: 20rpx; |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
} |
|||
|
|||
/* 小说信息样式 */ |
|||
.novel-info { |
|||
margin-bottom: 30rpx; |
|||
text-align: center; |
|||
} |
|||
.novel-title { |
|||
font-size: 36rpx; |
|||
font-weight: bold; |
|||
color: #fff; |
|||
margin-bottom: 10rpx; |
|||
display: block; |
|||
} |
|||
.novel-chapter { |
|||
font-size: 28rpx; |
|||
color: #fff; |
|||
display: block; |
|||
} |
|||
|
|||
/* 音频播放器样式 */ |
|||
.audio-player { |
|||
display: flex; |
|||
flex-direction: column; |
|||
gap: 25rpx; |
|||
} |
|||
|
|||
/* 播放按钮样式 */ |
|||
.play-btn { |
|||
width: 80rpx; |
|||
height: 80rpx; |
|||
border-radius: 50%; |
|||
background-color: #32c5ff; |
|||
display: flex; |
|||
justify-content: center; |
|||
align-items: center; |
|||
margin-bottom: 10rpx; |
|||
} |
|||
.play-btn-hover { |
|||
background-color: #28a4e0; |
|||
} |
|||
|
|||
/* 进度条样式 */ |
|||
.progress-container { |
|||
width: 100%; |
|||
height: 12rpx; |
|||
background-color: #eee; |
|||
border-radius: 6rpx; |
|||
position: relative; |
|||
touch-action: none; /* 防止移动端默认触摸行为 */ |
|||
} |
|||
.progress-played { |
|||
height: 100%; |
|||
background-color: #32c5ff; |
|||
border-radius: 6rpx; |
|||
} |
|||
.progress-thumb { |
|||
width: 24rpx; |
|||
height: 24rpx; |
|||
border-radius: 50%; |
|||
background-color: #32c5ff; |
|||
position: absolute; |
|||
top: 50%; |
|||
transform: translateY(-50%); |
|||
margin-left: -12rpx; |
|||
box-shadow: 0 0 10rpx rgba(50, 197, 255, 0.5); |
|||
} |
|||
|
|||
/* 时间显示样式 */ |
|||
.time-display { |
|||
font-size: 24rpx; |
|||
color: #999; |
|||
display: flex; |
|||
justify-content: space-between; |
|||
} |
|||
.time-split { |
|||
margin: 0 10rpx; |
|||
} |
|||
|
|||
/* 音量控制样式 */ |
|||
.volume-control { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15rpx; |
|||
} |
|||
.volume-slider { |
|||
flex: 1; |
|||
height: 8rpx; |
|||
} |
|||
|
|||
/* 清晰度选择样式 */ |
|||
.quality-select { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 15rpx; |
|||
font-size: 26rpx; |
|||
color: #666; |
|||
} |
|||
.quality-picker { |
|||
display: flex; |
|||
align-items: center; |
|||
gap: 8rpx; |
|||
color: #32c5ff; |
|||
} |
|||
.picker-icon { |
|||
margin-top: 4rpx; |
|||
} |
|||
</style> |
File diff suppressed because it is too large
Loading…
Reference in new issue