提交 30e49f4e authored 作者: 龙菲's avatar 龙菲

对接接口及增加小图预览

上级 91b6e002
VITE_API_BASE_URL = '/api'
\ No newline at end of file
VITE_API_BASE_URL = '/'
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,11 +9,16 @@ ...@@ -9,11 +9,16 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.4",
"element-plus": "^2.9.8",
"turn.js": "^1.0.5", "turn.js": "^1.0.5",
"vue": "^3.5.13" "vue": "^3.5.13",
"vue-easy-lightbox": "^1.19.0"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"unplugin-auto-import": "^19.1.2",
"unplugin-vue-components": "^28.5.0",
"vite": "^6.2.4", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2" "vite-plugin-vue-devtools": "^7.7.2"
} }
......
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue'
import BookReader from './components/BookReader.vue' import BookReader from './components/BookReader.vue'
import FileUpload from './components/FileUpload.vue' import FileUpload from './components/FileUpload.vue'
import { getDocumentDetail } from '@/api';
// const images = ref([])
const documentPages = ref([])
const fetchDocumentDetail = async (id) => {
try {
const res = await getDocumentDetail(id);
// images.value =res.map(item=>import.meta.env.VITE_API_BASE_URL +'/static/'+item.page_url)
documentPages.value = res.map(item=>{
return {
...item,
page_url:import.meta.env.VITE_API_BASE_URL +'/static/'+item.page_url,
images:item.images.map(img=>{
return {
...img,
url:import.meta.env.VITE_API_BASE_URL +'/static/'+img.url
}
})
}
})
} catch (error) {
// 错误已经被 request 拦截器处理,这里可以添加额外的错误处理逻辑
}
};
const images = ref([]) onMounted(() => {
fetchDocumentDetail('ececa7473dea4d3a4448c754068139fc');
})
const handleUploadComplete = (data) => { // const handleUploadComplete = (data) => {
images.value = data.page_images.map(img => img.path) // images.value = data.page_images.map(img => img.path)
} // }
</script> </script>
<template> <template>
<div class="app"> <div class="app">
<FileUpload @upload-complete="handleUploadComplete" /> <!-- <FileUpload @upload-complete="handleUploadComplete" /> -->
<BookReader v-if="images.length > 0" :pages="images" /> <BookReader v-if="documentPages.length > 0" :pages="documentPages" showExitMessage/>
<div v-else class="empty-state"> <!-- <div v-else class="empty-state">
请上传PDF文件开始阅读 请上传PDF文件开始阅读
</div> </div> -->
</div> </div>
</template> </template>
......
import request from '@/utils/request';
/**
* 获取文档详情
* @param {string} id - 文档ID
* @returns {Promise} 返回文档详情数据
*/
export function getDocumentDetail(id) {
return request({
url: `/micro-mgz/${id}/detail`,
method: 'get'
});
}
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BookRead: typeof import('./components/BookRead.vue')['default']
BookReader: typeof import('./components/BookReader.vue')['default']
FileUpload: typeof import('./components/FileUpload.vue')['default']
IconCommunity: typeof import('./components/icons/IconCommunity.vue')['default']
IconDocumentation: typeof import('./components/icons/IconDocumentation.vue')['default']
IconEcosystem: typeof import('./components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./components/icons/IconTooling.vue')['default']
WelcomeItem: typeof import('./components/WelcomeItem.vue')['default']
}
}
...@@ -12,8 +12,27 @@ ...@@ -12,8 +12,27 @@
'odd': !page.isWide && index % 2 !== 0, 'odd': !page.isWide && index % 2 !== 0,
'even': !page.isWide && index % 2 === 1 'even': !page.isWide && index % 2 === 1
}]"> }]">
<img :src="page.src" :alt="`第 ${index + 1} 页`" @error="handleImageError(index)" <img :src="page.src" :alt="`第 ${page.page_num} 页`" @error="handleImageError(index)"
:class="['page-image', { 'wide-image': page.isWide }]" /> :class="['page-image', { 'wide-image': page.isWide }]" />
<!-- 添加小图叠加层 -->
<div v-if="page.images && page.images.length > 0" class="small-images-overlay">
<div v-for="(smallImage, imgIndex) in page.images"
:key="imgIndex"
class="small-image-container"
:style="{
left: `${smallImage.position.x1 * 100}%`,
top: `${smallImage.position.y1 * 100}%`,
width: `${(smallImage.position.x2 - smallImage.position.x1) * 100}%`,
height: `${(smallImage.position.y2 - smallImage.position.y1) * 100}%`
}"
@click="handleSmallImageClick(smallImage, page.page_num)">
<img :src="smallImage.url"
:alt="`小图 ${imgIndex + 1}`"
class="small-image" />
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -44,6 +63,14 @@ ...@@ -44,6 +63,14 @@
<div v-if="showExitMessage" class="exit-message"> <div v-if="showExitMessage" class="exit-message">
按 ESC 键退出阅读 按 ESC 键退出阅读
</div> </div>
<!-- 替换为 vue-easy-lightbox 预览组件 -->
<vue-easy-lightbox
:visible="showViewer"
:imgs="previewImages"
:index="currentImageIndex"
@hide="showViewer = false"
/>
</div> </div>
</template> </template>
...@@ -51,10 +78,12 @@ ...@@ -51,10 +78,12 @@
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 VueEasyLightbox from 'vue-easy-lightbox'
const props = defineProps({ const props = defineProps({
pages: { pages: {
type: Array, type: Array,
default: ()=>[],
required: true required: true
} }
}) })
...@@ -67,11 +96,19 @@ const isInitialized = ref(false) ...@@ -67,11 +96,19 @@ const isInitialized = ref(false)
const imageMetadata = ref([]) const imageMetadata = ref([])
// 修改预览相关的状态
const showViewer = ref(false)
const currentImageIndex = ref(0)
const previewImages = ref([])
// 处理页面数据,移除宽图处理 // 处理页面数据,移除宽图处理
const processedPages = computed(() => { const processedPages = computed(() => {
return props.pages.map((src, index) => ({ return props.pages.map((item, index) => ({
src, src: item.page_url,
originalIndex: index originalIndex: index,
page_num: item.page_num, // 添加页码
images: item.images || [],
isWide: item.isWide
})) }))
}) })
...@@ -217,16 +254,16 @@ const loadImages = async () => { ...@@ -217,16 +254,16 @@ const loadImages = async () => {
loading.value = true loading.value = true
try { try {
// 获取所有图片的尺寸信息 // 获取所有图片的尺寸信息
const metadataPromises = props.pages.map(src => checkImageDimensions(src)) const metadataPromises = props.pages.map(item => checkImageDimensions(item.page_url))
imageMetadata.value = await Promise.all(metadataPromises) imageMetadata.value = await Promise.all(metadataPromises)
// 等待所有图片加载完成 // 等待所有图片加载完成
await Promise.all(props.pages.map(src => { await Promise.all(props.pages.map(item => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image() const img = new Image()
img.onload = resolve img.onload = resolve
img.onerror = reject img.onerror = reject
img.src = src img.src = item.page_url
}) })
})) }))
...@@ -239,7 +276,7 @@ const loadImages = async () => { ...@@ -239,7 +276,7 @@ const loadImages = async () => {
} }
const handleImageError = (index) => { const handleImageError = (index) => {
console.error(`图片加载失败: ${props.pages[index]}`) console.error(`图片加载失败: ${props.pages[index].page_url}`)
} }
const next = () => { const next = () => {
...@@ -462,6 +499,24 @@ watch(() => props.pages, async (newPages) => { ...@@ -462,6 +499,24 @@ watch(() => props.pages, async (newPages) => {
await loadImages() await loadImages()
} }
}, { deep: true }) }, { deep: true })
// 修改小图点击事件处理函数
const handleSmallImageClick = (smallImage, pageNum) => {
// 获取当前页面所有小图的URL
const currentPage = props.pages.find(page => page.page_num === pageNum);
if (currentPage && currentPage.images) {
previewImages.value = currentPage.images.map(img => ({
src: img.url,
title: `第 ${pageNum} 页`
}));
// 设置当前点击的图片索引
currentImageIndex.value = currentPage.images.findIndex(img => img.url === smallImage.url);
// 显示预览组件
showViewer.value = true;
}
};
</script> </script>
<style scoped> <style scoped>
...@@ -729,4 +784,79 @@ watch(() => props.pages, async (newPages) => { ...@@ -729,4 +784,79 @@ watch(() => props.pages, async (newPages) => {
.magazine-viewport.turn-page-wrapper .turn-page { .magazine-viewport.turn-page-wrapper .turn-page {
overflow: hidden !important; overflow: hidden !important;
} }
.small-images-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 允许点击穿透到下层 */
}
.small-image-container {
position: absolute;
cursor: pointer;
pointer-events: auto;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.small-image-container:hover {
border-color: rgba(255, 255, 255, 0.5);
box-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.small-image {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none; /* 防止图片本身接收点击事件 */
}
.debug-info {
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 2px 4px;
font-size: 10px;
pointer-events: none;
z-index: 1000;
}
/* 添加预览组件样式 */
:deep(.vel-modal) {
background-color: rgba(0, 0, 0, 0.9);
}
:deep(.vel-img) {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
:deep(.vel-btn) {
color: #fff;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
:deep(.vel-btn:hover) {
background-color: rgba(0, 0, 0, 0.7);
}
:deep(.vel-title) {
color: #fff;
background-color: rgba(0, 0, 0, 0.5);
padding: 8px 16px;
border-radius: 4px;
}
</style> </style>
\ No newline at end of file
/*
* @Author: 龙菲 1373694886@qq.com
* @Date: 2025-04-23 22:37:01
* @LastEditors: 龙菲 1373694886@qq.com
* @LastEditTime: 2025-04-23 22:41:24
* @FilePath: \pic-reader\src\utils\request.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import axios from 'axios';
import { ElMessage } from 'element-plus';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '', // 从环境变量获取基础URL
timeout: 15000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 预留 token 处理逻辑
// const token = localStorage.getItem('token');
// if (token) {
// config.headers['Authorization'] = `Bearer ${token}`;
// }
return config;
},
(error) => {
ElMessage.error('请求发送失败');
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
// 这里可以根据后端的响应结构进行调整
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败');
// 处理特定的错误码
switch (res.code) {
case 401:
// token 过期或未登录
// localStorage.removeItem('token');
// 可以在这里添加重定向到登录页的逻辑
break;
case 403:
ElMessage.error('没有权限访问该资源');
break;
case 404:
ElMessage.error('请求的资源不存在');
break;
case 500:
ElMessage.error('服务器内部错误');
break;
default:
ElMessage.error(res.message || '未知错误');
}
return Promise.reject(new Error(res.message || '请求失败'));
}
return res;
},
(error) => {
// 处理 HTTP 错误状态码
if (error.response) {
switch (error.response.status) {
case 401:
ElMessage.error('未授权,请重新登录');
// localStorage.removeItem('token');
// 可以在这里添加重定向到登录页的逻辑
break;
case 403:
ElMessage.error('拒绝访问');
break;
case 404:
ElMessage.error('请求的资源不存在');
break;
case 500:
ElMessage.error('服务器内部错误');
break;
default:
ElMessage.error(`请求失败: ${error.response.status}`);
}
} else if (error.request) {
ElMessage.error('网络错误,请检查您的网络连接');
} else {
ElMessage.error('请求配置错误');
}
return Promise.reject(error);
}
);
// 封装请求方法
export function request(config) {
return service(config);
}
export default service;
/*
* @Author: 龙菲 1373694886@qq.com
* @Date: 2025-04-23 22:37:01
* @LastEditors: 龙菲 1373694886@qq.com
* @LastEditTime: 2025-04-23 22:41:24
* @FilePath: \pic-reader\src\utils\request.js
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
import axios from 'axios';
import { ElMessage } from 'element-plus';
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '', // 从环境变量获取基础URL
timeout: 15000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=utf-8'
}
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 预留 token 处理逻辑
// const token = localStorage.getItem('token');
// if (token) {
// config.headers['Authorization'] = `Bearer ${token}`;
// }
return config;
},
(error) => {
ElMessage.error('请求发送失败');
return Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
return res;
},
(error) => {
// 处理 HTTP 错误状态码
if (error.response) {
switch (error.response.status) {
case 401:
ElMessage.error('未授权,请重新登录');
// localStorage.removeItem('token');
// 可以在这里添加重定向到登录页的逻辑
break;
case 403:
ElMessage.error('拒绝访问');
break;
case 404:
ElMessage.error('请求的资源不存在');
break;
case 500:
ElMessage.error('服务器内部错误');
break;
default:
ElMessage.error(`请求失败: ${error.response.status}`);
}
} else if (error.request) {
ElMessage.error('网络错误,请检查您的网络连接');
} else {
ElMessage.error('请求配置错误');
}
return Promise.reject(error);
}
);
// 封装请求方法
export function request(config) {
return service(config);
}
export default service;
/*
* @Author: 龙菲 1373694886@qq.com
* @Date: 2025-04-22 22:24:25
* @LastEditors: 龙菲 1373694886@qq.com
* @LastEditTime: 2025-04-23 22:52:11
* @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'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), vue(),
vueDevTools(), vueDevTools(),
AutoImport({
// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
imports: ['vue', 'vue-router', 'pinia'],
// 自动导入 Element Plus 相关函数
resolvers: [ElementPlusResolver()],
// 生成自动导入的TS声明文件
dts: 'src/auto-imports.d.ts',
}),
Components({
// 自动导入组件
resolvers: [ElementPlusResolver()],
// 生成自动导入的TS声明文件
dts: 'src/components.d.ts',
}),
], ],
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, },
}, },
server: {
proxy: {
'/api': {
target: 'http://222.85.214.245:9666',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
}) })
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论