commit 193975597521af1ffd768c3367f9219b85c897b8 Author: unknown Date: Wed Dec 18 16:15:08 2024 +0800 Initial Commit diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..115cc02 --- /dev/null +++ b/.eslintrc.js @@ -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: {}, +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14ea590 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Windows +[Dd]esktop.ini +Thumbs.db +$RECYCLE.BIN/ + +# macOS +.DS_Store +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes + +# Node.js +node_modules/ diff --git a/miniprogram/app.json b/miniprogram/app.json new file mode 100644 index 0000000..c3692b9 --- /dev/null +++ b/miniprogram/app.json @@ -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": "我的" + } + ] + } +} \ No newline at end of file diff --git a/miniprogram/app.ts b/miniprogram/app.ts new file mode 100644 index 0000000..845cdd5 --- /dev/null +++ b/miniprogram/app.ts @@ -0,0 +1,41 @@ +// app.ts +App({ + 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; + } +}); \ No newline at end of file diff --git a/miniprogram/app.wxss b/miniprogram/app.wxss new file mode 100644 index 0000000..06c6fc9 --- /dev/null +++ b/miniprogram/app.wxss @@ -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; +} diff --git a/miniprogram/components/reader/reader.json b/miniprogram/components/reader/reader.json new file mode 100644 index 0000000..b8c1f40 --- /dev/null +++ b/miniprogram/components/reader/reader.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} \ No newline at end of file diff --git a/miniprogram/components/reader/reader.ts b/miniprogram/components/reader/reader.ts new file mode 100644 index 0000000..832e79e --- /dev/null +++ b/miniprogram/components/reader/reader.ts @@ -0,0 +1,886 @@ +import request from '../../utils/request'; + +// 定义组件方法接口 +interface IComponentMethods { + loadChapter: (chapter: number) => Promise; + splitPages: () => Promise; + 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({ + 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 + }); + } + } +}); \ No newline at end of file diff --git a/miniprogram/components/reader/reader.wxml b/miniprogram/components/reader/reader.wxml new file mode 100644 index 0000000..a17dff4 --- /dev/null +++ b/miniprogram/components/reader/reader.wxml @@ -0,0 +1,120 @@ + + + + + + + {{bookTitle}} + + + + + 已阅读 {{Math.floor(readingDuration / 60)}}分{{readingDuration % 60}}秒 + + + + + + 第{{currentChapter}}章 + + {{pages[currentPage]}} + + {{currentPage + 1}}/{{pages.length}} + + + + + + 上一页 + {{currentChapter}}/{{totalChapters}} + 设置 + 下一页 + + + {{currentTime}} + 已读{{readingProgress}}% + + + + + + + 目录 + × + + + + 第{{index + 1}}章 + + + + + + + + 设置 + × + + + + + 字体大小 + + A- + {{fontSize}}px + A+ + + + + + + 主题 + + + {{theme.name}} + + + + + + + 屏幕亮度 + + + + + + + + + + + 添加笔记 + 添加书签 + + \ No newline at end of file diff --git a/miniprogram/components/reader/reader.wxss b/miniprogram/components/reader/reader.wxss new file mode 100644 index 0000000..0897b70 --- /dev/null +++ b/miniprogram/components/reader/reader.wxss @@ -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; +} \ No newline at end of file diff --git a/miniprogram/images/bookshelf/txt.jpg b/miniprogram/images/bookshelf/txt.jpg new file mode 100644 index 0000000..af9a84e Binary files /dev/null and b/miniprogram/images/bookshelf/txt.jpg differ diff --git a/miniprogram/images/tabbar/README.txt b/miniprogram/images/tabbar/README.txt new file mode 100644 index 0000000..0037555 --- /dev/null +++ b/miniprogram/images/tabbar/README.txt @@ -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 正常显示。 \ No newline at end of file diff --git a/miniprogram/images/tabbar/create_icons.ts b/miniprogram/images/tabbar/create_icons.ts new file mode 100644 index 0000000..c8297ba --- /dev/null +++ b/miniprogram/images/tabbar/create_icons.ts @@ -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. 背景色: 透明 \ No newline at end of file diff --git a/miniprogram/images/tabbar/home.png b/miniprogram/images/tabbar/home.png new file mode 100644 index 0000000..3c8150c Binary files /dev/null and b/miniprogram/images/tabbar/home.png differ diff --git a/miniprogram/pages/bookshelf/add.json b/miniprogram/pages/bookshelf/add.json new file mode 100644 index 0000000..8835af0 --- /dev/null +++ b/miniprogram/pages/bookshelf/add.json @@ -0,0 +1,3 @@ +{ + "usingComponents": {} +} \ No newline at end of file diff --git a/miniprogram/pages/bookshelf/add.ts b/miniprogram/pages/bookshelf/add.ts new file mode 100644 index 0000000..a7862ec --- /dev/null +++ b/miniprogram/pages/bookshelf/add.ts @@ -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(); + } + } + } +}); \ No newline at end of file diff --git a/miniprogram/pages/bookshelf/add.wxml b/miniprogram/pages/bookshelf/add.wxml new file mode 100644 index 0000000..84fa8a4 --- /dev/null +++ b/miniprogram/pages/bookshelf/add.wxml @@ -0,0 +1,100 @@ + +
+ + ISBN + + + + + 书名* + + + + + 作者* + + + + + 出版社 + + + + + 简介 +