提交 66b75ffd authored 作者: 龙菲's avatar 龙菲

增加目录列表

上级 82eb9b37
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
"name": "pic-reader", "name": "pic-reader",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4", "axios": "^1.8.4",
"element-plus": "^2.9.8", "element-plus": "^2.9.8",
"turn.js": "^1.0.5", "turn.js": "^1.0.5",
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4", "axios": "^1.8.4",
"element-plus": "^2.9.8", "element-plus": "^2.9.8",
"turn.js": "^1.0.5", "turn.js": "^1.0.5",
......
...@@ -9,6 +9,9 @@ export {} ...@@ -9,6 +9,9 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
BookReader: typeof import('./components/BookReader.vue')['default'] BookReader: typeof import('./components/BookReader.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElIcon: typeof import('element-plus/es')['ElIcon']
FileUpload: typeof import('./components/FileUpload.vue')['default'] FileUpload: typeof import('./components/FileUpload.vue')['default']
IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default'] IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default'] IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default']
......
...@@ -38,27 +38,22 @@ ...@@ -38,27 +38,22 @@
</div> </div>
</div> </div>
<div class="controls"> <div class="controls" :class="{ 'mobile-controls': isMobile }">
<button class="previous-button" @click="previous"> <el-button class="control-button" @click="previous" circle>
<svg viewBox="0 0 24 24" width="24" height="24"> <el-icon><ArrowLeft /></el-icon>
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor" /> </el-button>
</svg> <el-button class="control-button" @click="next" circle>
</button> <el-icon><ArrowRight /></el-icon>
<button class="next-button" @click="next"> </el-button>
<svg viewBox="0 0 24 24" width="24" height="24"> <el-button class="control-button" @click="zoomIn" circle>
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor" /> <el-icon><ZoomIn /></el-icon>
</svg> </el-button>
</button> <el-button class="control-button" @click="zoomOut" circle>
<button class="zoom-in" @click="zoomIn"> <el-icon><ZoomOut /></el-icon>
<svg viewBox="0 0 24 24" width="24" height="24"> </el-button>
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" /> <el-button class="control-button" @click="showDirectory = true" circle>
</svg> <el-icon><Menu /></el-icon>
</button> </el-button>
<button class="zoom-out" @click="zoomOut">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M19 13H5v-2h14v2z" fill="currentColor" />
</svg>
</button>
</div> </div>
<div v-if="showExitMessage" class="exit-message"> <div v-if="showExitMessage" class="exit-message">
...@@ -72,6 +67,24 @@ ...@@ -72,6 +67,24 @@
<div class="mobile-gesture-hint" v-if="isMobile"> <div class="mobile-gesture-hint" v-if="isMobile">
左右滑动切换页面 左右滑动切换页面
</div> </div>
<!-- Replace directory modal with Element Plus dialog -->
<el-dialog
v-model="showDirectory"
title="目录"
width="90%"
:close-on-click-modal="true"
:close-on-press-escape="true"
class="directory-dialog"
>
<div class="thumbnail-grid">
<div v-for="(img, index) in directoryImages" :key="index"
class="thumbnail-item" @click="goToPage(img.pageNum)">
<img :src="img.src" :alt="`第 ${img.pageNum} 页`" />
<span class="page-number">{{ img.pageNum }}</span>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
...@@ -79,6 +92,7 @@ ...@@ -79,6 +92,7 @@
import $ from 'jquery' import $ from 'jquery'
import 'turn.js' import 'turn.js'
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue' import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
import { ArrowLeft, ArrowRight, ZoomIn, ZoomOut, Menu } from '@element-plus/icons-vue'
import VueEasyLightbox from 'vue-easy-lightbox' import VueEasyLightbox from 'vue-easy-lightbox'
const props = defineProps({ const props = defineProps({
...@@ -112,6 +126,11 @@ const previewImages = ref([]) ...@@ -112,6 +126,11 @@ const previewImages = ref([])
// 将 processedPages 改为普通数据属性 // 将 processedPages 改为普通数据属性
const processedPages = ref([]) const processedPages = ref([])
// Add new refs for sound and directory
const pageTurnSound = ref(null)
const showDirectory = ref(false)
const directoryImages = ref([])
// 检测是否为移动设备 // 检测是否为移动设备
const checkMobile = () => { const checkMobile = () => {
isMobile.value = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) isMobile.value = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
...@@ -441,6 +460,7 @@ const next = () => { ...@@ -441,6 +460,7 @@ const next = () => {
// 延迟执行翻页,确保图片已加载 // 延迟执行翻页,确保图片已加载
setTimeout(() => { setTimeout(() => {
$magazine.turn('next', { duration: 1500 }) $magazine.turn('next', { duration: 1500 })
playPageTurnSound()
}, 100) }, 100)
} }
} }
...@@ -457,6 +477,7 @@ const previous = () => { ...@@ -457,6 +477,7 @@ const previous = () => {
// 延迟执行翻页,确保图片已加载 // 延迟执行翻页,确保图片已加载
setTimeout(() => { setTimeout(() => {
$magazine.turn('previous', { duration: 1500 }) $magazine.turn('previous', { duration: 1500 })
playPageTurnSound()
}, 100) }, 100)
} }
} }
...@@ -635,6 +656,13 @@ onMounted(async () => { ...@@ -635,6 +656,13 @@ onMounted(async () => {
await loadImages() await loadImages()
window.addEventListener('keydown', handleKeyDown) window.addEventListener('keydown', handleKeyDown)
window.addEventListener('resize', handleResize) window.addEventListener('resize', handleResize)
// Initialize page turn sound
pageTurnSound.value = new Audio('/src/assets/sounds/page-turn.mp3')
pageTurnSound.value.load()
// Load directory images
loadDirectoryImages()
}) })
onUnmounted(() => { onUnmounted(() => {
...@@ -669,6 +697,33 @@ const handleSmallImageClick = (smallImage, pageNum) => { ...@@ -669,6 +697,33 @@ const handleSmallImageClick = (smallImage, pageNum) => {
showViewer.value = true; showViewer.value = true;
} }
}; };
// Add new methods for sound and directory
const playPageTurnSound = () => {
if (pageTurnSound.value) {
pageTurnSound.value.currentTime = 0
pageTurnSound.value.play().catch(e => console.log('Audio play failed:', e))
}
}
const generateThumbnailUrl = (url) => {
return url.replace(/(\.[^.]+)$/, '_low$1')
}
const loadDirectoryImages = () => {
directoryImages.value = props.pages.map(page => ({
src: generateThumbnailUrl(page.page_url),
pageNum: page.page_num
}))
}
const goToPage = (pageNum) => {
if (magazine.value && isInitialized.value) {
const $magazine = $(magazine.value)
$magazine.turn('page', pageNum)
showDirectory.value = false
}
}
</script> </script>
<style scoped> <style scoped>
...@@ -779,10 +834,7 @@ const handleSmallImageClick = (smallImage, pageNum) => { ...@@ -779,10 +834,7 @@ const handleSmallImageClick = (smallImage, pageNum) => {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
} }
.previous-button, .control-button {
.next-button,
.zoom-in,
.zoom-out {
width: 35px; width: 35px;
height: 35px; height: 35px;
border: none; border: none;
...@@ -797,26 +849,17 @@ const handleSmallImageClick = (smallImage, pageNum) => { ...@@ -797,26 +849,17 @@ const handleSmallImageClick = (smallImage, pageNum) => {
color: #333; color: #333;
} }
.previous-button:hover, .control-button:hover {
.next-button:hover,
.zoom-in:hover,
.zoom-out:hover {
background-color: rgba(255, 255, 255, 0.9); background-color: rgba(255, 255, 255, 0.9);
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
} }
.previous-button::before, .control-button::before {
.next-button::before,
.zoom-in::before,
.zoom-out::before {
display: none; display: none;
} }
.previous-button svg, .control-button svg {
.next-button svg,
.zoom-in svg,
.zoom-out svg {
width: 20px; width: 20px;
height: 20px; height: 20px;
} }
...@@ -1037,22 +1080,16 @@ const handleSmallImageClick = (smallImage, pageNum) => { ...@@ -1037,22 +1080,16 @@ const handleSmallImageClick = (smallImage, pageNum) => {
@media (max-width: 768px) { @media (max-width: 768px) {
.controls { .controls {
bottom: 10px; bottom: 10px;
padding: 6px 12px; padding: 6px 14px;
gap: 10px; gap: 10px;
} }
.previous-button, .control-button {
.next-button,
.zoom-in,
.zoom-out {
width: 30px; width: 30px;
height: 30px; height: 30px;
} }
.previous-button svg, .control-button svg {
.next-button svg,
.zoom-in svg,
.zoom-out svg {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
...@@ -1078,4 +1115,117 @@ const handleSmallImageClick = (smallImage, pageNum) => { ...@@ -1078,4 +1115,117 @@ const handleSmallImageClick = (smallImage, pageNum) => {
object-fit: contain; object-fit: contain;
} }
} }
/* Update mobile controls styles */
.mobile-controls {
position: fixed;
bottom: 0;
/* left: 0; */
right: 0;
display: flex;
justify-content: space-around;
align-items: center;
background: rgba(255, 255, 255, 0.95);
padding: 15px 0;
z-index: 1000;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
width: 100vw;
}
.mobile-controls .control-button {
width: 60px;
height: 60px;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(255, 255, 255, 0.9);
border: none;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.mobile-controls .el-icon {
font-size: 32px;
color: #333;
}
.mobile-controls .control-button:hover {
background-color: rgba(255, 255, 255, 1);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Update directory dialog styles */
.directory-dialog :deep(.el-dialog__body) {
padding: 20px;
}
.directory-dialog :deep(.el-dialog) {
margin-top: 5vh !important;
max-height: 90vh;
}
.directory-dialog :deep(.el-dialog__header) {
margin-right: 0;
padding: 20px;
}
.directory-dialog :deep(.el-dialog__title) {
font-size: 18px;
font-weight: 600;
}
.thumbnail-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(25vw, 1fr));
gap: 10px;
max-height: 60vh;
overflow-y: auto;
}
.thumbnail-item {
position: relative;
cursor: pointer;
transition: transform 0.2s;
border-radius: 4px;
overflow: hidden;
}
.thumbnail-item:hover {
transform: scale(1.05);
}
.thumbnail-item img {
width: 100%;
height: auto;
display: block;
}
.page-number {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
text-align: center;
padding: 4px 0;
font-size: 14px;
}
/* Media query for mobile */
@media (max-width: 768px) {
.controls:not(.mobile-controls) {
display: none;
}
.mobile-controls {
display: flex;
}
.thumbnail-grid {
grid-template-columns: repeat(auto-fill, minmax(20vw, 1fr)) !important;
}
}
</style> </style>
\ No newline at end of file
import { createApp } from 'vue' import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue' import App from './App.vue'
const app = createApp(App) const app = createApp(App)
app.use(ElementPlus)
app.mount('#app') app.mount('#app')
/*
* @Author: 龙菲 1373694886@qq.com
* @Date: 2025-04-22 22:24:25
* @LastEditors: 龙菲 1373694886@qq.com
* @LastEditTime: 2025-04-24 22:37:56
* @FilePath: \pic-reader\vite.config.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论