在线文档支持
This commit is contained in:
parent
ce7ee34666
commit
4e17ee281c
1286
reading-platform-frontend/package-lock.json
generated
1286
reading-platform-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -39,10 +39,10 @@
|
||||
"@types/node": "^20.11.28",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"@playwright/test": "^1.58.2",
|
||||
"orval": "^8.5.3",
|
||||
"sass-embedded": "^1.97.3",
|
||||
"typescript": "~5.4.0",
|
||||
"unocss": "^66.6.6",
|
||||
"unplugin-auto-import": "^0.17.5",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"unplugin-vue-router": "^0.19.2",
|
||||
|
||||
@ -1,20 +1,8 @@
|
||||
<template>
|
||||
<a-modal
|
||||
v-model:open="visible"
|
||||
:title="title"
|
||||
:width="modalWidth"
|
||||
:footer="null"
|
||||
centered
|
||||
@cancel="handleClose"
|
||||
>
|
||||
<a-modal v-model:open="visible" :title="title" :width="modalWidth" :footer="null" centered @cancel="handleClose">
|
||||
<!-- PDF 预览 -->
|
||||
<div v-if="fileType === 'pdf'" class="preview-container pdf-container">
|
||||
<iframe
|
||||
v-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
class="pdf-iframe"
|
||||
frameborder="0"
|
||||
></iframe>
|
||||
<iframe v-if="previewUrl" :src="previewUrl" class="pdf-iframe" frameborder="0"></iframe>
|
||||
<div v-else class="preview-error">
|
||||
<FileTextOutlined style="font-size: 48px; color: #999;" />
|
||||
<p>无法预览此PDF文件</p>
|
||||
@ -31,13 +19,7 @@
|
||||
<div class="audio-info">
|
||||
<h3>{{ fileName }}</h3>
|
||||
</div>
|
||||
<audio
|
||||
ref="audioRef"
|
||||
:src="previewUrl"
|
||||
controls
|
||||
class="audio-element"
|
||||
@error="handleMediaError"
|
||||
>
|
||||
<audio ref="audioRef" :src="previewUrl" controls class="audio-element" @error="handleMediaError">
|
||||
您的浏览器不支持音频播放
|
||||
</audio>
|
||||
</div>
|
||||
@ -45,39 +27,53 @@
|
||||
|
||||
<!-- 视频播放器 -->
|
||||
<div v-else-if="fileType === 'video'" class="preview-container video-container">
|
||||
<video
|
||||
ref="videoRef"
|
||||
:src="previewUrl"
|
||||
controls
|
||||
class="video-element"
|
||||
@error="handleMediaError"
|
||||
>
|
||||
<video ref="videoRef" :src="previewUrl" controls class="video-element" @error="handleMediaError">
|
||||
您的浏览器不支持视频播放
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<!-- 图片预览 -->
|
||||
<div v-else-if="fileType === 'image'" class="preview-container image-container">
|
||||
<a-image
|
||||
:src="previewUrl"
|
||||
:preview="true"
|
||||
class="preview-image"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
<a-image :src="previewUrl" :preview="true" class="preview-image" @error="handleImageError" />
|
||||
</div>
|
||||
|
||||
<!-- PPT 预览(使用 Office Online 或 Google Docs 预览) -->
|
||||
<!-- PPT 预览 -->
|
||||
<div v-else-if="fileType === 'ppt'" class="preview-container ppt-container">
|
||||
<div class="ppt-notice">
|
||||
<FilePptOutlined style="font-size: 48px; color: #ff7a45;" />
|
||||
<h3>PPT 文件预览</h3>
|
||||
<p>PPT 文件建议下载后使用 PowerPoint 或 WPS 查看</p>
|
||||
<p>使用 WebOffice 在新页面中预览</p>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="downloadFile">
|
||||
<DownloadOutlined /> 下载文件
|
||||
<a-button type="primary" @click="openInWebOffice">
|
||||
<EyeOutlined /> WebOffice 预览
|
||||
</a-button>
|
||||
<a-button @click="openInOfficeOnline">
|
||||
<EyeOutlined /> 在线预览
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Word 预览 -->
|
||||
<div v-else-if="fileType === 'doc'" class="preview-container ppt-container">
|
||||
<div class="ppt-notice">
|
||||
<FileTextOutlined style="font-size: 48px; color: #1890ff;" />
|
||||
<h3>Word 文件预览</h3>
|
||||
<p>使用 WebOffice 在新页面中预览</p>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="openInWebOffice">
|
||||
<EyeOutlined /> WebOffice 预览
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Excel 预览 -->
|
||||
<div v-else-if="fileType === 'excel'" class="preview-container ppt-container">
|
||||
<div class="ppt-notice">
|
||||
<FileOutlined style="font-size: 48px; color: #52c41a;" />
|
||||
<h3>Excel 文件预览</h3>
|
||||
<p>使用 WebOffice 在新页面中预览</p>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="openInWebOffice">
|
||||
<EyeOutlined /> WebOffice 预览
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
@ -95,17 +91,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<div class="preview-footer" v-if="fileType !== 'other'">
|
||||
<a-space>
|
||||
<a-button @click="downloadFile">
|
||||
<DownloadOutlined /> 下载
|
||||
</a-button>
|
||||
<a-button type="link" @click="openInNewTab">
|
||||
<ExportOutlined /> 新窗口打开
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
@ -121,11 +106,14 @@ import {
|
||||
ExportOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { openWebOffice } from '@/views/office/webOffice';
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
fileUrl: string;
|
||||
fileName: string;
|
||||
/** 可选,用于 WebOffice 预览时的文件标识 */
|
||||
fileId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -150,6 +138,8 @@ const fileType = computed(() => {
|
||||
|
||||
if (checkStr.endsWith('.pdf')) return 'pdf';
|
||||
if (checkStr.endsWith('.ppt') || checkStr.endsWith('.pptx')) return 'ppt';
|
||||
if (checkStr.match(/\.(doc|docx)$/)) return 'doc';
|
||||
if (checkStr.match(/\.(xls|xlsx)$/)) return 'excel';
|
||||
if (checkStr.match(/\.(mp3|wav|ogg|aac|flac|m4a)$/)) return 'audio';
|
||||
if (checkStr.match(/\.(mp4|webm|ogg|mov|avi|mkv)$/)) return 'video';
|
||||
if (checkStr.match(/\.(jpg|jpeg|png|gif|webp|bmp|svg)$/)) return 'image';
|
||||
@ -223,18 +213,39 @@ const openInNewTab = () => {
|
||||
window.open(previewUrl.value, '_blank');
|
||||
};
|
||||
|
||||
// 使用 Office Online 预览 PPT
|
||||
const openInOfficeOnline = () => {
|
||||
// 需要 Office Online 服务支持,这里使用完整的 URL
|
||||
// 是否支持 WebOffice 预览(PPT、Word、Excel)
|
||||
const isOfficeFile = computed(
|
||||
() => ['ppt', 'doc', 'excel'].includes(fileType.value)
|
||||
);
|
||||
|
||||
// 解析文件名得到 name 和 type
|
||||
const parseFileName = (name: string) => {
|
||||
const lastDot = name.lastIndexOf('.');
|
||||
if (lastDot === -1) return { name: name, type: '' };
|
||||
return {
|
||||
name: name.slice(0, lastDot),
|
||||
type: name.slice(lastDot + 1).toLowerCase(),
|
||||
};
|
||||
};
|
||||
|
||||
// 使用 WebOffice 在新页面预览(阿里云 IMM)
|
||||
const openInWebOffice = () => {
|
||||
const fullUrl = previewUrl.value;
|
||||
if (!fullUrl.startsWith('http')) {
|
||||
message.warning('PPT 在线预览需要完整的文件 URL');
|
||||
message.warning('WebOffice 预览需要完整的文件 URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用微软 Office Online 预览服务
|
||||
const officeUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(fullUrl)}`;
|
||||
window.open(officeUrl, '_blank');
|
||||
const { name, type } = parseFileName(props.fileName);
|
||||
if (!type) {
|
||||
message.warning('无法识别文件类型');
|
||||
return;
|
||||
}
|
||||
openWebOffice({
|
||||
id: props.fileId || '',
|
||||
url: fullUrl,
|
||||
name,
|
||||
type,
|
||||
});
|
||||
};
|
||||
|
||||
// 媒体加载错误处理
|
||||
|
||||
@ -2,6 +2,7 @@ import { createApp } from 'vue';
|
||||
import { createPinia } from 'pinia';
|
||||
import Antd from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import 'virtual:uno.css';
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
|
||||
|
||||
@ -455,6 +455,16 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/office/WebOffice',
|
||||
name: 'WebOffice',
|
||||
component: () => import('@/views/office/WebOffice.vue'),
|
||||
meta: { requiresAuth: true, title: '在线预览' },
|
||||
},
|
||||
{
|
||||
path: '/weboffice',
|
||||
redirect: '/office/WebOffice',
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
name: 'NotFound',
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div v-if="!expire" ref="containerRef" class="w-full h-full"></div>
|
||||
|
||||
<div v-if="!expire" ref="containerRef" class="!w-full !h-full pos-fixed top-0 left-0 z-999"></div>
|
||||
<div v-else class="flex justify-center">
|
||||
<div class="my-60px">
|
||||
链接已失效!<span class=" cursor-pointer color-#0085FF ml-10px" @click="home">返回首页</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <Modal ref="modalRef" class="max-w-80%" width="1340px" v-model:open="open" :footer="null" title="在线资源">
|
||||
|
||||
<div class="flex min-h-600px bg-#f5f5f5 flex-col ">
|
||||
@ -40,22 +40,16 @@
|
||||
<script lang="ts" name="WebOffice" setup>
|
||||
import { onMounted, ref, nextTick, reactive, watch, onUnmounted, onBeforeUnmount } from 'vue';
|
||||
|
||||
import request from '/@/apis/fetch';
|
||||
// import { Modal, Pagination } from 'ant-design-vue';
|
||||
import {
|
||||
generateWebofficeToken,
|
||||
generateWebofficeTokenReadOnly,
|
||||
refreshWebofficeToken,
|
||||
} from '@/api/imm.api';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
// import { usePermission } from '@/hooks/web/usePermission';
|
||||
// import { insertPPTImage, insertWordImage } from './webOffice';
|
||||
import { getTemItem, TemObj } from './temObjs';
|
||||
import { createImgPreview } from '/@/components/Preview/index';
|
||||
|
||||
const containerRef = ref(null);
|
||||
const containerRef = ref<HTMLElement | null>(null);
|
||||
const route = useRoute();
|
||||
const open = ref(false);
|
||||
const expire = ref(false);
|
||||
const router = useRouter();
|
||||
let updateSizeInterval: any;
|
||||
@ -64,15 +58,9 @@ onMounted(() => {
|
||||
nextTick(() => {
|
||||
init(containerRef.value);
|
||||
})
|
||||
updateSizeInterval = setInterval(() => {
|
||||
if (baseInstance.value) {
|
||||
updateSize();
|
||||
}
|
||||
}, 1000 * 60 * 5)
|
||||
});
|
||||
const _temObj = ref<TemObj>({
|
||||
id: '',
|
||||
type: '',
|
||||
isEdit: false,
|
||||
name: '',
|
||||
url: '',
|
||||
@ -82,12 +70,6 @@ const updateSize = async () => {
|
||||
if (!onUnmountedUpdateSize.value) {
|
||||
return;
|
||||
}
|
||||
if (!expire.value && _temObj.value.isEdit) {
|
||||
var formData = new FormData();
|
||||
formData.append('id', _temObj.value.id);
|
||||
formData.append('type', _temObj.value.type);
|
||||
navigator.sendBeacon('/activity/cms/cmsFilePublic/updateSize', formData);
|
||||
}
|
||||
}
|
||||
onBeforeUnmount(() => {
|
||||
updateSize();
|
||||
@ -113,7 +95,7 @@ function home() {
|
||||
router.replace("/datas");
|
||||
}
|
||||
const baseInstance = ref<any>(null);
|
||||
async function init(mount, timeout = 10 * 60 * 1000) {
|
||||
async function init(mount: HTMLElement | null) {
|
||||
if (!mount) {
|
||||
console.error('确保挂载节点元素存在。 一般在 onMounted 钩子中调用。');
|
||||
}
|
||||
@ -129,7 +111,7 @@ async function init(mount, timeout = 10 * 60 * 1000) {
|
||||
|
||||
_temObj.value = temObj;
|
||||
|
||||
const url = decodeURIComponent(`oss://lesingle-activity${new URL(decodeURIComponent(temObj.url)).pathname}`);
|
||||
const url = decodeURIComponent(`oss://lesingle-kid-course${new URL(decodeURIComponent(temObj.url)).pathname}`);
|
||||
let tokenInfo = await getTokenFun(url, temObj);
|
||||
const instance = (window as any).aliyun.config({
|
||||
mount,
|
||||
@ -141,7 +123,7 @@ async function init(mount, timeout = 10 * 60 * 1000) {
|
||||
Object.assign(tokenInfo, data);
|
||||
return {
|
||||
token: tokenInfo.accessToken,
|
||||
timeout,
|
||||
timeout: 10 * 60 * 1000,
|
||||
};
|
||||
});
|
||||
},
|
||||
@ -149,7 +131,7 @@ async function init(mount, timeout = 10 * 60 * 1000) {
|
||||
baseInstance.value = instance;
|
||||
instance.setToken({
|
||||
token: tokenInfo.accessToken,
|
||||
timeout,
|
||||
timeout: 10 * 60 * 1000,
|
||||
});
|
||||
await instance.ready();
|
||||
// const imgurl = 'http://image.activity.lesingle.com/activitymaterial/poster/%E5%8A%A8%E7%89%A9%E7%BB%98%E6%9C%AC_1736908271102.jpg';
|
||||
@ -207,36 +189,4 @@ async function refreshTokenFun(tokenInfo: any) {
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.activity {
|
||||
font-weight: bold;
|
||||
background: rgba(24, 144, 255, 0.08);
|
||||
color: #1890FF;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.cardView {
|
||||
margin-left: 12px;
|
||||
padding-left: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
&>div {
|
||||
margin-right: 12px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/*
|
||||
img {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: contain;
|
||||
}
|
||||
*/
|
||||
}
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@ -1,19 +1,18 @@
|
||||
|
||||
import { ref, watch, } from 'vue';
|
||||
import { ref, watch } from "vue";
|
||||
/**
|
||||
* 临时localStorage缓存,使用后立即删除,适合跨页面使用
|
||||
*/
|
||||
const TemKel = '_t'
|
||||
const TemKel = "_t";
|
||||
/**
|
||||
* 设置缓存超时
|
||||
*/
|
||||
const base_time = 1000 * 60 * 60 * 8;
|
||||
type Tem = {
|
||||
[key: string]: {
|
||||
time: number,
|
||||
val: TemObj
|
||||
}
|
||||
}
|
||||
time: number;
|
||||
val: TemObj;
|
||||
};
|
||||
};
|
||||
export function initilTemItem() {
|
||||
const tem: Tem = JSON.parse(localStorage.getItem(TemKel) || "{}");
|
||||
const _time = Date.now();
|
||||
@ -21,7 +20,7 @@ export function initilTemItem() {
|
||||
for (let key in tem) {
|
||||
const val = tem[key];
|
||||
if (_time - val.time < base_time) {
|
||||
_tem[key] = val
|
||||
_tem[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,10 +30,12 @@ export type TemObj = {
|
||||
id: string;
|
||||
url: string;
|
||||
name: string;
|
||||
type: string;
|
||||
isEdit: boolean;
|
||||
}
|
||||
export function setTemItem(val: TemObj, time: Number = Date.now() + base_time): string {
|
||||
};
|
||||
export function setTemItem(
|
||||
val: TemObj,
|
||||
time: Number = Date.now() + base_time,
|
||||
): string {
|
||||
const key = `_t${Date.now()}${Math.floor(Math.random() * 100000)}`;
|
||||
let Tem: Tem = {};
|
||||
try {
|
||||
@ -42,17 +43,17 @@ export function setTemItem(val: TemObj, time: Number = Date.now() + base_time):
|
||||
} catch (error) {}
|
||||
Tem[key] = {
|
||||
time: Date.now(),
|
||||
val: val
|
||||
}
|
||||
val: val,
|
||||
};
|
||||
localStorage.setItem(TemKel, JSON.stringify(Tem));
|
||||
|
||||
return key
|
||||
return key;
|
||||
}
|
||||
export function getTemItem(key: string): TemObj | null {
|
||||
let Tem: Tem = {};
|
||||
try {
|
||||
Tem = JSON.parse(localStorage.getItem(TemKel) || "{}");
|
||||
return Tem[key].val
|
||||
return Tem[key].val;
|
||||
} catch (error) {}
|
||||
return null;
|
||||
}
|
||||
@ -62,8 +63,11 @@ export function getTemItem(key: string): TemObj | null {
|
||||
*/
|
||||
export function getCacheVal(key: string, defaultVal: any) {
|
||||
const _val = ref(Number(localStorage.getItem(key) || defaultVal));
|
||||
watch(() => _val.value, () => {
|
||||
localStorage.setItem(key, `${_val.value}`)
|
||||
})
|
||||
return _val
|
||||
watch(
|
||||
() => _val.value,
|
||||
() => {
|
||||
localStorage.setItem(key, `${_val.value}`);
|
||||
},
|
||||
);
|
||||
return _val;
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
9
reading-platform-frontend/uno.config.ts
Normal file
9
reading-platform-frontend/uno.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from 'unocss';
|
||||
|
||||
export default defineConfig({
|
||||
shortcuts: {
|
||||
'flex-center': 'flex items-center justify-center',
|
||||
'flex-between': 'flex items-center justify-between',
|
||||
'flex-col-center': 'flex flex-col items-center justify-center',
|
||||
},
|
||||
});
|
||||
@ -6,10 +6,12 @@ import Components from 'unplugin-vue-components/vite';
|
||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
|
||||
import viteCompression from 'vite-plugin-compression';
|
||||
import fileRouter from 'unplugin-vue-router/vite';
|
||||
import UnoCSS from 'unocss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
UnoCSS(),
|
||||
fileRouter({
|
||||
routesFolder: 'src/views',
|
||||
extensions: ['.vue'],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user