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

增加目录列表

上级 82eb9b37
......@@ -8,6 +8,7 @@
"name": "pic-reader",
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"element-plus": "^2.9.8",
"turn.js": "^1.0.5",
......
......@@ -9,6 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.8.4",
"element-plus": "^2.9.8",
"turn.js": "^1.0.5",
......
......@@ -9,6 +9,9 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
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']
IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default']
......
......@@ -38,27 +38,22 @@
</div>
</div>
<div class="controls">
<button class="previous-button" @click="previous">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z" fill="currentColor" />
</svg>
</button>
<button class="next-button" @click="next">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="currentColor" />
</svg>
</button>
<button class="zoom-in" @click="zoomIn">
<svg viewBox="0 0 24 24" width="24" height="24">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" />
</svg>
</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 class="controls" :class="{ 'mobile-controls': isMobile }">
<el-button class="control-button" @click="previous" circle>
<el-icon><ArrowLeft /></el-icon>
</el-button>
<el-button class="control-button" @click="next" circle>
<el-icon><ArrowRight /></el-icon>
</el-button>
<el-button class="control-button" @click="zoomIn" circle>
<el-icon><ZoomIn /></el-icon>
</el-button>
<el-button class="control-button" @click="zoomOut" circle>
<el-icon><ZoomOut /></el-icon>
</el-button>
<el-button class="control-button" @click="showDirectory = true" circle>
<el-icon><Menu /></el-icon>
</el-button>
</div>
<div v-if="showExitMessage" class="exit-message">
......@@ -72,6 +67,24 @@
<div class="mobile-gesture-hint" v-if="isMobile">
左右滑动切换页面
</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>
</template>
......@@ -79,6 +92,7 @@
import $ from 'jquery'
import 'turn.js'
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'
const props = defineProps({
......@@ -112,6 +126,11 @@ const previewImages = ref([])
// 将 processedPages 改为普通数据属性
const processedPages = ref([])
// Add new refs for sound and directory
const pageTurnSound = ref(null)
const showDirectory = ref(false)
const directoryImages = ref([])
// 检测是否为移动设备
const checkMobile = () => {
isMobile.value = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
......@@ -441,6 +460,7 @@ const next = () => {
// 延迟执行翻页,确保图片已加载
setTimeout(() => {
$magazine.turn('next', { duration: 1500 })
playPageTurnSound()
}, 100)
}
}
......@@ -457,6 +477,7 @@ const previous = () => {
// 延迟执行翻页,确保图片已加载
setTimeout(() => {
$magazine.turn('previous', { duration: 1500 })
playPageTurnSound()
}, 100)
}
}
......@@ -635,6 +656,13 @@ onMounted(async () => {
await loadImages()
window.addEventListener('keydown', handleKeyDown)
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(() => {
......@@ -669,6 +697,33 @@ const handleSmallImageClick = (smallImage, pageNum) => {
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>
<style scoped>
......@@ -779,10 +834,7 @@ const handleSmallImageClick = (smallImage, pageNum) => {
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.previous-button,
.next-button,
.zoom-in,
.zoom-out {
.control-button {
width: 35px;
height: 35px;
border: none;
......@@ -797,26 +849,17 @@ const handleSmallImageClick = (smallImage, pageNum) => {
color: #333;
}
.previous-button:hover,
.next-button:hover,
.zoom-in:hover,
.zoom-out:hover {
.control-button:hover {
background-color: rgba(255, 255, 255, 0.9);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.previous-button::before,
.next-button::before,
.zoom-in::before,
.zoom-out::before {
.control-button::before {
display: none;
}
.previous-button svg,
.next-button svg,
.zoom-in svg,
.zoom-out svg {
.control-button svg {
width: 20px;
height: 20px;
}
......@@ -1037,22 +1080,16 @@ const handleSmallImageClick = (smallImage, pageNum) => {
@media (max-width: 768px) {
.controls {
bottom: 10px;
padding: 6px 12px;
padding: 6px 14px;
gap: 10px;
}
.previous-button,
.next-button,
.zoom-in,
.zoom-out {
.control-button {
width: 30px;
height: 30px;
}
.previous-button svg,
.next-button svg,
.zoom-in svg,
.zoom-out svg {
.control-button svg {
width: 18px;
height: 18px;
}
......@@ -1078,4 +1115,117 @@ const handleSmallImageClick = (smallImage, pageNum) => {
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>
\ No newline at end of file
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
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'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论