895 lines
21 KiB
Vue
895 lines
21 KiB
Vue
<!-- Manga.vue -->
|
||
<script setup lang="ts">
|
||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||
import { useRoute } from 'vue-router'
|
||
import axios from 'axios'
|
||
|
||
const route = useRoute()
|
||
|
||
// 漫画专辑数据
|
||
interface AlbumData {
|
||
album_id: string
|
||
name: string
|
||
image_urls: string[]
|
||
nums: number[]
|
||
authors?: string[]
|
||
tags?: string[]
|
||
}
|
||
|
||
// 漫画图片列表(从 jm.vue 中获取)
|
||
const mangaImages = ref<string[]>([])
|
||
const mangaNums = ref<number[]>([])
|
||
const albumInfo = ref<Omit<AlbumData, 'image_urls' | 'nums'> | null>(null)
|
||
|
||
// 阅读器状态
|
||
const showMenu = ref(false)
|
||
const isFullscreen = ref(false)
|
||
const showDrawer = ref(false)
|
||
const currentImageIndex = ref(0) // 当前显示的图片索引
|
||
const imageStates = ref<Array<{ scale: number; translateX: number; translateY: number }>>([])
|
||
const loading = ref(true)
|
||
const canvasImages = ref<string[]>([]) // 解码后的图片
|
||
const abortLoading = ref(false) // 取消加载标志
|
||
|
||
// 计算是否应该隐藏头部(用于样式调整)
|
||
const shouldHideHeader = computed(() => {
|
||
return route.meta?.hideHeader === true
|
||
})
|
||
|
||
// 计算当前页码信息
|
||
const currentPageInfo = computed(() => {
|
||
return `第 ${currentImageIndex.value + 1} / ${mangaImages.value.length} 页`
|
||
})
|
||
|
||
// 获取专辑数据
|
||
const fetchAlbum = async (id: string) => {
|
||
console.log('尝试获取专辑数据,ID:', id)
|
||
if (!id) {
|
||
console.log('ID为空,无法获取专辑数据')
|
||
return
|
||
}
|
||
|
||
try {
|
||
loading.value = true
|
||
console.log('发送请求到:', `/api/manga/read?mangaId=${id}`)
|
||
const response = await axios.get(`/api/manga/read?mangaId=${id}`, {
|
||
maxRedirects: 5,
|
||
validateStatus: (status) => status < 500,
|
||
})
|
||
console.log('收到响应:', response)
|
||
|
||
let data: AlbumData
|
||
|
||
if (response.status === 307 || response.status === 302) {
|
||
const newUrl = new URL(response.headers.location, response.config.url).href
|
||
const redirectResponse = await axios.get(newUrl)
|
||
data = redirectResponse.data
|
||
} else {
|
||
data = response.data
|
||
}
|
||
|
||
// 设置专辑信息
|
||
albumInfo.value = {
|
||
album_id: data.album_id,
|
||
name: data.name,
|
||
authors: data.authors || [],
|
||
tags: data.tags || []
|
||
}
|
||
|
||
// 设置图片 URL 和解码参数
|
||
mangaImages.value = data.image_urls || []
|
||
mangaNums.value = data.nums || []
|
||
|
||
// 初始化图片状态
|
||
imageStates.value = (data.image_urls || []).map(() => ({
|
||
scale: 1,
|
||
translateX: 0,
|
||
translateY: 0
|
||
}))
|
||
|
||
// 初始化画布图片数组
|
||
canvasImages.value = new Array((data.image_urls || []).length)
|
||
|
||
// 开始加载图片
|
||
if (data.image_urls && data.nums) {
|
||
loadImagesInBatches(data.image_urls, data.nums)
|
||
} else {
|
||
loading.value = false
|
||
}
|
||
} catch (error) {
|
||
console.error('加载专辑失败', error)
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 分批加载图片(每次加载4张)
|
||
const loadImagesInBatches = async (urls: string[], nums: number[]) => {
|
||
const batchSize = 4 // 每批加载4张图片
|
||
let currentIndex = 0
|
||
abortLoading.value = false // 开始新加载任务前重置标志
|
||
|
||
while (currentIndex < urls.length && !abortLoading.value) {
|
||
const batchUrls = urls.slice(currentIndex, currentIndex + batchSize)
|
||
const batchNums = nums.slice(currentIndex, currentIndex + batchSize)
|
||
|
||
const loadPromises = batchUrls.map((url, index) => {
|
||
return new Promise<void>((resolve) => {
|
||
if (abortLoading.value) return resolve()
|
||
|
||
const img = new Image()
|
||
img.crossOrigin = 'anonymous'
|
||
img.src = url
|
||
|
||
img.onload = () => {
|
||
if (abortLoading.value) return resolve()
|
||
|
||
if (batchNums[index] !== 0) {
|
||
const decodedCanvas = decodeImage(img, batchNums[index])
|
||
const base64 = decodedCanvas.toDataURL()
|
||
canvasImages.value[currentIndex + index] = base64
|
||
} else {
|
||
canvasImages.value[currentIndex + index] = url
|
||
}
|
||
resolve()
|
||
}
|
||
|
||
img.onerror = () => {
|
||
if (abortLoading.value) return resolve()
|
||
console.error(`图片加载失败: ${url}`)
|
||
// 加载失败时使用原始 URL
|
||
canvasImages.value[currentIndex + index] = url
|
||
resolve()
|
||
}
|
||
})
|
||
})
|
||
|
||
await Promise.all(loadPromises)
|
||
currentIndex += batchSize
|
||
}
|
||
|
||
loading.value = false
|
||
}
|
||
|
||
// JS 版 decodeImage(等效 Java 方法)
|
||
const decodeImage = (imgSrc: HTMLImageElement, num: number): HTMLCanvasElement => {
|
||
if (num === 0) {
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = imgSrc.width
|
||
canvas.height = imgSrc.height
|
||
const ctx = canvas.getContext('2d')
|
||
ctx?.drawImage(imgSrc, 0, 0)
|
||
return canvas
|
||
}
|
||
|
||
const w = imgSrc.width
|
||
const h = imgSrc.height
|
||
const canvas = document.createElement('canvas')
|
||
canvas.width = w
|
||
canvas.height = h
|
||
const ctx = canvas.getContext('2d')!
|
||
|
||
const over = h % num
|
||
|
||
for (let i = 0; i < num; i++) {
|
||
let move = Math.floor(h / num)
|
||
let ySrc = h - move * (i + 1) - over
|
||
let yDst = move * i
|
||
|
||
if (i === 0) {
|
||
move += over
|
||
} else {
|
||
yDst += over
|
||
}
|
||
|
||
const srcRect = { x: 0, y: ySrc, width: w, height: move }
|
||
const dstRect = { x: 0, y: yDst, width: w, height: move }
|
||
|
||
const tempCanvas = document.createElement('canvas')
|
||
tempCanvas.width = w
|
||
tempCanvas.height = move
|
||
const tempCtx = tempCanvas.getContext('2d')!
|
||
tempCtx.drawImage(
|
||
imgSrc,
|
||
srcRect.x,
|
||
srcRect.y,
|
||
srcRect.width,
|
||
srcRect.height,
|
||
0,
|
||
0,
|
||
srcRect.width,
|
||
srcRect.height
|
||
)
|
||
|
||
ctx.drawImage(
|
||
tempCanvas,
|
||
0,
|
||
0,
|
||
tempCanvas.width,
|
||
tempCanvas.height,
|
||
dstRect.x,
|
||
dstRect.y,
|
||
dstRect.width,
|
||
dstRect.height
|
||
)
|
||
}
|
||
|
||
return canvas
|
||
}
|
||
|
||
// 切换抽屉菜单显示
|
||
const toggleDrawer = () => {
|
||
showDrawer.value = !showDrawer.value
|
||
}
|
||
|
||
// 切换顶部菜单显示
|
||
const toggleMenu = () => {
|
||
showMenu.value = !showMenu.value
|
||
}
|
||
|
||
// 进入全屏模式
|
||
const enterFullscreen = () => {
|
||
const element = document.documentElement
|
||
if (element.requestFullscreen) {
|
||
element.requestFullscreen()
|
||
isFullscreen.value = true
|
||
}
|
||
}
|
||
|
||
// 退出全屏模式
|
||
const exitFullscreen = () => {
|
||
if (document.exitFullscreen) {
|
||
document.exitFullscreen()
|
||
isFullscreen.value = false
|
||
}
|
||
}
|
||
|
||
// 重置所有图片的缩放
|
||
const resetAllImages = () => {
|
||
imageStates.value = imageStates.value.map(() => ({
|
||
scale: 1,
|
||
translateX: 0,
|
||
translateY: 0
|
||
}))
|
||
}
|
||
|
||
// 处理双指缩放(只响应真正的触摸事件)
|
||
let initialDistance = 0
|
||
let initialScale = 1
|
||
|
||
const getDistance = (touch1: Touch, touch2: Touch) => {
|
||
const dx = touch1.clientX - touch2.clientX
|
||
const dy = touch1.clientY - touch2.clientY
|
||
return Math.sqrt(dx * dx + dy * dy)
|
||
}
|
||
|
||
const handleTouchStart = (index: number, event: TouchEvent) => {
|
||
if (event.touches.length === 2) {
|
||
// 双指触摸开始
|
||
initialDistance = getDistance(event.touches[0], event.touches[1])
|
||
initialScale = imageStates.value[index].scale
|
||
} else if (event.touches.length === 1) {
|
||
// 单指触摸,可能是滚动,不阻止默认行为
|
||
return
|
||
}
|
||
}
|
||
|
||
const handleTouchMove = (index: number, event: TouchEvent) => {
|
||
if (event.touches.length === 2) {
|
||
// 双指缩放
|
||
event.preventDefault() // 只在双指操作时阻止默认行为
|
||
const currentDistance = getDistance(event.touches[0], event.touches[1])
|
||
const scale = initialScale * (currentDistance / initialDistance)
|
||
|
||
// 限制缩放范围
|
||
const clampedScale = Math.min(Math.max(scale, 1), 3)
|
||
imageStates.value[index].scale = clampedScale
|
||
}
|
||
}
|
||
|
||
// 处理滚轮缩放(仅鼠标滚轮)
|
||
const handleWheel = (index: number, event: WheelEvent) => {
|
||
// 确保是鼠标滚轮事件而不是触摸板手势
|
||
if (Math.abs(event.deltaX) > Math.abs(event.deltaY) * 2) {
|
||
// 可能是水平滚动,不处理缩放
|
||
return
|
||
}
|
||
|
||
event.preventDefault()
|
||
const currentState = imageStates.value[index]
|
||
|
||
if (event.deltaY < 0) {
|
||
// 向上滚动放大
|
||
if (currentState.scale < 3) {
|
||
currentState.scale *= 1.1
|
||
}
|
||
} else {
|
||
// 向下滚动缩小
|
||
if (currentState.scale > 1) {
|
||
currentState.scale /= 1.1
|
||
if (currentState.scale <= 1) {
|
||
currentState.scale = 1
|
||
currentState.translateX = 0
|
||
currentState.translateY = 0
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 跳转到指定图片
|
||
const scrollToImage = (index: number) => {
|
||
if (index < 0 || index >= mangaImages.value.length) return
|
||
|
||
const element = document.getElementById(`image-${index}`)
|
||
if (element) {
|
||
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||
currentImageIndex.value = index
|
||
// 关闭抽屉菜单
|
||
showDrawer.value = false
|
||
}
|
||
}
|
||
|
||
// 跳转到下一张图片
|
||
const nextImage = () => {
|
||
if (currentImageIndex.value < mangaImages.value.length - 1) {
|
||
scrollToImage(currentImageIndex.value + 1)
|
||
}
|
||
}
|
||
|
||
// 跳转到上一张图片
|
||
const prevImage = () => {
|
||
if (currentImageIndex.value > 0) {
|
||
scrollToImage(currentImageIndex.value - 1)
|
||
}
|
||
}
|
||
|
||
// 节流函数
|
||
const throttle = (func: Function, limit: number) => {
|
||
let inThrottle: boolean
|
||
return function() {
|
||
const args = arguments
|
||
const context = this
|
||
if (!inThrottle) {
|
||
func.apply(context, args)
|
||
inThrottle = true
|
||
setTimeout(() => inThrottle = false, limit)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 监听滚动事件,更新当前图片索引
|
||
const handleScroll = () => {
|
||
const containers = document.querySelectorAll('.image-container')
|
||
let currentIndex = 0
|
||
|
||
// 使用更精确的计算方式确定当前视口中的图片
|
||
for (let i = 0; i < containers.length; i++) {
|
||
const rect = containers[i].getBoundingClientRect()
|
||
// 当图片的任意部分在视口中时就认为是当前图片
|
||
if (rect.bottom > 0 && rect.top < window.innerHeight) {
|
||
// 优先选择图片中心点在视口中央的图片
|
||
if (Math.abs(rect.top + rect.height/2 - window.innerHeight/2) <
|
||
Math.abs(containers[currentIndex].getBoundingClientRect().top +
|
||
containers[currentIndex].getBoundingClientRect().height/2 - window.innerHeight/2)) {
|
||
currentIndex = i
|
||
}
|
||
}
|
||
}
|
||
|
||
// 只有当索引真正改变时才更新
|
||
if (currentImageIndex.value !== currentIndex) {
|
||
currentImageIndex.value = currentIndex
|
||
}
|
||
}
|
||
|
||
// 使用节流优化滚动处理
|
||
const throttledHandleScroll = throttle(handleScroll, 100)
|
||
|
||
// 键盘事件处理
|
||
const handleKeydown = (event: KeyboardEvent) => {
|
||
switch (event.key) {
|
||
case 'ArrowLeft':
|
||
prevImage()
|
||
event.preventDefault()
|
||
break
|
||
case 'ArrowRight':
|
||
nextImage()
|
||
event.preventDefault()
|
||
break
|
||
case 'Escape':
|
||
if (isFullscreen.value) {
|
||
exitFullscreen()
|
||
}
|
||
showDrawer.value = false
|
||
break
|
||
}
|
||
}
|
||
|
||
// 监听路由参数变化
|
||
watch(
|
||
() => route.params.id,
|
||
(newId) => {
|
||
console.log('路由参数变化:', newId)
|
||
if (newId) {
|
||
abortLoading.value = true // 取消之前的加载
|
||
fetchAlbum(newId as string)
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
)
|
||
|
||
// 组件挂载时添加事件监听器
|
||
onMounted(() => {
|
||
window.addEventListener('keydown', handleKeydown)
|
||
// 直接监听 manga-reader 元素的滚动事件,而不是 manga-content
|
||
const mangaReader = document.querySelector('.manga-reader')
|
||
if (mangaReader) {
|
||
mangaReader.addEventListener('scroll', throttledHandleScroll)
|
||
}
|
||
|
||
// 初始化当前图片索引
|
||
handleScroll()
|
||
})
|
||
|
||
// 组件卸载时移除事件监听器
|
||
onUnmounted(() => {
|
||
window.removeEventListener('keydown', handleKeydown)
|
||
const mangaReader = document.querySelector('.manga-content')
|
||
if (mangaReader) {
|
||
mangaReader.removeEventListener('scroll', throttledHandleScroll)
|
||
}
|
||
abortLoading.value = true // 取消加载
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<div class="manga-reader"
|
||
@click="toggleMenu"
|
||
:class="{ 'no-header': shouldHideHeader }">
|
||
|
||
<!-- <!– 始终可见的页码显示 –>-->
|
||
<!-- <div class="page-indicator">-->
|
||
<!-- {{ currentPageInfo }}-->
|
||
<!-- </div>-->
|
||
|
||
<!-- 顶部菜单栏 -->
|
||
<div
|
||
class="menu-bar"
|
||
:class="{ visible: showMenu }"
|
||
@click.stop
|
||
>
|
||
<div class="menu-content">
|
||
<span class="page-info">{{ currentPageInfo }}</span>
|
||
<button @click="prevImage" :disabled="currentImageIndex === 0 || loading">上一张</button>
|
||
<button @click="nextImage" :disabled="currentImageIndex === mangaImages.length - 1 || loading">下一张</button>
|
||
<button @click="isFullscreen ? exitFullscreen() : enterFullscreen()">
|
||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||
</button>
|
||
<button @click="toggleDrawer">菜单</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 左侧抽屉菜单 -->
|
||
<div
|
||
class="drawer-overlay"
|
||
:class="{ 'drawer-open': showDrawer }"
|
||
@click="toggleDrawer"
|
||
>
|
||
<div
|
||
class="drawer"
|
||
:class="{ 'drawer-open': showDrawer }"
|
||
@click.stop
|
||
>
|
||
<div class="drawer-content">
|
||
<div class="drawer-header">
|
||
<h3>ReiJM</h3>
|
||
<button @click="toggleDrawer" class="close-btn">×</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<button @click="isFullscreen ? exitFullscreen() : enterFullscreen()" class="drawer-item">
|
||
{{ isFullscreen ? '退出全屏' : '进入全屏' }}
|
||
</button>
|
||
<div class="page-list">
|
||
<h4>页面列表</h4>
|
||
<div
|
||
v-for="(image, index) in mangaImages"
|
||
:key="index"
|
||
class="page-item"
|
||
:class="{ 'active': index === currentImageIndex }"
|
||
@click="scrollToImage(index)"
|
||
>
|
||
第 {{ index + 1 }} 页
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 漫画内容 -->
|
||
<div class="manga-content">
|
||
<!-- 专辑信息 -->
|
||
<div v-if="albumInfo" class="comic-header">
|
||
<h1 class="comic-title">{{ albumInfo.name }}</h1>
|
||
<div class="comic-meta">
|
||
<div v-if="albumInfo.authors && albumInfo.authors.length" class="meta-item">
|
||
<span class="label">作者:</span>
|
||
<span class="value">{{ albumInfo.authors.join(' / ') }}</span>
|
||
</div>
|
||
<div v-if="albumInfo.tags && albumInfo.tags.length" class="meta-item">
|
||
<span class="label">标签:</span>
|
||
<span class="value tags">
|
||
<span v-for="(tag, idx) in albumInfo.tags" :key="idx" class="tag">{{ tag }}</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 加载状态 -->
|
||
<div v-if="loading" class="loading">加载中...</div>
|
||
|
||
<!-- 漫画图片 -->
|
||
<div
|
||
v-for="(image, index) in canvasImages"
|
||
:key="index"
|
||
class="image-container"
|
||
:id="`image-${index}`"
|
||
>
|
||
<img
|
||
v-if="image"
|
||
:src="image"
|
||
:alt="`漫画第 ${index + 1} 页`"
|
||
class="manga-image"
|
||
:style="{
|
||
transform: `scale(${imageStates[index].scale}) translate(${imageStates[index].translateX}px, ${imageStates[index].translateY}px)`
|
||
}"
|
||
@wheel="handleWheel(index, $event)"
|
||
@touchstart="handleTouchStart(index, $event)"
|
||
@touchmove="handleTouchMove(index, $event)"
|
||
/>
|
||
<div v-else class="image-placeholder">图片加载中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 点击屏幕显示菜单的提示区域 -->
|
||
<div
|
||
class="screen-click-area"
|
||
@click="toggleMenu"
|
||
></div>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.manga-reader {
|
||
position: relative;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
background-color: #000;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
user-select: none;
|
||
padding-top: 0; /* 移除顶部内边距 */
|
||
padding-bottom: 60px; /* 添加底部内边距 */
|
||
}
|
||
|
||
.manga-reader.no-header {
|
||
padding-bottom: 0; /* 无头部时也无底部内边距 */
|
||
}
|
||
|
||
/* 添加页码指示器样式 */
|
||
.page-indicator {
|
||
position: fixed;
|
||
top: 10px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background-color: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
padding: 5px 15px;
|
||
border-radius: 20px;
|
||
font-size: 14px;
|
||
z-index: 90;
|
||
pointer-events: none; /* 不拦截点击事件 */
|
||
}
|
||
|
||
.menu-bar {
|
||
position: fixed;
|
||
bottom: 0; /* 改为底部定位 */
|
||
left: 0;
|
||
right: 0;
|
||
background-color: rgba(0, 0, 0, 0.8);
|
||
color: white;
|
||
padding: 10px 20px;
|
||
z-index: 100;
|
||
transform: translateY(100%); /* 改为向上偏移 */
|
||
transition: transform 0.3s ease;
|
||
}
|
||
|
||
.menu-bar.visible {
|
||
transform: translateY(0); /* 回到正常位置 */
|
||
}
|
||
|
||
.menu-content {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.menu-content button {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
margin-left: 10px;
|
||
}
|
||
|
||
.menu-content button:hover:not(:disabled) {
|
||
background-color: rgba(255, 255, 255, 0.3);
|
||
}
|
||
|
||
.menu-content button:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.page-info {
|
||
font-size: 16px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* 左侧抽屉菜单样式 */
|
||
.drawer-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 199;
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||
}
|
||
|
||
.drawer-overlay.drawer-open {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
.drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 300px;
|
||
height: 100vh;
|
||
background-color: rgba(0, 0, 0, 0.9);
|
||
color: white;
|
||
z-index: 200;
|
||
transform: translateX(-100%);
|
||
transition: transform 0.3s ease;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.drawer.drawer-open {
|
||
transform: translateX(0);
|
||
}
|
||
|
||
.drawer-content {
|
||
padding: 20px;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.drawer-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #333;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.drawer-header h3 {
|
||
margin: 0;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.drawer-body {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.drawer-item {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px;
|
||
margin-bottom: 10px;
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
text-align: left;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.drawer-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
}
|
||
|
||
.page-list h4 {
|
||
margin: 20px 0 10px 0;
|
||
color: #ccc;
|
||
}
|
||
|
||
.page-item {
|
||
padding: 8px;
|
||
margin: 5px 0;
|
||
background-color: rgba(255, 255, 255, 0.05);
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.page-item:hover {
|
||
background-color: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.page-item.active {
|
||
background-color: rgba(255, 255, 255, 0.2);
|
||
font-weight: bold;
|
||
}
|
||
|
||
.manga-content {
|
||
padding: 20px 0 0 0; /* 只保留顶部内边距 */
|
||
}
|
||
|
||
.image-container {
|
||
width: 100%;
|
||
display: flex;
|
||
justify-content: center;
|
||
margin-bottom: 0; /* 移除图片间的间距 */
|
||
padding: 0; /* 移除内边距 */
|
||
}
|
||
|
||
.manga-image {
|
||
max-width: 100%;
|
||
height: auto;
|
||
cursor: zoom-in;
|
||
transform-origin: center center;
|
||
transition: transform 0.2s ease;
|
||
user-select: none;
|
||
-webkit-user-drag: none;
|
||
display: block; /* 避免 inline 元素带来的空白间隙 */
|
||
}
|
||
|
||
.manga-image:active {
|
||
cursor: grabbing;
|
||
}
|
||
|
||
.image-placeholder {
|
||
color: #999;
|
||
font-size: 16px;
|
||
padding: 20px;
|
||
}
|
||
|
||
/* 顶部信息 */
|
||
.comic-header {
|
||
margin-bottom: 24px;
|
||
z-index: 2;
|
||
position: relative;
|
||
padding: 0 20px;
|
||
}
|
||
|
||
.comic-title {
|
||
font-size: 28px;
|
||
font-weight: bold;
|
||
margin-bottom: 16px;
|
||
line-height: 1.2;
|
||
color: white;
|
||
}
|
||
|
||
.comic-meta {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.meta-item {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.label {
|
||
font-weight: bold;
|
||
color: #aaa;
|
||
width: 50px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.value {
|
||
color: #ddd;
|
||
}
|
||
|
||
.tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
|
||
.tag {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
padding: 4px 10px;
|
||
border-radius: 12px;
|
||
font-size: 12px;
|
||
color: #ccc;
|
||
}
|
||
|
||
.loading {
|
||
text-align: center;
|
||
padding: 40px;
|
||
color: #999;
|
||
font-size: 18px;
|
||
}
|
||
|
||
/* 屏幕点击区域 */
|
||
.screen-click-area {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 10;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.menu-content {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
}
|
||
|
||
.menu-content > * {
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.manga-image {
|
||
max-width: 100%;
|
||
}
|
||
|
||
.drawer {
|
||
width: 280px;
|
||
}
|
||
|
||
.page-info {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.comic-header {
|
||
padding: 0 10px;
|
||
}
|
||
|
||
.comic-title {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.page-indicator {
|
||
top: 5px;
|
||
font-size: 12px;
|
||
padding: 3px 10px;
|
||
}
|
||
}
|
||
</style>
|