提交 cf12f22a authored 作者: 龙菲's avatar 龙菲

增加书签

上级 3327d190
...@@ -3,7 +3,11 @@ ...@@ -3,7 +3,11 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=1050, user-scalable=no" /> <!-- <meta name="viewport" content="width=1050, user-scalable=no" /> -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/>
<title>PDF阅读器</title> <title>PDF阅读器</title>
<script src="https://code.jquery.com/jquery-1.12.0.min.js"></script> <script src="https://code.jquery.com/jquery-1.12.0.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.5.3/modernizr.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/modernizr/2.5.3/modernizr.min.js"></script>
......
...@@ -13,7 +13,8 @@ ...@@ -13,7 +13,8 @@
"element-plus": "^2.9.8", "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" "vue-easy-lightbox": "^1.19.0",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
...@@ -1411,6 +1412,11 @@ ...@@ -1411,6 +1412,11 @@
"@vue/shared": "3.5.13" "@vue/shared": "3.5.13"
} }
}, },
"node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"node_modules/@vue/devtools-core": { "node_modules/@vue/devtools-core": {
"version": "7.7.5", "version": "7.7.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-7.7.5.tgz", "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-7.7.5.tgz",
...@@ -4003,6 +4009,20 @@ ...@@ -4003,6 +4009,20 @@
"vue": "^3.0.0" "vue": "^3.0.0"
} }
}, },
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/webpack-virtual-modules": { "node_modules/webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
...@@ -4895,6 +4915,11 @@ ...@@ -4895,6 +4915,11 @@
"@vue/shared": "3.5.13" "@vue/shared": "3.5.13"
} }
}, },
"@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"@vue/devtools-core": { "@vue/devtools-core": {
"version": "7.7.5", "version": "7.7.5",
"resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-7.7.5.tgz", "resolved": "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-7.7.5.tgz",
...@@ -6510,6 +6535,14 @@ ...@@ -6510,6 +6535,14 @@
"integrity": "sha512-YxLXgjEn91UF3DuK1y8u3Pyx2sJ7a/MnBpkyrBSQkvU1glzEJASyAZ7N+5yDpmxBQDVMwCsL2VmxWGIiFrWCgA==", "integrity": "sha512-YxLXgjEn91UF3DuK1y8u3Pyx2sJ7a/MnBpkyrBSQkvU1glzEJASyAZ7N+5yDpmxBQDVMwCsL2VmxWGIiFrWCgA==",
"requires": {} "requires": {}
}, },
"vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"requires": {
"@vue/devtools-api": "^6.6.4"
}
},
"webpack-virtual-modules": { "webpack-virtual-modules": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
......
...@@ -14,7 +14,8 @@ ...@@ -14,7 +14,8 @@
"element-plus": "^2.9.8", "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" "vue-easy-lightbox": "^1.19.0",
"vue-router": "^4.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
......
<script setup>
import { ref } from 'vue'
import BookReader from './components/BookReader.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 拦截器处理,这里可以添加额外的错误处理逻辑
}
};
onMounted(() => {
fetchDocumentDetail('ececa7473dea4d3a4448c754068139fc');
})
// const handleUploadComplete = (data) => {
// images.value = data.page_images.map(img => img.path)
// }
</script>
<template> <template>
<div class="app"> <div class="app">
<!-- <FileUpload @upload-complete="handleUploadComplete" /> --> <!-- <FileUpload @upload-complete="handleUploadComplete" /> -->
<BookReader v-if="documentPages.length > 0" :pages="documentPages" showExitMessage /> <!-- <List /> -->
<!-- <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> -->
<router-view />
</div> </div>
</template> </template>
<script setup>
// import { ref } from "vue";
// import { getDocumentDetail } from "@/api";
// const documentPages = ref([]);
// const fetchDocumentDetail = async (id) => {
// try {
// const res = await getDocumentDetail(id);
// 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 拦截器处理,这里可以添加额外的错误处理逻辑
// }
// };
// onMounted(() => {
// fetchDocumentDetail("ececa7473dea4d3a4448c754068139fc");
// });
// const handleUploadComplete = (data) => {
// images.value = data.page_images.map(img => img.path)
// }
</script>
<style> <style>
* { * {
......
import request from '@/utils/request'; import request from '@/utils/request';
/**
* 获取文档列表
* @param {string} id - 文档ID
* @returns {Promise} 返回文档详情数据
*/
export function getDocList() {
return request({
url: `/micro-mgz`,
method: 'get'
});
}
/** /**
* 获取文档详情 * 获取文档详情
* @param {string} id - 文档ID * @param {string} id - 文档ID
......
...@@ -9,17 +9,30 @@ export {} ...@@ -9,17 +9,30 @@ 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']
copy: typeof import('./components/Guide copy.vue')['default']
ElBacktop: typeof import('element-plus/es')['ElBacktop'] ElBacktop: typeof import('element-plus/es')['ElBacktop']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption']
ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTabPane: typeof import('element-plus/es')['ElTabPane']
ElTabs: typeof import('element-plus/es')['ElTabs']
ElTree: typeof import('element-plus/es')['ElTree']
FileUpload: typeof import('./components/FileUpload.vue')['default'] FileUpload: typeof import('./components/FileUpload.vue')['default']
Guide: typeof import('./components/Guide.vue')['default'] Guide: typeof import('./components/Guide.vue')['default']
'Guide copy': typeof import('./components/Guide copy.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']
IconEcosystem: typeof import('./components/icons/IconEcosystem.vue')['default'] IconEcosystem: typeof import('./components/icons/IconEcosystem.vue')['default']
IconSupport: typeof import('./components/icons/IconSupport.vue')['default'] IconSupport: typeof import('./components/icons/IconSupport.vue')['default']
IconTooling: typeof import('./components/icons/IconTooling.vue')['default'] IconTooling: typeof import('./components/icons/IconTooling.vue')['default']
List: typeof import('./components/List.vue')['default']
WelcomeItem: typeof import('./components/WelcomeItem.vue')['default'] WelcomeItem: typeof import('./components/WelcomeItem.vue')['default']
} }
} }
...@@ -102,12 +102,7 @@ ...@@ -102,12 +102,7 @@
<div class="mobile-gesture-hint" v-if="isMobile">左右滑动切换页面</div> <div class="mobile-gesture-hint" v-if="isMobile">左右滑动切换页面</div>
<!-- Replace directory modal with Element Plus dialog --> <!-- Replace directory modal with Element Plus dialog -->
<Guide <Guide ref="guideRef" :bookmarks="bookmarks" @goToPage="goToPage"></Guide>
:directoryImages="directoryImages"
ref="guideRef"
:currentPage="currentPage"
@goToPage="goToPage"
></Guide>
</div> </div>
</template> </template>
...@@ -131,6 +126,10 @@ const props = defineProps({ ...@@ -131,6 +126,10 @@ const props = defineProps({
default: () => [], default: () => [],
required: true, required: true,
}, },
bookmarks: {
type: Array,
default: () => [],
},
}); });
const magazine = ref(null); const magazine = ref(null);
......
<template>
<el-dialog
v-model="showDirectory"
title="目录"
width="50%"
:close-on-click-modal="true"
:close-on-press-escape="true"
class="directory-dialog"
>
<div class="content">
<div class="thumbnail-grid" ref="thumbnailGrid" @scroll="handleScroll">
<div
v-for="(img, index) in directoryImages"
:key="index"
:class="['thumbnail-item', { active: currentPage == index + 2 }]"
@click="goToPage(img.pageNum)"
:ref="setThumbnailRef"
>
<img :src="img.src" :alt="`第 ${img.pageNum} 页`" />
<span class="page-number">{{ img.pageNum }}</span>
</div>
</div>
<el-button
v-show="showBackTop"
:icon="ArrowUpBold"
class="back-top"
color="#000"
circle
size="large"
type="info"
@click="scrollToTop"
></el-button>
</div>
</el-dialog>
</template>
<script setup>
import { ArrowUpBold } from "@element-plus/icons-vue";
const $emit = defineEmits(["goToPage"]);
const props = defineProps({
directoryImages: {
type: Array,
default: () => [],
},
currentPage: {
type: Number,
default: 0,
},
});
const showDirectory = ref(false);
const thumbnailGrid = ref(null); // 获取滚动容器的引用
const showBackTop = ref(false); // 控制按钮显示
const thumbnailRefs = ref([]); // 存储所有缩略图的DOM引用
const setThumbnailRef = (el) => {
if (el) {
thumbnailRefs.value.push(el);
}
};
function show() {
showDirectory.value = true;
nextTick(() => {
scrollToCurrentPage();
});
}
function scrollToCurrentPage() {
if (
props.currentPage > 0 &&
thumbnailGrid.value &&
thumbnailRefs.value.length > 0
) {
// 找到当前页码对应的缩略图索引
const currentIndex = props.directoryImages.findIndex(
(img) => img.pageNum === props.currentPage
);
if (currentIndex >= 0) {
const thumbnailElement = thumbnailRefs.value[currentIndex + 2];
if (thumbnailElement) {
// 计算滚动位置
const gridRect = thumbnailGrid.value.getBoundingClientRect();
const thumbRect = thumbnailElement.getBoundingClientRect();
const scrollTop =
thumbRect.top - gridRect.top + thumbnailGrid.value.scrollTop;
// 平滑滚动到该位置
thumbnailGrid.value.scrollTo({
top: scrollTop - 50, // 减去50px让缩略图不是紧贴顶部
behavior: "smooth",
});
}
}
}
}
function goToPage(pageNum) {
$emit("goToPage", pageNum);
showDirectory.value = false;
}
// 返回顶部函数
function scrollToTop() {
if (thumbnailGrid.value) {
thumbnailGrid.value.scrollTo({
top: 0,
behavior: "smooth", // 平滑滚动
});
}
}
// 监听滚动
function handleScroll() {
if (thumbnailGrid.value) {
showBackTop.value = thumbnailGrid.value.scrollTop > 100; // 滚动超过 100px 时显示
}
}
defineExpose({
show,
});
</script>
<style lang="scss" scoped>
.thumbnail-grid {
max-height: 70vh;
overflow-y: auto;
display: flex;
flex-wrap: wrap;
gap: 10px; /* 添加行间距和列间距 */
position: relative;
}
.content {
.back-top {
position: absolute;
bottom: 40px;
right: 40px;
}
}
.thumbnail-item {
position: relative;
cursor: pointer;
transition: transform 0.2s;
overflow: hidden;
width: calc(33.333% - 10px); /* 调整宽度,减去间距 */
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid transparent;
&:hover {
border: 1px solid #000;
}
img {
width: 100%;
height: 200px;
object-fit: contain;
}
.page-number {
font-size: 20px;
position: absolute;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.65);
width: 100%;
text-align: center;
color: #fff;
}
}
.active {
border: 1px solid #000;
.page-number {
color: var(--el-color-success);
background-color: #fff;
}
}
@media (max-width: 768px) {
.thumbnail-grid {
gap: 10px; /* 保持间距 */
}
.thumbnail-item {
width: calc(33.333% - 10px); /* 保持宽度调整 */
}
}
</style>
<template> <template>
<el-dialog <el-drawer v-model="showDirectory" direction="ltr" style="wdith: 40%">
v-model="showDirectory" <template #header>
title="目录" <h4>目录</h4>
width="50%" </template>
:close-on-click-modal="true" <template #default>
:close-on-press-escape="true" <el-tree
class="directory-dialog" :data="bookmarks"
> :props="defaultProps"
<div class="content"> @node-click="handleNodeClick"
<div class="thumbnail-grid" ref="thumbnailGrid" @scroll="handleScroll"> />
<div </template>
v-for="(img, index) in directoryImages" <template #footer>
:key="index" <div style="flex: auto">
:class="['thumbnail-item', { active: currentPage == index + 2 }]" <el-button type="primary" @click="confirmClick">关闭</el-button>
@click="goToPage(img.pageNum)"
:ref="setThumbnailRef"
>
<img :src="img.src" :alt="`第 ${img.pageNum} 页`" />
<span class="page-number">{{ img.pageNum }}</span>
</div>
</div> </div>
<el-button </template>
v-show="showBackTop" </el-drawer>
:icon="ArrowUpBold"
class="back-top"
color="#000"
circle
size="large"
type="info"
@click="scrollToTop"
></el-button>
</div>
</el-dialog>
</template> </template>
<script setup> <script setup>
import { ArrowUpBold } from "@element-plus/icons-vue"; import { ArrowUpBold } from "@element-plus/icons-vue";
const $emit = defineEmits(["goToPage"]); const $emit = defineEmits(["goToPage"]);
const props = defineProps({ const props = defineProps({
directoryImages: { bookmarks: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
...@@ -51,6 +35,10 @@ const thumbnailGrid = ref(null); // 获取滚动容器的引用 ...@@ -51,6 +35,10 @@ const thumbnailGrid = ref(null); // 获取滚动容器的引用
const showBackTop = ref(false); // 控制按钮显示 const showBackTop = ref(false); // 控制按钮显示
const thumbnailRefs = ref([]); // 存储所有缩略图的DOM引用 const thumbnailRefs = ref([]); // 存储所有缩略图的DOM引用
const defaultProps = ref({
children: "children",
label: "title",
});
const setThumbnailRef = (el) => { const setThumbnailRef = (el) => {
if (el) { if (el) {
...@@ -118,6 +106,11 @@ function handleScroll() { ...@@ -118,6 +106,11 @@ function handleScroll() {
} }
} }
function handleNodeClick(node) {
console.log(node);
goToPage(node.page);
}
defineExpose({ defineExpose({
show, show,
}); });
......
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>
...@@ -2,7 +2,9 @@ import { createApp } from 'vue' ...@@ -2,7 +2,9 @@ import { createApp } from 'vue'
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css' import 'element-plus/dist/index.css'
import App from './App.vue' import App from './App.vue'
import router from './router';
const app = createApp(App) const app = createApp(App)
app.use(ElementPlus) app.use(ElementPlus)
app.use(router)
app.mount('#app') app.mount('#app')
import { createWebHistory, createRouter } from 'vue-router';
// 公共路由
export const constantRoutes = [
{
path: '/',
redirect: '/index',
},
{
path: '/index',
component: () => import('@/views/index/index.vue'),
},
{
path: '/detail/:id',
component: () => import('@/views/detail/index.vue'),
},
{
path: '/:pathMatch(.*)*',
component: () => import('@/views/error/404.vue'),
},
{
path: '/401',
component: () => import('@/views/error/401.vue'),
},
{
path: '',
redirect: '/index',
},
];
/**
* 创建路由
*/
const router = createRouter({
history: createWebHistory('/'),
routes: constantRoutes,
// 刷新时,滚动条位置还原
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
}
});
export default router;
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;
<template>
<div>
<div class="nav"></div>
<BookReader :pages="pages" :bookmarks="bookmarks" />
</div>
</template>
<script setup>
import { getDocumentDetail } from "@/api";
import BookReader from "@/components/BookReader.vue";
const route = useRoute();
/* props */
/* emit */
/* data */
const pages = ref([]);
const bookmarks = ref([]);
/* computed */
/* methods */
async function loadData(id) {
const res = await getDocumentDetail(id);
pages.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,
};
}),
};
});
}
onMounted(() => {
const { id } = route.params;
bookmarks.value = JSON.parse(route.query.bookmarks);
loadData(id);
});
</script>
<style lang="scss" scoped></style>
<template>
<div class="errPage-container">
<el-button icon="arrow-left" class="pan-back-btn" @click="back"> 返回 </el-button>
<el-row>
<el-col :span="12">
<h1 class="text-jumbo text-ginormous">401错误!</h1>
<h2>您没有访问权限!</h2>
<h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
<ul class="list-unstyled">
<li class="link-type">
<router-link to="/"> 回首页 </router-link>
</li>
</ul>
</el-col>
<el-col :span="12">
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import errImage from '@/assets/401_images/401.gif';
let { proxy } = getCurrentInstance() as ComponentInternalInstance;
const errGif = ref(errImage + '?' + +new Date());
function back() {
if (proxy?.$route.query.noGoBack) {
proxy.$router.push({ path: '/' });
} else {
proxy?.$router.go(-1);
}
}
</script>
<style lang="scss" scoped>
.errPage-container {
width: 800px;
max-width: 100%;
margin: 100px auto;
.pan-back-btn {
background: #008489;
color: #fff;
border: none !important;
}
.pan-gif {
margin: 0 auto;
display: block;
}
.pan-img {
display: block;
margin: 0 auto;
width: 100%;
}
.text-jumbo {
font-size: 60px;
font-weight: 700;
color: #484848;
}
.list-unstyled {
font-size: 14px;
li {
padding-bottom: 5px;
}
a {
color: #008489;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}
</style>
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404" />
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404" />
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404" />
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404" />
</div>
<div class="bullshit">
<div class="bullshit__oops">404错误!</div>
<div class="bullshit__headline">
{{ message }}
</div>
<div class="bullshit__info">
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
</div>
<router-link to="/index" class="bullshit__return-home"> 返回首页 </router-link>
</div>
</div>
</div>
</template>
<script setup lang="ts">
let message = computed(() => {
return '找不到网页!';
});
</script>
<style lang="scss" scoped>
.wscn-http404-container {
transform: translate(-50%, -50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>
<template>
<div class="book-list-container">
<el-row :gutter="16" class="book-card-list">
<el-col
v-for="(book, index) in books"
:key="book.book_id + index"
:span="8"
>
<div class="book-card" shadow="hover" @click="openBook(book)">
<!-- 这里可根据实际书籍封面逻辑替换,示例用纯色背景占位 -->
<div class="book-cover">{{ book.name }}</div>
<div class="book-name">{{ book.name }}</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from "vue";
import { getDocList } from "@/api/index";
import router from "@/router";
// 模拟接口返回数据类型
interface Book {
book_id: string;
name: string;
upload_time: string;
sort_order: number;
publish_time: string;
bookmarks: string;
coverUrl?: string; // 实际开发可补充封面字段,这里模拟可选
}
const books = ref<Book[]>();
async function loadData() {
const res = await getDocList();
books.value = res;
}
function openBook(book) {
console.log(book);
const query = {
bookmarks: book.bookmarks,
};
const path = `/detail/${book.book_id}`;
router.push({
query,
path,
});
}
onMounted(() => {
loadData();
});
</script>
<style scoped lang="scss">
.book-list-container {
height: 100vh;
padding: 30px;
.book-card {
border-radius: 8px;
.book-cover {
background-color: rgba(65, 184, 131, 0.4);
padding: 50px 10px;
border-radius: 8px;
display: flex;
justify-content: center;
font-family: 楷体;
font-size: 18px;
margin-bottom: 10px;
}
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论