Initial Commit
This commit is contained in:
commit
1939755975
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Eslint config file
|
||||
* Documentation: https://eslint.org/docs/user-guide/configuring/
|
||||
* Install the Eslint extension before using this feature.
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
es6: true,
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
ecmaFeatures: {
|
||||
modules: true,
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
wx: true,
|
||||
App: true,
|
||||
Page: true,
|
||||
getCurrentPages: true,
|
||||
getApp: true,
|
||||
Component: true,
|
||||
requirePlugin: true,
|
||||
requireMiniProgram: true,
|
||||
},
|
||||
// extends: 'eslint:recommended',
|
||||
rules: {},
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
# Windows
|
||||
[Dd]esktop.ini
|
||||
Thumbs.db
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"pages": [
|
||||
"pages/login/index",
|
||||
"pages/index/index",
|
||||
"pages/bookshelf/add",
|
||||
"pages/logs/logs",
|
||||
"pages/bookshelf/list",
|
||||
"pages/notes/notes",
|
||||
"pages/profile/profile",
|
||||
"pages/bookshelf/reader/reader"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "阅读管理",
|
||||
"navigationBarBackgroundColor": "#ffffff"
|
||||
},
|
||||
"style": "v2",
|
||||
"componentFramework": "glass-easel",
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#1989fa",
|
||||
"backgroundColor": "#ffffff",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "images/tabbar/home.png",
|
||||
"selectedIconPath": "images/tabbar/home.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/bookshelf/list",
|
||||
"text": "书架"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/notes/notes",
|
||||
"text": "笔记"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/profile/profile",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// app.ts
|
||||
App<IAppOption>({
|
||||
globalData: {
|
||||
userInfo: null,
|
||||
isLoggedIn: false
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 检查登录状态
|
||||
const token = wx.getStorageSync('token');
|
||||
const userInfo = wx.getStorageSync('userInfo');
|
||||
|
||||
if (token && userInfo) {
|
||||
this.globalData.isLoggedIn = true;
|
||||
this.globalData.userInfo = userInfo;
|
||||
} else {
|
||||
// 未登录则跳转到登录页
|
||||
wx.redirectTo({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
}
|
||||
|
||||
// 登录
|
||||
wx.login({
|
||||
success: res => {
|
||||
console.log('微信登录成功,code:', res.code);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
// 检查登录状态的方法
|
||||
checkLogin() {
|
||||
if (!this.globalData.isLoggedIn) {
|
||||
wx.redirectTo({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
/**app.wxss**/
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 200rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,886 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
// 定义组件方法接口
|
||||
interface IComponentMethods {
|
||||
loadChapter: (chapter: number) => Promise<void>;
|
||||
splitPages: () => Promise<void>;
|
||||
prevPage: () => void;
|
||||
nextPage: () => void;
|
||||
touchStart: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
touchEnd: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleToolbar: () => void;
|
||||
toggleMenu: () => void;
|
||||
prevChapter: () => void;
|
||||
nextChapter: () => void;
|
||||
jumpToChapter: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
goBack: () => void;
|
||||
onScroll: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleSettings: () => void;
|
||||
decreaseFontSize: () => void;
|
||||
increaseFontSize: () => void;
|
||||
startTimeUpdate: () => void;
|
||||
calculateProgress: () => void;
|
||||
closeAllPanels: () => void;
|
||||
switchTheme: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
adjustBrightness: (e: WechatMiniprogram.SliderChange) => void;
|
||||
toggleAutoReading: (e: WechatMiniprogram.SwitchChange) => void;
|
||||
setAutoReadingInterval: (e: WechatMiniprogram.PickerChange) => void;
|
||||
startAutoReading: () => void;
|
||||
stopAutoReading: () => void;
|
||||
toggleBookmarks: () => void;
|
||||
addBookmark: () => void;
|
||||
loadBookmarks: () => void;
|
||||
jumpToBookmark: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleNotes: () => void;
|
||||
showTextMenu: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
addNote: () => void;
|
||||
loadNotes: () => void;
|
||||
copyText: () => void;
|
||||
startReadingTimer: () => void;
|
||||
updateReadingTime: () => void;
|
||||
loadTodayReadingTime: () => void;
|
||||
saveReadingProgress: () => void;
|
||||
restoreSettings: () => void;
|
||||
toggleMore: () => void;
|
||||
onSelectText: (e: any) => void;
|
||||
addBookmarkWithSelection: () => void;
|
||||
addNoteWithSelection: () => void;
|
||||
copySelectedText: () => void;
|
||||
hideTextMenu: () => void;
|
||||
startReadingTimer: () => void;
|
||||
stopReadingTimer: () => void;
|
||||
saveReadingRecord: (duration: number) => void;
|
||||
}
|
||||
|
||||
// 定义组件数据接口
|
||||
interface IComponentData {
|
||||
bookTitle: string;
|
||||
content: string;
|
||||
currentChapter: number;
|
||||
totalChapters: number;
|
||||
showToolbar: boolean;
|
||||
showMenu: boolean;
|
||||
scrollTop: number;
|
||||
chapters: number[];
|
||||
lastTapTime: number;
|
||||
pages: string[];
|
||||
currentPage: number;
|
||||
pageHeight: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
showSettings: boolean;
|
||||
minFontSize: number;
|
||||
maxFontSize: number;
|
||||
currentTime: string;
|
||||
readingProgress: number;
|
||||
themes: Array<{id: string; name: string; bg: string; color: string}>;
|
||||
currentTheme: string;
|
||||
brightness: number;
|
||||
autoReading: boolean;
|
||||
autoReadingInterval: number;
|
||||
showBookmarks: boolean;
|
||||
showNotes: boolean;
|
||||
showTextMenu: boolean;
|
||||
textMenuTop: number;
|
||||
textMenuLeft: number;
|
||||
selectedText: string;
|
||||
bookmarks: any[];
|
||||
notes: any[];
|
||||
readingStartTime: number;
|
||||
todayReadingTime: number;
|
||||
showMore: boolean;
|
||||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
readingDuration: number;
|
||||
isReading: boolean;
|
||||
readingTimer: any;
|
||||
}
|
||||
|
||||
// 定义组件属性接口
|
||||
interface IComponentProperties {
|
||||
bookId: string;
|
||||
bookUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
Component<IComponentData, IComponentProperties, IComponentMethods>({
|
||||
data: {
|
||||
bookTitle: '',
|
||||
content: '',
|
||||
currentChapter: 1,
|
||||
totalChapters: 2541,
|
||||
showToolbar: true,
|
||||
showMenu: false,
|
||||
scrollTop: 0,
|
||||
chapters: [] as number[],
|
||||
lastTapTime: 0,
|
||||
pages: [] as string[],
|
||||
currentPage: 0,
|
||||
pageHeight: 0,
|
||||
lineHeight: 36,
|
||||
fontSize: 32,
|
||||
showSettings: false,
|
||||
minFontSize: 24,
|
||||
maxFontSize: 48,
|
||||
currentTime: '',
|
||||
readingProgress: 0,
|
||||
themes: [
|
||||
{ id: 'default', name: '默认', bg: '#f4ecd8', color: '#333' },
|
||||
{ id: 'night', name: '夜间', bg: '#222', color: '#999' },
|
||||
{ id: 'green', name: '护眼', bg: '#cce8cf', color: '#333' },
|
||||
{ id: 'paper', name: '纸张', bg: '#e8e2d3', color: '#333' }
|
||||
],
|
||||
currentTheme: 'default',
|
||||
brightness: 100,
|
||||
autoReading: false,
|
||||
autoReadingInterval: 5000,
|
||||
showBookmarks: false,
|
||||
showNotes: false,
|
||||
showTextMenu: false,
|
||||
textMenuTop: 0,
|
||||
textMenuLeft: 0,
|
||||
selectedText: '',
|
||||
bookmarks: [] as any[],
|
||||
notes: [] as any[],
|
||||
readingStartTime: 0,
|
||||
todayReadingTime: 0,
|
||||
showMore: false,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1,
|
||||
readingDuration: 0,
|
||||
isReading: false,
|
||||
readingTimer: null as any,
|
||||
},
|
||||
|
||||
properties: {
|
||||
bookId: String,
|
||||
bookUrl: String,
|
||||
title: String
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({
|
||||
bookTitle: this.properties.title || '大爱仙尊',
|
||||
chapters: Array.from({length: 2541}, (_, i) => i + 1)
|
||||
});
|
||||
this.loadChapter(1).then(() => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
const savedFontSize = wx.getStorageSync('reader_font_size');
|
||||
if (savedFontSize) {
|
||||
this.setData({
|
||||
fontSize: savedFontSize
|
||||
});
|
||||
}
|
||||
this.startTimeUpdate();
|
||||
this.startReadingTimer();
|
||||
this.loadBookmarks();
|
||||
this.loadNotes();
|
||||
this.restoreSettings();
|
||||
|
||||
// 获取当前屏幕亮度
|
||||
wx.getScreenBrightness({
|
||||
success: (res) => {
|
||||
this.setData({
|
||||
brightness: Math.round(res.value * 100)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
detached() {
|
||||
if (this.data.readingTimer) {
|
||||
clearInterval(this.data.readingTimer);
|
||||
}
|
||||
this.updateReadingTime();
|
||||
this.saveReadingProgress();
|
||||
if (this.autoReadingTimer) {
|
||||
clearInterval(this.autoReadingTimer);
|
||||
}
|
||||
this.stopReadingTimer();
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
hide() {
|
||||
this.stopReadingTimer();
|
||||
},
|
||||
|
||||
show() {
|
||||
if (!this.data.readingDuration) {
|
||||
this.startReadingTimer();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载章节内容
|
||||
async loadChapter(chapter: number) {
|
||||
try {
|
||||
wx.showLoading({ title: '加载中...' });
|
||||
const res = await request.get('/common/getBookContent', {
|
||||
bookName: this.data.bookTitle,
|
||||
id: chapter
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
// 处理换行符
|
||||
const content = res.data.replace(/\\r\\n/g, '\n');
|
||||
this.setData({
|
||||
content,
|
||||
currentChapter: chapter // 更新当前章节号
|
||||
});
|
||||
// 分页处理
|
||||
await this.splitPages();
|
||||
this.calculateProgress();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载章节失败:', error);
|
||||
wx.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 分页处理
|
||||
async splitPages() {
|
||||
// 获取容器尺寸
|
||||
const query = this.createSelectorQuery();
|
||||
query.select('.content').boundingClientRect();
|
||||
const rect = await new Promise(resolve => query.exec(resolve));
|
||||
const containerHeight = rect[0].height;
|
||||
|
||||
// 计算每页能显示的行数
|
||||
const lineHeight = this.data.lineHeight;
|
||||
const linesPerPage = Math.floor((containerHeight - 160) / (lineHeight * 2 / 750 * wx.getSystemInfoSync().windowWidth));
|
||||
|
||||
// 按自然段落分割内容
|
||||
const paragraphs = this.data.content.split('\n').filter(p => p.trim());
|
||||
|
||||
// 分页
|
||||
const pages = [];
|
||||
let currentPage = [];
|
||||
let currentLines = 0;
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
// 计算段落需要的行数
|
||||
const paragraphLines = Math.ceil(paragraph.length * (this.data.fontSize / 32) / 20);
|
||||
|
||||
if (currentLines + paragraphLines > linesPerPage) {
|
||||
// 当前页放不下这段,新建一页
|
||||
if (currentPage.length > 0) {
|
||||
pages.push(currentPage.join('\n'));
|
||||
currentPage = [];
|
||||
currentLines = 0;
|
||||
}
|
||||
|
||||
// 如果单个段落超过一页
|
||||
if (paragraphLines > linesPerPage) {
|
||||
const chars = Math.floor(20 * linesPerPage);
|
||||
let p = paragraph;
|
||||
while (p.length > 0) {
|
||||
const pageContent = p.slice(0, chars);
|
||||
pages.push(pageContent);
|
||||
p = p.slice(chars);
|
||||
}
|
||||
} else {
|
||||
currentPage.push(paragraph);
|
||||
currentLines = paragraphLines;
|
||||
}
|
||||
} else {
|
||||
// 当前页能放下这段
|
||||
currentPage.push(paragraph);
|
||||
currentLines += paragraphLines;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一页
|
||||
if (currentPage.length > 0) {
|
||||
pages.push(currentPage.join('\n'));
|
||||
}
|
||||
|
||||
this.setData({
|
||||
pages,
|
||||
currentPage: 0,
|
||||
pageHeight: containerHeight
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
},
|
||||
|
||||
// 上一页
|
||||
prevPage() {
|
||||
if (this.data.currentPage > 0) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage - 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
} else if (this.data.currentChapter > 1) {
|
||||
// 上一章最后一页
|
||||
const prevChapter = this.data.currentChapter - 1;
|
||||
this.loadChapter(prevChapter).then(() => {
|
||||
this.setData({
|
||||
currentPage: this.data.pages.length - 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
nextPage() {
|
||||
if (this.data.currentPage < this.data.pages.length - 1) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage + 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
} else if (this.data.currentChapter < this.data.totalChapters) {
|
||||
// 下一章第一页
|
||||
const nextChapter = this.data.currentChapter + 1;
|
||||
this.loadChapter(nextChapter).then(() => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 触摸开始
|
||||
touchStart(e: any) {
|
||||
this.touchStartX = e.touches[0].pageX;
|
||||
},
|
||||
|
||||
// 触摸结束
|
||||
touchEnd(e: any) {
|
||||
const touchEndX = e.changedTouches[0].pageX;
|
||||
const diff = touchEndX - this.touchStartX;
|
||||
|
||||
if (Math.abs(diff) > 50) { // 滑动距离大于50px才触发翻页
|
||||
if (diff > 0) {
|
||||
this.prevPage();
|
||||
} else {
|
||||
this.nextPage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 切换工具栏显示
|
||||
toggleToolbar() {
|
||||
const now = Date.now();
|
||||
if (now - this.data.lastTapTime < 300) {
|
||||
// 双击
|
||||
this.setData({
|
||||
showToolbar: !this.data.showToolbar
|
||||
});
|
||||
}
|
||||
this.setData({ lastTapTime: now });
|
||||
},
|
||||
|
||||
// 切换目录显示
|
||||
toggleMenu() {
|
||||
this.setData({
|
||||
showMenu: !this.data.showMenu
|
||||
});
|
||||
},
|
||||
|
||||
// 上一章
|
||||
prevChapter() {
|
||||
if (this.data.currentChapter > 1) {
|
||||
this.loadChapter(this.data.currentChapter - 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 下一章
|
||||
nextChapter() {
|
||||
if (this.data.currentChapter < this.data.totalChapters) {
|
||||
this.loadChapter(this.data.currentChapter + 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到指定章节
|
||||
jumpToChapter(e: any) {
|
||||
const chapter = e.currentTarget.dataset.chapter;
|
||||
this.loadChapter(Number(chapter)); // 确保 chapter 是数字
|
||||
this.toggleMenu();
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
this.stopReadingTimer(); // 停止计时
|
||||
|
||||
// 打印阅读时间
|
||||
const minutes = Math.floor(this.data.readingDuration / 60);
|
||||
const seconds = this.data.readingDuration % 60;
|
||||
console.log(`本次阅读时长: ${minutes}分${seconds}秒`);
|
||||
console.log('详细信息:', {
|
||||
开始时间: new Date(this.data.readingStartTime).toLocaleString(),
|
||||
结束时间: new Date().toLocaleString(),
|
||||
总秒数: this.data.readingDuration,
|
||||
格式化时长: `${minutes}分${seconds}秒`
|
||||
});
|
||||
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
// 滚动处理
|
||||
onScroll(e: any) {
|
||||
// 可以在这里处理阅读进度保存等逻辑
|
||||
},
|
||||
|
||||
// 切换设置面板
|
||||
toggleSettings() {
|
||||
console.log('切换设置面板', this.data.showSettings);
|
||||
this.setData({
|
||||
showSettings: !this.data.showSettings,
|
||||
showMenu: false // 确保目录菜单关闭
|
||||
});
|
||||
},
|
||||
|
||||
// 减小字体
|
||||
decreaseFontSize() {
|
||||
if (this.data.fontSize > this.data.minFontSize) {
|
||||
const newSize = this.data.fontSize - 2;
|
||||
this.setData({
|
||||
fontSize: newSize
|
||||
}, () => {
|
||||
// 重新计算分页
|
||||
this.splitPages();
|
||||
// 保存设置到本地
|
||||
wx.setStorageSync('reader_font_size', newSize);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 增大字体
|
||||
increaseFontSize() {
|
||||
if (this.data.fontSize < this.data.maxFontSize) {
|
||||
const newSize = this.data.fontSize + 2;
|
||||
this.setData({
|
||||
fontSize: newSize
|
||||
}, () => {
|
||||
// 重新计算分页
|
||||
this.splitPages();
|
||||
// 保存设置到本地
|
||||
wx.setStorageSync('reader_font_size', newSize);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
startTimeUpdate() {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
this.setData({
|
||||
currentTime: `${hours}:${minutes}`
|
||||
});
|
||||
};
|
||||
|
||||
// 立即更新一次
|
||||
updateTime();
|
||||
// 每分钟更新一次
|
||||
this.timeInterval = setInterval(updateTime, 60000);
|
||||
},
|
||||
|
||||
// 计算阅读进度
|
||||
calculateProgress() {
|
||||
// 当前章节进度
|
||||
const currentChapterProgress = this.data.pages.length > 0
|
||||
? (this.data.currentPage + 1) / this.data.pages.length
|
||||
: 0;
|
||||
|
||||
// 总进度计算:(当前章节 - 1 + 当前章节阅读进度) / 总章节数
|
||||
const totalProgress = Math.floor(
|
||||
((this.data.currentChapter - 1 + currentChapterProgress) / this.data.totalChapters) * 100
|
||||
);
|
||||
|
||||
console.log('Progress calculation:', {
|
||||
currentChapter: this.data.currentChapter,
|
||||
totalChapters: this.data.totalChapters,
|
||||
currentPage: this.data.currentPage,
|
||||
totalPages: this.data.pages.length,
|
||||
chapterProgress: currentChapterProgress,
|
||||
totalProgress
|
||||
});
|
||||
|
||||
this.setData({
|
||||
readingProgress: totalProgress
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭所有面板
|
||||
closeAllPanels() {
|
||||
this.setData({
|
||||
showSettings: false,
|
||||
showMenu: false
|
||||
});
|
||||
},
|
||||
|
||||
// 切换主题
|
||||
switchTheme(e: any) {
|
||||
const themeId = e.currentTarget.dataset.theme;
|
||||
this.setData({
|
||||
currentTheme: themeId,
|
||||
showMore: false // 选择后关闭面板
|
||||
});
|
||||
wx.setStorageSync('reader_theme', themeId);
|
||||
},
|
||||
|
||||
// 调整屏幕亮度
|
||||
adjustBrightness(e: WechatMiniprogram.SliderChange) {
|
||||
const value = e.detail.value;
|
||||
console.log('调整亮度:', value); // 添加调试日志
|
||||
wx.setScreenBrightness({
|
||||
value: value / 100,
|
||||
success: () => {
|
||||
this.setData({ brightness: value });
|
||||
wx.setStorageSync('reader_brightness', value);
|
||||
console.log('亮度设置成功:', value);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('设置亮度失败:', error);
|
||||
wx.showToast({
|
||||
title: '设置亮度失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 自动阅读相关
|
||||
toggleAutoReading(e: any) {
|
||||
const isChecked = e.detail.value;
|
||||
this.setData({ autoReading: isChecked });
|
||||
if (isChecked) {
|
||||
this.startAutoReading();
|
||||
} else {
|
||||
this.stopAutoReading();
|
||||
}
|
||||
},
|
||||
|
||||
setAutoReadingInterval(e: any) {
|
||||
const interval = [3000, 5000, 8000, 10000][e.detail.value];
|
||||
this.setData({ autoReadingInterval: interval });
|
||||
if (this.data.autoReading) {
|
||||
this.stopAutoReading();
|
||||
this.startAutoReading();
|
||||
}
|
||||
},
|
||||
|
||||
startAutoReading() {
|
||||
this.autoReadingTimer = setInterval(() => {
|
||||
this.nextPage();
|
||||
}, this.data.autoReadingInterval);
|
||||
},
|
||||
|
||||
stopAutoReading() {
|
||||
if (this.autoReadingTimer) {
|
||||
clearInterval(this.autoReadingTimer);
|
||||
}
|
||||
},
|
||||
|
||||
// 书签相关
|
||||
toggleBookmarks() {
|
||||
this.setData({
|
||||
showBookmarks: !this.data.showBookmarks,
|
||||
showSettings: false,
|
||||
showNotes: false
|
||||
});
|
||||
},
|
||||
|
||||
addBookmark() {
|
||||
const bookmark = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: this.data.pages[this.data.currentPage].slice(0, 50),
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const bookmarks = [...this.data.bookmarks, bookmark];
|
||||
this.setData({ bookmarks });
|
||||
wx.setStorageSync(`bookmarks_${this.data.bookTitle}`, bookmarks);
|
||||
wx.showToast({ title: '添加书签成功', icon: 'success' });
|
||||
},
|
||||
|
||||
loadBookmarks() {
|
||||
const bookmarks = wx.getStorageSync(`bookmarks_${this.data.bookTitle}`) || [];
|
||||
this.setData({ bookmarks });
|
||||
},
|
||||
|
||||
jumpToBookmark(e: any) {
|
||||
const bookmark = e.currentTarget.dataset.bookmark;
|
||||
this.loadChapter(bookmark.chapter).then(() => {
|
||||
this.setData({
|
||||
currentPage: bookmark.page,
|
||||
showBookmarks: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 笔记相关
|
||||
toggleNotes() {
|
||||
this.setData({
|
||||
showNotes: !this.data.showNotes,
|
||||
showSettings: false,
|
||||
showBookmarks: false
|
||||
});
|
||||
},
|
||||
|
||||
showTextMenu(e: any) {
|
||||
const text = this.data.pages[this.data.currentPage];
|
||||
const start = 0;
|
||||
const end = text.length;
|
||||
|
||||
this.setData({
|
||||
selectionStart: start,
|
||||
selectionEnd: end,
|
||||
selectedText: text.substring(start, end),
|
||||
showTextMenu: true,
|
||||
textMenuTop: e.touches[0].clientY,
|
||||
textMenuLeft: e.touches[0].clientX
|
||||
});
|
||||
},
|
||||
|
||||
addNote() {
|
||||
wx.showModal({
|
||||
title: '添加笔记',
|
||||
editable: true,
|
||||
placeholderText: '请输入笔记内容',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const note = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: res.content,
|
||||
selectedText: this.data.selectedText,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const notes = [...this.data.notes, note];
|
||||
this.setData({
|
||||
notes,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`notes_${this.data.bookTitle}`, notes);
|
||||
wx.showToast({ title: '添加笔记成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadNotes() {
|
||||
const notes = wx.getStorageSync(`notes_${this.data.bookTitle}`) || [];
|
||||
this.setData({ notes });
|
||||
},
|
||||
|
||||
copyText() {
|
||||
wx.setClipboardData({
|
||||
data: this.data.selectedText,
|
||||
success: () => {
|
||||
this.setData({ showTextMenu: false });
|
||||
wx.showToast({ title: '复制成功', icon: 'success' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 阅读统计相关
|
||||
startReadingTimer() {
|
||||
this.setData({
|
||||
readingStartTime: Date.now(),
|
||||
isReading: true,
|
||||
readingDuration: 0
|
||||
});
|
||||
|
||||
// 每秒更新阅读时长
|
||||
this.data.readingTimer = setInterval(() => {
|
||||
const duration = Math.floor((Date.now() - this.data.readingStartTime) / 1000);
|
||||
this.setData({ readingDuration: duration });
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
updateReadingTime() {
|
||||
const duration = Math.floor((Date.now() - this.data.readingStartTime) / 1000);
|
||||
const today = new Date().toDateString();
|
||||
const newTodayTime = this.data.todayReadingTime + duration;
|
||||
this.setData({ todayReadingTime: newTodayTime });
|
||||
|
||||
let statistics = wx.getStorageSync('reading_statistics') || {};
|
||||
statistics[today] = newTodayTime;
|
||||
wx.setStorageSync('reading_statistics', statistics);
|
||||
},
|
||||
|
||||
loadTodayReadingTime() {
|
||||
const today = new Date().toDateString();
|
||||
const statistics = wx.getStorageSync('reading_statistics') || {};
|
||||
this.setData({ todayReadingTime: statistics[today] || 0 });
|
||||
},
|
||||
|
||||
// 保存和恢复设置
|
||||
saveReadingProgress() {
|
||||
const progress = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
wx.setStorageSync(`reading_progress_${this.data.bookTitle}`, progress);
|
||||
},
|
||||
|
||||
restoreSettings() {
|
||||
// 恢复主题
|
||||
const theme = wx.getStorageSync('reader_theme');
|
||||
if (theme) {
|
||||
this.setData({ currentTheme: theme });
|
||||
}
|
||||
|
||||
// 恢复亮度
|
||||
const brightness = wx.getStorageSync('reader_brightness');
|
||||
if (brightness !== undefined) {
|
||||
this.setData({ brightness });
|
||||
wx.setScreenBrightness({ value: brightness / 100 });
|
||||
}
|
||||
|
||||
// 恢复阅读进度
|
||||
const progress = wx.getStorageSync(`reading_progress_${this.data.bookTitle}`);
|
||||
if (progress) {
|
||||
this.loadChapter(progress.chapter).then(() => {
|
||||
this.setData({ currentPage: progress.page });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleMore() {
|
||||
this.setData({
|
||||
showMore: !this.data.showMore,
|
||||
showSettings: false,
|
||||
showMenu: false
|
||||
});
|
||||
},
|
||||
|
||||
// 处理文本选择事件
|
||||
onSelectText(e: any) {
|
||||
console.log('Selected text:', e.detail);
|
||||
const selectedText = e.detail.text;
|
||||
if (selectedText && selectedText.trim()) {
|
||||
this.setData({ selectedText });
|
||||
}
|
||||
},
|
||||
|
||||
// 隐藏文本菜单
|
||||
hideTextMenu() {
|
||||
this.setData({
|
||||
showTextMenu: false,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1
|
||||
});
|
||||
},
|
||||
|
||||
// 添加书签
|
||||
addBookmarkWithSelection() {
|
||||
const bookmark = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: this.data.selectedText,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const bookmarks = [...this.data.bookmarks, bookmark];
|
||||
this.setData({
|
||||
bookmarks,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`bookmarks_${this.data.bookTitle}`, bookmarks);
|
||||
wx.showToast({
|
||||
title: '添加书签成功',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 添加笔记
|
||||
addNoteWithSelection() {
|
||||
wx.showModal({
|
||||
title: '添加笔记',
|
||||
editable: true,
|
||||
placeholderText: '请输入笔记内容',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const note = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
selectedText: this.data.selectedText,
|
||||
content: res.content,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const notes = [...this.data.notes, note];
|
||||
this.setData({
|
||||
notes,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`notes_${this.data.bookTitle}`, notes);
|
||||
wx.showToast({
|
||||
title: '添加笔记成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 复制选中文本
|
||||
copySelectedText() {
|
||||
if (this.data.selectedText) {
|
||||
wx.setClipboardData({
|
||||
data: this.data.selectedText,
|
||||
success: () => {
|
||||
this.setData({ showTextMenu: false });
|
||||
wx.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 结束计时并保存
|
||||
stopReadingTimer() {
|
||||
if (!this.data.isReading) return;
|
||||
|
||||
if (this.data.readingTimer) {
|
||||
clearInterval(this.data.readingTimer);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = Math.floor((endTime - this.data.readingStartTime) / 1000);
|
||||
|
||||
this.setData({
|
||||
readingDuration: duration,
|
||||
isReading: false
|
||||
});
|
||||
|
||||
// 保存阅读记录
|
||||
this.saveReadingRecord(duration);
|
||||
},
|
||||
|
||||
// 保存阅读记录
|
||||
saveReadingRecord(duration: number) {
|
||||
const readingRecord = {
|
||||
bookId: this.data.bookId,
|
||||
startTime: this.data.readingStartTime,
|
||||
duration: duration,
|
||||
endTime: Date.now()
|
||||
};
|
||||
|
||||
// 可以选择保存到本地或发送到服务器
|
||||
wx.setStorageSync('lastReadingRecord', readingRecord);
|
||||
|
||||
wx.showToast({
|
||||
title: `本次阅读 ${Math.floor(duration / 60)} 分钟`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
<view class="reader" style="background: {{themes[currentTheme].bg}}; color: {{themes[currentTheme].color}}">
|
||||
<!-- 顶部工具栏 -->
|
||||
<view class="toolbar {{showToolbar ? '' : 'hidden'}}">
|
||||
<view class="back" bindtap="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
</view>
|
||||
<view class="title">{{bookTitle}}</view>
|
||||
<view class="menu" bindtap="toggleMenu">
|
||||
<text class="menu-icon">≡</text>
|
||||
</view>
|
||||
<view class="reading-time" wx:if="{{isReading}}">
|
||||
已阅读 {{Math.floor(readingDuration / 60)}}分{{readingDuration % 60}}秒
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读区域 -->
|
||||
<view class="content"
|
||||
bindtap="hideTextMenu"
|
||||
bindtouchstart="touchStart"
|
||||
bindtouchend="touchEnd">
|
||||
<view class="chapter-title">第{{currentChapter}}章</view>
|
||||
<view class="page-content" style="font-size: {{fontSize}}rpx;">
|
||||
<text user-select="true"
|
||||
selection-start="{{selectionStart}}"
|
||||
selection-end="{{selectionEnd}}"
|
||||
bindlongpress="showTextMenu"
|
||||
catch:tap="toggleToolbar">{{pages[currentPage]}}</text>
|
||||
</view>
|
||||
<view class="page-number">{{currentPage + 1}}/{{pages.length}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部工具栏 -->
|
||||
<view class="bottom-toolbar {{showToolbar ? '' : 'hidden'}}">
|
||||
<view class="toolbar-top">
|
||||
<view class="prev" bindtap="prevPage">上一页</view>
|
||||
<view class="progress">{{currentChapter}}/{{totalChapters}}</view>
|
||||
<view class="settings" bindtap="toggleSettings">设置</view>
|
||||
<view class="next" bindtap="nextPage">下一页</view>
|
||||
</view>
|
||||
<view class="toolbar-bottom">
|
||||
<view class="current-time">{{currentTime}}</view>
|
||||
<view class="reading-progress">已读{{readingProgress}}%</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录菜单 -->
|
||||
<view class="menu-panel {{showMenu ? 'show' : ''}}">
|
||||
<view class="menu-header">
|
||||
<text>目录</text>
|
||||
<text class="close" bindtap="toggleMenu">×</text>
|
||||
</view>
|
||||
<scroll-view class="chapter-list" scroll-y>
|
||||
<view class="chapter-item {{currentChapter === index + 1 ? 'active' : ''}}"
|
||||
wx:for="{{chapters}}"
|
||||
wx:key="index"
|
||||
bindtap="jumpToChapter"
|
||||
data-chapter="{{index + 1}}">
|
||||
第{{index + 1}}章
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<view class="settings-panel {{showSettings ? 'show' : ''}}">
|
||||
<view class="settings-header">
|
||||
<text>设置</text>
|
||||
<text class="close" bindtap="toggleSettings">×</text>
|
||||
</view>
|
||||
<view class="settings-content">
|
||||
<!-- 字体大小设置 -->
|
||||
<view class="setting-item">
|
||||
<text>字体大小</text>
|
||||
<view class="font-size-control">
|
||||
<view class="size-btn" bindtap="decreaseFontSize">A-</view>
|
||||
<text class="current-size">{{fontSize}}px</text>
|
||||
<view class="size-btn" bindtap="increaseFontSize">A+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主题设置 -->
|
||||
<view class="setting-item">
|
||||
<text>主题</text>
|
||||
<view class="theme-list">
|
||||
<view class="theme-item {{currentTheme === theme.id ? 'active' : ''}}"
|
||||
wx:for="{{themes}}"
|
||||
wx:key="id"
|
||||
wx:for-item="theme"
|
||||
bindtap="switchTheme"
|
||||
data-theme="{{theme.id}}"
|
||||
style="background: {{theme.bg}};">
|
||||
<text style="color: {{theme.color}}">{{theme.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 亮度调节 -->
|
||||
<view class="setting-item">
|
||||
<text>屏幕亮度</text>
|
||||
<slider class="brightness-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value="{{brightness}}"
|
||||
bind:change="adjustBrightness"
|
||||
block-size="20"
|
||||
activeColor="#1989fa"/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<view class="mask {{showSettings || showMenu ? 'show' : ''}}"
|
||||
bindtap="closeAllPanels"></view>
|
||||
|
||||
<!-- 文本选择菜单 -->
|
||||
<view class="text-menu {{showTextMenu ? 'show' : ''}}"
|
||||
style="top: {{textMenuTop}}px; left: {{textMenuLeft}}px">
|
||||
<view class="menu-item" bindtap="addNote">添加笔记</view>
|
||||
<view class="menu-item" bindtap="addBookmark">添加书签</view>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,402 @@
|
|||
.reader {
|
||||
height: 100vh;
|
||||
background: #f4ecd8;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 88rpx;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
transition: all 0.3s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.toolbar.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.back, .menu {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 阅读区域 */
|
||||
.content {
|
||||
height: 100vh;
|
||||
padding: 0rpx 0rpx 0rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 700rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
line-height: 1.8;
|
||||
text-align: justify;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-content text {
|
||||
text-indent: 2em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 底部页码 */
|
||||
.page-info {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
/* 底部评论区域 */
|
||||
.bottom-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookmark-btn {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.add-bookmark {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-bookmark .close {
|
||||
margin-left: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 评论列表 */
|
||||
.comment-list {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 底部工具栏 */
|
||||
.bottom-toolbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
transition: all 0.3s;
|
||||
z-index: 100;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.toolbar-top {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.toolbar-bottom {
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 40rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prev, .next, .settings {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.current-time, .reading-progress {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 设置面板 */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
transform: translateY(100%);
|
||||
transition: all 0.3s;
|
||||
z-index: 999;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.settings-panel.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.settings-header .close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.setting-item text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 字体大小控制 */
|
||||
.font-size-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f7f7f7;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.size-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.current-size {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 主题列表 */
|
||||
.theme-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.theme-item.active {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4rpx 12rpx rgba(25, 137, 250, 0.2);
|
||||
}
|
||||
|
||||
/* 亮度调节 */
|
||||
.brightness-slider {
|
||||
margin-top: 16rpx;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.brightness-slider slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.mask.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 目录菜单 */
|
||||
.menu-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 80%;
|
||||
background: #fff;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-panel.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.menu-header .close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.chapter-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chapter-item.active {
|
||||
color: #1989fa;
|
||||
background: rgba(25, 137, 250, 0.1);
|
||||
}
|
||||
|
||||
/* 文本选择菜单 */
|
||||
.text-menu {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-radius: 8rpx;
|
||||
padding: 10rpx 0;
|
||||
transform: translate(-50%, -100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.text-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.reading-time {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
margin-right: 20rpx;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
|
@ -0,0 +1,27 @@
|
|||
Tabbar 图标规范说明
|
||||
|
||||
1. 图标列表
|
||||
- home.png & home-active.png // 首页图标
|
||||
- bookshelf.png & bookshelf-active.png // 书架图标
|
||||
- notes.png & notes-active.png // 笔记图标
|
||||
- profile.png & profile-active.png // 我的图标
|
||||
|
||||
2. 图标规格要求
|
||||
- 尺寸: 81px × 81px
|
||||
- 格式: PNG
|
||||
- 背景: 透明背景
|
||||
- 普通态颜色: #999999
|
||||
- 选中态颜色: #1989fa
|
||||
|
||||
3. 设计建议
|
||||
- 线条粗细统一
|
||||
- 图标风格保持一致
|
||||
- 预留出边距,不要顶边
|
||||
- 避免过于复杂的细节
|
||||
|
||||
4. 图标来源建议
|
||||
- iconfont (https://www.iconfont.cn/)
|
||||
- 设计师定制
|
||||
- 其他开源图标库
|
||||
|
||||
注意:替换图标时需保持文件名不变,以确保 tabbar 正常显示。
|
|
@ -0,0 +1,16 @@
|
|||
// 创建 tabbar 图标目录
|
||||
const iconFiles = [
|
||||
'home.png', // 首页-未选中
|
||||
'home-active.png', // 首页-选中
|
||||
'book.png', // 书架-未选中
|
||||
'book-active.png', // 书架-选中
|
||||
'note.png', // 笔记-未选中
|
||||
'note-active.png', // 笔记-选中
|
||||
'user.png', // 我的-未选中
|
||||
'user-active.png' // 我的-选中
|
||||
];
|
||||
|
||||
// 由于微信小程序对图标有特定要求,建议:
|
||||
// 1. 图标尺寸: 81px × 81px
|
||||
// 2. 图标格式: PNG
|
||||
// 3. 背景色: 透明
|
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,236 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
interface BookForm {
|
||||
isbn: string;
|
||||
title: string;
|
||||
author: string;
|
||||
publisher: string;
|
||||
description: string;
|
||||
bookUrl: string;
|
||||
coverUrl: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
language: string;
|
||||
publishDate: string;
|
||||
}
|
||||
|
||||
Component({
|
||||
data: {
|
||||
form: {
|
||||
isbn: '',
|
||||
title: '',
|
||||
author: '',
|
||||
publisher: '',
|
||||
description: '',
|
||||
bookUrl: '',
|
||||
coverUrl: '',
|
||||
category: '',
|
||||
tags: '',
|
||||
language: '汉语',
|
||||
publishDate: ''
|
||||
} as BookForm,
|
||||
categories: ['小说', '文学', '历史', '科技', '教育', '其他']
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 输入框变化处理
|
||||
onInput(e: any) {
|
||||
const { field } = e.currentTarget.dataset;
|
||||
this.setData({
|
||||
[`form.${field}`]: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 选择分类
|
||||
onCategoryChange(e: any) {
|
||||
this.setData({
|
||||
'form.category': e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 选择日期
|
||||
onDateChange(e: any) {
|
||||
const date = e.detail.value; // 格式: "2024-12-16"
|
||||
// 添加时间部分,转换为标准格式
|
||||
const dateTime = `${date} 00:00:00`;
|
||||
this.setData({
|
||||
'form.publishDate': dateTime
|
||||
});
|
||||
},
|
||||
|
||||
// 上传封面
|
||||
async uploadCover() {
|
||||
try {
|
||||
const res = await wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sizeType: ['compressed']
|
||||
});
|
||||
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: 'http://localhost:8084/api/common/uploadFile',
|
||||
filePath: res.tempFiles[0].tempFilePath,
|
||||
name: 'file',
|
||||
formData: {
|
||||
bucketName: 'photo'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('上传成功:', res);
|
||||
resolve(res);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('上传失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('上传响应:', uploadRes);
|
||||
|
||||
if (uploadRes.statusCode !== 200) {
|
||||
throw new Error(`服务器响应错误: ${uploadRes.statusCode}`);
|
||||
}
|
||||
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
console.log('解析后的数据:', data);
|
||||
|
||||
if(data.code === 200) {
|
||||
this.setData({
|
||||
'form.coverUrl': data.data
|
||||
});
|
||||
wx.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.message || '上传失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传封面失败:', error);
|
||||
wx.showToast({
|
||||
title: error instanceof Error ? error.message : '上传失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 上传电子书
|
||||
async uploadBook() {
|
||||
try {
|
||||
const res = await wx.chooseMessageFile({
|
||||
count: 1,
|
||||
type: 'file',
|
||||
extension: ['txt']
|
||||
});
|
||||
|
||||
wx.showLoading({ title: '上传中...' });
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: 'http://localhost:8084/api/common/uploadFile',
|
||||
filePath: res.tempFiles[0].path,
|
||||
name: 'file',
|
||||
formData: {
|
||||
bucketName: 'txt'
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('上传成功:', res);
|
||||
resolve(res);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('上传失败:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
console.log('上传响应:', uploadRes);
|
||||
|
||||
if (uploadRes.statusCode !== 200) {
|
||||
throw new Error(`服务器响应错误: ${uploadRes.statusCode}`);
|
||||
}
|
||||
|
||||
const data = JSON.parse(uploadRes.data);
|
||||
console.log('解析后的数据:', data);
|
||||
|
||||
if(data.code === 200) {
|
||||
this.setData({
|
||||
'form.bookUrl': data.data
|
||||
});
|
||||
wx.showToast({
|
||||
title: '上传成功',
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
throw new Error(data.message || '上传失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('上传文件失败:', error);
|
||||
wx.showToast({
|
||||
title: error instanceof Error ? error.message : '上传失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 提交表单
|
||||
async submitForm() {
|
||||
try {
|
||||
const { form } = this.data;
|
||||
|
||||
// 表单验证
|
||||
if (!form.title || !form.author || !form.bookUrl) {
|
||||
wx.showToast({
|
||||
title: '请填写必要信息',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保日期格式正确
|
||||
if (form.publishDate && !form.publishDate.includes(':')) {
|
||||
form.publishDate = `${form.publishDate} 00:00:00`;
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '提交中...' });
|
||||
|
||||
const res = await request.post('/books/add', {
|
||||
...form,
|
||||
// 添加当前时间作为创建和更新时间
|
||||
createdTime: new Date().toISOString().replace('T', ' ').split('.')[0],
|
||||
updatedTime: new Date().toISOString().replace('T', ' ').split('.')[0]
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
wx.showToast({
|
||||
title: '添加成功',
|
||||
icon: 'success'
|
||||
});
|
||||
// 返回书架页面
|
||||
setTimeout(() => {
|
||||
wx.navigateBack();
|
||||
}, 1500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
wx.showToast({
|
||||
title: '提交失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
<view class="add-book">
|
||||
<form>
|
||||
<view class="form-item">
|
||||
<text class="label">ISBN</text>
|
||||
<input class="input"
|
||||
placeholder="请输入ISBN"
|
||||
value="{{form.isbn}}"
|
||||
data-field="isbn"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">书名*</text>
|
||||
<input class="input"
|
||||
placeholder="请输入书名"
|
||||
value="{{form.title}}"
|
||||
data-field="title"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">作者*</text>
|
||||
<input class="input"
|
||||
placeholder="请输入作者"
|
||||
value="{{form.author}}"
|
||||
data-field="author"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">出版社</text>
|
||||
<input class="input"
|
||||
placeholder="请输入出版社"
|
||||
value="{{form.publisher}}"
|
||||
data-field="publisher"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">简介</text>
|
||||
<textarea class="textarea"
|
||||
placeholder="请输入简介"
|
||||
value="{{form.description}}"
|
||||
data-field="description"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">分类</text>
|
||||
<picker mode="selector"
|
||||
range="{{categories}}"
|
||||
bindchange="onCategoryChange">
|
||||
<view class="picker">
|
||||
{{form.category || '请选择分类'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">标签</text>
|
||||
<input class="input"
|
||||
placeholder="请输入标签,多个用逗号分隔"
|
||||
value="{{form.tags}}"
|
||||
data-field="tags"
|
||||
bindinput="onInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">出版日期</text>
|
||||
<picker mode="date"
|
||||
bindchange="onDateChange">
|
||||
<view class="picker">
|
||||
{{form.publishDate || '请选择日期'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">封面*</text>
|
||||
<view class="upload-box" bindtap="uploadCover">
|
||||
<image wx:if="{{form.coverUrl}}"
|
||||
src="{{form.coverUrl}}"
|
||||
mode="aspectFill"
|
||||
class="preview"/>
|
||||
<view wx:else class="upload-btn">上传封面</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<text class="label">电子书*</text>
|
||||
<view class="upload-box" bindtap="uploadBook">
|
||||
<view class="{{form.bookUrl ? 'file-name' : 'upload-btn'}}">
|
||||
{{form.bookUrl ? '已选择文件' : '上传TXT文件'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn" bindtap="submitForm">提交</button>
|
||||
</form>
|
||||
</view>
|
|
@ -0,0 +1,81 @@
|
|||
.add-book {
|
||||
padding: 30rpx;
|
||||
background: #f7f8fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 30rpx;
|
||||
background: #fff;
|
||||
padding: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker {
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
padding: 0 20rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 80rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
border: 1rpx solid #eee;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #1989fa;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 60rpx;
|
||||
background: #1989fa;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
Component({
|
||||
methods: {
|
||||
// 更新阅读进度
|
||||
updateProgress(e: any) {
|
||||
const { currentPage } = e.detail;
|
||||
const { book } = this.data;
|
||||
|
||||
// 更新进度
|
||||
wx.cloud.callFunction({
|
||||
name: 'updateReadingProgress',
|
||||
data: {
|
||||
bookId: book.id,
|
||||
currentPage,
|
||||
readingTime: this.data.currentReadingTime
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 开始计时
|
||||
startReading() {
|
||||
this.startTime = Date.now();
|
||||
// 开始计时逻辑
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"navigationBarTitleText": "书架",
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
books: [] as Book[],
|
||||
categories: ['全部', '在读', '已读', '想读'],
|
||||
currentCategory: '全部',
|
||||
loading: false,
|
||||
current: 1,
|
||||
size: 10,
|
||||
total: 0
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.getBookList();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取书籍列表
|
||||
async getBookList() {
|
||||
if(this.data.loading) return;
|
||||
|
||||
this.setData({ loading: true });
|
||||
try {
|
||||
const res = await request.get<BookListResponse>('/books', {
|
||||
current: this.data.current,
|
||||
size: this.data.size,
|
||||
category: this.data.currentCategory === '全部' ? '' : this.data.currentCategory
|
||||
});
|
||||
|
||||
if(res.code === 200) {
|
||||
|
||||
console.log(res.data);
|
||||
|
||||
|
||||
this.setData({
|
||||
books: res.data.records,
|
||||
total: res.data.total
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: '获取书籍列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取书籍列表失败:', error);
|
||||
wx.showToast({
|
||||
title: '获取书籍列表失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 切换分类
|
||||
async switchCategory(e: any) {
|
||||
const category = e.currentTarget.dataset.category;
|
||||
this.setData({
|
||||
currentCategory: category,
|
||||
current: 1 // 切换分类时重置页码
|
||||
});
|
||||
await this.getBookList();
|
||||
},
|
||||
|
||||
// 打开阅读器
|
||||
openReader(e: any) {
|
||||
const { id, bookUrl } = e.currentTarget.dataset;
|
||||
console.log('准备跳转,参数:', e.currentTarget.dataset);
|
||||
|
||||
wx.navigateTo({
|
||||
url: `/pages/bookshelf/reader/reader?id=${id}&bookUrl=${encodeURIComponent(bookUrl)}&title=${encodeURIComponent(this.data.books.find(b => b.id === id)?.title || '')}`,
|
||||
success: () => {
|
||||
console.log('跳转成功');
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('跳转失败:', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到添加书籍页
|
||||
goToAddBook() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/bookshelf/add'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
<view class="bookshelf">
|
||||
<!-- 分类选择 -->
|
||||
<view class="categories">
|
||||
<view class="category-item {{currentCategory === item ? 'active' : ''}}"
|
||||
wx:for="{{categories}}"
|
||||
wx:key="*this"
|
||||
data-category="{{item}}"
|
||||
bindtap="switchCategory">
|
||||
{{item}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书籍列表 -->
|
||||
<view class="book-list">
|
||||
<view class="book-item"
|
||||
wx:for="{{books}}"
|
||||
wx:key="id"
|
||||
bindtap="openReader"
|
||||
data-id="{{item.id}}"
|
||||
data-book-url="{{item.bookUrl}}">
|
||||
<image class="book-cover" src="{{item.coverUrl}}" mode="aspectFill"/>
|
||||
<view class="book-info">
|
||||
<text class="book-title">{{item.title}}</text>
|
||||
<text class="book-author">{{item.author}}</text>
|
||||
<text class="book-desc">{{item.description}}</text>
|
||||
<view class="book-meta">
|
||||
<text class="book-category">{{item.category}}</text>
|
||||
<!-- <text class="book-date">出版时间:{{item.publishDate}}</text> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<view class="no-more" wx:if="{{books.length >= total && total > 0}}">没有更多了</view>
|
||||
<view class="empty" wx:if="{{books.length === 0 && !loading}}">暂无书籍</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="add-btn" bindtap="goToAddBook">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,133 @@
|
|||
.bookshelf {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.categories {
|
||||
display: flex;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.category-item {
|
||||
padding: 10rpx 30rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.category-item.active {
|
||||
background: #1989fa;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.book-list {
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.book-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
width: 160rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.book-author {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin: 10rpx 0;
|
||||
}
|
||||
|
||||
.book-progress {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #1989fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(25, 137, 250, 0.3);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 60rpx;
|
||||
}
|
||||
|
||||
.book-type {
|
||||
font-size: 24rpx;
|
||||
color: #1989fa;
|
||||
background: rgba(25, 137, 250, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.book-desc {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin: 8rpx 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.book-category {
|
||||
font-size: 24rpx;
|
||||
color: #1989fa;
|
||||
background: rgba(25, 137, 250, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
|
||||
.book-date {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading,
|
||||
.no-more,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
color: #999;
|
||||
font-size: 28rpx;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bookId: '',
|
||||
bookUrl: '',
|
||||
title: ''
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const { id, bookUrl, title } = options;
|
||||
this.setData({
|
||||
bookId: id,
|
||||
bookUrl: decodeURIComponent(bookUrl),
|
||||
title: decodeURIComponent(title)
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
<reader-component
|
||||
bookId="{{bookId}}"
|
||||
bookUrl="{{bookUrl}}"
|
||||
title="{{title}}"
|
||||
/>
|
|
@ -0,0 +1,27 @@
|
|||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
|
||||
const request = require('../../../utils/request');
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bookId: '',
|
||||
bookUrl: '',
|
||||
title: ''
|
||||
},
|
||||
onLoad(options) {
|
||||
const { id, bookUrl, title } = options;
|
||||
this.setData({
|
||||
bookId: id,
|
||||
bookUrl: decodeURIComponent(bookUrl),
|
||||
title: decodeURIComponent(title)
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"navigationBarTitleText": "阅读",
|
||||
"usingComponents": {
|
||||
"reader-component": "./reader"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import request from '../../../utils/request';
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bookId: '',
|
||||
bookUrl: '',
|
||||
title: ''
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const { id, bookUrl, title } = options;
|
||||
console.log('Reader页面接收到的参数:', {
|
||||
id,
|
||||
bookUrl: decodeURIComponent(bookUrl),
|
||||
title: decodeURIComponent(title)
|
||||
});
|
||||
|
||||
this.setData({
|
||||
bookId: id,
|
||||
bookUrl: decodeURIComponent(bookUrl),
|
||||
title: decodeURIComponent(title)
|
||||
});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// pages/bookshelf/reader/read.ts
|
||||
Page({
|
||||
|
||||
/**
|
||||
* 页面的初始数据
|
||||
*/
|
||||
data: {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面加载
|
||||
*/
|
||||
onLoad() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面初次渲染完成
|
||||
*/
|
||||
onReady() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面显示
|
||||
*/
|
||||
onShow() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面隐藏
|
||||
*/
|
||||
onHide() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 生命周期函数--监听页面卸载
|
||||
*/
|
||||
onUnload() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面相关事件处理函数--监听用户下拉动作
|
||||
*/
|
||||
onPullDownRefresh() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 页面上拉触底事件的处理函数
|
||||
*/
|
||||
onReachBottom() {
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* 用户点击右上角分享
|
||||
*/
|
||||
onShareAppMessage() {
|
||||
|
||||
}
|
||||
})
|
|
@ -0,0 +1,2 @@
|
|||
<!--pages/bookshelf/reader/read.wxml-->
|
||||
<text>pages/bookshelf/reader/read.wxml</text>
|
|
@ -0,0 +1 @@
|
|||
/* pages/bookshelf/reader/read.wxss */
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,892 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
// 定义组件方法接口
|
||||
interface IComponentMethods {
|
||||
loadChapter: (chapter: number) => Promise<void>;
|
||||
splitPages: () => Promise<void>;
|
||||
prevPage: () => void;
|
||||
nextPage: () => void;
|
||||
touchStart: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
touchEnd: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleToolbar: () => void;
|
||||
toggleMenu: () => void;
|
||||
prevChapter: () => void;
|
||||
nextChapter: () => void;
|
||||
jumpToChapter: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
goBack: () => void;
|
||||
onScroll: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleSettings: () => void;
|
||||
decreaseFontSize: () => void;
|
||||
increaseFontSize: () => void;
|
||||
startTimeUpdate: () => void;
|
||||
calculateProgress: () => void;
|
||||
closeAllPanels: () => void;
|
||||
switchTheme: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
adjustBrightness: (e: WechatMiniprogram.SliderChange) => void;
|
||||
toggleAutoReading: (e: WechatMiniprogram.SwitchChange) => void;
|
||||
setAutoReadingInterval: (e: WechatMiniprogram.PickerChange) => void;
|
||||
startAutoReading: () => void;
|
||||
stopAutoReading: () => void;
|
||||
toggleBookmarks: () => void;
|
||||
addBookmark: () => void;
|
||||
loadBookmarks: () => void;
|
||||
jumpToBookmark: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
toggleNotes: () => void;
|
||||
showTextMenu: (e: WechatMiniprogram.TouchEvent) => void;
|
||||
addNote: () => void;
|
||||
loadNotes: () => void;
|
||||
copyText: () => void;
|
||||
startReadingTimer: () => void;
|
||||
updateReadingTime: () => void;
|
||||
loadTodayReadingTime: () => void;
|
||||
saveReadingProgress: () => void;
|
||||
restoreSettings: () => void;
|
||||
toggleMore: () => void;
|
||||
onSelectText: (e: any) => void;
|
||||
addBookmarkWithSelection: () => void;
|
||||
addNoteWithSelection: () => void;
|
||||
copySelectedText: () => void;
|
||||
hideTextMenu: () => void;
|
||||
startReadingTimer: () => void;
|
||||
stopReadingTimer: () => void;
|
||||
saveReadingRecord: (duration: number) => void;
|
||||
}
|
||||
|
||||
// 定义组件数据接口
|
||||
interface IComponentData {
|
||||
bookTitle: string;
|
||||
content: string;
|
||||
currentChapter: number;
|
||||
totalChapters: number;
|
||||
showToolbar: boolean;
|
||||
showMenu: boolean;
|
||||
scrollTop: number;
|
||||
chapters: number[];
|
||||
lastTapTime: number;
|
||||
pages: string[];
|
||||
currentPage: number;
|
||||
pageHeight: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
showSettings: boolean;
|
||||
minFontSize: number;
|
||||
maxFontSize: number;
|
||||
currentTime: string;
|
||||
readingProgress: number;
|
||||
themes: Array<{id: string; name: string; bg: string; color: string}>;
|
||||
currentTheme: string;
|
||||
brightness: number;
|
||||
autoReading: boolean;
|
||||
autoReadingInterval: number;
|
||||
showBookmarks: boolean;
|
||||
showNotes: boolean;
|
||||
showTextMenu: boolean;
|
||||
textMenuTop: number;
|
||||
textMenuLeft: number;
|
||||
selectedText: string;
|
||||
bookmarks: any[];
|
||||
notes: any[];
|
||||
readingStartTime: number;
|
||||
todayReadingTime: number;
|
||||
showMore: boolean;
|
||||
selectionStart: number;
|
||||
selectionEnd: number;
|
||||
readingDuration: number;
|
||||
isReading: boolean;
|
||||
readingTimer: any;
|
||||
}
|
||||
|
||||
// 定义组件属性接口
|
||||
interface IComponentProperties {
|
||||
bookId: string;
|
||||
bookUrl: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
Component<IComponentData, IComponentProperties, IComponentMethods>({
|
||||
data: {
|
||||
bookTitle: '',
|
||||
content: '',
|
||||
currentChapter: 1,
|
||||
totalChapters: 2541,
|
||||
showToolbar: true,
|
||||
showMenu: false,
|
||||
scrollTop: 0,
|
||||
chapters: [] as number[],
|
||||
lastTapTime: 0,
|
||||
pages: [] as string[],
|
||||
currentPage: 0,
|
||||
pageHeight: 0,
|
||||
lineHeight: 36,
|
||||
fontSize: 32,
|
||||
showSettings: false,
|
||||
minFontSize: 24,
|
||||
maxFontSize: 48,
|
||||
currentTime: '',
|
||||
readingProgress: 0,
|
||||
themes: [
|
||||
{ id: 'default', name: '默认', bg: '#f4ecd8', color: '#333' },
|
||||
{ id: 'night', name: '夜间', bg: '#222', color: '#999' },
|
||||
{ id: 'green', name: '护眼', bg: '#cce8cf', color: '#333' },
|
||||
{ id: 'paper', name: '纸张', bg: '#e8e2d3', color: '#333' }
|
||||
],
|
||||
currentTheme: 'default',
|
||||
brightness: 100,
|
||||
autoReading: false,
|
||||
autoReadingInterval: 5000,
|
||||
showBookmarks: false,
|
||||
showNotes: false,
|
||||
showTextMenu: false,
|
||||
textMenuTop: 0,
|
||||
textMenuLeft: 0,
|
||||
selectedText: '',
|
||||
bookmarks: [] as any[],
|
||||
notes: [] as any[],
|
||||
readingStartTime: 0,
|
||||
todayReadingTime: 0,
|
||||
showMore: false,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1,
|
||||
readingDuration: 0,
|
||||
isReading: false,
|
||||
readingTimer: null as any,
|
||||
},
|
||||
|
||||
properties: {
|
||||
bookId: String,
|
||||
bookUrl: String,
|
||||
title: String
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
console.log('Reader组件接收到的属性:', {
|
||||
bookId: this.properties.bookId,
|
||||
bookUrl: this.properties.bookUrl,
|
||||
title: this.properties.title
|
||||
});
|
||||
|
||||
this.setData({
|
||||
bookTitle: this.properties.title || '大爱仙尊',
|
||||
chapters: Array.from({length: 2541}, (_, i) => i + 1)
|
||||
});
|
||||
this.loadChapter(1).then(() => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
const savedFontSize = wx.getStorageSync('reader_font_size');
|
||||
if (savedFontSize) {
|
||||
this.setData({
|
||||
fontSize: savedFontSize
|
||||
});
|
||||
}
|
||||
this.startTimeUpdate();
|
||||
this.startReadingTimer();
|
||||
this.loadBookmarks();
|
||||
this.loadNotes();
|
||||
this.restoreSettings();
|
||||
|
||||
// 获取当前屏幕亮度
|
||||
wx.getScreenBrightness({
|
||||
success: (res) => {
|
||||
this.setData({
|
||||
brightness: Math.round(res.value * 100)
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
detached() {
|
||||
if (this.data.readingTimer) {
|
||||
clearInterval(this.data.readingTimer);
|
||||
}
|
||||
this.updateReadingTime();
|
||||
this.saveReadingProgress();
|
||||
if (this.autoReadingTimer) {
|
||||
clearInterval(this.autoReadingTimer);
|
||||
}
|
||||
this.stopReadingTimer();
|
||||
}
|
||||
},
|
||||
|
||||
pageLifetimes: {
|
||||
hide() {
|
||||
this.stopReadingTimer();
|
||||
},
|
||||
|
||||
show() {
|
||||
if (!this.data.readingDuration) {
|
||||
this.startReadingTimer();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 加载章节内容
|
||||
async loadChapter(chapter: number) {
|
||||
try {
|
||||
wx.showLoading({ title: '加载中...' });
|
||||
const res = await request.get('/common/getBookContent', {
|
||||
bookName: this.data.bookTitle,
|
||||
id: chapter
|
||||
});
|
||||
|
||||
if (res.code === 200) {
|
||||
// 处理换行符
|
||||
const content = res.data.replace(/\\r\\n/g, '\n');
|
||||
this.setData({
|
||||
content,
|
||||
currentChapter: chapter // 更新当前章节号
|
||||
});
|
||||
// 分页处理
|
||||
await this.splitPages();
|
||||
this.calculateProgress();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载章节失败:', error);
|
||||
wx.showToast({
|
||||
title: '加载失败',
|
||||
icon: 'none'
|
||||
});
|
||||
} finally {
|
||||
wx.hideLoading();
|
||||
}
|
||||
},
|
||||
|
||||
// 分页处理
|
||||
async splitPages() {
|
||||
// 获取容器尺寸
|
||||
const query = this.createSelectorQuery();
|
||||
query.select('.content').boundingClientRect();
|
||||
const rect = await new Promise(resolve => query.exec(resolve));
|
||||
const containerHeight = rect[0].height;
|
||||
|
||||
// 计算每页能显示的行数
|
||||
const lineHeight = this.data.lineHeight;
|
||||
const linesPerPage = Math.floor((containerHeight - 160) / (lineHeight * 2 / 750 * wx.getSystemInfoSync().windowWidth));
|
||||
|
||||
// 按自然段落分割内容
|
||||
const paragraphs = this.data.content.split('\n').filter(p => p.trim());
|
||||
|
||||
// 分页
|
||||
const pages = [];
|
||||
let currentPage = [];
|
||||
let currentLines = 0;
|
||||
|
||||
for (const paragraph of paragraphs) {
|
||||
// 计算段落需要的行数
|
||||
const paragraphLines = Math.ceil(paragraph.length * (this.data.fontSize / 32) / 20);
|
||||
|
||||
if (currentLines + paragraphLines > linesPerPage) {
|
||||
// 当前页放不下这段,新建一页
|
||||
if (currentPage.length > 0) {
|
||||
pages.push(currentPage.join('\n'));
|
||||
currentPage = [];
|
||||
currentLines = 0;
|
||||
}
|
||||
|
||||
// 如果单个段落超过一页
|
||||
if (paragraphLines > linesPerPage) {
|
||||
const chars = Math.floor(20 * linesPerPage);
|
||||
let p = paragraph;
|
||||
while (p.length > 0) {
|
||||
const pageContent = p.slice(0, chars);
|
||||
pages.push(pageContent);
|
||||
p = p.slice(chars);
|
||||
}
|
||||
} else {
|
||||
currentPage.push(paragraph);
|
||||
currentLines = paragraphLines;
|
||||
}
|
||||
} else {
|
||||
// 当前页能放下这段
|
||||
currentPage.push(paragraph);
|
||||
currentLines += paragraphLines;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存最后一页
|
||||
if (currentPage.length > 0) {
|
||||
pages.push(currentPage.join('\n'));
|
||||
}
|
||||
|
||||
this.setData({
|
||||
pages,
|
||||
currentPage: 0,
|
||||
pageHeight: containerHeight
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
},
|
||||
|
||||
// 上一页
|
||||
prevPage() {
|
||||
if (this.data.currentPage > 0) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage - 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
} else if (this.data.currentChapter > 1) {
|
||||
// 上一章最后一页
|
||||
const prevChapter = this.data.currentChapter - 1;
|
||||
this.loadChapter(prevChapter).then(() => {
|
||||
this.setData({
|
||||
currentPage: this.data.pages.length - 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 下一页
|
||||
nextPage() {
|
||||
if (this.data.currentPage < this.data.pages.length - 1) {
|
||||
this.setData({
|
||||
currentPage: this.data.currentPage + 1
|
||||
}, () => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
} else if (this.data.currentChapter < this.data.totalChapters) {
|
||||
// 下一章第一页
|
||||
const nextChapter = this.data.currentChapter + 1;
|
||||
this.loadChapter(nextChapter).then(() => {
|
||||
this.calculateProgress();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 触摸开始
|
||||
touchStart(e: any) {
|
||||
this.touchStartX = e.touches[0].pageX;
|
||||
},
|
||||
|
||||
// 触摸结束
|
||||
touchEnd(e: any) {
|
||||
const touchEndX = e.changedTouches[0].pageX;
|
||||
const diff = touchEndX - this.touchStartX;
|
||||
|
||||
if (Math.abs(diff) > 50) { // 滑动距离大于50px才触发翻页
|
||||
if (diff > 0) {
|
||||
this.prevPage();
|
||||
} else {
|
||||
this.nextPage();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 切换工具栏显示
|
||||
toggleToolbar() {
|
||||
const now = Date.now();
|
||||
if (now - this.data.lastTapTime < 300) {
|
||||
// 双击
|
||||
this.setData({
|
||||
showToolbar: !this.data.showToolbar
|
||||
});
|
||||
}
|
||||
this.setData({ lastTapTime: now });
|
||||
},
|
||||
|
||||
// 切换目录显示
|
||||
toggleMenu() {
|
||||
this.setData({
|
||||
showMenu: !this.data.showMenu
|
||||
});
|
||||
},
|
||||
|
||||
// 上一章
|
||||
prevChapter() {
|
||||
if (this.data.currentChapter > 1) {
|
||||
this.loadChapter(this.data.currentChapter - 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 下一章
|
||||
nextChapter() {
|
||||
if (this.data.currentChapter < this.data.totalChapters) {
|
||||
this.loadChapter(this.data.currentChapter + 1);
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到指定章节
|
||||
jumpToChapter(e: any) {
|
||||
const chapter = e.currentTarget.dataset.chapter;
|
||||
this.loadChapter(Number(chapter)); // 确保 chapter 是数字
|
||||
this.toggleMenu();
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
this.stopReadingTimer(); // 停止计时
|
||||
|
||||
// 打印阅读时间
|
||||
const minutes = Math.floor(this.data.readingDuration / 60);
|
||||
const seconds = this.data.readingDuration % 60;
|
||||
console.log(`本次阅读时长: ${minutes}分${seconds}秒`);
|
||||
console.log('详细信息:', {
|
||||
开始时间: new Date(this.data.readingStartTime).toLocaleString(),
|
||||
结束时间: new Date().toLocaleString(),
|
||||
总秒数: this.data.readingDuration,
|
||||
格式化时长: `${minutes}分${seconds}秒`
|
||||
});
|
||||
|
||||
wx.navigateBack();
|
||||
},
|
||||
|
||||
// 滚动处理
|
||||
onScroll(e: any) {
|
||||
// 可以在这里处理阅读进度保存等逻辑
|
||||
},
|
||||
|
||||
// 切换设置面板
|
||||
toggleSettings() {
|
||||
console.log('切换设置面板', this.data.showSettings);
|
||||
this.setData({
|
||||
showSettings: !this.data.showSettings,
|
||||
showMenu: false // 确保目录菜单关闭
|
||||
});
|
||||
},
|
||||
|
||||
// 减小字体
|
||||
decreaseFontSize() {
|
||||
if (this.data.fontSize > this.data.minFontSize) {
|
||||
const newSize = this.data.fontSize - 2;
|
||||
this.setData({
|
||||
fontSize: newSize
|
||||
}, () => {
|
||||
// 重新计算分页
|
||||
this.splitPages();
|
||||
// 保存设置到本地
|
||||
wx.setStorageSync('reader_font_size', newSize);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 增大字体
|
||||
increaseFontSize() {
|
||||
if (this.data.fontSize < this.data.maxFontSize) {
|
||||
const newSize = this.data.fontSize + 2;
|
||||
this.setData({
|
||||
fontSize: newSize
|
||||
}, () => {
|
||||
// 重新计算分页
|
||||
this.splitPages();
|
||||
// 保存设置到本地
|
||||
wx.setStorageSync('reader_font_size', newSize);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 更新时间
|
||||
startTimeUpdate() {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
this.setData({
|
||||
currentTime: `${hours}:${minutes}`
|
||||
});
|
||||
};
|
||||
|
||||
// 立即更新一次
|
||||
updateTime();
|
||||
// 每分钟更新一次
|
||||
this.timeInterval = setInterval(updateTime, 60000);
|
||||
},
|
||||
|
||||
// 计算阅读进度
|
||||
calculateProgress() {
|
||||
// 当前章节进度
|
||||
const currentChapterProgress = this.data.pages.length > 0
|
||||
? (this.data.currentPage + 1) / this.data.pages.length
|
||||
: 0;
|
||||
|
||||
// 总进度计算:(当前章节 - 1 + 当前章节阅读进度) / 总章节数
|
||||
const totalProgress = Math.floor(
|
||||
((this.data.currentChapter - 1 + currentChapterProgress) / this.data.totalChapters) * 100
|
||||
);
|
||||
|
||||
console.log('Progress calculation:', {
|
||||
currentChapter: this.data.currentChapter,
|
||||
totalChapters: this.data.totalChapters,
|
||||
currentPage: this.data.currentPage,
|
||||
totalPages: this.data.pages.length,
|
||||
chapterProgress: currentChapterProgress,
|
||||
totalProgress
|
||||
});
|
||||
|
||||
this.setData({
|
||||
readingProgress: totalProgress
|
||||
});
|
||||
},
|
||||
|
||||
// 关闭所有面板
|
||||
closeAllPanels() {
|
||||
this.setData({
|
||||
showSettings: false,
|
||||
showMenu: false
|
||||
});
|
||||
},
|
||||
|
||||
// 切换主题
|
||||
switchTheme(e: any) {
|
||||
const themeId = e.currentTarget.dataset.theme;
|
||||
this.setData({
|
||||
currentTheme: themeId,
|
||||
showMore: false // 选择后关闭面板
|
||||
});
|
||||
wx.setStorageSync('reader_theme', themeId);
|
||||
},
|
||||
|
||||
// 调整屏幕亮度
|
||||
adjustBrightness(e: WechatMiniprogram.SliderChange) {
|
||||
const value = e.detail.value;
|
||||
console.log('调整亮度:', value); // 添加调试日志
|
||||
wx.setScreenBrightness({
|
||||
value: value / 100,
|
||||
success: () => {
|
||||
this.setData({ brightness: value });
|
||||
wx.setStorageSync('reader_brightness', value);
|
||||
console.log('亮度设置成功:', value);
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('设置亮度失败:', error);
|
||||
wx.showToast({
|
||||
title: '设置亮度失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 自动阅读相关
|
||||
toggleAutoReading(e: any) {
|
||||
const isChecked = e.detail.value;
|
||||
this.setData({ autoReading: isChecked });
|
||||
if (isChecked) {
|
||||
this.startAutoReading();
|
||||
} else {
|
||||
this.stopAutoReading();
|
||||
}
|
||||
},
|
||||
|
||||
setAutoReadingInterval(e: any) {
|
||||
const interval = [3000, 5000, 8000, 10000][e.detail.value];
|
||||
this.setData({ autoReadingInterval: interval });
|
||||
if (this.data.autoReading) {
|
||||
this.stopAutoReading();
|
||||
this.startAutoReading();
|
||||
}
|
||||
},
|
||||
|
||||
startAutoReading() {
|
||||
this.autoReadingTimer = setInterval(() => {
|
||||
this.nextPage();
|
||||
}, this.data.autoReadingInterval);
|
||||
},
|
||||
|
||||
stopAutoReading() {
|
||||
if (this.autoReadingTimer) {
|
||||
clearInterval(this.autoReadingTimer);
|
||||
}
|
||||
},
|
||||
|
||||
// 书签相关
|
||||
toggleBookmarks() {
|
||||
this.setData({
|
||||
showBookmarks: !this.data.showBookmarks,
|
||||
showSettings: false,
|
||||
showNotes: false
|
||||
});
|
||||
},
|
||||
|
||||
addBookmark() {
|
||||
const bookmark = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: this.data.pages[this.data.currentPage].slice(0, 50),
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const bookmarks = [...this.data.bookmarks, bookmark];
|
||||
this.setData({ bookmarks });
|
||||
wx.setStorageSync(`bookmarks_${this.data.bookTitle}`, bookmarks);
|
||||
wx.showToast({ title: '添加书签成功', icon: 'success' });
|
||||
},
|
||||
|
||||
loadBookmarks() {
|
||||
const bookmarks = wx.getStorageSync(`bookmarks_${this.data.bookTitle}`) || [];
|
||||
this.setData({ bookmarks });
|
||||
},
|
||||
|
||||
jumpToBookmark(e: any) {
|
||||
const bookmark = e.currentTarget.dataset.bookmark;
|
||||
this.loadChapter(bookmark.chapter).then(() => {
|
||||
this.setData({
|
||||
currentPage: bookmark.page,
|
||||
showBookmarks: false
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// 笔记相关
|
||||
toggleNotes() {
|
||||
this.setData({
|
||||
showNotes: !this.data.showNotes,
|
||||
showSettings: false,
|
||||
showBookmarks: false
|
||||
});
|
||||
},
|
||||
|
||||
showTextMenu(e: any) {
|
||||
const text = this.data.pages[this.data.currentPage];
|
||||
const start = 0;
|
||||
const end = text.length;
|
||||
|
||||
this.setData({
|
||||
selectionStart: start,
|
||||
selectionEnd: end,
|
||||
selectedText: text.substring(start, end),
|
||||
showTextMenu: true,
|
||||
textMenuTop: e.touches[0].clientY,
|
||||
textMenuLeft: e.touches[0].clientX
|
||||
});
|
||||
},
|
||||
|
||||
addNote() {
|
||||
wx.showModal({
|
||||
title: '添加笔记',
|
||||
editable: true,
|
||||
placeholderText: '请输入笔记内容',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const note = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: res.content,
|
||||
selectedText: this.data.selectedText,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const notes = [...this.data.notes, note];
|
||||
this.setData({
|
||||
notes,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`notes_${this.data.bookTitle}`, notes);
|
||||
wx.showToast({ title: '添加笔记成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
loadNotes() {
|
||||
const notes = wx.getStorageSync(`notes_${this.data.bookTitle}`) || [];
|
||||
this.setData({ notes });
|
||||
},
|
||||
|
||||
copyText() {
|
||||
wx.setClipboardData({
|
||||
data: this.data.selectedText,
|
||||
success: () => {
|
||||
this.setData({ showTextMenu: false });
|
||||
wx.showToast({ title: '复制成功', icon: 'success' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 阅读统计相关
|
||||
startReadingTimer() {
|
||||
this.setData({
|
||||
readingStartTime: Date.now(),
|
||||
isReading: true,
|
||||
readingDuration: 0
|
||||
});
|
||||
|
||||
// 每秒更新阅读时长
|
||||
this.data.readingTimer = setInterval(() => {
|
||||
const duration = Math.floor((Date.now() - this.data.readingStartTime) / 1000);
|
||||
this.setData({ readingDuration: duration });
|
||||
}, 1000);
|
||||
},
|
||||
|
||||
updateReadingTime() {
|
||||
const duration = Math.floor((Date.now() - this.data.readingStartTime) / 1000);
|
||||
const today = new Date().toDateString();
|
||||
const newTodayTime = this.data.todayReadingTime + duration;
|
||||
this.setData({ todayReadingTime: newTodayTime });
|
||||
|
||||
let statistics = wx.getStorageSync('reading_statistics') || {};
|
||||
statistics[today] = newTodayTime;
|
||||
wx.setStorageSync('reading_statistics', statistics);
|
||||
},
|
||||
|
||||
loadTodayReadingTime() {
|
||||
const today = new Date().toDateString();
|
||||
const statistics = wx.getStorageSync('reading_statistics') || {};
|
||||
this.setData({ todayReadingTime: statistics[today] || 0 });
|
||||
},
|
||||
|
||||
// 保存和<E5AD98><E5928C>复设置
|
||||
saveReadingProgress() {
|
||||
const progress = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
wx.setStorageSync(`reading_progress_${this.data.bookTitle}`, progress);
|
||||
},
|
||||
|
||||
restoreSettings() {
|
||||
// 恢复主题
|
||||
const theme = wx.getStorageSync('reader_theme');
|
||||
if (theme) {
|
||||
this.setData({ currentTheme: theme });
|
||||
}
|
||||
|
||||
// 恢复亮度
|
||||
const brightness = wx.getStorageSync('reader_brightness');
|
||||
if (brightness !== undefined) {
|
||||
this.setData({ brightness });
|
||||
wx.setScreenBrightness({ value: brightness / 100 });
|
||||
}
|
||||
|
||||
// 恢复阅读进度
|
||||
const progress = wx.getStorageSync(`reading_progress_${this.data.bookTitle}`);
|
||||
if (progress) {
|
||||
this.loadChapter(progress.chapter).then(() => {
|
||||
this.setData({ currentPage: progress.page });
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleMore() {
|
||||
this.setData({
|
||||
showMore: !this.data.showMore,
|
||||
showSettings: false,
|
||||
showMenu: false
|
||||
});
|
||||
},
|
||||
|
||||
// 处理文本选择事件
|
||||
onSelectText(e: any) {
|
||||
console.log('Selected text:', e.detail);
|
||||
const selectedText = e.detail.text;
|
||||
if (selectedText && selectedText.trim()) {
|
||||
this.setData({ selectedText });
|
||||
}
|
||||
},
|
||||
|
||||
// 隐藏文本菜单
|
||||
hideTextMenu() {
|
||||
this.setData({
|
||||
showTextMenu: false,
|
||||
selectionStart: -1,
|
||||
selectionEnd: -1
|
||||
});
|
||||
},
|
||||
|
||||
// 添加书签
|
||||
addBookmarkWithSelection() {
|
||||
const bookmark = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
content: this.data.selectedText,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const bookmarks = [...this.data.bookmarks, bookmark];
|
||||
this.setData({
|
||||
bookmarks,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`bookmarks_${this.data.bookTitle}`, bookmarks);
|
||||
wx.showToast({
|
||||
title: '添加书签成功',
|
||||
icon: 'success'
|
||||
});
|
||||
},
|
||||
|
||||
// 添加笔记
|
||||
addNoteWithSelection() {
|
||||
wx.showModal({
|
||||
title: '添加笔记',
|
||||
editable: true,
|
||||
placeholderText: '请输入笔记内容',
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const note = {
|
||||
chapter: this.data.currentChapter,
|
||||
page: this.data.currentPage,
|
||||
selectedText: this.data.selectedText,
|
||||
content: res.content,
|
||||
time: new Date().toLocaleString()
|
||||
};
|
||||
const notes = [...this.data.notes, note];
|
||||
this.setData({
|
||||
notes,
|
||||
showTextMenu: false
|
||||
});
|
||||
wx.setStorageSync(`notes_${this.data.bookTitle}`, notes);
|
||||
wx.showToast({
|
||||
title: '添加笔记成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 复制选中文本
|
||||
copySelectedText() {
|
||||
if (this.data.selectedText) {
|
||||
wx.setClipboardData({
|
||||
data: this.data.selectedText,
|
||||
success: () => {
|
||||
this.setData({ showTextMenu: false });
|
||||
wx.showToast({
|
||||
title: '复制成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 结束计时并保存
|
||||
stopReadingTimer() {
|
||||
if (!this.data.isReading) return;
|
||||
|
||||
if (this.data.readingTimer) {
|
||||
clearInterval(this.data.readingTimer);
|
||||
}
|
||||
|
||||
const endTime = Date.now();
|
||||
const duration = Math.floor((endTime - this.data.readingStartTime) / 1000);
|
||||
|
||||
this.setData({
|
||||
readingDuration: duration,
|
||||
isReading: false
|
||||
});
|
||||
|
||||
// 保存阅读记录
|
||||
this.saveReadingRecord(duration);
|
||||
},
|
||||
|
||||
// 保存阅读记录
|
||||
saveReadingRecord(duration: number) {
|
||||
const readingRecord = {
|
||||
bookId: this.data.bookId,
|
||||
startTime: this.data.readingStartTime,
|
||||
duration: duration,
|
||||
endTime: Date.now()
|
||||
};
|
||||
|
||||
// 可以选择保存到本地或发送到服务器
|
||||
wx.setStorageSync('lastReadingRecord', readingRecord);
|
||||
|
||||
wx.showToast({
|
||||
title: `本次阅读 ${Math.floor(duration / 60)} 分钟`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,120 @@
|
|||
<view class="reader" style="background: {{themes[currentTheme].bg}}; color: {{themes[currentTheme].color}}">
|
||||
<!-- 顶部工具栏 -->
|
||||
<view class="toolbar {{showToolbar ? '' : 'hidden'}}">
|
||||
<view class="back" bindtap="goBack">
|
||||
<text class="back-icon">←</text>
|
||||
</view>
|
||||
<view class="title">{{bookTitle}}</view>
|
||||
<view class="menu" bindtap="toggleMenu">
|
||||
<text class="menu-icon">≡</text>
|
||||
</view>
|
||||
<view class="reading-time" wx:if="{{isReading}}">
|
||||
已阅读 {{Math.floor(readingDuration / 60)}}分{{readingDuration % 60}}秒
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读区域 -->
|
||||
<view class="content"
|
||||
bindtap="hideTextMenu"
|
||||
bindtouchstart="touchStart"
|
||||
bindtouchend="touchEnd">
|
||||
<view class="chapter-title">第{{currentChapter}}章</view>
|
||||
<view class="page-content" style="font-size: {{fontSize}}rpx;">
|
||||
<text user-select="true"
|
||||
selection-start="{{selectionStart}}"
|
||||
selection-end="{{selectionEnd}}"
|
||||
bindlongpress="showTextMenu"
|
||||
catch:tap="toggleToolbar">{{pages[currentPage]}}</text>
|
||||
</view>
|
||||
<view class="page-number">{{currentPage + 1}}/{{pages.length}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部工具栏 -->
|
||||
<view class="bottom-toolbar {{showToolbar ? '' : 'hidden'}}">
|
||||
<view class="toolbar-top">
|
||||
<view class="prev" bindtap="prevPage">上一页</view>
|
||||
<view class="progress">{{currentChapter}}/{{totalChapters}}</view>
|
||||
<view class="settings" bindtap="toggleSettings">设置</view>
|
||||
<view class="next" bindtap="nextPage">下一页</view>
|
||||
</view>
|
||||
<view class="toolbar-bottom">
|
||||
<view class="current-time">{{currentTime}}</view>
|
||||
<view class="reading-progress">已读{{readingProgress}}%</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录菜单 -->
|
||||
<view class="menu-panel {{showMenu ? 'show' : ''}}">
|
||||
<view class="menu-header">
|
||||
<text>目录</text>
|
||||
<text class="close" bindtap="toggleMenu">×</text>
|
||||
</view>
|
||||
<scroll-view class="chapter-list" scroll-y>
|
||||
<view class="chapter-item {{currentChapter === index + 1 ? 'active' : ''}}"
|
||||
wx:for="{{chapters}}"
|
||||
wx:key="index"
|
||||
bindtap="jumpToChapter"
|
||||
data-chapter="{{index + 1}}">
|
||||
第{{index + 1}}章
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 设置面板 -->
|
||||
<view class="settings-panel {{showSettings ? 'show' : ''}}">
|
||||
<view class="settings-header">
|
||||
<text>设置</text>
|
||||
<text class="close" bindtap="toggleSettings">×</text>
|
||||
</view>
|
||||
<view class="settings-content">
|
||||
<!-- 字体大小设置 -->
|
||||
<view class="setting-item">
|
||||
<text>字体大小</text>
|
||||
<view class="font-size-control">
|
||||
<view class="size-btn" bindtap="decreaseFontSize">A-</view>
|
||||
<text class="current-size">{{fontSize}}px</text>
|
||||
<view class="size-btn" bindtap="increaseFontSize">A+</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主题设置 -->
|
||||
<view class="setting-item">
|
||||
<text>主题</text>
|
||||
<view class="theme-list">
|
||||
<view class="theme-item {{currentTheme === theme.id ? 'active' : ''}}"
|
||||
wx:for="{{themes}}"
|
||||
wx:key="id"
|
||||
wx:for-item="theme"
|
||||
bindtap="switchTheme"
|
||||
data-theme="{{theme.id}}"
|
||||
style="background: {{theme.bg}};">
|
||||
<text style="color: {{theme.color}}">{{theme.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 亮度调节 -->
|
||||
<view class="setting-item">
|
||||
<text>屏幕亮度</text>
|
||||
<slider class="brightness-slider"
|
||||
min="0"
|
||||
max="100"
|
||||
value="{{brightness}}"
|
||||
bind:change="adjustBrightness"
|
||||
block-size="20"
|
||||
activeColor="#1989fa"/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 遮罩层 -->
|
||||
<view class="mask {{showSettings || showMenu ? 'show' : ''}}"
|
||||
bindtap="closeAllPanels"></view>
|
||||
|
||||
<!-- 文本选择菜单 -->
|
||||
<view class="text-menu {{showTextMenu ? 'show' : ''}}"
|
||||
style="top: {{textMenuTop}}px; left: {{textMenuLeft}}px">
|
||||
<view class="menu-item" bindtap="addNote">添加笔记</view>
|
||||
<view class="menu-item" bindtap="addBookmark">添加书签</view>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,402 @@
|
|||
.reader {
|
||||
height: 100vh;
|
||||
background: #f4ecd8;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 顶部工具栏 */
|
||||
.toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 88rpx;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 30rpx;
|
||||
transition: all 0.3s;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.toolbar.hidden {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.back, .menu {
|
||||
color: #fff;
|
||||
font-size: 40rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.title {
|
||||
flex: 1;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 阅读区域 */
|
||||
.content {
|
||||
height: 100vh;
|
||||
padding: 0rpx 0rpx 0rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
max-width: 700rpx;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin: 20rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-content {
|
||||
flex: 1;
|
||||
line-height: 1.8;
|
||||
text-align: justify;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-content text {
|
||||
text-indent: 2em;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
/* 底部页码 */
|
||||
.page-info {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-size: 24rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
/* 底部评论区域 */
|
||||
.bottom-section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bookmark-btn {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.add-bookmark {
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
padding: 10rpx 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-bookmark .close {
|
||||
margin-left: 10rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 评论列表 */
|
||||
.comment-list {
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.comment-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.comment-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
/* 底部工具栏 */
|
||||
.bottom-toolbar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
transition: all 0.3s;
|
||||
z-index: 100;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.toolbar-top {
|
||||
height: 100rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.toolbar-bottom {
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 40rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.prev, .next, .settings {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
}
|
||||
|
||||
.progress {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.current-time, .reading-progress {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
/* 设置面板 */
|
||||
.settings-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #fff;
|
||||
transform: translateY(100%);
|
||||
transition: all 0.3s;
|
||||
z-index: 999;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.settings-panel.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.settings-header .close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.setting-item text {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 字体大小控制 */
|
||||
.font-size-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #f7f7f7;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.size-btn {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.current-size {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 主题列表 */
|
||||
.theme-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
aspect-ratio: 1;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.theme-item.active {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4rpx 12rpx rgba(25, 137, 250, 0.2);
|
||||
}
|
||||
|
||||
/* 亮度调节 */
|
||||
.brightness-slider {
|
||||
margin-top: 16rpx;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
|
||||
.brightness-slider slider {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s;
|
||||
z-index: 998;
|
||||
}
|
||||
|
||||
.mask.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 目录菜单 */
|
||||
.menu-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 80%;
|
||||
background: #fff;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.menu-panel.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.menu-header .close {
|
||||
font-size: 40rpx;
|
||||
color: #999;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.chapter-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.chapter-item {
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
color: #333;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.chapter-item.active {
|
||||
color: #1989fa;
|
||||
background: rgba(25, 137, 250, 0.1);
|
||||
}
|
||||
|
||||
/* 文本选择菜单 */
|
||||
.text-menu {
|
||||
position: fixed;
|
||||
background: rgba(0,0,0,0.8);
|
||||
border-radius: 8rpx;
|
||||
padding: 10rpx 0;
|
||||
transform: translate(-50%, -100%);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.2s;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.text-menu.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
padding: 16rpx 30rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.menu-item:active {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.reading-time {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
margin-right: 20rpx;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// index.ts
|
||||
// 获取应用实例
|
||||
const app = getApp<IAppOption>()
|
||||
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
||||
|
||||
Component({
|
||||
data: {
|
||||
userInfo: {
|
||||
avatarUrl: '/images/default-avatar.png',
|
||||
nickName: '体验卡已过期'
|
||||
},
|
||||
recentBooks: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Java编程思想',
|
||||
author: 'Bruce Eckel',
|
||||
coverUrl: 'https://img3.doubanio.com/view/subject/l/public/s27243455.jpg',
|
||||
bookUrl: '',
|
||||
description: '本书赢得了全球程序员的广泛赞誉'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'C Primer Plus',
|
||||
author: 'Stephen Prata',
|
||||
coverUrl: 'https://img2.doubanio.com/view/subject/l/public/s29196249.jpg',
|
||||
bookUrl: '',
|
||||
description: 'C语言程序设计经典教程'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '狂帆',
|
||||
author: '血红',
|
||||
coverUrl: 'https://img1.doubanio.com/view/subject/l/public/s29934566.jpg',
|
||||
bookUrl: '',
|
||||
description: '一本精彩的玄幻小说'
|
||||
}
|
||||
] as Book[],
|
||||
recommendBooks: [
|
||||
{
|
||||
id: 4,
|
||||
title: '龙族IV:奥丁之渊',
|
||||
author: '江南',
|
||||
coverUrl: 'https://img2.doubanio.com/view/subject/l/public/s29850307.jpg',
|
||||
bookUrl: '',
|
||||
description: '青春如同奔流的江水,一去不回来不及道别',
|
||||
rating: 89.8,
|
||||
status: '大家都在读'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '白鹿原',
|
||||
author: '陈忠实',
|
||||
coverUrl: 'https://img1.doubanio.com/view/subject/l/public/s28111905.jpg',
|
||||
bookUrl: '',
|
||||
description: '一部渭河平原五十年变迁的雄奇史诗',
|
||||
rating: 91.9,
|
||||
status: '神作'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Linux是怎样工作的',
|
||||
author: '[日]武内觉',
|
||||
coverUrl: 'https://img3.doubanio.com/view/subject/l/public/s33654311.jpg',
|
||||
bookUrl: '',
|
||||
description: '从命令行、Shell脚本到内核原理',
|
||||
rating: 80.0,
|
||||
status: '技术经典'
|
||||
}
|
||||
] as Book[],
|
||||
currentTab: 'recommend',
|
||||
loading: false
|
||||
},
|
||||
|
||||
lifetimes: {
|
||||
attached() {
|
||||
// 使用模拟数据,暂时注释掉实际的API调用
|
||||
// this.getUserInfo();
|
||||
// this.getRecentBooks();
|
||||
// this.getRecommendBooks();
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 获取用户信息
|
||||
async getUserInfo() {
|
||||
const userInfo = wx.getStorageSync('userInfo');
|
||||
if (userInfo) {
|
||||
this.setData({ userInfo });
|
||||
}
|
||||
},
|
||||
|
||||
// 获取最近阅读的书籍
|
||||
async getRecentBooks() {
|
||||
try {
|
||||
const res = await request.get<BookListResponse>('/books/recent');
|
||||
if (res.code === 200) {
|
||||
this.setData({
|
||||
recentBooks: res.data.records
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取最近阅读失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取推荐书籍
|
||||
async getRecommendBooks() {
|
||||
try {
|
||||
this.setData({ loading: true });
|
||||
const res = await request.get<BookListResponse>('/books/recommend');
|
||||
if (res.code === 200) {
|
||||
this.setData({
|
||||
recommendBooks: res.data.records
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取推荐书籍失败:', error);
|
||||
} finally {
|
||||
this.setData({ loading: false });
|
||||
}
|
||||
},
|
||||
|
||||
// 切换标签
|
||||
switchTab(e: any) {
|
||||
const tab = e.currentTarget.dataset.tab;
|
||||
this.setData({ currentTab: tab });
|
||||
// TODO: 根据不同标签加载不同内容
|
||||
},
|
||||
|
||||
// 打开书籍
|
||||
openBook(e: any) {
|
||||
const { id, url } = e.currentTarget.dataset;
|
||||
wx.navigateTo({
|
||||
url: `/pages/bookshelf/reader?id=${id}&bookUrl=${encodeURIComponent(url)}`
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到书架
|
||||
goToBookshelf() {
|
||||
wx.switchTab({
|
||||
url: '/pages/bookshelf/list'
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到设置
|
||||
goToSettings() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/profile/settings'
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,80 @@
|
|||
<!--index.wxml-->
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<view class="container">
|
||||
<!-- 顶部标题栏 -->
|
||||
<view class="header">
|
||||
<text class="title">阅读管理</text>
|
||||
<view class="settings" bindtap="goToSettings">设置</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<view class="search-box">
|
||||
<icon type="search" size="16" color="#999"/>
|
||||
<input class="search-input" placeholder="搜索" placeholder-class="search-placeholder"/>
|
||||
</view>
|
||||
|
||||
<!-- 用户信息栏 -->
|
||||
<view class="user-bar">
|
||||
<text class="status-text">体验卡已过期</text>
|
||||
</view>
|
||||
|
||||
<!-- 书架预览 -->
|
||||
<scroll-view class="book-list" scroll-x enable-flex>
|
||||
<view class="book-item"
|
||||
wx:for="{{recentBooks}}"
|
||||
wx:key="id"
|
||||
bindtap="openBook"
|
||||
data-id="{{item.id}}"
|
||||
data-url="{{item.bookUrl}}">
|
||||
<image class="book-cover" src="{{item.coverUrl}}" mode="aspectFill"/>
|
||||
</view>
|
||||
<view class="book-more" bindtap="goToBookshelf">
|
||||
<text>书架</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 导航标签 -->
|
||||
<scroll-view class="nav-tabs" scroll-x enable-flex>
|
||||
<view class="tab {{currentTab === 'recommend' ? 'active' : ''}}"
|
||||
data-tab="recommend"
|
||||
bindtap="switchTab">推荐</view>
|
||||
<view class="tab {{currentTab === 'category' ? 'active' : ''}}"
|
||||
data-tab="category"
|
||||
bindtap="switchTab">分类</view>
|
||||
<view class="tab {{currentTab === 'rank' ? 'active' : ''}}"
|
||||
data-tab="rank"
|
||||
bindtap="switchTab">排行</view>
|
||||
<view class="tab {{currentTab === 'novel' ? 'active' : ''}}"
|
||||
data-tab="novel"
|
||||
bindtap="switchTab">精品小说</view>
|
||||
<view class="tab {{currentTab === 'literature' ? 'active' : ''}}"
|
||||
data-tab="literature"
|
||||
bindtap="switchTab">文学</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content">
|
||||
<!-- 推荐列表 -->
|
||||
<view class="book-list" wx:if="{{currentTab === 'recommend'}}">
|
||||
<view class="book-item"
|
||||
wx:for="{{recommendBooks}}"
|
||||
wx:key="id"
|
||||
bindtap="openBook"
|
||||
data-id="{{item.id}}"
|
||||
data-url="{{item.bookUrl}}">
|
||||
<image class="book-cover" src="{{item.coverUrl}}" mode="aspectFill"/>
|
||||
<view class="book-info">
|
||||
<text class="book-title">{{item.title}}</text>
|
||||
<view class="book-meta">
|
||||
<text class="book-author">{{item.author}}</text>
|
||||
<text class="book-rating">推荐值 {{item.rating}}%</text>
|
||||
</view>
|
||||
<text class="book-desc">{{item.description}}</text>
|
||||
<text class="book-status">{{item.status}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
|
@ -0,0 +1,351 @@
|
|||
/**index.wxss**/
|
||||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.userinfo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: #aaa;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.userinfo-avatar {
|
||||
overflow: hidden;
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
margin: 20rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.usermotto {
|
||||
margin-top: 200px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
padding: 0;
|
||||
width: 56px !important;
|
||||
border-radius: 8px;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
display: block;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.nickname-wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
border-top: .5px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
|
||||
color: black;
|
||||
}
|
||||
|
||||
.nickname-label {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.nickname-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
|
||||
/* 顶部标题栏 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 30rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.settings {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 搜索框 */
|
||||
.search-box {
|
||||
margin: 20rpx 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f2f3f5;
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
flex: 1;
|
||||
margin-left: 16rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 用户信息栏 */
|
||||
.user-bar {
|
||||
padding: 20rpx 30rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.status-text {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 书架预览 */
|
||||
.book-list {
|
||||
padding: 20rpx 30rpx;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.book-item {
|
||||
display: inline-block;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
width: 180rpx;
|
||||
height: 240rpx;
|
||||
border-radius: 8rpx;
|
||||
box-shadow: 0 4rpx 8rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.book-more {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 180rpx;
|
||||
height: 240rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 8rpx;
|
||||
color: #666;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
margin-top: 10rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 导航标签 */
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
background: #fff;
|
||||
padding: 0 20rpx;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 20rpx 30rpx;
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 6rpx;
|
||||
background: #333;
|
||||
border-radius: 3rpx;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.content .book-item {
|
||||
display: flex;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
.content .book-cover {
|
||||
width: 160rpx;
|
||||
height: 220rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.book-author {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.book-rating {
|
||||
font-size: 26rpx;
|
||||
color: #ff9500;
|
||||
}
|
||||
|
||||
.book-desc {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.book-status {
|
||||
font-size: 24rpx;
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.today-stats {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 24rpx;
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.note-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.note-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin: 10rpx 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.reading-goal {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 30rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.goal-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.goal-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
import request from '../../utils/request';
|
||||
|
||||
Component({
|
||||
data: {
|
||||
username: '',
|
||||
password: '',
|
||||
isRegister: false // 控制是登录还是注册模式
|
||||
},
|
||||
|
||||
methods: {
|
||||
// 切换登录/注册模式
|
||||
switchMode() {
|
||||
this.setData({
|
||||
isRegister: !this.data.isRegister
|
||||
});
|
||||
},
|
||||
|
||||
// 输入手机号
|
||||
onPhoneInput(e: any) {
|
||||
this.setData({
|
||||
username: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 输入密码
|
||||
onPasswordInput(e: any) {
|
||||
this.setData({
|
||||
password: e.detail.value
|
||||
});
|
||||
},
|
||||
|
||||
// 登录
|
||||
async handleLogin() {
|
||||
console.log('开始登录:', this.data);
|
||||
|
||||
const { username, password } = this.data;
|
||||
if (!username || !password) {
|
||||
wx.showToast({
|
||||
title: '请输入账号和密码',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('发送登录请求...');
|
||||
const res = await request.post('/user/login', {
|
||||
activeTab: "account",
|
||||
username,
|
||||
password
|
||||
});
|
||||
console.log('登录响应:', res);
|
||||
|
||||
if (res.code === 200) {
|
||||
console.log('登录成功,保存信息...');
|
||||
// 保存用户信息和token
|
||||
wx.setStorageSync('userInfo', res.data);
|
||||
wx.setStorageSync('token', res.data);
|
||||
|
||||
console.log('准备跳转...');
|
||||
// 显示成功提示并跳转
|
||||
wx.showToast({
|
||||
title: '登录成功',
|
||||
icon: 'success',
|
||||
mask: true,
|
||||
duration: 1500,
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
wx.switchTab({
|
||||
url: '/pages/index/index',
|
||||
success: () => {
|
||||
console.log('跳转成功');
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('跳转失败:', error);
|
||||
}
|
||||
});
|
||||
}, 1500);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('登录失败:', res.message);
|
||||
wx.showToast({
|
||||
title: res.message || '登录失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录出错:', error);
|
||||
wx.showToast({
|
||||
title: '登录失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 注册
|
||||
async handleRegister() {
|
||||
const { username, password } = this.data;
|
||||
if (!username || !password) {
|
||||
wx.showToast({
|
||||
title: '请输入手机号和密码',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await request.post('/user/register', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
wx.showToast({
|
||||
title: '注册成功',
|
||||
icon: 'success'
|
||||
});
|
||||
this.setData({
|
||||
isRegister: false
|
||||
});
|
||||
} else {
|
||||
wx.showToast({
|
||||
title: res.message || '注册失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('注册失败:', error);
|
||||
wx.showToast({
|
||||
title: '注册失败',
|
||||
icon: 'none'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
<view class="login-container">
|
||||
<view class="logo">
|
||||
<image src="/images/logo.png" mode="aspectFit"/>
|
||||
</view>
|
||||
|
||||
<view class="form">
|
||||
<view class="form-item">
|
||||
<input type="number"
|
||||
placeholder="请输入账号"
|
||||
value="{{username}}"
|
||||
bindinput="onPhoneInput"/>
|
||||
</view>
|
||||
|
||||
<view class="form-item">
|
||||
<input type="password"
|
||||
placeholder="请输入密码"
|
||||
value="{{password}}"
|
||||
bindinput="onPasswordInput"/>
|
||||
</view>
|
||||
|
||||
<button class="submit-btn"
|
||||
bindtap="{{isRegister ? 'handleRegister' : 'handleLogin'}}">
|
||||
{{isRegister ? '注册' : '登录'}}
|
||||
</button>
|
||||
|
||||
<view class="switch-mode" bindtap="switchMode">
|
||||
{{isRegister ? '已有账号?去登录' : '没有账号?去注册'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,43 @@
|
|||
.login-container {
|
||||
height: 100vh;
|
||||
padding: 40rpx;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 80rpx auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo image {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 60rpx;
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 40rpx;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.form-item input {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 60rpx;
|
||||
background: #1989fa;
|
||||
color: #fff;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.switch-mode {
|
||||
margin-top: 40rpx;
|
||||
text-align: center;
|
||||
color: #1989fa;
|
||||
font-size: 28rpx;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// logs.ts
|
||||
// const util = require('../../utils/util.js')
|
||||
import { formatTime } from '../../utils/util'
|
||||
|
||||
Component({
|
||||
data: {
|
||||
logs: [],
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
this.setData({
|
||||
logs: (wx.getStorageSync('logs') || []).map((log: string) => {
|
||||
return {
|
||||
date: formatTime(new Date(log)),
|
||||
timeStamp: log
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
|
@ -0,0 +1,6 @@
|
|||
<!--logs.wxml-->
|
||||
<scroll-view class="scrollarea" scroll-y type="list">
|
||||
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
|
||||
<view class="log-item">{{index + 1}}. {{log.date}}</view>
|
||||
</block>
|
||||
</scroll-view>
|
|
@ -0,0 +1,16 @@
|
|||
page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.scrollarea {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.log-item {
|
||||
margin-top: 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.log-item:last-child {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"navigationBarTitleText": "笔记",
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
Component({
|
||||
data: {
|
||||
notes: []
|
||||
},
|
||||
methods: {
|
||||
// 获取笔记列表
|
||||
getNoteList() {
|
||||
// TODO: 从服务器获取笔记列表
|
||||
},
|
||||
|
||||
// 跳转到笔记详情
|
||||
goToNoteDetail(e: any) {
|
||||
const noteId = e.currentTarget.dataset.id;
|
||||
wx.navigateTo({
|
||||
url: `/pages/notes/detail?id=${noteId}`
|
||||
});
|
||||
},
|
||||
|
||||
// 创建新笔记
|
||||
createNote() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/notes/edit'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
<view class="notes">
|
||||
<!-- 笔记列表 -->
|
||||
<view class="note-list">
|
||||
<view class="note-item"
|
||||
wx:for="{{notes}}"
|
||||
wx:key="id"
|
||||
bindtap="goToNoteDetail"
|
||||
data-id="{{item.id}}">
|
||||
<view class="note-title">{{item.title}}</view>
|
||||
<view class="note-content">{{item.content}}</view>
|
||||
<view class="note-meta">
|
||||
<text class="note-book">《{{item.bookTitle}}》</text>
|
||||
<text class="note-time">{{item.createTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="add-btn" bindtap="createNote">
|
||||
<text class="add-icon">+</text>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,59 @@
|
|||
.notes {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.note-list {
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.note-item {
|
||||
background: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 12rpx;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.note-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.note-meta {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 40rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #1989fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 16rpx rgba(25, 137, 250, 0.3);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
color: #fff;
|
||||
font-size: 60rpx;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"navigationBarTitleText": "笔记",
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
Page({
|
||||
data: {
|
||||
activeTab: 'notes',
|
||||
notes: [
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 1,
|
||||
selectedText: '"方源,乖乖地交出春秋蝉,我给你个痛快!"',
|
||||
content: '反派的第一句台词,霸气十足。',
|
||||
time: '2024-03-17 15:30'
|
||||
},
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 1,
|
||||
selectedText: '"方老魔,你不要妄图反抗了,今日我们正道各大派联手起来,就是要踏破你的魔窟。这里早已经布下天罗地网,这次你必定身首异处!"',
|
||||
content: '正派群起而攻之,场面宏大。',
|
||||
time: '2024-03-17 16:45'
|
||||
},
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 2,
|
||||
selectedText: '一直静如雕塑的方源,慢慢转身。群雄顿时一阵骚动,齐齐后退一大步。',
|
||||
content: '反派气场强大,一个转身就震慑群雄。',
|
||||
time: '2024-03-17 17:00'
|
||||
}
|
||||
],
|
||||
bookmarks: [
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 1,
|
||||
content: '围攻他的正道群雄,不是堂堂一派之长者尊贵,就是名动四方之少年英豪。此时军军包围着方源,有的在咆哮,有的在冷笑,有的双眼眯起闪着警惕的光,有的捂着伤口恨恨地望着。',
|
||||
page: 0,
|
||||
time: '2024-03-17 15:20'
|
||||
},
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 3,
|
||||
content: '他们没有动手,都忌惮着方源的临死反扑。就这样紧张地对峙了三个时辰,夕阳西下,落日的余晖将山边的晚霞点燃,一时间绚烂如火。',
|
||||
page: 2,
|
||||
time: '2024-03-17 17:00'
|
||||
},
|
||||
{
|
||||
bookTitle: '大爱仙尊',
|
||||
chapter: 5,
|
||||
content: '方源站在悬崖边上,背后是万丈深渊。他的黑袍在风中猎猎作响,长发飞扬,眼神中透露出一丝决绝。',
|
||||
page: 1,
|
||||
time: '2024-03-17 17:30'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 如果从书签跳转过来,自动切换到书签标签
|
||||
if (options.tab === 'bookmarks') {
|
||||
this.setData({ activeTab: 'bookmarks' });
|
||||
}
|
||||
},
|
||||
|
||||
// 切换标签
|
||||
switchTab(e: any) {
|
||||
const tab = e.currentTarget.dataset.tab;
|
||||
this.setData({ activeTab: tab });
|
||||
},
|
||||
|
||||
// 编辑笔记
|
||||
editNote(e: any) {
|
||||
const note = e.currentTarget.dataset.note;
|
||||
wx.showModal({
|
||||
title: '编辑笔记',
|
||||
editable: true,
|
||||
content: note.content,
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const notes = this.data.notes.map(item => {
|
||||
if (item.time === note.time) {
|
||||
return { ...item, content: res.content };
|
||||
}
|
||||
return item;
|
||||
});
|
||||
this.setData({ notes });
|
||||
wx.showToast({ title: '编辑成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 删除笔记
|
||||
deleteNote(e: any) {
|
||||
const note = e.currentTarget.dataset.note;
|
||||
wx.showModal({
|
||||
title: '删除笔记',
|
||||
content: '确定要删除这条笔记吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const notes = this.data.notes.filter(item => item.time !== note.time);
|
||||
this.setData({ notes });
|
||||
wx.showToast({ title: '删除成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到书签位置
|
||||
jumpToBookmark(e: any) {
|
||||
const bookmark = e.currentTarget.dataset.bookmark;
|
||||
wx.navigateTo({
|
||||
url: `/pages/reader/reader?bookTitle=${bookmark.bookTitle}&chapter=${bookmark.chapter}&page=${bookmark.page}`
|
||||
});
|
||||
},
|
||||
|
||||
// 删除书签
|
||||
deleteBookmark(e: any) {
|
||||
const bookmark = e.currentTarget.dataset.bookmark;
|
||||
wx.showModal({
|
||||
title: '删除书签',
|
||||
content: '确定要删除这个书签吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const bookmarks = this.data.bookmarks.filter(item => item.time !== bookmark.time);
|
||||
this.setData({ bookmarks });
|
||||
wx.showToast({ title: '删除成功', icon: 'success' });
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// 跳转到阅读器
|
||||
navigateToReader() {
|
||||
wx.navigateTo({
|
||||
url: '/pages/reader/reader'
|
||||
});
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从本地存储获取最新数据
|
||||
const notes = wx.getStorageSync('all_notes') || this.data.notes;
|
||||
const bookmarks = wx.getStorageSync('all_bookmarks') || this.data.bookmarks;
|
||||
this.setData({ notes, bookmarks });
|
||||
}
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
<view class="notes-page">
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-item {{activeTab === 'notes' ? 'active' : ''}}"
|
||||
bindtap="switchTab" data-tab="notes">笔记</view>
|
||||
<view class="nav-item {{activeTab === 'bookmarks' ? 'active' : ''}}"
|
||||
bindtap="switchTab" data-tab="bookmarks">书签</view>
|
||||
</view>
|
||||
|
||||
<!-- 笔记列表 -->
|
||||
<view class="notes-list" wx:if="{{activeTab === 'notes'}}">
|
||||
<block wx:if="{{notes.length > 0}}">
|
||||
<view class="note-item" wx:for="{{notes}}" wx:key="time">
|
||||
<view class="note-header">
|
||||
<text class="book-title">{{item.bookTitle}}</text>
|
||||
<text class="chapter">第{{item.chapter}}章</text>
|
||||
</view>
|
||||
<view class="note-content">
|
||||
<text class="selected-text">{{item.selectedText}}</text>
|
||||
<text class="note-text">{{item.content}}</text>
|
||||
</view>
|
||||
<view class="note-footer">
|
||||
<text class="time">{{item.time}}</text>
|
||||
<view class="actions">
|
||||
<text class="action" bindtap="editNote" data-note="{{item}}">编辑</text>
|
||||
<text class="action" bindtap="deleteNote" data-note="{{item}}">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="empty-state" wx:else>
|
||||
<text>暂无笔记</text>
|
||||
<text class="tip">阅读时长按文字添加笔记</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书签列表 -->
|
||||
<view class="bookmarks-list" wx:if="{{activeTab === 'bookmarks'}}">
|
||||
<block wx:if="{{bookmarks.length > 0}}">
|
||||
<view class="bookmark-item" wx:for="{{bookmarks}}" wx:key="time">
|
||||
<view class="bookmark-header">
|
||||
<text class="book-title">{{item.bookTitle}}</text>
|
||||
<text class="chapter">第{{item.chapter}}章</text>
|
||||
</view>
|
||||
<view class="bookmark-content">{{item.content}}</view>
|
||||
<view class="bookmark-footer">
|
||||
<text class="time">{{item.time}}</text>
|
||||
<view class="actions">
|
||||
<text class="action" bindtap="jumpToBookmark" data-bookmark="{{item}}">跳转</text>
|
||||
<text class="action" bindtap="deleteBookmark" data-bookmark="{{item}}">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="empty-state" wx:else>
|
||||
<text>暂无书签</text>
|
||||
<text class="tip">阅读时点击右上角添加书签</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<view class="add-btn" bindtap="navigateToReader">
|
||||
<text class="plus">+</text>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,168 @@
|
|||
.notes-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
background: #fff;
|
||||
padding: 20rpx 40rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
padding: 20rpx 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #1989fa;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-item.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background: #1989fa;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
|
||||
/* 笔记列表 */
|
||||
.notes-list, .bookmarks-list {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.note-item, .bookmark-item {
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 20rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.note-header, .bookmark-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chapter {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.note-content {
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
|
||||
.selected-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
padding: 16rpx;
|
||||
border-radius: 8rpx;
|
||||
display: block;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.bookmark-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
margin: 16rpx 0;
|
||||
}
|
||||
|
||||
.note-footer, .bookmark-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.action {
|
||||
font-size: 24rpx;
|
||||
color: #1989fa;
|
||||
padding: 8rpx 16rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.empty-state text {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-state .tip {
|
||||
font-size: 24rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* 添加按钮 */
|
||||
.add-btn {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: calc(40rpx + env(safe-area-inset-bottom));
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: #1989fa;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4rpx 12rpx rgba(25, 137, 250, 0.4);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.add-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.add-btn .plus {
|
||||
color: #fff;
|
||||
font-size: 48rpx;
|
||||
font-weight: 300;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"navigationBarTitleText": "我的",
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
Component({
|
||||
data: {
|
||||
userInfo: {},
|
||||
readingStats: {
|
||||
totalBooks: 0,
|
||||
totalPages: 0,
|
||||
totalTime: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 获取用户信息
|
||||
getUserInfo() {
|
||||
// TODO: 获取用户信息
|
||||
},
|
||||
|
||||
// 获取阅读统计
|
||||
getReadingStats() {
|
||||
// TODO: 获取阅读统计数据
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,39 @@
|
|||
<view class="profile">
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info">
|
||||
<image class="avatar" src="{{userInfo.avatarUrl}}" mode="aspectFill"/>
|
||||
<text class="nickname">{{userInfo.nickName}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<view class="stats">
|
||||
<view class="stats-item">
|
||||
<text class="stats-num">{{readingStats.totalBooks}}</text>
|
||||
<text class="stats-label">读过的书</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-num">{{readingStats.totalPages}}</text>
|
||||
<text class="stats-label">阅读页数</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="stats-num">{{readingStats.totalTime}}</text>
|
||||
<text class="stats-label">阅读时长</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="menu-list">
|
||||
<view class="menu-item">
|
||||
<text class="menu-label">阅读目标</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item">
|
||||
<text class="menu-label">阅读报告</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
<view class="menu-item">
|
||||
<text class="menu-label">设置</text>
|
||||
<text class="menu-arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,70 @@
|
|||
.profile {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
background: #fff;
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
margin-left: 30rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stats {
|
||||
margin: 30rpx 0;
|
||||
padding: 30rpx;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.stats-num {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #1989fa;
|
||||
}
|
||||
|
||||
.stats-label {
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-list {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-arrow {
|
||||
color: #999;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"usingComponents": {}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
Page({
|
||||
data: {
|
||||
userInfo: {} as WechatMiniprogram.UserInfo,
|
||||
todayReadingTime: 0,
|
||||
totalBooks: 0,
|
||||
totalNotes: 0,
|
||||
totalBookmarks: 0,
|
||||
totalReadingDays: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadUserInfo();
|
||||
this.loadStatistics();
|
||||
},
|
||||
|
||||
// 加载用户信息
|
||||
loadUserInfo() {
|
||||
const userInfo = wx.getStorageSync('userInfo');
|
||||
if (userInfo) {
|
||||
this.setData({ userInfo });
|
||||
}
|
||||
},
|
||||
|
||||
// 加载统计数据
|
||||
loadStatistics() {
|
||||
// 获取今日阅读时间
|
||||
const today = new Date().toDateString();
|
||||
const statistics = wx.getStorageSync('reading_statistics') || {};
|
||||
const todayReadingTime = Math.floor((statistics[today] || 0) / 60); // 转换为分钟
|
||||
|
||||
// 获取笔记和书签数量
|
||||
const notes = wx.getStorageSync('all_notes') || [];
|
||||
const bookmarks = wx.getStorageSync('all_bookmarks') || [];
|
||||
|
||||
// 获取阅读过的书籍
|
||||
const books = wx.getStorageSync('reading_history') || [];
|
||||
|
||||
// 获取总阅读天数
|
||||
const readingDays = Object.keys(statistics).length;
|
||||
|
||||
this.setData({
|
||||
todayReadingTime,
|
||||
totalBooks: books.length,
|
||||
totalNotes: notes.length,
|
||||
totalBookmarks: bookmarks.length,
|
||||
totalReadingDays: readingDays
|
||||
});
|
||||
},
|
||||
|
||||
// 页面跳转
|
||||
navigateToNotes() {
|
||||
wx.navigateTo({ url: '/pages/notes/notes' });
|
||||
},
|
||||
|
||||
navigateToBookmarks() {
|
||||
wx.navigateTo({ url: '/pages/notes/notes?tab=bookmarks' });
|
||||
},
|
||||
|
||||
navigateToHistory() {
|
||||
wx.navigateTo({ url: '/pages/history/history' });
|
||||
},
|
||||
|
||||
navigateToSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' });
|
||||
}
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
<view class="profile-page">
|
||||
<!-- 用户信息 -->
|
||||
<view class="user-info">
|
||||
<image class="avatar" src="{{userInfo.avatarUrl || '/images/default-avatar.png'}}"></image>
|
||||
<view class="info">
|
||||
<text class="nickname">{{userInfo.nickName || '未登录'}}</text>
|
||||
<text class="reading-time">今日阅读 {{todayReadingTime}}分钟</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<view class="stats-section">
|
||||
<view class="stats-header">
|
||||
<text>阅读统计</text>
|
||||
<text class="more">查看更多</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-item">
|
||||
<text class="number">{{totalBooks}}</text>
|
||||
<text class="label">阅读书籍</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="number">{{totalNotes}}</text>
|
||||
<text class="label">笔记数量</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="number">{{totalBookmarks}}</text>
|
||||
<text class="label">书签数量</text>
|
||||
</view>
|
||||
<view class="stats-item">
|
||||
<text class="number">{{totalReadingDays}}</text>
|
||||
<text class="label">阅读天数</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能列表 -->
|
||||
<view class="feature-list">
|
||||
<view class="feature-item" bindtap="navigateToNotes">
|
||||
<text class="icon">📝</text>
|
||||
<text>我的笔记</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
<view class="feature-item" bindtap="navigateToBookmarks">
|
||||
<text class="icon">🔖</text>
|
||||
<text>我的书签</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
<view class="feature-item" bindtap="navigateToHistory">
|
||||
<text class="icon">📚</text>
|
||||
<text>阅读历史</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
<view class="feature-item" bindtap="navigateToSettings">
|
||||
<text class="icon">⚙️</text>
|
||||
<text>设置</text>
|
||||
<text class="arrow">></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
|
@ -0,0 +1,118 @@
|
|||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f8f8;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 用户信息 */
|
||||
.user-info {
|
||||
background: #fff;
|
||||
padding: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 60rpx;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.reading-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 阅读统计 */
|
||||
.stats-section {
|
||||
background: #fff;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.stats-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.stats-header text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.more {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.stats-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 36rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 功能列表 */
|
||||
.feature-list {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 30rpx 40rpx;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.feature-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 36rpx;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.feature-item text:nth-child(2) {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
这是一个示例TXT文档
|
||||
|
||||
阅读管理小程序使用说明:
|
||||
|
||||
1. 书籍管理
|
||||
- 添加新书
|
||||
- 更新阅读进度
|
||||
- 书籍分类管理
|
||||
|
||||
2. 笔记功能
|
||||
- 创建阅读笔记
|
||||
- 笔记分类
|
||||
- 笔记搜索
|
||||
|
||||
3. 统计功能
|
||||
- 阅读时长统计
|
||||
- 阅读页数统计
|
||||
- 月度/年度报告
|
||||
|
||||
4. 使用建议
|
||||
- 及时更新阅读进度
|
||||
- 做好笔记整理
|
||||
- 设定合理的阅读目标
|
||||
|
||||
祝您阅读愉快!
|
|
@ -0,0 +1 @@
|
|||
hahha
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"component": true
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 24 24" width="24" height="24">
|
||||
<!-- SVG 路径内容 -->
|
||||
</svg>
|
After Width: | Height: | Size: 84 B |
|
@ -0,0 +1,24 @@
|
|||
interface Book {
|
||||
id: number;
|
||||
isbn: string;
|
||||
title: string;
|
||||
author: string;
|
||||
publisher: string;
|
||||
description: string;
|
||||
bookUrl: string;
|
||||
coverUrl: string;
|
||||
category: string;
|
||||
tags: string;
|
||||
language: string;
|
||||
publishDate: string;
|
||||
createdTime: string;
|
||||
updatedTime: string;
|
||||
}
|
||||
|
||||
interface BookListResponse {
|
||||
records: Book[];
|
||||
total: number;
|
||||
size: number;
|
||||
current: number;
|
||||
pages: number;
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
interface RequestHeader {
|
||||
'Content-Type': string;
|
||||
'Authorization'?: string;
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
|
||||
interface RequestOptions extends WechatMiniprogram.RequestOption {
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
interface ResponseData<T = any> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
time?: string;
|
||||
}
|
||||
|
||||
const baseURL = 'http://localhost:8084/api';
|
||||
|
||||
const request = {
|
||||
async request<T>(options: RequestOptions): Promise<ResponseData<T>> {
|
||||
const token = wx.getStorageSync('token');
|
||||
console.log(token);
|
||||
|
||||
|
||||
// 定义header类型
|
||||
const header: RequestHeader = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header,
|
||||
};
|
||||
|
||||
// 统一添加token
|
||||
if (token) {
|
||||
console.log('Token值:', token);
|
||||
header['Authorization'] = `Bearer ${token}`;
|
||||
console.log('完整Authorization:', header['Authorization']);
|
||||
}
|
||||
|
||||
if (options.loading) {
|
||||
wx.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('发送请求的header:', header);
|
||||
const res = await new Promise<WechatMiniprogram.RequestSuccessCallbackResult>((resolve, reject) => {
|
||||
wx.request({
|
||||
url: baseURL + options.url,
|
||||
method: options.method,
|
||||
data: options.data,
|
||||
header,
|
||||
success: (res) => {
|
||||
console.log('请求响应:', res);
|
||||
// 先检查状态码
|
||||
if (res.statusCode === 401) {
|
||||
// 清除本地token
|
||||
wx.removeStorageSync('token');
|
||||
wx.removeStorageSync('userInfo');
|
||||
// 跳转到登录页
|
||||
wx.redirectTo({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
reject(new Error('登录已过期,请重新登录'));
|
||||
return;
|
||||
}
|
||||
resolve(res);
|
||||
},
|
||||
fail: reject
|
||||
});
|
||||
});
|
||||
|
||||
const data = res.data as ResponseData<T>;
|
||||
|
||||
// 检查业务状态码
|
||||
if (data.code !== 200) {
|
||||
throw new Error(data.message || '请求失败');
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('请求错误:', error);
|
||||
// 统一错误提示
|
||||
wx.showToast({
|
||||
title: error instanceof Error ? error.message : '请求失败',
|
||||
icon: 'none'
|
||||
});
|
||||
throw error;
|
||||
} finally {
|
||||
if (options.loading) {
|
||||
wx.hideLoading();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// GET请求
|
||||
get<T>(url: string, data?: object, options: Partial<RequestOptions> = {}): Promise<ResponseData<T>> {
|
||||
return this.request<T>({
|
||||
method: 'GET',
|
||||
url,
|
||||
data,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
// POST请求
|
||||
post<T>(url: string, data?: object, options: Partial<RequestOptions> = {}): Promise<ResponseData<T>> {
|
||||
return this.request<T>({
|
||||
method: 'POST',
|
||||
url,
|
||||
data,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
// PUT请求
|
||||
put<T>(url: string, data?: object, options: Partial<RequestOptions> = {}): Promise<ResponseData<T>> {
|
||||
return this.request<T>({
|
||||
method: 'PUT',
|
||||
url,
|
||||
data,
|
||||
...options
|
||||
});
|
||||
},
|
||||
|
||||
// DELETE请求
|
||||
delete<T>(url: string, data?: object, options: Partial<RequestOptions> = {}): Promise<ResponseData<T>> {
|
||||
return this.request<T>({
|
||||
method: 'DELETE',
|
||||
url,
|
||||
data,
|
||||
...options
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default request;
|
|
@ -0,0 +1,19 @@
|
|||
export const formatTime = (date: Date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
const hour = date.getHours()
|
||||
const minute = date.getMinutes()
|
||||
const second = date.getSeconds()
|
||||
|
||||
return (
|
||||
[year, month, day].map(formatNumber).join('/') +
|
||||
' ' +
|
||||
[hour, minute, second].map(formatNumber).join(':')
|
||||
)
|
||||
}
|
||||
|
||||
const formatNumber = (n: number) => {
|
||||
const s = n.toString()
|
||||
return s[1] ? s : '0' + s
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "miniprogram-ts-quickstart",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
},
|
||||
"devDependencies": {
|
||||
"miniprogram-api-typings": "^2.8.3-1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
{
|
||||
"description": "项目配置文件",
|
||||
"miniprogramRoot": "miniprogram/",
|
||||
"cloudfunctionRoot": "cloudfunctions/",
|
||||
"compileType": "miniprogram",
|
||||
"setting": {
|
||||
"useCompilerPlugins": [
|
||||
"typescript"
|
||||
],
|
||||
"babelSetting": {
|
||||
"ignore": [],
|
||||
"disablePlugins": [],
|
||||
"outputPath": ""
|
||||
},
|
||||
"coverView": true,
|
||||
"postcss": true,
|
||||
"minified": true,
|
||||
"enhance": true,
|
||||
"showShadowRootInWxmlPanel": true,
|
||||
"packNpmRelationList": [],
|
||||
"ignoreUploadUnusedFiles": true,
|
||||
"compileHotReLoad": false,
|
||||
"skylineRenderEnable": true,
|
||||
"urlCheck": true,
|
||||
"es6": true,
|
||||
"preloadBackgroundData": false,
|
||||
"newFeature": true,
|
||||
"nodeModules": false,
|
||||
"autoAudits": false,
|
||||
"scopeDataCheck": false,
|
||||
"uglifyFileName": false,
|
||||
"checkInvalidKey": true,
|
||||
"checkSiteMap": true,
|
||||
"uploadWithSourceMap": true,
|
||||
"lazyloadPlaceholderEnable": false,
|
||||
"useMultiFrameRuntime": true,
|
||||
"useApiHook": true,
|
||||
"useApiHostProcess": true
|
||||
},
|
||||
"simulatorType": "wechat",
|
||||
"simulatorPluginLibVersion": {},
|
||||
"condition": {},
|
||||
"srcMiniprogramRoot": "miniprogram/",
|
||||
"editorSetting": {
|
||||
"tabIndent": "insertSpaces",
|
||||
"tabSize": 2
|
||||
},
|
||||
"packOptions": {
|
||||
"ignore": [],
|
||||
"include": []
|
||||
},
|
||||
"libVersion": "3.0.0",
|
||||
"appid": "wx2aeaa9db5139384c",
|
||||
"projectname": "reading-manager",
|
||||
"cloudfunctionTemplateRoot": "cloudfunctionTemplate/"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||
"projectname": "miniprogram-1",
|
||||
"setting": {
|
||||
"compileHotReLoad": true,
|
||||
"urlCheck": false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"module": "CommonJS",
|
||||
"target": "ES2020",
|
||||
"allowJs": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"noImplicitThis": true,
|
||||
"noImplicitReturns": true,
|
||||
"alwaysStrict": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"strict": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"lib": ["ES2020"],
|
||||
"typeRoots": [
|
||||
"./typings"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
interface Book {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
cover: string;
|
||||
isbn: string;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
status: 'reading' | 'finished' | 'planning';
|
||||
startDate?: Date;
|
||||
finishDate?: Date;
|
||||
category: string[];
|
||||
notes: string[];
|
||||
rating?: number;
|
||||
createTime: Date;
|
||||
updateTime: Date;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
interface Note {
|
||||
id: string;
|
||||
bookId: string;
|
||||
title: string;
|
||||
content: string;
|
||||
tags: string[];
|
||||
page?: number;
|
||||
createTime: Date;
|
||||
updateTime: Date;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
interface ReadingStats {
|
||||
userId: string;
|
||||
dailyReadingTime: number; // 单位:分钟
|
||||
dailyPages: number;
|
||||
monthlyStats: {
|
||||
month: string; // YYYY-MM
|
||||
totalTime: number;
|
||||
totalPages: number;
|
||||
finishedBooks: number;
|
||||
}[];
|
||||
yearlyGoal: {
|
||||
books: number;
|
||||
pages: number;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
/// <reference path="./types/index.d.ts" />
|
||||
|
||||
interface IAppOption {
|
||||
globalData: {
|
||||
userInfo?: WechatMiniprogram.UserInfo,
|
||||
}
|
||||
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/// <reference path="./wx/index.d.ts" />
|
|
@ -0,0 +1,163 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
/// <reference path="./lib.wx.app.d.ts" />
|
||||
/// <reference path="./lib.wx.page.d.ts" />
|
||||
/// <reference path="./lib.wx.api.d.ts" />
|
||||
/// <reference path="./lib.wx.cloud.d.ts" />
|
||||
/// <reference path="./lib.wx.canvas.d.ts" />
|
||||
/// <reference path="./lib.wx.component.d.ts" />
|
||||
/// <reference path="./lib.wx.behavior.d.ts" />
|
||||
/// <reference path="./lib.wx.event.d.ts" />
|
||||
/// <reference path="./lib.wx.wasm.d.ts" />
|
||||
|
||||
declare namespace WechatMiniprogram {
|
||||
type IAnyObject = Record<string, any>
|
||||
type Optional<F> = F extends (arg: infer P) => infer R ? (arg?: P) => R : F
|
||||
type OptionalInterface<T> = { [K in keyof T]: Optional<T[K]> }
|
||||
interface AsyncMethodOptionLike {
|
||||
success?: (...args: any[]) => void
|
||||
}
|
||||
type PromisifySuccessResult<
|
||||
P,
|
||||
T extends AsyncMethodOptionLike
|
||||
> = P extends {
|
||||
success: any
|
||||
}
|
||||
? void
|
||||
: P extends { fail: any }
|
||||
? void
|
||||
: P extends { complete: any }
|
||||
? void
|
||||
: Promise<Parameters<Exclude<T['success'], undefined>>[0]>
|
||||
|
||||
// TODO: Extract real definition from `lib.dom.d.ts` to replace this
|
||||
type IIRFilterNode = any
|
||||
type WaveShaperNode = any
|
||||
type ConstantSourceNode = any
|
||||
type OscillatorNode = any
|
||||
type GainNode = any
|
||||
type BiquadFilterNode = any
|
||||
type PeriodicWaveNode = any
|
||||
type AudioNode = any
|
||||
type ChannelSplitterNode = any
|
||||
type ChannelMergerNode = any
|
||||
type DelayNode = any
|
||||
type DynamicsCompressorNode = any
|
||||
type ScriptProcessorNode = any
|
||||
type PannerNode = any
|
||||
type AnalyserNode = any
|
||||
type WebGLTexture = any
|
||||
type WebGLRenderingContext = any
|
||||
|
||||
// TODO: fill worklet type
|
||||
type WorkletFunction = (...args: any) => any
|
||||
type AnimationObject = any
|
||||
type SharedValue<T = any> = T
|
||||
type DerivedValue<T = any> = T
|
||||
}
|
||||
|
||||
declare let console: WechatMiniprogram.Console
|
||||
|
||||
declare let wx: WechatMiniprogram.Wx
|
||||
/** 引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */
|
||||
interface Require {
|
||||
(
|
||||
/** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */
|
||||
module: string,
|
||||
/** 用于异步获取其他分包中的模块的引用结果,详见 [分包异步化]((subpackages/async)) */
|
||||
callback?: (moduleExport: any) => void,
|
||||
/** 异步获取分包失败时的回调,详见 [分包异步化]((subpackages/async)) */
|
||||
errorCallback?: (err: any) => void
|
||||
): any
|
||||
/** 以 Promise 形式异步引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */
|
||||
async(
|
||||
/** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */
|
||||
module: string
|
||||
): Promise<any>
|
||||
}
|
||||
declare const require: Require
|
||||
/** 引入插件。返回插件通过 `main` 暴露的接口。 */
|
||||
interface RequirePlugin {
|
||||
(
|
||||
/** 需要引入的插件的 alias */
|
||||
module: string,
|
||||
/** 用于异步获取其他分包中的插件的引用结果,详见 [分包异步化]((subpackages/async)) */
|
||||
callback?: (pluginExport: any) => void
|
||||
): any
|
||||
/** 以 Promise 形式异步引入插件。返回插件通过 `main` 暴露的接口。 */
|
||||
async(
|
||||
/** 需要引入的插件的 alias */
|
||||
module: string
|
||||
): Promise<any>
|
||||
}
|
||||
declare const requirePlugin: RequirePlugin
|
||||
/** 插件引入当前使用者小程序。返回使用者小程序通过 [插件配置中 `export` 暴露的接口](https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/using.html#%E5%AF%BC%E5%87%BA%E5%88%B0%E6%8F%92%E4%BB%B6)。
|
||||
*
|
||||
* 该接口只在插件中存在
|
||||
*
|
||||
* 最低基础库: `2.11.1` */
|
||||
declare function requireMiniProgram(): any
|
||||
/** 当前模块对象 */
|
||||
declare let module: {
|
||||
/** 模块向外暴露的对象,使用 `require` 引用该模块时可以获取 */
|
||||
exports: any
|
||||
}
|
||||
/** `module.exports` 的引用 */
|
||||
declare let exports: any
|
||||
|
||||
/** [clearInterval(number intervalID)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/clearInterval.html)
|
||||
*
|
||||
* 取消由 setInterval 设置的定时器。 */
|
||||
declare function clearInterval(
|
||||
/** 要取消的定时器的 ID */
|
||||
intervalID: number
|
||||
): void
|
||||
/** [clearTimeout(number timeoutID)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/clearTimeout.html)
|
||||
*
|
||||
* 取消由 setTimeout 设置的定时器。 */
|
||||
declare function clearTimeout(
|
||||
/** 要取消的定时器的 ID */
|
||||
timeoutID: number
|
||||
): void
|
||||
/** [number setInterval(function callback, number delay, any rest)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/setInterval.html)
|
||||
*
|
||||
* 设定一个定时器。按照指定的周期(以毫秒计)来执行注册的回调函数 */
|
||||
declare function setInterval(
|
||||
/** 回调函数 */
|
||||
callback: (...args: any[]) => any,
|
||||
/** 执行回调函数之间的时间间隔,单位 ms。 */
|
||||
delay?: number,
|
||||
/** param1, param2, ..., paramN 等附加参数,它们会作为参数传递给回调函数。 */
|
||||
rest?: any
|
||||
): number
|
||||
/** [number setTimeout(function callback, number delay, any rest)](https://developers.weixin.qq.com/miniprogram/dev/api/base/timer/setTimeout.html)
|
||||
*
|
||||
* 设定一个定时器。在定时到期以后执行注册的回调函数 */
|
||||
declare function setTimeout(
|
||||
/** 回调函数 */
|
||||
callback: (...args: any[]) => any,
|
||||
/** 延迟的时间,函数的调用会在该延迟之后发生,单位 ms。 */
|
||||
delay?: number,
|
||||
/** param1, param2, ..., paramN 等附加参数,它们会作为参数传递给回调函数。 */
|
||||
rest?: any
|
||||
): number
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,424 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
declare namespace WechatMiniprogram.App {
|
||||
type SceneValues =
|
||||
| 1000
|
||||
| 1001
|
||||
| 1005
|
||||
| 1006
|
||||
| 1007
|
||||
| 1008
|
||||
| 1010
|
||||
| 1011
|
||||
| 1012
|
||||
| 1013
|
||||
| 1014
|
||||
| 1017
|
||||
| 1019
|
||||
| 1020
|
||||
| 1022
|
||||
| 1023
|
||||
| 1024
|
||||
| 1025
|
||||
| 1026
|
||||
| 1027
|
||||
| 1028
|
||||
| 1029
|
||||
| 1030
|
||||
| 1031
|
||||
| 1032
|
||||
| 1034
|
||||
| 1035
|
||||
| 1036
|
||||
| 1037
|
||||
| 1038
|
||||
| 1039
|
||||
| 1042
|
||||
| 1043
|
||||
| 1044
|
||||
| 1045
|
||||
| 1046
|
||||
| 1047
|
||||
| 1048
|
||||
| 1049
|
||||
| 1052
|
||||
| 1053
|
||||
| 1054
|
||||
| 1056
|
||||
| 1057
|
||||
| 1058
|
||||
| 1059
|
||||
| 1060
|
||||
| 1064
|
||||
| 1065
|
||||
| 1067
|
||||
| 1068
|
||||
| 1069
|
||||
| 1071
|
||||
| 1072
|
||||
| 1073
|
||||
| 1074
|
||||
| 1077
|
||||
| 1078
|
||||
| 1079
|
||||
| 1081
|
||||
| 1082
|
||||
| 1084
|
||||
| 1088
|
||||
| 1089
|
||||
| 1090
|
||||
| 1091
|
||||
| 1092
|
||||
| 1095
|
||||
| 1096
|
||||
| 1097
|
||||
| 1099
|
||||
| 1100
|
||||
| 1101
|
||||
| 1102
|
||||
| 1103
|
||||
| 1104
|
||||
| 1106
|
||||
| 1107
|
||||
| 1113
|
||||
| 1114
|
||||
| 1119
|
||||
| 1120
|
||||
| 1121
|
||||
| 1124
|
||||
| 1125
|
||||
| 1126
|
||||
| 1129
|
||||
| 1131
|
||||
| 1133
|
||||
| 1135
|
||||
| 1144
|
||||
| 1145
|
||||
| 1146
|
||||
| 1148
|
||||
| 1150
|
||||
| 1151
|
||||
| 1152
|
||||
| 1153
|
||||
| 1154
|
||||
| 1155
|
||||
| 1157
|
||||
| 1158
|
||||
| 1160
|
||||
| 1167
|
||||
| 1168
|
||||
| 1169
|
||||
| 1171
|
||||
| 1173
|
||||
| 1175
|
||||
| 1176
|
||||
| 1177
|
||||
| 1178
|
||||
| 1179
|
||||
| 1181
|
||||
| 1183
|
||||
| 1184
|
||||
| 1185
|
||||
| 1186
|
||||
| 1187
|
||||
| 1189
|
||||
| 1191
|
||||
| 1192
|
||||
| 1193
|
||||
| 1194
|
||||
| 1195
|
||||
| 1196
|
||||
| 1197
|
||||
| 1198
|
||||
| 1200
|
||||
| 1201
|
||||
| 1202
|
||||
| 1203
|
||||
| 1206
|
||||
| 1207
|
||||
| 1208
|
||||
| 1212
|
||||
| 1215
|
||||
| 1216
|
||||
| 1223
|
||||
| 1228
|
||||
| 1231
|
||||
|
||||
interface LaunchShowOption {
|
||||
/** 打开小程序的路径 */
|
||||
path: string
|
||||
/** 打开小程序的query */
|
||||
query: IAnyObject
|
||||
/** 打开小程序的场景值
|
||||
* - 1000:其他
|
||||
* - 1001:发现栏小程序主入口,「最近使用」列表(基础库 2.2.4 版本起包含「我的小程序」列表)
|
||||
* - 1005:微信首页顶部搜索框的搜索结果页
|
||||
* - 1006:发现栏小程序主入口搜索框的搜索结果页
|
||||
* - 1007:单人聊天会话中的小程序消息卡片
|
||||
* - 1008:群聊会话中的小程序消息卡片
|
||||
* - 1010:收藏夹
|
||||
* - 1011:扫描二维码
|
||||
* - 1012:长按图片识别二维码
|
||||
* - 1013:扫描手机相册中选取的二维码
|
||||
* - 1014:小程序订阅消息(与 1107 相同)
|
||||
* - 1017:前往小程序体验版的入口页
|
||||
* - 1019:微信钱包(微信客户端 7.0.0 版本改为支付入口)
|
||||
* - 1020:公众号 profile 页相关小程序列表(已废弃)
|
||||
* - 1022:聊天顶部置顶小程序入口(微信客户端 6.6.1 版本起废弃)
|
||||
* - 1023:安卓系统桌面图标
|
||||
* - 1024:小程序 profile 页
|
||||
* - 1025:扫描一维码
|
||||
* - 1026:发现栏小程序主入口,「附近的小程序」列表
|
||||
* - 1027:微信首页顶部搜索框搜索结果页「使用过的小程序」列表
|
||||
* - 1028:我的卡包
|
||||
* - 1029:小程序中的卡券详情页
|
||||
* - 1030:自动化测试下打开小程序
|
||||
* - 1031:长按图片识别一维码
|
||||
* - 1032:扫描手机相册中选取的一维码
|
||||
* - 1034:微信支付完成页
|
||||
* - 1035:公众号自定义菜单
|
||||
* - 1036:App 分享消息卡片
|
||||
* - 1037:小程序打开小程序
|
||||
* - 1038:从另一个小程序返回
|
||||
* - 1039:摇电视
|
||||
* - 1042:添加好友搜索框的搜索结果页
|
||||
* - 1043:公众号模板消息
|
||||
* - 1044:带 shareTicket 的小程序消息卡片 [详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html)
|
||||
* - 1045:朋友圈广告
|
||||
* - 1046:朋友圈广告详情页
|
||||
* - 1047:扫描小程序码
|
||||
* - 1048:长按图片识别小程序码
|
||||
* - 1049:扫描手机相册中选取的小程序码
|
||||
* - 1052:卡券的适用门店列表
|
||||
* - 1053:搜一搜的结果页
|
||||
* - 1054:顶部搜索框小程序快捷入口(微信客户端版本 6.7.4 起废弃)
|
||||
* - 1056:聊天顶部音乐播放器右上角菜单
|
||||
* - 1057:钱包中的银行卡详情页
|
||||
* - 1058:公众号文章
|
||||
* - 1059:体验版小程序绑定邀请页
|
||||
* - 1060:微信支付完成页(与 1034 相同)
|
||||
* - 1064:微信首页连 Wi-Fi 状态栏
|
||||
* - 1065:URL scheme [详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-scheme.html)
|
||||
* - 1067:公众号文章广告
|
||||
* - 1068:附近小程序列表广告(已废弃)
|
||||
* - 1069:移动应用通过 openSDK 进入微信,打开小程序
|
||||
* - 1071:钱包中的银行卡列表页
|
||||
* - 1072:二维码收款页面
|
||||
* - 1073:客服消息列表下发的小程序消息卡片
|
||||
* - 1074:公众号会话下发的小程序消息卡片
|
||||
* - 1077:摇周边
|
||||
* - 1078:微信连 Wi-Fi 成功提示页
|
||||
* - 1079:微信游戏中心
|
||||
* - 1081:客服消息下发的文字链
|
||||
* - 1082:公众号会话下发的文字链
|
||||
* - 1084:朋友圈广告原生页
|
||||
* - 1088:会话中查看系统消息,打开小程序
|
||||
* - 1089:微信聊天主界面下拉,「最近使用」栏(基础库 2.2.4 版本起包含「我的小程序」栏)
|
||||
* - 1090:长按小程序右上角菜单唤出最近使用历史
|
||||
* - 1091:公众号文章商品卡片
|
||||
* - 1092:城市服务入口
|
||||
* - 1095:小程序广告组件
|
||||
* - 1096:聊天记录,打开小程序
|
||||
* - 1097:微信支付签约原生页,打开小程序
|
||||
* - 1099:页面内嵌插件
|
||||
* - 1100:红包封面详情页打开小程序
|
||||
* - 1101:远程调试热更新(开发者工具中,预览 -> 自动预览 -> 编译并预览)
|
||||
* - 1102:公众号 profile 页服务预览
|
||||
* - 1103:发现栏小程序主入口,「我的小程序」列表(基础库 2.2.4 版本起废弃)
|
||||
* - 1104:微信聊天主界面下拉,「我的小程序」栏(基础库 2.2.4 版本起废弃)
|
||||
* - 1106:聊天主界面下拉,从顶部搜索结果页,打开小程序
|
||||
* - 1107:订阅消息,打开小程序
|
||||
* - 1113:安卓手机负一屏,打开小程序(三星)
|
||||
* - 1114:安卓手机侧边栏,打开小程序(三星)
|
||||
* - 1119:【企业微信】工作台内打开小程序
|
||||
* - 1120:【企业微信】个人资料页内打开小程序
|
||||
* - 1121:【企业微信】聊天加号附件框内打开小程序
|
||||
* - 1124:扫“一物一码”打开小程序
|
||||
* - 1125:长按图片识别“一物一码”
|
||||
* - 1126:扫描手机相册中选取的“一物一码”
|
||||
* - 1129:微信爬虫访问 [详情](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/sitemap.html)
|
||||
* - 1131:浮窗(8.0 版本起仅包含被动浮窗)
|
||||
* - 1133:硬件设备打开小程序 [详情](https://developers.weixin.qq.com/doc/oplatform/Miniprogram_Frame/)
|
||||
* - 1135:小程序 profile 页相关小程序列表,打开小程序
|
||||
* - 1144:公众号文章 - 视频贴片
|
||||
* - 1145:发现栏 - 发现小程序
|
||||
* - 1146:地理位置信息打开出行类小程序
|
||||
* - 1148:卡包-交通卡,打开小程序
|
||||
* - 1150:扫一扫商品条码结果页打开小程序
|
||||
* - 1151:发现栏 - 我的订单
|
||||
* - 1152:订阅号视频打开小程序
|
||||
* - 1153:“识物”结果页打开小程序
|
||||
* - 1154:朋友圈内打开“单页模式”
|
||||
* - 1155:“单页模式”打开小程序
|
||||
* - 1157:服务号会话页打开小程序
|
||||
* - 1158:群工具打开小程序
|
||||
* - 1160:群待办
|
||||
* - 1167:H5 通过开放标签打开小程序 [详情](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_Open_Tag.html)
|
||||
* - 1168:移动应用直接运行小程序
|
||||
* - 1169:发现栏小程序主入口,各个生活服务入口(例如快递服务、出行服务等)
|
||||
* - 1171:微信运动记录(仅安卓)
|
||||
* - 1173:聊天素材用小程序打开 [详情](https://developers.weixin.qq.com/miniprogram/dev/framework/material/support_material.html)
|
||||
* - 1175:视频号主页商店入口
|
||||
* - 1176:视频号直播间主播打开小程序
|
||||
* - 1177:视频号直播商品
|
||||
* - 1178:在电脑打开手机上打开的小程序
|
||||
* - 1179:#话题页打开小程序
|
||||
* - 1181:网站应用打开 PC 小程序
|
||||
* - 1183:PC 微信 - 小程序面板 - 发现小程序 - 搜索
|
||||
* - 1184:视频号链接打开小程序
|
||||
* - 1185:群公告
|
||||
* - 1186:收藏 - 笔记
|
||||
* - 1187:浮窗(8.0 版本起)
|
||||
* - 1189:表情雨广告
|
||||
* - 1191:视频号活动
|
||||
* - 1192:企业微信联系人 profile 页
|
||||
* - 1193:视频号主页服务菜单打开小程序
|
||||
* - 1194:URL Link [详情](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-link.html)
|
||||
* - 1195:视频号主页商品 tab
|
||||
* - 1196:个人状态打开小程序
|
||||
* - 1197:视频号主播从直播间返回小游戏
|
||||
* - 1198:视频号开播界面打开小游戏
|
||||
* - 1200:视频号广告打开小程序
|
||||
* - 1201:视频号广告详情页打开小程序
|
||||
* - 1202:企微客服号会话打开小程序卡片
|
||||
* - 1203:微信小程序压测工具的请求
|
||||
* - 1206:视频号小游戏直播间打开小游戏
|
||||
* - 1207:企微客服号会话打开小程序文字链
|
||||
* - 1208:聊天打开商品卡片
|
||||
* - 1212:青少年模式申请页打开小程序
|
||||
* - 1215:广告预约打开小程序
|
||||
* - 1216:视频号订单中心打开小程序
|
||||
* - 1223:安卓桌面 Widget 打开小程序
|
||||
* - 1228:视频号原生广告组件打开小程序
|
||||
* - 1231:动态消息提醒入口打开小程序
|
||||
*/
|
||||
scene: SceneValues
|
||||
/** shareTicket,详见 [获取更多转发信息]((转发#获取更多转发信息)) */
|
||||
shareTicket: string
|
||||
/** 当场景为由从另一个小程序或公众号或App打开时,返回此字段 */
|
||||
referrerInfo?: ReferrerInfo
|
||||
/** 打开的文件信息数组,只有从聊天素材场景打开(scene为1173)才会携带该参数 */
|
||||
forwardMaterials: ForwardMaterials[]
|
||||
/** 从微信群聊/单聊打开小程序时,chatType 表示具体微信群聊/单聊类型
|
||||
*
|
||||
* 可选值:
|
||||
* - 1: 微信联系人单聊;
|
||||
* - 2: 企业微信联系人单聊;
|
||||
* - 3: 普通微信群聊;
|
||||
* - 4: 企业微信互通群聊; */
|
||||
chatType?: 1 | 2 | 3 | 4
|
||||
/** 需要基础库: `2.20.0`
|
||||
*
|
||||
* API 类别
|
||||
*
|
||||
* 可选值:
|
||||
* - 'default': 默认类别;
|
||||
* - 'nativeFunctionalized': 原生功能化,视频号直播商品、商品橱窗等场景打开的小程序;
|
||||
* - 'browseOnly': 仅浏览,朋友圈快照页等场景打开的小程序;
|
||||
* - 'embedded': 内嵌,通过打开半屏小程序能力打开的小程序; */
|
||||
apiCategory:
|
||||
| 'default'
|
||||
| 'nativeFunctionalized'
|
||||
| 'browseOnly'
|
||||
| 'embedded'
|
||||
}
|
||||
|
||||
interface PageNotFoundOption {
|
||||
/** 不存在页面的路径 */
|
||||
path: string
|
||||
/** 打开不存在页面的 query */
|
||||
query: IAnyObject
|
||||
/** 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) */
|
||||
isEntryPage: boolean
|
||||
}
|
||||
|
||||
interface Option {
|
||||
/** 生命周期回调—监听小程序初始化
|
||||
*
|
||||
* 小程序初始化完成时触发,全局只触发一次。
|
||||
*/
|
||||
onLaunch(options: LaunchShowOption): void
|
||||
/** 生命周期回调—监听小程序显示
|
||||
*
|
||||
* 小程序启动,或从后台进入前台显示时
|
||||
*/
|
||||
onShow(options: LaunchShowOption): void
|
||||
/** 生命周期回调—监听小程序隐藏
|
||||
*
|
||||
* 小程序从前台进入后台时
|
||||
*/
|
||||
onHide(): void
|
||||
/** 错误监听函数
|
||||
*
|
||||
* 小程序发生脚本错误,或者 api
|
||||
*/
|
||||
onError(/** 错误信息,包含堆栈 */ error: string): void
|
||||
/** 页面不存在监听函数
|
||||
*
|
||||
* 小程序要打开的页面不存在时触发,会带上页面信息回调该函数
|
||||
*
|
||||
* **注意:**
|
||||
* 1. 如果开发者没有添加 `onPageNotFound` 监听,当跳转页面不存在时,将推入微信客户端原生的页面不存在提示页面。
|
||||
* 2. 如果 `onPageNotFound` 回调中又重定向到另一个不存在的页面,将推入微信客户端原生的页面不存在提示页面,并且不再回调 `onPageNotFound`。
|
||||
*
|
||||
* 最低基础库: 1.9.90
|
||||
*/
|
||||
onPageNotFound(options: PageNotFoundOption): void
|
||||
/**
|
||||
* 小程序有未处理的 Promise 拒绝时触发。也可以使用 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 绑定监听。注意事项请参考 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html)。
|
||||
* **参数**:与 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 一致
|
||||
*/
|
||||
onUnhandledRejection: OnUnhandledRejectionCallback
|
||||
/**
|
||||
* 系统切换主题时触发。也可以使用 wx.onThemeChange 绑定监听。
|
||||
*
|
||||
* 最低基础库: 2.11.0
|
||||
*/
|
||||
onThemeChange: OnThemeChangeCallback
|
||||
}
|
||||
|
||||
type Instance<T extends IAnyObject> = Option & T
|
||||
type Options<T extends IAnyObject> = Partial<Option> &
|
||||
T &
|
||||
ThisType<Instance<T>>
|
||||
type TrivialInstance = Instance<IAnyObject>
|
||||
|
||||
interface Constructor {
|
||||
<T extends IAnyObject>(options: Options<T>): void
|
||||
}
|
||||
|
||||
interface GetAppOption {
|
||||
/** 在 `App` 未定义时返回默认实现。当App被调用时,默认实现中定义的属性会被覆盖合并到App中。一般用于独立分包
|
||||
*
|
||||
* 最低基础库: 2.2.4
|
||||
*/
|
||||
allowDefault?: boolean
|
||||
}
|
||||
|
||||
interface GetApp {
|
||||
<T extends IAnyObject = IAnyObject>(opts?: GetAppOption): Instance<T>
|
||||
}
|
||||
}
|
||||
|
||||
declare let App: WechatMiniprogram.App.Constructor
|
||||
declare let getApp: WechatMiniprogram.App.GetApp
|
|
@ -0,0 +1,85 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
declare namespace WechatMiniprogram.Behavior {
|
||||
|
||||
type BehaviorIdentifier<
|
||||
TData extends DataOption = {},
|
||||
TProperty extends PropertyOption = {},
|
||||
TMethod extends MethodOption = {},
|
||||
TBehavior extends BehaviorOption = []
|
||||
> = string & {
|
||||
[key in 'BehaviorType']?: {
|
||||
data: Component.FilterUnknownType<TData> & Component.MixinData<TBehavior>
|
||||
properties: Component.FilterUnknownType<TProperty> & Component.MixinProperties<TBehavior, true>
|
||||
methods: Component.FilterUnknownType<TMethod> & Component.MixinMethods<TBehavior>
|
||||
}
|
||||
}
|
||||
type Instance<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends MethodOption,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||
> = Component.Instance<TData, TProperty, TMethod, TBehavior, TCustomInstanceProperty>
|
||||
type TrivialInstance = Instance<IAnyObject, IAnyObject, IAnyObject, Component.IEmptyArray>
|
||||
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject, Component.IEmptyArray>
|
||||
type Options<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends MethodOption,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||
> = Partial<Data<TData>> &
|
||||
Partial<Property<TProperty>> &
|
||||
Partial<Method<TMethod>> &
|
||||
Partial<Behavior<TBehavior>> &
|
||||
Partial<OtherOption> &
|
||||
Partial<Lifetimes> &
|
||||
ThisType<Instance<TData, TProperty, TMethod, TBehavior, TCustomInstanceProperty>>
|
||||
interface Constructor {
|
||||
<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends MethodOption,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = Record<string, never>
|
||||
>(
|
||||
options: Options<TData, TProperty, TMethod, TBehavior, TCustomInstanceProperty>
|
||||
): BehaviorIdentifier<TData, TProperty, TMethod, TBehavior>
|
||||
}
|
||||
|
||||
type DataOption = Component.DataOption
|
||||
type PropertyOption = Component.PropertyOption
|
||||
type MethodOption = Component.MethodOption
|
||||
type BehaviorOption = Component.BehaviorOption
|
||||
type Data<D extends DataOption> = Component.Data<D>
|
||||
type Property<P extends PropertyOption> = Component.Property<P>
|
||||
type Method<M extends MethodOption> = Component.Method<M>
|
||||
type Behavior<B extends BehaviorOption> = Component.Behavior<B>
|
||||
|
||||
type DefinitionFilter = Component.DefinitionFilter
|
||||
type Lifetimes = Component.Lifetimes
|
||||
type OtherOption = Omit<Component.OtherOption, 'options'>
|
||||
}
|
||||
/** 注册一个 `behavior`,接受一个 `Object` 类型的参数。*/
|
||||
declare let Behavior: WechatMiniprogram.Behavior.Constructor
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,771 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
declare namespace WechatMiniprogram.Component {
|
||||
type FilterUnknownType<T> = string extends keyof T ? {} : T
|
||||
type Instance<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends Partial<MethodOption>,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = {},
|
||||
TIsPage extends boolean = false
|
||||
> = InstanceProperties &
|
||||
InstanceMethods<TData> &
|
||||
TMethod &
|
||||
MixinMethods<TBehavior> &
|
||||
(TIsPage extends true ? Page.ILifetime : {}) &
|
||||
Omit<TCustomInstanceProperty, 'properties' | 'methods' | 'data'> & {
|
||||
/** 组件数据,**包括内部数据和属性值** */
|
||||
data: FilterUnknownType<TData> & MixinData<TBehavior> &
|
||||
MixinProperties<TBehavior> & PropertyOptionToData<FilterUnknownType<TProperty>>
|
||||
/** 组件数据,**包括内部数据和属性值**(与 `data` 一致) */
|
||||
properties: FilterUnknownType<TData> & MixinData<TBehavior> &
|
||||
MixinProperties<TBehavior> & PropertyOptionToData<FilterUnknownType<TProperty>>
|
||||
}
|
||||
|
||||
type IEmptyArray = []
|
||||
type TrivialInstance = Instance<
|
||||
IAnyObject,
|
||||
IAnyObject,
|
||||
IAnyObject,
|
||||
IEmptyArray,
|
||||
IAnyObject
|
||||
>
|
||||
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject, IEmptyArray, IAnyObject>
|
||||
type Options<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends MethodOption,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = {},
|
||||
TIsPage extends boolean = false
|
||||
> = Partial<Data<TData>> &
|
||||
Partial<Property<TProperty>> &
|
||||
Partial<Method<TMethod, TIsPage>> &
|
||||
Partial<Behavior<TBehavior>> &
|
||||
Partial<OtherOption> &
|
||||
Partial<Lifetimes> &
|
||||
ThisType<
|
||||
Instance<
|
||||
TData,
|
||||
TProperty,
|
||||
TMethod,
|
||||
TBehavior,
|
||||
TCustomInstanceProperty,
|
||||
TIsPage
|
||||
>
|
||||
>
|
||||
interface Constructor {
|
||||
<
|
||||
TData extends DataOption,
|
||||
TProperty extends PropertyOption,
|
||||
TMethod extends MethodOption,
|
||||
TBehavior extends BehaviorOption,
|
||||
TCustomInstanceProperty extends IAnyObject = {},
|
||||
TIsPage extends boolean = false
|
||||
>(
|
||||
options: Options<
|
||||
TData,
|
||||
TProperty,
|
||||
TMethod,
|
||||
TBehavior,
|
||||
TCustomInstanceProperty,
|
||||
TIsPage
|
||||
>
|
||||
): string
|
||||
}
|
||||
type DataOption = Record<string, any>
|
||||
type PropertyOption = Record<string, AllProperty>
|
||||
type MethodOption = Record<string, Function>
|
||||
|
||||
type BehaviorOption = Behavior.BehaviorIdentifier[]
|
||||
type ExtractBehaviorType<T> = T extends { BehaviorType?: infer B } ? B : never
|
||||
type ExtractData<T> = T extends { data: infer D } ? D : never
|
||||
type ExtractProperties<T, TIsBehavior extends boolean = false> = T extends { properties: infer P } ?
|
||||
TIsBehavior extends true ? P : PropertyOptionToData<P extends PropertyOption ? P : {}> : never
|
||||
type ExtractMethods<T> = T extends { methods: infer M } ? M : never
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never
|
||||
type MixinData<T extends any[]> = UnionToIntersection<ExtractData<ExtractBehaviorType<T[number]>>>
|
||||
type MixinProperties<T extends any[], TIsBehavior extends boolean = false> = UnionToIntersection<ExtractProperties<ExtractBehaviorType<T[number]>, TIsBehavior>>
|
||||
type MixinMethods<T extends any[]> = UnionToIntersection<ExtractMethods<ExtractBehaviorType<T[number]>>>
|
||||
|
||||
interface Behavior<B extends BehaviorOption> {
|
||||
/** 类似于mixins和traits的组件间代码复用机制,参见 [behaviors](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html) */
|
||||
behaviors?: B
|
||||
}
|
||||
|
||||
interface Data<D extends DataOption> {
|
||||
/** 组件的内部数据,和 `properties` 一同用于组件的模板渲染 */
|
||||
data?: D
|
||||
}
|
||||
interface Property<P extends PropertyOption> {
|
||||
/** 组件的对外属性,是属性名到属性设置的映射表 */
|
||||
properties: P
|
||||
}
|
||||
interface Method<M extends MethodOption, TIsPage extends boolean = false> {
|
||||
/** 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 [组件间通信与事件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html) */
|
||||
methods: M & (TIsPage extends true ? Partial<Page.ILifetime> : {})
|
||||
}
|
||||
type PropertyType =
|
||||
| StringConstructor
|
||||
| NumberConstructor
|
||||
| BooleanConstructor
|
||||
| ArrayConstructor
|
||||
| ObjectConstructor
|
||||
| null
|
||||
type ValueType<T extends PropertyType> = T extends null
|
||||
? any
|
||||
: T extends StringConstructor
|
||||
? string
|
||||
: T extends NumberConstructor
|
||||
? number
|
||||
: T extends BooleanConstructor
|
||||
? boolean
|
||||
: T extends ArrayConstructor
|
||||
? any[]
|
||||
: T extends ObjectConstructor
|
||||
? IAnyObject
|
||||
: never
|
||||
type FullProperty<T extends PropertyType> = {
|
||||
/** 属性类型 */
|
||||
type: T
|
||||
/** 属性初始值 */
|
||||
value?: ValueType<T>
|
||||
/** 属性值被更改时的响应函数 */
|
||||
observer?:
|
||||
| string
|
||||
| ((
|
||||
newVal: ValueType<T>,
|
||||
oldVal: ValueType<T>,
|
||||
changedPath: Array<string | number>
|
||||
) => void)
|
||||
/** 属性的类型(可以指定多个) */
|
||||
optionalTypes?: ShortProperty[]
|
||||
}
|
||||
type AllFullProperty =
|
||||
| FullProperty<StringConstructor>
|
||||
| FullProperty<NumberConstructor>
|
||||
| FullProperty<BooleanConstructor>
|
||||
| FullProperty<ArrayConstructor>
|
||||
| FullProperty<ObjectConstructor>
|
||||
| FullProperty<null>
|
||||
type ShortProperty =
|
||||
| StringConstructor
|
||||
| NumberConstructor
|
||||
| BooleanConstructor
|
||||
| ArrayConstructor
|
||||
| ObjectConstructor
|
||||
| null
|
||||
type AllProperty = AllFullProperty | ShortProperty
|
||||
type PropertyToData<T extends AllProperty> = T extends ShortProperty
|
||||
? ValueType<T>
|
||||
: FullPropertyToData<Exclude<T, ShortProperty>>
|
||||
type ArrayOrObject = ArrayConstructor | ObjectConstructor
|
||||
type FullPropertyToData<T extends AllFullProperty> = T['type'] extends ArrayOrObject ? unknown extends T['value'] ? ValueType<T['type']> : T['value'] : ValueType<T['type']>
|
||||
type PropertyOptionToData<P extends PropertyOption> = {
|
||||
[name in keyof P]: PropertyToData<P[name]>
|
||||
}
|
||||
|
||||
interface Router {
|
||||
switchTab: Wx['switchTab']
|
||||
reLaunch: Wx['reLaunch']
|
||||
redirectTo: Wx['redirectTo']
|
||||
navigateTo: Wx['navigateTo']
|
||||
navigateBack: Wx['navigateBack']
|
||||
}
|
||||
|
||||
interface InstanceProperties {
|
||||
/** 组件的文件路径 */
|
||||
is: string
|
||||
/** 节点id */
|
||||
id: string
|
||||
/** 节点dataset */
|
||||
dataset: Record<string, string>
|
||||
/** 上一次退出前 onSaveExitState 保存的数据 */
|
||||
exitState: any
|
||||
/** 相对于当前自定义组件的 Router 对象 */
|
||||
router: Router
|
||||
/** 相对于当前自定义组件所在页面的 Router 对象 */
|
||||
pageRouter: Router
|
||||
/** 渲染当前组件的渲染后端 */
|
||||
renderer: 'webview' | 'skyline'
|
||||
}
|
||||
|
||||
interface InstanceMethods<D extends DataOption> {
|
||||
/** `setData` 函数用于将数据从逻辑层发送到视图层
|
||||
*(异步),同时改变对应的 `this.data` 的值(同步)。
|
||||
*
|
||||
* **注意:**
|
||||
*
|
||||
* 1. **直接修改 this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致**。
|
||||
* 1. 仅支持设置可 JSON 化的数据。
|
||||
* 1. 单次设置的数据不能超过1024kB,请尽量避免一次设置过多的数据。
|
||||
* 1. 请不要把 data 中任何一项的 value 设为 `undefined` ,否则这一项将不被设置并可能遗留一些潜在问题。
|
||||
*/
|
||||
setData(
|
||||
/** 这次要改变的数据
|
||||
*
|
||||
* 以 `key: value` 的形式表示,将 `this.data` 中的 `key` 对应的值改变成 `value`。
|
||||
*
|
||||
* 其中 `key` 可以以数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如 `array[2].message`,`a.b.c.d`,并且不需要在 this.data 中预先定义。
|
||||
*/
|
||||
data: Partial<D> & IAnyObject,
|
||||
/** setData引起的界面更新渲染完毕后的回调函数,最低基础库: `1.5.0` */
|
||||
callback?: () => void
|
||||
): void
|
||||
|
||||
/** 检查组件是否具有 `behavior` (检查时会递归检查被直接或间接引入的所有behavior) */
|
||||
hasBehavior(behavior: Behavior.BehaviorIdentifier): void
|
||||
/** 触发事件,参见组件事件 */
|
||||
triggerEvent<DetailType = any>(
|
||||
name: string,
|
||||
detail?: DetailType,
|
||||
options?: TriggerEventOption
|
||||
): void
|
||||
/** 创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内 */
|
||||
createSelectorQuery(): SelectorQuery
|
||||
/** 创建一个 IntersectionObserver 对象,选择器选取范围为这个组件实例内 */
|
||||
createIntersectionObserver(
|
||||
options: CreateIntersectionObserverOption
|
||||
): IntersectionObserver
|
||||
/** 创建一个 MediaQueryObserver 对象 */
|
||||
createMediaQueryObserver(): MediaQueryObserver
|
||||
/** 使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象(会被 `wx://component-export` 影响) */
|
||||
selectComponent(selector: string): TrivialInstance
|
||||
/** 使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组 */
|
||||
selectAllComponents(selector: string): TrivialInstance[]
|
||||
/**
|
||||
* 选取当前组件节点所在的组件实例(即组件的引用者),返回它的组件实例对象(会被 `wx://component-export` 影响)
|
||||
*
|
||||
* 最低基础库版本:[`2.8.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
selectOwnerComponent(): TrivialInstance
|
||||
/** 获取这个关系所对应的所有关联节点,参见 组件间关系 */
|
||||
getRelationNodes(relationKey: string): TrivialInstance[]
|
||||
/**
|
||||
* 立刻执行 callback ,其中的多个 setData 之间不会触发界面绘制(只有某些特殊场景中需要,如用于在不同组件同时 setData 时进行界面绘制同步)
|
||||
*
|
||||
* 最低基础库版本:[`2.4.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
groupSetData(callback?: () => void): void
|
||||
/**
|
||||
* 返回当前页面的 custom-tab-bar 的组件实例
|
||||
*
|
||||
* 最低基础库版本:[`2.6.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
getTabBar(): TrivialInstance
|
||||
/**
|
||||
* 返回页面标识符(一个字符串),可以用来判断几个自定义组件实例是不是在同一个页面内
|
||||
*
|
||||
* 最低基础库版本:[`2.7.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
getPageId(): string
|
||||
/**
|
||||
* 执行关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
animate(
|
||||
selector: string,
|
||||
keyFrames: KeyFrame[],
|
||||
duration: number,
|
||||
callback?: () => void
|
||||
): void
|
||||
/**
|
||||
* 执行关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
animate(
|
||||
selector: string,
|
||||
keyFrames: ScrollTimelineKeyframe[],
|
||||
duration: number,
|
||||
scrollTimeline: ScrollTimelineOption
|
||||
): void
|
||||
/**
|
||||
* 清除关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
clearAnimation(selector: string, callback: () => void): void
|
||||
/**
|
||||
* 清除关键帧动画,详见[动画](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
**/
|
||||
clearAnimation(
|
||||
selector: string,
|
||||
options?: ClearAnimationOptions,
|
||||
callback?: () => void
|
||||
): void
|
||||
/**
|
||||
* 当从另一页面跳转到该页面时,获得与来源页面实例通信当事件通道,详见 [wx.navigateTo]((wx.navigateTo))
|
||||
*
|
||||
* 最低基础库版本:[`2.7.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
getOpenerEventChannel(): EventChannel
|
||||
/**
|
||||
* 绑定由 worklet 驱动的样式到相应的节点,详见 [worklet 动画](https://developers.weixin.qq.com/miniprogram/dev/framework/runtime/skyline/worklet.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.29.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
applyAnimatedStyle(
|
||||
selector: string,
|
||||
updater: () => Record<string, string>,
|
||||
userConfig?: { immediate: boolean, flush: 'sync' | 'async' },
|
||||
callback?: (res: { styleId: number }) => void
|
||||
): void
|
||||
/**
|
||||
* 清除节点上 worklet 驱动样式的绑定关系
|
||||
*
|
||||
* 最低基础库版本:[`2.30.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
clearAnimatedStyle(
|
||||
selector: string,
|
||||
styleIds: number[],
|
||||
callback?: () => void
|
||||
): void
|
||||
/**
|
||||
* 获取更新性能统计信息,详见 [获取更新性能统计信息]((custom-component/update-perf-stat))
|
||||
*
|
||||
*
|
||||
* 最低基础库版本:[`2.12.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
setUpdatePerformanceListener<WithDataPath extends boolean = false>(
|
||||
options: SetUpdatePerformanceListenerOption<WithDataPath>,
|
||||
callback?: UpdatePerformanceListener<WithDataPath>
|
||||
): void
|
||||
|
||||
/**
|
||||
* 在运行时获取页面或组件所在页面 `touch` 相关事件的 passive 配置,详见 [enablePassiveEvent]((configuration/app#enablePassiveEvent))
|
||||
*
|
||||
* 最低基础库版本:[`2.25.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
getPassiveEvent(callback: (config: PassiveConfig) => void): void
|
||||
/**
|
||||
* 在运行时切换页面或组件所在页面 `touch` 相关事件的 passive 配置,详见 [enablePassiveEvent]((configuration/app#enablePassiveEvent))
|
||||
*
|
||||
* 最低基础库版本:[`2.25.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
setPassiveEvent(config: PassiveConfig): void
|
||||
}
|
||||
|
||||
interface ComponentOptions {
|
||||
/**
|
||||
* [启用多slot支持](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件wxml的slot)
|
||||
*/
|
||||
multipleSlots?: boolean
|
||||
/**
|
||||
* [组件样式隔离](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
|
||||
*/
|
||||
addGlobalClass?: boolean
|
||||
/**
|
||||
* [组件样式隔离](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
|
||||
*/
|
||||
styleIsolation?:
|
||||
| 'isolated'
|
||||
| 'apply-shared'
|
||||
| 'shared'
|
||||
| 'page-isolated'
|
||||
| 'page-apply-shared'
|
||||
| 'page-shared'
|
||||
/**
|
||||
* [纯数据字段](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/pure-data.html) 是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。从小程序基础库版本 2.8.2 开始支持。
|
||||
*/
|
||||
pureDataPattern?: RegExp
|
||||
/**
|
||||
* [虚拟化组件节点](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#%E8%99%9A%E6%8B%9F%E5%8C%96%E7%BB%84%E4%BB%B6%E8%8A%82%E7%82%B9) 使自定义组件内部的第一层节点由自定义组件本身完全决定。从小程序基础库版本 [`2.11.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 开始支持 */
|
||||
virtualHost?: boolean
|
||||
}
|
||||
|
||||
interface TriggerEventOption {
|
||||
/** 事件是否冒泡
|
||||
*
|
||||
* 默认值: `false`
|
||||
*/
|
||||
bubbles?: boolean
|
||||
/** 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部
|
||||
*
|
||||
* 默认值: `false`
|
||||
*/
|
||||
composed?: boolean
|
||||
/** 事件是否拥有捕获阶段
|
||||
*
|
||||
* 默认值: `false`
|
||||
*/
|
||||
capturePhase?: boolean
|
||||
}
|
||||
|
||||
interface RelationOption {
|
||||
/** 目标组件的相对关系 */
|
||||
type: 'parent' | 'child' | 'ancestor' | 'descendant'
|
||||
/** 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 */
|
||||
linked?(target: TrivialInstance): void
|
||||
/** 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 */
|
||||
linkChanged?(target: TrivialInstance): void
|
||||
/** 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 */
|
||||
unlinked?(target: TrivialInstance): void
|
||||
/** 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 */
|
||||
target?: string
|
||||
}
|
||||
|
||||
interface PageLifetimes {
|
||||
/** 页面生命周期回调—监听页面显示
|
||||
*
|
||||
* 页面显示/切入前台时触发。
|
||||
*/
|
||||
show(): void
|
||||
/** 页面生命周期回调—监听页面隐藏
|
||||
*
|
||||
* 页面隐藏/切入后台时触发。 如 `navigateTo` 或底部 `tab` 切换到其他页面,小程序切入后台等。
|
||||
*/
|
||||
hide(): void
|
||||
/** 页面生命周期回调—监听页面尺寸变化
|
||||
*
|
||||
* 所在页面尺寸变化时执行
|
||||
*/
|
||||
resize(size: Page.IResizeOption): void
|
||||
/** 页面生命周期回调—监听页面路由动画完成
|
||||
*
|
||||
* 所在页面路由动画完成时执行
|
||||
*/
|
||||
routeDone(): void
|
||||
}
|
||||
|
||||
type DefinitionFilter = <T extends TrivialOption>(
|
||||
/** 使用该 behavior 的 component/behavior 的定义对象 */
|
||||
defFields: T,
|
||||
/** 该 behavior 所使用的 behavior 的 definitionFilter 函数列表 */
|
||||
definitionFilterArr?: DefinitionFilter[]
|
||||
) => void
|
||||
|
||||
interface Lifetimes {
|
||||
/** 组件生命周期声明对象,组件的生命周期:`created`、`attached`、`ready`、`moved`、`detached` 将收归到 `lifetimes` 字段内进行声明,原有声明方式仍旧有效,如同时存在两种声明方式,则 `lifetimes` 字段内声明方式优先级最高
|
||||
*
|
||||
* 最低基础库: `2.2.3` */
|
||||
lifetimes: Partial<{
|
||||
/**
|
||||
* 在组件实例刚刚被创建时执行,注意此时不能调用 `setData`
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
created(): void
|
||||
/**
|
||||
* 在组件实例进入页面节点树时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
attached(): void
|
||||
/**
|
||||
* 在组件在视图层布局完成后执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
ready(): void
|
||||
/**
|
||||
* 在组件实例被移动到节点树另一个位置时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
moved(): void
|
||||
/**
|
||||
* 在组件实例被从页面节点树移除时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
detached(): void
|
||||
/**
|
||||
* 每当组件方法抛出错误时执行
|
||||
*
|
||||
* 最低基础库版本:[`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
error(err: Error): void
|
||||
}>
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 在组件实例刚刚被创建时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
created(): void
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 在组件实例进入页面节点树时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
attached(): void
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 在组件在视图层布局完成后执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
ready(): void
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 在组件实例被移动到节点树另一个位置时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
moved(): void
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 在组件实例被从页面节点树移除时执行
|
||||
*
|
||||
* 最低基础库版本:[`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
detached(): void
|
||||
/**
|
||||
* @deprecated 旧式的定义方式,基础库 `2.2.3` 起请在 lifetimes 中定义
|
||||
*
|
||||
* 每当组件方法抛出错误时执行
|
||||
*
|
||||
* 最低基础库版本:[`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
error(err: Error): void
|
||||
}
|
||||
|
||||
interface OtherOption {
|
||||
/**
|
||||
* 组件数据字段监听器,用于监听 properties 和 data 的变化,参见 [数据监听器](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/observer.html)
|
||||
*
|
||||
* 最低基础库版本:[`2.6.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
|
||||
*/
|
||||
observers: Record<string, (...args: any[]) => any>
|
||||
/** 组件间关系定义,参见 [组件间关系](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html) */
|
||||
relations: {
|
||||
[componentName: string]: RelationOption
|
||||
}
|
||||
/** 组件接受的外部样式类,参见 [外部样式类](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html) */
|
||||
externalClasses?: string[]
|
||||
/** 组件所在页面的生命周期声明对象,参见 [组件生命周期](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html)
|
||||
*
|
||||
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||
pageLifetimes?: Partial<PageLifetimes>
|
||||
/** 一些选项(文档中介绍相关特性时会涉及具体的选项设置,这里暂不列举) */
|
||||
options: ComponentOptions
|
||||
|
||||
/** 定义段过滤器,用于自定义组件扩展,参见 [自定义组件扩展](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/extend.html)
|
||||
*
|
||||
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||
definitionFilter?: DefinitionFilter
|
||||
/**
|
||||
* 组件自定义导出,当使用 `behavior: wx://component-export` 时,这个定义段可以用于指定组件被 selectComponent 调用时的返回值,参见 [组件间通信与事件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html)
|
||||
* 最低基础库版本: [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
|
||||
export: () => IAnyObject
|
||||
}
|
||||
|
||||
interface KeyFrame {
|
||||
/** 关键帧的偏移,范围[0-1] */
|
||||
offset?: number
|
||||
/** 动画缓动函数 */
|
||||
ease?: string
|
||||
/** 基点位置,即 CSS transform-origin */
|
||||
transformOrigin?: string
|
||||
/** 背景颜色,即 CSS background-color */
|
||||
backgroundColor?: string
|
||||
/** 底边位置,即 CSS bottom */
|
||||
bottom?: number | string
|
||||
/** 高度,即 CSS height */
|
||||
height?: number | string
|
||||
/** 左边位置,即 CSS left */
|
||||
left?: number | string
|
||||
/** 宽度,即 CSS width */
|
||||
width?: number | string
|
||||
/** 不透明度,即 CSS opacity */
|
||||
opacity?: number | string
|
||||
/** 右边位置,即 CSS right */
|
||||
right?: number | string
|
||||
/** 顶边位置,即 CSS top */
|
||||
top?: number | string
|
||||
/** 变换矩阵,即 CSS transform matrix */
|
||||
matrix?: number[]
|
||||
/** 三维变换矩阵,即 CSS transform matrix3d */
|
||||
matrix3d?: number[]
|
||||
/** 旋转,即 CSS transform rotate */
|
||||
rotate?: number
|
||||
/** 三维旋转,即 CSS transform rotate3d */
|
||||
rotate3d?: number[]
|
||||
/** X 方向旋转,即 CSS transform rotateX */
|
||||
rotateX?: number
|
||||
/** Y 方向旋转,即 CSS transform rotateY */
|
||||
rotateY?: number
|
||||
/** Z 方向旋转,即 CSS transform rotateZ */
|
||||
rotateZ?: number
|
||||
/** 缩放,即 CSS transform scale */
|
||||
scale?: number[]
|
||||
/** 三维缩放,即 CSS transform scale3d */
|
||||
scale3d?: number[]
|
||||
/** X 方向缩放,即 CSS transform scaleX */
|
||||
scaleX?: number
|
||||
/** Y 方向缩放,即 CSS transform scaleY */
|
||||
scaleY?: number
|
||||
/** Z 方向缩放,即 CSS transform scaleZ */
|
||||
scaleZ?: number
|
||||
/** 倾斜,即 CSS transform skew */
|
||||
skew?: number[]
|
||||
/** X 方向倾斜,即 CSS transform skewX */
|
||||
skewX?: number
|
||||
/** Y 方向倾斜,即 CSS transform skewY */
|
||||
skewY?: number
|
||||
/** 位移,即 CSS transform translate */
|
||||
translate?: Array<number | string>
|
||||
/** 三维位移,即 CSS transform translate3d */
|
||||
translate3d?: Array<number | string>
|
||||
/** X 方向位移,即 CSS transform translateX */
|
||||
translateX?: number | string
|
||||
/** Y 方向位移,即 CSS transform translateY */
|
||||
translateY?: number | string
|
||||
/** Z 方向位移,即 CSS transform translateZ */
|
||||
translateZ?: number | string
|
||||
}
|
||||
interface ClearAnimationOptions {
|
||||
/** 基点位置,即 CSS transform-origin */
|
||||
transformOrigin?: boolean
|
||||
/** 背景颜色,即 CSS background-color */
|
||||
backgroundColor?: boolean
|
||||
/** 底边位置,即 CSS bottom */
|
||||
bottom?: boolean
|
||||
/** 高度,即 CSS height */
|
||||
height?: boolean
|
||||
/** 左边位置,即 CSS left */
|
||||
left?: boolean
|
||||
/** 宽度,即 CSS width */
|
||||
width?: boolean
|
||||
/** 不透明度,即 CSS opacity */
|
||||
opacity?: boolean
|
||||
/** 右边位置,即 CSS right */
|
||||
right?: boolean
|
||||
/** 顶边位置,即 CSS top */
|
||||
top?: boolean
|
||||
/** 变换矩阵,即 CSS transform matrix */
|
||||
matrix?: boolean
|
||||
/** 三维变换矩阵,即 CSS transform matrix3d */
|
||||
matrix3d?: boolean
|
||||
/** 旋转,即 CSS transform rotate */
|
||||
rotate?: boolean
|
||||
/** 三维旋转,即 CSS transform rotate3d */
|
||||
rotate3d?: boolean
|
||||
/** X 方向旋转,即 CSS transform rotateX */
|
||||
rotateX?: boolean
|
||||
/** Y 方向旋转,即 CSS transform rotateY */
|
||||
rotateY?: boolean
|
||||
/** Z 方向旋转,即 CSS transform rotateZ */
|
||||
rotateZ?: boolean
|
||||
/** 缩放,即 CSS transform scale */
|
||||
scale?: boolean
|
||||
/** 三维缩放,即 CSS transform scale3d */
|
||||
scale3d?: boolean
|
||||
/** X 方向缩放,即 CSS transform scaleX */
|
||||
scaleX?: boolean
|
||||
/** Y 方向缩放,即 CSS transform scaleY */
|
||||
scaleY?: boolean
|
||||
/** Z 方向缩放,即 CSS transform scaleZ */
|
||||
scaleZ?: boolean
|
||||
/** 倾斜,即 CSS transform skew */
|
||||
skew?: boolean
|
||||
/** X 方向倾斜,即 CSS transform skewX */
|
||||
skewX?: boolean
|
||||
/** Y 方向倾斜,即 CSS transform skewY */
|
||||
skewY?: boolean
|
||||
/** 位移,即 CSS transform translate */
|
||||
translate?: boolean
|
||||
/** 三维位移,即 CSS transform translate3d */
|
||||
translate3d?: boolean
|
||||
/** X 方向位移,即 CSS transform translateX */
|
||||
translateX?: boolean
|
||||
/** Y 方向位移,即 CSS transform translateY */
|
||||
translateY?: boolean
|
||||
/** Z 方向位移,即 CSS transform translateZ */
|
||||
translateZ?: boolean
|
||||
}
|
||||
interface ScrollTimelineKeyframe {
|
||||
composite?: 'replace' | 'add' | 'accumulate' | 'auto'
|
||||
easing?: string
|
||||
offset?: number | null
|
||||
[property: string]: string | number | null | undefined
|
||||
}
|
||||
interface ScrollTimelineOption {
|
||||
/** 指定滚动元素的选择器(只支持 scroll-view),该元素滚动时会驱动动画的进度 */
|
||||
scrollSource: string
|
||||
/** 指定滚动的方向。有效值为 horizontal 或 vertical */
|
||||
orientation?: string
|
||||
/** 指定开始驱动动画进度的滚动偏移量,单位 px */
|
||||
startScrollOffset: number
|
||||
/** 指定停止驱动动画进度的滚动偏移量,单位 px */
|
||||
endScrollOffset: number
|
||||
/** 起始和结束的滚动范围映射的时间长度,该时间可用于与关键帧动画里的时间 (duration) 相匹配,单位 ms */
|
||||
timeRange: number
|
||||
}
|
||||
|
||||
interface SetUpdatePerformanceListenerOption<WithDataPath> {
|
||||
/** 是否返回变更的 data 字段信息 */
|
||||
withDataPaths?: WithDataPath
|
||||
}
|
||||
interface UpdatePerformanceListener<WithDataPath> {
|
||||
(res: UpdatePerformance<WithDataPath>): void
|
||||
}
|
||||
interface UpdatePerformance<WithDataPath> {
|
||||
/** 此次更新过程的 ID */
|
||||
updateProcessId: number
|
||||
/** 对于子更新,返回它所属的更新过程 ID */
|
||||
parentUpdateProcessId?: number
|
||||
/** 是否是被合并更新,如果是,则 updateProcessId 表示被合并到的更新过程 ID */
|
||||
isMergedUpdate: boolean
|
||||
/** 此次更新的 data 字段信息,只有 withDataPaths 设为 true 时才会返回 */
|
||||
dataPaths: WithDataPath extends true ? string[] : undefined
|
||||
/** 此次更新进入等待队列时的时间戳 */
|
||||
pendingStartTimestamp: number
|
||||
/** 更新运算开始时的时间戳 */
|
||||
updateStartTimestamp: number
|
||||
/** 更新运算结束时的时间戳 */
|
||||
updateEndTimestamp: number
|
||||
}
|
||||
|
||||
type PassiveConfig =
|
||||
| {
|
||||
/** 是否设置 touchmove 事件为 passive,默认为 `false` */
|
||||
touchmove?: boolean
|
||||
/** 是否设置 touchstart 事件为 passive,默认为 `false` */
|
||||
touchstart?: boolean
|
||||
/** 是否设置 wheel 事件为 passive,默认为 `false` */
|
||||
wheel?: boolean
|
||||
}
|
||||
| boolean
|
||||
}
|
||||
/** Component构造器可用于定义组件,调用Component构造器时可以指定组件的属性、数据、方法等。
|
||||
*
|
||||
* * 使用 `this.data` 可以获取内部数据和属性值,但不要直接修改它们,应使用 `setData` 修改。
|
||||
* * 生命周期函数无法在组件方法中通过 `this` 访问到。
|
||||
* * 属性名应避免以 data 开头,即不要命名成 `dataXyz` 这样的形式,因为在 WXML 中, `data-xyz=""` 会被作为节点 dataset 来处理,而不是组件属性。
|
||||
* * 在一个组件的定义和使用时,组件的属性名和 data 字段相互间都不能冲突(尽管它们位于不同的定义段中)。
|
||||
* * 从基础库 `2.0.9` 开始,对象类型的属性和 data 字段中可以包含函数类型的子字段,即可以通过对象类型的属性字段来传递函数。低于这一版本的基础库不支持这一特性。
|
||||
* * `bug` : 对于 type 为 Object 或 Array 的属性,如果通过该组件自身的 `this.setData` 来改变属性值的一个子字段,则依旧会触发属性 observer ,且 observer 接收到的 `newVal` 是变化的那个子字段的值, `oldVal` 为空, `changedPath` 包含子字段的字段名相关信息。
|
||||
*/
|
||||
declare let Component: WechatMiniprogram.Component.Constructor
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,299 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
declare namespace WechatMiniprogram.Page {
|
||||
type Instance<
|
||||
TData extends DataOption,
|
||||
TCustom extends CustomOption
|
||||
> = OptionalInterface<ILifetime> &
|
||||
InstanceProperties &
|
||||
InstanceMethods<TData> &
|
||||
Data<TData> &
|
||||
TCustom
|
||||
type Options<
|
||||
TData extends DataOption,
|
||||
TCustom extends CustomOption
|
||||
> = (TCustom &
|
||||
Partial<Data<TData>> &
|
||||
Partial<ILifetime> & {
|
||||
options?: Component.ComponentOptions
|
||||
}) &
|
||||
ThisType<Instance<TData, TCustom>>
|
||||
type TrivialInstance = Instance<IAnyObject, IAnyObject>
|
||||
interface Constructor {
|
||||
<TData extends DataOption, TCustom extends CustomOption>(
|
||||
options: Options<TData, TCustom>
|
||||
): void
|
||||
}
|
||||
interface ILifetime {
|
||||
/** 生命周期回调—监听页面加载
|
||||
*
|
||||
* 页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数。
|
||||
*/
|
||||
onLoad(
|
||||
/** 打开当前页面路径中的参数 */
|
||||
query: Record<string, string | undefined>
|
||||
): void | Promise<void>
|
||||
/** 生命周期回调—监听页面显示
|
||||
*
|
||||
* 页面显示/切入前台时触发。
|
||||
*/
|
||||
onShow(): void | Promise<void>
|
||||
/** 生命周期回调—监听页面初次渲染完成
|
||||
*
|
||||
* 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互。
|
||||
*
|
||||
|
||||
* 注意:对界面内容进行设置的 API 如`wx.setNavigationBarTitle`,请在`onReady`之后进行。
|
||||
*/
|
||||
onReady(): void | Promise<void>
|
||||
/** 生命周期回调—监听页面隐藏
|
||||
*
|
||||
* 页面隐藏/切入后台时触发。 如 `navigateTo` 或底部 `tab` 切换到其他页面,小程序切入后台等。
|
||||
*/
|
||||
onHide(): void | Promise<void>
|
||||
/** 生命周期回调—监听页面卸载
|
||||
*
|
||||
* 页面卸载时触发。如`redirectTo`或`navigateBack`到其他页面时。
|
||||
*/
|
||||
onUnload(): void | Promise<void>
|
||||
/** 生命周期回调—监听路由动画完成
|
||||
*
|
||||
* 路由动画完成时触发。如 wx.navigateTo 页面完全推入后 或 wx.navigateBack 页面完全恢复时。
|
||||
*/
|
||||
onRouteDone(): void | Promise<void>
|
||||
/** 监听用户下拉动作
|
||||
*
|
||||
* 监听用户下拉刷新事件。
|
||||
* - 需要在`app.json`的`window`选项中或页面配置中开启`enablePullDownRefresh`。
|
||||
* - 可以通过`wx.startPullDownRefresh`触发下拉刷新,调用后触发下拉刷新动画,效果与用户手动下拉刷新一致。
|
||||
* - 当处理完数据刷新后,`wx.stopPullDownRefresh`可以停止当前页面的下拉刷新。
|
||||
*/
|
||||
onPullDownRefresh(): void | Promise<void>
|
||||
/** 页面上拉触底事件的处理函数
|
||||
*
|
||||
* 监听用户上拉触底事件。
|
||||
* - 可以在`app.json`的`window`选项中或页面配置中设置触发距离`onReachBottomDistance`。
|
||||
* - 在触发距离内滑动期间,本事件只会被触发一次。
|
||||
*/
|
||||
onReachBottom(): void | Promise<void>
|
||||
/** 用户点击右上角转发
|
||||
*
|
||||
* 监听用户点击页面内转发按钮(`<button>` 组件 `open-type="share"`)或右上角菜单“转发”按钮的行为,并自定义转发内容。
|
||||
*
|
||||
* **注意:只有定义了此事件处理函数,右上角菜单才会显示“转发”按钮**
|
||||
*
|
||||
* 此事件需要 return 一个 Object,用于自定义转发内容
|
||||
*/
|
||||
onShareAppMessage(
|
||||
/** 分享发起来源参数 */
|
||||
options: IShareAppMessageOption
|
||||
):
|
||||
| ICustomShareContent
|
||||
| IAsyncCustomShareContent
|
||||
| Promise<ICustomShareContent>
|
||||
| void
|
||||
| Promise<void>
|
||||
/**
|
||||
* 监听右上角菜单“分享到朋友圈”按钮的行为,并自定义分享内容
|
||||
*
|
||||
* 本接口为 Beta 版本,暂只在 Android 平台支持,详见 [分享到朋友圈 (Beta)](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html)
|
||||
*
|
||||
* 基础库 2.11.3 开始支持,低版本需做兼容处理。
|
||||
*/
|
||||
onShareTimeline(): ICustomTimelineContent | void
|
||||
|
||||
/** 页面滚动触发事件的处理函数
|
||||
*
|
||||
* 监听用户滑动页面事件。
|
||||
*/
|
||||
onPageScroll(
|
||||
/** 页面滚动参数 */
|
||||
options: IPageScrollOption
|
||||
): void | Promise<void>
|
||||
|
||||
/** 当前是 tab 页时,点击 tab 时触发,最低基础库: `1.9.0` */
|
||||
onTabItemTap(
|
||||
/** tab 点击参数 */
|
||||
options: ITabItemTapOption
|
||||
): void | Promise<void>
|
||||
|
||||
/** 窗口尺寸改变时触发,最低基础库:`2.4.0` */
|
||||
onResize(
|
||||
/** 窗口尺寸参数 */
|
||||
options: IResizeOption
|
||||
): void | Promise<void>
|
||||
|
||||
/**
|
||||
* 监听用户点击右上角菜单“收藏”按钮的行为,并自定义收藏内容。
|
||||
* 基础库 2.10.3,安卓 7.0.15 版本起支持,iOS 暂不支持
|
||||
*/
|
||||
onAddToFavorites(options: IAddToFavoritesOption): IAddToFavoritesContent
|
||||
|
||||
/** 每当小程序可能被销毁之前会被调用,可以进行退出状态的保存。最低基础库: `2.7.4` */
|
||||
onSaveExitState(): ISaveExitState
|
||||
}
|
||||
interface InstanceProperties {
|
||||
/** 页面的文件路径 */
|
||||
is: string
|
||||
|
||||
/** 到当前页面的路径 */
|
||||
route: string
|
||||
|
||||
/** 打开当前页面路径中的参数 */
|
||||
options: Record<string, string | undefined>
|
||||
|
||||
/** 上一次退出前 onSaveExitState 保存的数据 */
|
||||
exitState: any
|
||||
|
||||
/** 相对于当前页面的 Router 对象 */
|
||||
router: Component.Router
|
||||
|
||||
/** 相对于当前页面的 Router 对象 */
|
||||
pageRouter: Component.Router
|
||||
|
||||
/** 渲染当前页面的渲染后端 */
|
||||
renderer: 'webview' | 'skyline'
|
||||
}
|
||||
|
||||
type DataOption = Record<string, any>
|
||||
type CustomOption = Record<string, any>
|
||||
|
||||
type InstanceMethods<D extends DataOption> = Component.InstanceMethods<D>
|
||||
|
||||
interface Data<D extends DataOption> {
|
||||
/** 页面的初始数据
|
||||
*
|
||||
* `data` 是页面第一次渲染使用的**初始数据**。
|
||||
*
|
||||
* 页面加载时,`data` 将会以`JSON`字符串的形式由逻辑层传至渲染层,因此`data`中的数据必须是可以转成`JSON`的类型:字符串,数字,布尔值,对象,数组。
|
||||
*
|
||||
* 渲染层可以通过 `WXML` 对数据进行绑定。
|
||||
*/
|
||||
data: D
|
||||
}
|
||||
|
||||
interface ICustomShareContent {
|
||||
/** 转发标题。默认值:当前小程序名称 */
|
||||
title?: string
|
||||
/** 转发路径,必须是以 / 开头的完整路径。默认值:当前页面 path */
|
||||
path?: string
|
||||
/** 自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持PNG及JPG。显示图片长宽比是 5:4,最低基础库: `1.5.0`。默认值:使用默认截图 */
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
interface IAsyncCustomShareContent extends ICustomShareContent {
|
||||
promise: Promise<ICustomShareContent>
|
||||
}
|
||||
|
||||
interface ICustomTimelineContent {
|
||||
/** 自定义标题,即朋友圈列表页上显示的标题。默认值:当前小程序名称 */
|
||||
title?: string
|
||||
/** 自定义页面路径中携带的参数,如 `path?a=1&b=2` 的 “?” 后面部分 默认值:当前页面路径携带的参数 */
|
||||
query?: string
|
||||
/** 自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持 PNG 及 JPG。显示图片长宽比是 1:1。默认值:默认使用小程序 Logo*/
|
||||
imageUrl?: string
|
||||
}
|
||||
|
||||
interface IPageScrollOption {
|
||||
/** 页面在垂直方向已滚动的距离(单位px) */
|
||||
scrollTop: number
|
||||
}
|
||||
|
||||
interface IShareAppMessageOption {
|
||||
/** 转发事件来源。
|
||||
*
|
||||
* 可选值:
|
||||
* - `button`:页面内转发按钮;
|
||||
* - `menu`:右上角转发菜单。
|
||||
*
|
||||
* 最低基础库: `1.2.4`
|
||||
*/
|
||||
from: 'button' | 'menu'
|
||||
/** 如果 `from` 值是 `button`,则 `target` 是触发这次转发事件的 `button`,否则为 `undefined`
|
||||
*
|
||||
* 最低基础库: `1.2.4` */
|
||||
target: any
|
||||
/** 页面中包含`<web-view>`组件时,返回当前`<web-view>`的url
|
||||
*
|
||||
* 最低基础库: `1.6.4`
|
||||
*/
|
||||
webViewUrl?: string
|
||||
}
|
||||
|
||||
interface ITabItemTapOption {
|
||||
/** 被点击tabItem的序号,从0开始,最低基础库: `1.9.0` */
|
||||
index: string
|
||||
/** 被点击tabItem的页面路径,最低基础库: `1.9.0` */
|
||||
pagePath: string
|
||||
/** 被点击tabItem的按钮文字,最低基础库: `1.9.0` */
|
||||
text: string
|
||||
}
|
||||
|
||||
interface IResizeOption {
|
||||
size: {
|
||||
/** 变化后的窗口宽度,单位 px */
|
||||
windowWidth: number
|
||||
/** 变化后的窗口高度,单位 px */
|
||||
windowHeight: number
|
||||
}
|
||||
}
|
||||
|
||||
interface IAddToFavoritesOption {
|
||||
/** 页面中包含web-view组件时,返回当前web-view的url */
|
||||
webviewUrl?: string
|
||||
}
|
||||
|
||||
interface IAddToFavoritesContent {
|
||||
/** 自定义标题,默认值:页面标题或账号名称 */
|
||||
title?: string
|
||||
/** 自定义图片,显示图片长宽比为 1:1,默认值:页面截图 */
|
||||
imageUrl?: string
|
||||
/** 自定义query字段,默认值:当前页面的query */
|
||||
query?: string
|
||||
}
|
||||
|
||||
interface ISaveExitState {
|
||||
/** 需要保存的数据(只能是 JSON 兼容的数据) */
|
||||
data: any
|
||||
/** 超时时刻,在这个时刻后,保存的数据保证一定被丢弃,默认为 (当前时刻 + 1 天) */
|
||||
expireTimeStamp?: number
|
||||
}
|
||||
|
||||
interface GetCurrentPages {
|
||||
(): Array<Instance<IAnyObject, IAnyObject>>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册小程序中的一个页面。接受一个 `Object` 类型参数,其指定页面的初始数据、生命周期回调、事件处理函数等。
|
||||
*/
|
||||
declare let Page: WechatMiniprogram.Page.Constructor
|
||||
/**
|
||||
* 获取当前页面栈。数组中第一个元素为首页,最后一个元素为当前页面。
|
||||
|
||||
* __注意:__
|
||||
|
||||
* - __不要尝试修改页面栈,会导致路由以及页面状态错误。__
|
||||
* - 不要在 `App.onLaunch` 的时候调用 `getCurrentPages()`,此时 `page` 还没有生成。
|
||||
*/
|
||||
declare let getCurrentPages: WechatMiniprogram.Page.GetCurrentPages
|
|
@ -0,0 +1,409 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
declare namespace phys3D {
|
||||
// pvd调试配置
|
||||
export interface PhysicsDebugConfig {
|
||||
isNetwork: boolean // 采用网络的方式
|
||||
ip?: string // 如果isNetwork为true,调试信息会通过tcp转发的方式转发到打开了pvd调试软件的电脑,需要注意的是,防火墙要对pvd打开
|
||||
port?: 5425 // pvd默认接口
|
||||
timeout?: 1000 // 默认耗时
|
||||
path?: string // 如果isNetwork为false,调试信息会通过写本地文件的方式落地,文件名建议为xxx.pxd2,导入pvd调试即可
|
||||
}
|
||||
|
||||
export enum QueryTriggerInteraction {
|
||||
UseGlobal = 0,
|
||||
Ignore = 1,
|
||||
Collide = 2
|
||||
}
|
||||
|
||||
export class PhysSystem {
|
||||
constructor(config?: PhysicsDebugConfig)
|
||||
gravity: RawVec3f
|
||||
bounceThreshold: number
|
||||
defaultMaxAngularSpeed: number
|
||||
defaultSolverIterations: number
|
||||
defaultSolverVelocityIterations: number
|
||||
sleepThreshold: number
|
||||
defaultContactOffset: number
|
||||
destroyScene: () => void
|
||||
createScene: () => number
|
||||
Simulate: (step: number) => void
|
||||
SyncFromTransforms: (() => void) | undefined // added in 2021.06
|
||||
SetCollisionMask: (mask: ArrayBuffer) => void
|
||||
Raycast: (
|
||||
origin: RawVec3f,
|
||||
unitDir: RawVec3f,
|
||||
distance: number,
|
||||
hit: RaycastHit,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => boolean
|
||||
RaycastAll: (
|
||||
origin: RawVec3f,
|
||||
unitDir: RawVec3f,
|
||||
distance: number,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => RaycastHit[]
|
||||
CapsuleCast(
|
||||
p1: RawVec3f,
|
||||
p2: RawVec3f,
|
||||
radius: number,
|
||||
direction: RawVec3f,
|
||||
hit: RaycastHit,
|
||||
maxDistance: number,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
): void
|
||||
CapsuleCastAll: (
|
||||
p1: RawVec3f,
|
||||
p2: RawVec3f,
|
||||
radius: number,
|
||||
direction: RawVec3f,
|
||||
maxDistance: number,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => RaycastHit[]
|
||||
BoxCast(
|
||||
center: RawVec3f,
|
||||
halfExt: RawVec3f,
|
||||
direction: RawVec3f,
|
||||
hit: RaycastHit,
|
||||
orientation: RawQuaternion,
|
||||
maxDistance: number,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
): void
|
||||
BoxCastAll: (
|
||||
center: RawVec3f,
|
||||
halfExt: RawVec3f,
|
||||
direction: RawVec3f,
|
||||
orientation: RawQuaternion,
|
||||
maxDistance: number,
|
||||
layerMask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => RaycastHit[]
|
||||
OverlapBox: (
|
||||
center: RawVec3f,
|
||||
halfExt: RawVec3f,
|
||||
orientation: RawQuaternion,
|
||||
layermask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => Collider[]
|
||||
OverlapCapsule: (
|
||||
p1: RawVec3f,
|
||||
p2: RawVec3f,
|
||||
radius: number,
|
||||
layermask?: number,
|
||||
queryTriggerInteraction?: QueryTriggerInteraction
|
||||
) => Collider[]
|
||||
}
|
||||
|
||||
export class Rigidbody {
|
||||
constructor(system: PhysSystem)
|
||||
enabled?: boolean // since 2021.06
|
||||
position: RawVec3f
|
||||
rotation: RawQuaternion
|
||||
AttachToEntity: (pollObj: any, id: number) => void
|
||||
Remove(): void
|
||||
Detach(): void
|
||||
IsAttached(): boolean
|
||||
}
|
||||
|
||||
export enum CollisionDetectionMode {
|
||||
Discrete = 0,
|
||||
Continuous = 1,
|
||||
ContinuousDynamic = 2,
|
||||
ContinuousSpeculative = 3
|
||||
}
|
||||
|
||||
export enum RigidbodyConstraints {
|
||||
None = 0,
|
||||
FreezePositionX = 1 << 0,
|
||||
FreezePositionY = 1 << 1,
|
||||
FreezePositionZ = 1 << 2,
|
||||
FreezeRotationX = 1 << 3,
|
||||
FreezeRotationY = 1 << 4,
|
||||
FreezeRotationZ = 1 << 5,
|
||||
FreezePosition = FreezePositionX | FreezePositionY | FreezePositionZ,
|
||||
FreezeRotation = FreezeRotationX | FreezeRotationY | FreezeRotationZ,
|
||||
FreezeAll = FreezePosition | FreezeRotation
|
||||
}
|
||||
|
||||
export enum ForceMode {
|
||||
kForceModeForce = 0,
|
||||
kForceModeImpulse = 1 << 0,
|
||||
kForceModeVelocityChange = 1 << 1,
|
||||
kForceModeAcceleration = 1 << 2
|
||||
}
|
||||
|
||||
export enum CombineMode {
|
||||
eAverage = 0,
|
||||
eMin,
|
||||
eMultiply,
|
||||
eMax
|
||||
}
|
||||
|
||||
export enum CookingFlag {
|
||||
None = 0,
|
||||
CookForFasterSimulation = 1 << 0,
|
||||
EnableMeshCleaning = 1 << 1,
|
||||
WeldColocatedVertices = 1 << 2
|
||||
}
|
||||
|
||||
export enum CollisionFlags {
|
||||
None = 0,
|
||||
Sides = 1 << 0,
|
||||
Above = 1 << 1,
|
||||
Below = 1 << 2
|
||||
}
|
||||
|
||||
export class RawVec3f {
|
||||
constructor()
|
||||
constructor(x: number, y: number, z: number)
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export class RawQuaternion {
|
||||
constructor()
|
||||
constructor(x: number, y: number, z: number, w: number)
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
w: number
|
||||
}
|
||||
|
||||
export class Collider {
|
||||
attachedRigidbody: Rigidbody
|
||||
bounds: Bounds
|
||||
name: string
|
||||
contactOffset: number
|
||||
enabled: boolean
|
||||
isTrigger: boolean
|
||||
scale: RawVec3f
|
||||
material?: Material
|
||||
sharedMateiral?: Material
|
||||
ClosestPoint: (raw: RawVec3f) => RawVec3f
|
||||
ClosestPointOnBounds: (raw: RawVec3f) => RawVec3f
|
||||
|
||||
onCollisionEnter?: (collision: Collision) => void
|
||||
onCollisionExit?: (collision: Collision) => void
|
||||
onCollisionStay?: (collision: Collision) => void
|
||||
onTriggerEnter?: (collision: Collision) => void
|
||||
onTriggerExit?: (collision: Collision) => void
|
||||
onTriggerStay?: (collision: Collision) => void
|
||||
|
||||
dettachRigidbody?: () => void
|
||||
|
||||
userData?: unknown
|
||||
layer: number
|
||||
}
|
||||
|
||||
export class BoxCollider extends Collider {
|
||||
constructor(system: PhysSystem, center: RawVec3f, size: RawVec3f)
|
||||
center: RawVec3f
|
||||
size: RawVec3f
|
||||
}
|
||||
|
||||
export class SphereCollider extends Collider {
|
||||
constructor(system: PhysSystem, center: RawVec3f, radius: number)
|
||||
center: RawVec3f
|
||||
radius: number
|
||||
}
|
||||
|
||||
export class CapsuleCollider extends Collider {
|
||||
constructor(
|
||||
system: PhysSystem,
|
||||
center: RawVec3f,
|
||||
height: number,
|
||||
radius: number
|
||||
)
|
||||
center: RawVec3f
|
||||
height: number
|
||||
radius: number
|
||||
}
|
||||
|
||||
export class MeshCollider extends Collider {
|
||||
constructor(
|
||||
system: PhysSystem,
|
||||
convex: boolean,
|
||||
cookingOptions: number,
|
||||
sharedMesh: PhysMesh
|
||||
)
|
||||
cookingOptions: number
|
||||
sharedMesh: PhysMesh | null
|
||||
convex: boolean
|
||||
}
|
||||
|
||||
export class CharacterController extends Collider {
|
||||
constructor(system: PhysSystem)
|
||||
position: RawVec3f
|
||||
center: RawVec3f
|
||||
collisionFlags: CollisionFlags
|
||||
detectCollisions: boolean
|
||||
enableOverlapRecovery: boolean
|
||||
height: number
|
||||
isGrounded: boolean
|
||||
minMoveDistance: number
|
||||
radius: number
|
||||
skinWidth: number
|
||||
slopeLimit: number
|
||||
stepOffset: number
|
||||
velocity: RawVec3f
|
||||
|
||||
Move: (movement: RawVec3f) => CollisionFlags
|
||||
SimpleMove: (speed: RawVec3f) => boolean
|
||||
AttachToEntity: (pollObj: any, id: number) => void
|
||||
|
||||
OnControllerColliderHit?: (hit: ControllerColliderHit) => void
|
||||
}
|
||||
|
||||
export interface ContactPoint {
|
||||
normal: RawVec3f
|
||||
this_collider: Collider
|
||||
other_collider: Collider
|
||||
point: RawVec3f
|
||||
separation: number
|
||||
}
|
||||
|
||||
export interface Collision {
|
||||
collider: Collider
|
||||
contacts: ContactPoint[]
|
||||
impulse: RawVec3f
|
||||
relative_velocity: RawVec3f
|
||||
}
|
||||
|
||||
export interface ControllerColliderHit {
|
||||
collider: Collider
|
||||
controller: CharacterController
|
||||
moveDirection: RawVec3f
|
||||
normal: RawVec3f
|
||||
moveLength: number
|
||||
point: RawVec3f
|
||||
}
|
||||
|
||||
export class Bounds {
|
||||
constructor(center: RawVec3f, size: RawVec3f)
|
||||
|
||||
center: RawVec3f
|
||||
extents: RawVec3f
|
||||
max: RawVec3f
|
||||
min: RawVec3f
|
||||
size: RawVec3f
|
||||
|
||||
ClosestPoint: (point: RawVec3f) => RawVec3f
|
||||
Contains: (point: RawVec3f) => boolean
|
||||
Expand: (amount: number) => void
|
||||
Intersects: (bounds: Bounds) => boolean
|
||||
SetMinMax: (min: RawVec3f, max: RawVec3f) => void
|
||||
SqrDistance: (point: RawVec3f) => number
|
||||
}
|
||||
|
||||
export class Material {
|
||||
constructor(system: PhysSystem)
|
||||
dynamicFriction: number
|
||||
staticFriction: number
|
||||
bounciness: number
|
||||
frictionCombine: CombineMode
|
||||
bounceCombine: CombineMode
|
||||
id: number
|
||||
}
|
||||
|
||||
export class DynamicRigidbody extends Rigidbody {
|
||||
mass: number
|
||||
angularDamping: number
|
||||
angularVelocity: RawVec3f
|
||||
centerOfMass: RawVec3f
|
||||
collisionDetectionMode: CollisionDetectionMode
|
||||
constraints: number
|
||||
detectCollisions: boolean
|
||||
linearDamping: number
|
||||
freezeRotation: boolean
|
||||
inertiaTensor: number
|
||||
// inertiaTensorRotation
|
||||
// interpolation
|
||||
isKinematic: boolean
|
||||
maxAngularVelocity: number
|
||||
maxDepenetrationVelocity: number
|
||||
sleepThreshold: number
|
||||
solverIterations: number
|
||||
solverVelocityIterations: number
|
||||
useGravity: boolean
|
||||
velocity: RawVec3f
|
||||
userData?: unknown
|
||||
|
||||
GetWorldCenterOfMass: () => RawVec3f
|
||||
AddForce: (force: RawVec3f, mode: ForceMode) => void
|
||||
AddTorque: (torque: RawVec3f, mode: ForceMode) => void
|
||||
IsSleeping: () => boolean
|
||||
Sleep: () => void
|
||||
WakeUp: () => void
|
||||
AddExplosionForce: (
|
||||
explosionForce: number,
|
||||
explosionPosition: RawVec3f,
|
||||
explosionRadius: number,
|
||||
upwardsModifier: number,
|
||||
mode: ForceMode
|
||||
) => void
|
||||
AddForceAtPosition: (
|
||||
force: RawVec3f,
|
||||
position: RawVec3f,
|
||||
mode: ForceMode
|
||||
) => void
|
||||
AddRelativeForce: (force: RawVec3f, mode: ForceMode) => void
|
||||
AddRelativeTorque: (torque: RawVec3f, mode: ForceMode) => void
|
||||
ClosestPointOnBounds: (position: RawVec3f) => RawVec3f
|
||||
GetPointVelocity: (worldPoint: RawVec3f) => RawVec3f
|
||||
GetRelativePointVelocity: (relativePoint: RawVec3f) => RawVec3f
|
||||
MovePosition: (position: RawVec3f) => void
|
||||
MoveRotation: (rotation: RawQuaternion) => void
|
||||
ResetCenterOfMass: () => void
|
||||
ResetInertiaTensor: () => void
|
||||
SetDensity: (density: number) => void
|
||||
// SweepTest: () => void;
|
||||
// SweepTestAll: () => void;
|
||||
}
|
||||
|
||||
export class PhysMesh {
|
||||
constructor(system: PhysSystem)
|
||||
// set vertices
|
||||
SetVertices: (buffer: Float32Array, count: number) => void
|
||||
// set indices
|
||||
SetTriangles: (
|
||||
buffer: Uint16Array | Uint32Array,
|
||||
count: number,
|
||||
useUint16: boolean
|
||||
) => void
|
||||
}
|
||||
|
||||
export class RaycastHit {
|
||||
constructor()
|
||||
collider: Collider
|
||||
distance: number
|
||||
normal: RawVec3f
|
||||
point: RawVec3f
|
||||
rigidbody: Rigidbody
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*! *****************************************************************************
|
||||
Copyright (c) 2024 Tencent, Inc. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
***************************************************************************** */
|
||||
|
||||
/** [WXWebAssembly](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html)
|
||||
*
|
||||
* WXWebAssembly */
|
||||
declare namespace WXWebAssembly {
|
||||
type BufferSource = ArrayBufferView | ArrayBuffer
|
||||
|
||||
type CompileError = Error
|
||||
|
||||
const CompileError: {
|
||||
prototype: CompileError
|
||||
new (message?: string): CompileError
|
||||
(message?: string): CompileError
|
||||
}
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance) */
|
||||
interface Instance {
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Instance/exports) */
|
||||
readonly exports: Exports
|
||||
}
|
||||
|
||||
const Instance: {
|
||||
prototype: Instance
|
||||
new (module: Module, importObject?: Imports): Instance
|
||||
}
|
||||
|
||||
type LinkError = Error
|
||||
|
||||
const LinkError: {
|
||||
prototype: LinkError
|
||||
new (message?: string): LinkError
|
||||
(message?: string): LinkError
|
||||
}
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory) */
|
||||
interface Memory {
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory/buffer) */
|
||||
readonly buffer: ArrayBuffer
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory/grow) */
|
||||
grow(delta: number): number
|
||||
}
|
||||
|
||||
const Memory: {
|
||||
prototype: Memory
|
||||
new (descriptor: MemoryDescriptor): Memory
|
||||
}
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module) */
|
||||
interface Module {}
|
||||
|
||||
const Module: {
|
||||
prototype: Module
|
||||
new (bytes: BufferSource): Module
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/customSections) */
|
||||
customSections(moduleObject: Module, sectionName: string): ArrayBuffer[]
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/exports) */
|
||||
exports(moduleObject: Module): ModuleExportDescriptor[]
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Module/imports) */
|
||||
imports(moduleObject: Module): ModuleImportDescriptor[]
|
||||
}
|
||||
|
||||
interface RuntimeError extends Error {}
|
||||
|
||||
const RuntimeError: {
|
||||
prototype: RuntimeError
|
||||
new (message?: string): RuntimeError
|
||||
(message?: string): RuntimeError
|
||||
}
|
||||
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table) */
|
||||
interface Table {
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/length) */
|
||||
readonly length: number
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/get) */
|
||||
get(index: number): any
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/grow) */
|
||||
grow(delta: number, value?: any): number
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table/set) */
|
||||
set(index: number, value?: any): void
|
||||
}
|
||||
|
||||
const Table: {
|
||||
prototype: Table
|
||||
new (descriptor: TableDescriptor, value?: any): Table
|
||||
}
|
||||
|
||||
interface MemoryDescriptor {
|
||||
initial: number
|
||||
maximum?: number
|
||||
shared?: boolean
|
||||
}
|
||||
|
||||
interface ModuleExportDescriptor {
|
||||
kind: ImportExportKind
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ModuleImportDescriptor {
|
||||
kind: ImportExportKind
|
||||
module: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface TableDescriptor {
|
||||
element: TableKind
|
||||
initial: number
|
||||
maximum?: number
|
||||
}
|
||||
|
||||
type ImportExportKind = 'function' | 'global' | 'memory' | 'table'
|
||||
type TableKind = 'anyfunc' | 'externref'
|
||||
type ValueType =
|
||||
| 'anyfunc'
|
||||
| 'externref'
|
||||
| 'f32'
|
||||
| 'f64'
|
||||
| 'i32'
|
||||
| 'i64'
|
||||
| 'v128'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
type ExportValue = Function | Memory | Table
|
||||
type Exports = Record<string, ExportValue>
|
||||
type ImportValue = ExportValue | number
|
||||
type Imports = Record<string, ModuleImports>
|
||||
type ModuleImports = Record<string, ImportValue>
|
||||
/** [WXWebAssembly](https://developers.weixin.qq.com/miniprogram/dev/framework/performance/wasm.html) */
|
||||
function instantiate(
|
||||
path: string,
|
||||
importObject?: Imports
|
||||
): Promise<Instance>
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue