initial
This commit is contained in:
@@ -7,24 +7,24 @@
|
||||
|
||||
<!-- 基础 Meta 标签 -->
|
||||
<title>Powered by Reisa</title>
|
||||
<meta name="description" content="Union 官网" />
|
||||
<meta name="keywords" content="ReisaPage,Vue,Vite,ServerMonitoring,FindMaimai,Maimai,Reisa,Spasol" />
|
||||
<meta name="description" content="ReiJM" />
|
||||
<meta name="keywords" content="ReiJM,Vue,Vite,ServerMonitoring ,Maimai" />
|
||||
<meta name="author" content="Reisa" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://www.godserver.cn" />
|
||||
<meta property="og:title" content="Reisa Spasol" />
|
||||
<meta property="og:description" content="Union 网站" />
|
||||
<meta property="og:title" content="ReiJM" />
|
||||
<meta property="og:description" content="ReiJM 网站" />
|
||||
<meta property="og:image" content="/src/assets/logo.png" />
|
||||
<meta property="og:locale" content="zh_CN" />
|
||||
<meta property="og:site_name" content="Reisa Spasol" />
|
||||
<meta property="og:site_name" content="ReiJM" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@Spaso1" />
|
||||
<meta name="twitter:title" content="Reisa Spasol" />
|
||||
<meta name="twitter:description" content="Reisa 个人网站" />
|
||||
<meta name="twitter:site" content="@ReiJM" />
|
||||
<meta name="twitter:title" content="ReiJM" />
|
||||
<meta name="twitter:description" content="ReiJM" />
|
||||
<meta name="twitter:image" content="/src/assets/logo.png" />
|
||||
|
||||
<!-- 主题色 -->
|
||||
@@ -35,7 +35,7 @@
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Organization",
|
||||
"name": " Powered by Reisa",
|
||||
"name": " Powered by ReiJM",
|
||||
"url": "https://www.godserver.cn",
|
||||
"logo": "/src/assets/logo.png",
|
||||
"sameAs": ["https://github.com/Spaso1", "https://twitter.com/Spaso1"]
|
||||
|
||||
@@ -85,7 +85,7 @@ const toggleMenu = () => {
|
||||
<span
|
||||
class="text-xl md:text-2xl font-bold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent bg-[length:200%_auto] hover:animate-gradient whitespace-nowrap"
|
||||
>
|
||||
Union
|
||||
ReiJM
|
||||
</span>
|
||||
</router-link>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const siteConfig = {
|
||||
// 基本信息
|
||||
name: "Powered by Reisa", // 作者名称
|
||||
name: "Re", // 作者名称
|
||||
title: "FindMaimaiDX开发者 学生", // 职位头衔
|
||||
siteName: "ReisaSpasol | MaimaiDX", // 网站标题
|
||||
siteDescription:
|
||||
|
||||
@@ -1,17 +1,581 @@
|
||||
<script setup lang="ts">import { useRouter } from 'vue-router'
|
||||
<!-- HomeView.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Album {
|
||||
album_id: string
|
||||
name: string
|
||||
image_urls: string[]
|
||||
nums: number[]
|
||||
authors?: string[]
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const albums = ref<Album[]>([])
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const hasMore = ref(true)
|
||||
const observer = ref<IntersectionObserver | null>(null)
|
||||
|
||||
// 搜索相关
|
||||
const searchKeyword = ref('')
|
||||
const searchResults = ref<Album[]>([])
|
||||
const searching = ref(false)
|
||||
const showSearchResults = ref(false)
|
||||
|
||||
// 存储已解码的封面图片
|
||||
const coverImages = reactive<Record<string, string>>({})
|
||||
// 记录已处理的专辑,避免重复处理
|
||||
const processedAlbums = new Set<string>()
|
||||
// 记录已解码完成的专辑,用于控制显示
|
||||
const decodedAlbums = reactive<Record<string, boolean>>({})
|
||||
|
||||
// 获取推荐漫画
|
||||
const fetchRecommendedManga = async () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await axios.get(`/api/manga/weeking?page=${page.value}`)
|
||||
const newAlbums: Album[] = response.data
|
||||
|
||||
if (newAlbums.length === 0) {
|
||||
hasMore.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 添加新专辑到列表
|
||||
albums.value = [...albums.value, ...newAlbums]
|
||||
|
||||
// 为新专辑解码封面
|
||||
newAlbums.forEach(album => {
|
||||
if (!processedAlbums.has(album.album_id)) {
|
||||
decodeAndCacheCover(album)
|
||||
processedAlbums.add(album.album_id)
|
||||
}
|
||||
})
|
||||
|
||||
page.value++
|
||||
} catch (error) {
|
||||
console.error('获取推荐漫画失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 解码图片
|
||||
const decodeImage = (imgSrc: HTMLImageElement, num: number): string => {
|
||||
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.toDataURL()
|
||||
}
|
||||
|
||||
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.toDataURL()
|
||||
}
|
||||
|
||||
// 解码并缓存封面图片
|
||||
const decodeAndCacheCover = (album: Album) => {
|
||||
// 如果没有图片URL,返回默认图片
|
||||
if (!album.image_urls || album.image_urls.length === 0) {
|
||||
decodedAlbums[album.album_id] = true
|
||||
return
|
||||
}
|
||||
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.src = album.image_urls[0]
|
||||
|
||||
img.onload = () => {
|
||||
// 使用第一张图片的解码参数
|
||||
const num = album.nums && album.nums.length > 0 ? album.nums[0] : 0
|
||||
if (num !== 0) {
|
||||
const decodedImage = decodeImage(img, num)
|
||||
coverImages[album.album_id] = decodedImage
|
||||
} else {
|
||||
coverImages[album.album_id] = img.src
|
||||
}
|
||||
// 标记解码完成
|
||||
decodedAlbums[album.album_id] = true
|
||||
}
|
||||
|
||||
img.onerror = () => {
|
||||
// 加载失败时使用原始URL
|
||||
coverImages[album.album_id] = album.image_urls[0]
|
||||
// 标记解码完成(即使失败也标记完成)
|
||||
decodedAlbums[album.album_id] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 获取封面图片URL
|
||||
const getCoverImageUrl = (album: Album): string => {
|
||||
// 如果已经解码过,直接返回解码后的图片
|
||||
if (coverImages[album.album_id]) {
|
||||
return coverImages[album.album_id]
|
||||
}
|
||||
|
||||
// 如果有原始URL,返回原始URL
|
||||
if (album.image_urls && album.image_urls.length > 0) {
|
||||
return album.image_urls[0]
|
||||
}
|
||||
|
||||
// 否则返回空
|
||||
return ''
|
||||
}
|
||||
|
||||
// 检查专辑封面是否已解码完成
|
||||
const isAlbumDecoded = (album: Album): boolean => {
|
||||
return decodedAlbums[album.album_id] === true
|
||||
}
|
||||
|
||||
// 跳转到漫画详情页
|
||||
const goToManga = (albumId: string) => {
|
||||
router.push(`/manga/${albumId}`)
|
||||
}
|
||||
|
||||
// 搜索功能
|
||||
const searchManga = async () => {
|
||||
if (!searchKeyword.value.trim()) {
|
||||
searchResults.value = []
|
||||
showSearchResults.value = false
|
||||
return
|
||||
}
|
||||
|
||||
searching.value = true
|
||||
showSearchResults.value = true
|
||||
try {
|
||||
const response = await axios.get(`/api/manga/search`, {
|
||||
params: {
|
||||
keyword: searchKeyword.value,
|
||||
page: 1,
|
||||
type: 0
|
||||
}
|
||||
})
|
||||
searchResults.value = response.data
|
||||
|
||||
// 为搜索结果解码封面
|
||||
searchResults.value.forEach(album => {
|
||||
if (!processedAlbums.has(album.album_id)) {
|
||||
decodeAndCacheCover(album)
|
||||
processedAlbums.add(album.album_id)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索漫画失败:', error)
|
||||
} finally {
|
||||
searching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理回车键搜索
|
||||
const handleSearchKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
searchManga()
|
||||
}
|
||||
}
|
||||
|
||||
// 清空搜索
|
||||
const clearSearch = () => {
|
||||
searchKeyword.value = ''
|
||||
searchResults.value = []
|
||||
showSearchResults.value = false
|
||||
}
|
||||
|
||||
// 设置无限滚动观察器
|
||||
const setupInfiniteScroll = () => {
|
||||
observer.value = new IntersectionObserver((entries) => {
|
||||
const target = entries[0]
|
||||
if (target.isIntersecting && !loading.value && hasMore.value && !showSearchResults.value) {
|
||||
fetchRecommendedManga()
|
||||
}
|
||||
}, {
|
||||
rootMargin: '100px' // 提前100px触发加载
|
||||
})
|
||||
|
||||
const loadMoreTrigger = document.getElementById('load-more-trigger')
|
||||
if (loadMoreTrigger && observer.value) {
|
||||
observer.value.observe(loadMoreTrigger)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(() => {
|
||||
fetchRecommendedManga().then(() => {
|
||||
// 等待DOM更新后设置无限滚动
|
||||
setTimeout(setupInfiniteScroll, 0)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-view">
|
||||
<div class="header">
|
||||
<h1>{{ showSearchResults ? `搜索结果: ${searchKeyword}` : '推荐漫画' }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="manga-grid">
|
||||
<div
|
||||
v-for="album in (showSearchResults ? searchResults : albums)"
|
||||
:key="album.album_id"
|
||||
class="manga-card"
|
||||
@click="goToManga(album.album_id)"
|
||||
>
|
||||
<div class="manga-cover">
|
||||
<!-- 只有解码完成后才显示图片 -->
|
||||
<img
|
||||
v-if="isAlbumDecoded(album) && getCoverImageUrl(album)"
|
||||
:src="getCoverImageUrl(album)"
|
||||
:alt="album.name"
|
||||
@error="($event) => {
|
||||
const target = $event.target as HTMLImageElement;
|
||||
if (album.image_urls && album.image_urls.length > 0) {
|
||||
target.src = album.image_urls[0];
|
||||
}
|
||||
}"
|
||||
/>
|
||||
<div v-else class="loading-cover">加载中...</div>
|
||||
</div>
|
||||
<div class="manga-info">
|
||||
<h3 class="manga-title">{{ album.name }}</h3>
|
||||
<div v-if="album.authors && album.authors.length" class="manga-authors">
|
||||
作者: {{ album.authors.join(', ') }}
|
||||
</div>
|
||||
<div v-if="album.tags && album.tags.length" class="manga-tags">
|
||||
<span
|
||||
v-for="(tag, index) in album.tags.slice(0, 3)"
|
||||
:key="index"
|
||||
class="tag"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!showSearchResults"
|
||||
id="load-more-trigger"
|
||||
class="load-more-trigger"
|
||||
>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="!hasMore" class="no-more">没有更多了</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container">
|
||||
<div class="search-box">
|
||||
<div class="search-input-container">
|
||||
<input
|
||||
v-model="searchKeyword"
|
||||
type="text"
|
||||
placeholder="输入漫画名称或作者..."
|
||||
class="search-input"
|
||||
@keyup="handleSearchKey"
|
||||
/>
|
||||
<button
|
||||
v-if="searchKeyword"
|
||||
class="clear-button"
|
||||
@click="clearSearch"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<div v-if="searching" class="search-loading">搜索中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.home-view {
|
||||
background-color: black;
|
||||
background-color: #000;
|
||||
color: white;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
padding-bottom: 100px; /* 为底部搜索框留出空间 */
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 28px;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.manga-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.manga-card {
|
||||
background-color: #1a1a1a;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.manga-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.manga-cover {
|
||||
width: 100%;
|
||||
aspect-ratio: 2/3;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.manga-cover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.loading-cover, .no-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #333;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.manga-info {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.manga-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin: 0 0 8px 0;
|
||||
color: #fff;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.manga-authors {
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
margin-bottom: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.manga-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background-color: #333;
|
||||
color: #ccc;
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.load-more-trigger {
|
||||
text-align: center;
|
||||
padding: 30px 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.no-more {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 搜索框样式 */
|
||||
.search-container {
|
||||
position: fixed;
|
||||
bottom: 20mm;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
background-color: rgba(26, 26, 26, 0.95);
|
||||
border-radius: 12px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 12px 40px 12px 15px;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #444;
|
||||
background-color: #222;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: #1e90ff;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.search-loading {
|
||||
position: absolute;
|
||||
right: 40px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 平板适配 */
|
||||
@media (max-width: 768px) {
|
||||
.home-view {
|
||||
padding: 15px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.manga-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 手机适配 */
|
||||
@media (max-width: 480px) {
|
||||
.home-view {
|
||||
padding: 10px;
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
|
||||
.manga-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.manga-info {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.manga-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.manga-authors {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import router from "@/router";
|
||||
|
||||
const route = useRoute()
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// 漫画专辑数据
|
||||
interface AlbumData {
|
||||
@@ -25,7 +27,7 @@ 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) // 当前显示的图片索引
|
||||
let currentImageIndex = ref(0) // 当前显示的图片索引
|
||||
const imageStates = ref<Array<{ scale: number; translateX: number; translateY: number }>>([])
|
||||
const loading = ref(true)
|
||||
const canvasImages = ref<string[]>([]) // 解码后的图片
|
||||
@@ -35,6 +37,20 @@ const abortLoading = ref(false) // 取消加载标志
|
||||
const shouldHideHeader = computed(() => {
|
||||
return route.meta?.hideHeader === true
|
||||
})
|
||||
// 在现有代码中添加新的函数
|
||||
const reportReadManga = async (mangaId: string, index: number) => {
|
||||
try {
|
||||
await axios.post('/api/manga/read', {}, {
|
||||
headers: {
|
||||
Token: token || '',
|
||||
mangaId: mangaId,
|
||||
index: index.toString()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('上报阅读进度失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算当前页码信息
|
||||
const currentPageInfo = computed(() => {
|
||||
@@ -51,10 +67,12 @@ const fetchAlbum = async (id: string) => {
|
||||
|
||||
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,
|
||||
headers: {
|
||||
Token: `${token}`
|
||||
}
|
||||
})
|
||||
console.log('收到响应:', response)
|
||||
|
||||
@@ -96,6 +114,9 @@ const fetchAlbum = async (id: string) => {
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
scrollToImage(data.readIndex)
|
||||
currentImageIndex.value = data.readIndex
|
||||
console.log(currentImageIndex.value)
|
||||
} catch (error) {
|
||||
console.error('加载专辑失败', error)
|
||||
loading.value = false
|
||||
@@ -286,7 +307,7 @@ const handleTouchMove = (index: number, event: TouchEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理滚轮缩放(仅鼠标滚轮)
|
||||
// 处理滚轮缩放(以鼠标位置为中心点缩放)
|
||||
const handleWheel = (index: number, event: WheelEvent) => {
|
||||
// 确保是鼠标滚轮事件而不是触摸板手势
|
||||
if (Math.abs(event.deltaX) > Math.abs(event.deltaY) * 2) {
|
||||
@@ -297,15 +318,50 @@ const handleWheel = (index: number, event: WheelEvent) => {
|
||||
event.preventDefault()
|
||||
const currentState = imageStates.value[index]
|
||||
|
||||
// 获取图片元素
|
||||
const imgElement = document.getElementById(`image-${index}`)?.querySelector('.manga-image')
|
||||
if (!imgElement) return
|
||||
|
||||
// 计算鼠标在图片中的相对位置
|
||||
const rect = imgElement.getBoundingClientRect()
|
||||
const mouseX = event.clientX - rect.left
|
||||
const mouseY = event.clientY - rect.top
|
||||
|
||||
// 计算当前鼠标位置相对于图片中心的偏移(减小移动幅度)
|
||||
const centerX = rect.width / 2.7
|
||||
const centerY = rect.height / 2.5
|
||||
const offsetX = (mouseX - centerX) * 0.4 // 减少移动幅度
|
||||
const offsetY = (mouseY - centerY) * 0.4 // 减少移动幅度
|
||||
|
||||
if (event.deltaY < 0) {
|
||||
// 向上滚动放大
|
||||
if (currentState.scale < 3) {
|
||||
currentState.scale *= 1.1
|
||||
const newScale = currentState.scale * 1.1
|
||||
const scaleRatio = newScale / currentState.scale
|
||||
|
||||
// 以鼠标位置为中心进行缩放(减小位移)
|
||||
const newTranslateX = currentState.translateX - offsetX * (scaleRatio - 1)
|
||||
const newTranslateY = currentState.translateY - offsetY * (scaleRatio - 1)
|
||||
|
||||
currentState.scale = newScale
|
||||
currentState.translateX = newTranslateX
|
||||
currentState.translateY = newTranslateY
|
||||
}
|
||||
} else {
|
||||
// 向下滚动缩小
|
||||
if (currentState.scale > 1) {
|
||||
currentState.scale /= 1.1
|
||||
const newScale = currentState.scale / 1.1
|
||||
const scaleRatio = newScale / currentState.scale
|
||||
|
||||
// 以鼠标位置为中心进行缩放(减小位移)
|
||||
const newTranslateX = currentState.translateX - offsetX * (scaleRatio - 1)
|
||||
const newTranslateY = currentState.translateY - offsetY * (scaleRatio - 1)
|
||||
|
||||
currentState.scale = newScale
|
||||
currentState.translateX = newTranslateX
|
||||
currentState.translateY = newTranslateY
|
||||
|
||||
// 如果缩放到原始大小,重置位置
|
||||
if (currentState.scale <= 1) {
|
||||
currentState.scale = 1
|
||||
currentState.translateX = 0
|
||||
@@ -381,6 +437,10 @@ const handleScroll = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 使用节流优化滚动处理
|
||||
const throttledHandleScroll = throttle(handleScroll, 100)
|
||||
|
||||
@@ -416,9 +476,31 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
let refreshInterval: number | null = null;
|
||||
|
||||
// 新增:刷新token的函数
|
||||
const refreshToken = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const response = await axios.post('/api/user/ref', {
|
||||
data: token,
|
||||
timestamp: Date.now()
|
||||
}, {
|
||||
headers: {
|
||||
'Token' : token,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Refresh token failed:', error);
|
||||
}
|
||||
};
|
||||
// 组件挂载时添加事件监听器
|
||||
onMounted(() => {
|
||||
refreshInterval = window.setInterval(refreshToken, 30000);
|
||||
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
// 直接监听 manga-reader 元素的滚动事件,而不是 manga-content
|
||||
const mangaReader = document.querySelector('.manga-reader')
|
||||
@@ -429,6 +511,16 @@ onMounted(() => {
|
||||
// 初始化当前图片索引
|
||||
handleScroll()
|
||||
})
|
||||
// 替换现有的 watch 监听器
|
||||
watch([currentImageIndex, loading, mangaImages], ([newIndex, isLoading, images]) => {
|
||||
// 只有在非加载状态且索引有效时才发送请求
|
||||
if (albumInfo.value && !isLoading && newIndex >= 0 &&
|
||||
newIndex < images.length) {
|
||||
reportReadManga(albumInfo.value.album_id, newIndex);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 组件卸载时移除事件监听器
|
||||
onUnmounted(() => {
|
||||
@@ -446,10 +538,10 @@ onUnmounted(() => {
|
||||
@click="toggleMenu"
|
||||
:class="{ 'no-header': shouldHideHeader }">
|
||||
|
||||
<!-- <!– 始终可见的页码显示 –>-->
|
||||
<!-- <div class="page-indicator">-->
|
||||
<!-- {{ currentPageInfo }}-->
|
||||
<!-- </div>-->
|
||||
<!-- <!– 始终可见的页码显示 –>-->
|
||||
<!-- <div class="page-indicator">-->
|
||||
<!-- {{ currentPageInfo }}-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!-- 顶部菜单栏 -->
|
||||
<div
|
||||
@@ -459,8 +551,18 @@ onUnmounted(() => {
|
||||
>
|
||||
<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="prevImage"
|
||||
:disabled="currentImageIndex <= 0 || loading || mangaImages.length === 0"
|
||||
>
|
||||
上一张
|
||||
</button>
|
||||
<button
|
||||
@click="nextImage"
|
||||
:disabled="currentImageIndex >= mangaImages.length - 1 || loading || mangaImages.length === 0"
|
||||
>
|
||||
下一张
|
||||
</button>
|
||||
<button @click="isFullscreen ? exitFullscreen() : enterFullscreen()">
|
||||
{{ isFullscreen ? '退出全屏' : '全屏' }}
|
||||
</button>
|
||||
@@ -481,7 +583,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<div class="drawer-content">
|
||||
<div class="drawer-header">
|
||||
<h3>ReiJM</h3>
|
||||
<button @click="back" class="back-btn">ReiJM</button>
|
||||
<button @click="toggleDrawer" class="close-btn">×</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
|
||||
@@ -32,8 +32,11 @@ const getUrlParameter = (name: string): string | null => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const token = getUrlParameter('token')
|
||||
|
||||
let token = getUrlParameter('token')
|
||||
// 如果URL中没有token,则从localStorage中获取
|
||||
if (!token || token === 'error') {
|
||||
token = localStorage.getItem('token')
|
||||
}
|
||||
if (!token || token === 'error') {
|
||||
if (token === 'error') {
|
||||
error.value = '登录失败'
|
||||
@@ -42,8 +45,8 @@ onMounted(async () => {
|
||||
isLoggedIn.value = false
|
||||
return
|
||||
}
|
||||
//写到localStorage
|
||||
localStorage.setItem('token', token)
|
||||
//写到localStorage
|
||||
try {
|
||||
// 调用API获取用户信息(需要后端提供此接口)
|
||||
const response = await fetch(`/api/user/data`, { headers: { Token: `${token}` }})
|
||||
|
||||
Reference in New Issue
Block a user