492 lines
18 KiB
Vue
492 lines
18 KiB
Vue
<template>
|
||
<view class="file-preview-container" :style="themeColor">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 微信小程序:web-view 是原生组件,必须用 cover-view 盖在上面;cover-view 不支持 iconfont,改用 cover-image PNG -->
|
||
<cover-view class="mp-header">
|
||
<cover-view class="mp-header-inner">
|
||
<cover-view class="nav-btn back-btn" @click="goBack">
|
||
<cover-image src="/static/img/icon-back.png" class="nav-icon"></cover-image>
|
||
</cover-view>
|
||
<cover-view class="header-title">{{ headerTitle }}</cover-view>
|
||
<button class="nav-btn share-btn" open-type="share" hover-class="none">
|
||
<cover-image src="/static/img/icon-share.png" class="nav-icon"></cover-image>
|
||
</button>
|
||
</cover-view>
|
||
<!-- 状态栏占位(navigationStyle custom 时,避免被时间/电量遮挡) -->
|
||
<cover-view class="mp-status-bar" :style="{ height: statusBarHeight + 'px' }"></cover-view>
|
||
</cover-view>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifndef MP-WEIXIN -->
|
||
<!-- 非小程序:普通 view + iconfont 即可 -->
|
||
<view class="header">
|
||
<view class="nav-btn back-btn" @click="goBack">
|
||
<text class="iconfont icon-back"></text>
|
||
</view>
|
||
<view class="header-title">{{ headerTitle }}</view>
|
||
<button class="nav-btn share-btn" open-type="share" hover-class="none" @click.stop>
|
||
<text class="iconfont icon-fenxiang"></text>
|
||
</button>
|
||
</view>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<!-- 微信小程序 web-view 是全屏原生组件,父容器 padding/margin 对它无效,必须在它前面放一个等高的占位 view 把它顶下去 -->
|
||
<view class="mp-content-spacer" :style="{ height: headerTotalHeightPx + 'px' }"></view>
|
||
<!-- #endif -->
|
||
|
||
<view class="content">
|
||
<!-- PDF 文件预览 -->
|
||
<view v-if="fileType === 'pdf'" class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<!-- H5 端:web-view 渲染为 iframe,若被 X-Frame-Options/CORS 拦截则显示空白;额外提供"新标签页打开"与"下载"按钮兜底 -->
|
||
<view class="h5-action-bar">
|
||
<view class="h5-action-tip">如页面空白,请使用下方按钮:</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="openInNewTab">新标签页打开</button>
|
||
<button class="h5-action-btn" @click="triggerDownload">下载文件</button>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- Word 文件预览 -->
|
||
<view v-else-if="fileType === 'word'" class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<view class="h5-action-bar">
|
||
<view class="h5-action-tip">浏览器可能无法直接内嵌预览 Word,建议下载后查看:</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="triggerDownload">下载文件</button>
|
||
<button class="h5-action-btn" @click="openInNewTab">新标签页打开</button>
|
||
</view>
|
||
</view>
|
||
<!-- #endif -->
|
||
<web-view v-if="fileUrl" :src="fileUrl"></web-view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- 视频文件预览 -->
|
||
<view v-else-if="fileType === 'video'" class="video-viewer">
|
||
<video v-if="fileUrl" id="filePreviewVideo" :src="fileUrl" controls class="video-player"></video>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.videoUrlNotExist') }}</view>
|
||
</view>
|
||
|
||
<!-- 其他文件类型 -->
|
||
<view v-else class="file-viewer">
|
||
<!-- #ifdef H5 -->
|
||
<view v-if="fileUrl" class="h5-action-bar">
|
||
<view class="h5-action-tip">{{ $lang('file-preview.file-preview.unsupportedFileType') }}</view>
|
||
<view class="h5-action-btns">
|
||
<button class="h5-action-btn primary" @click="triggerDownload">下载文件</button>
|
||
<button class="h5-action-btn" @click="openInNewTab">新标签页打开</button>
|
||
</view>
|
||
</view>
|
||
<view v-else class="error-message">{{ $lang('file-preview.file-preview.fileUrlNotExist') }}</view>
|
||
<!-- #endif -->
|
||
<!-- #ifndef H5 -->
|
||
<view class="error-message">{{ $lang('file-preview.file-preview.unsupportedFileType') }}</view>
|
||
<!-- #endif -->
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
export default {
|
||
data() {
|
||
return {
|
||
fileName: '',
|
||
fileUrl: '',
|
||
fileType: '',
|
||
statusBarHeight: 20,
|
||
windowWidth: 375
|
||
};
|
||
},
|
||
computed: {
|
||
// #ifdef MP-WEIXIN
|
||
// 整个自定义 header 高度(状态栏 + 标题栏 88rpx),单位 px —— 用于占位 view 和内容区下移
|
||
headerTotalHeightPx() {
|
||
const rpxToPx = this.windowWidth / 750;
|
||
return this.statusBarHeight + Math.round(88 * rpxToPx);
|
||
},
|
||
// #endif
|
||
// 页面标题栏显示的短标题(避免超长挤压分享按钮;对用户无意义的tmp名统一显示通用"文件预览")
|
||
headerTitle() {
|
||
const name = (this.fileName || '').trim();
|
||
// 如果是临时文件名(tmp_ 开头 或 带路径hash的长串),统一显示通用"文件预览"
|
||
if (/^tmp_/i.test(name) || (name.length > 30 && /[a-f0-9]{20,}/i.test(name))) {
|
||
return this.$lang('file-preview.file-preview.title');
|
||
}
|
||
// 否则显示原始文件名(列表里传入的,如"乐瑞固企业介绍",本身就短)
|
||
if (name) {
|
||
// JS 层保险截断(超过16字 + …),应对 cover-view CSS 省略号不生效
|
||
if (name.length > 16) return name.slice(0, 16) + '…';
|
||
return name;
|
||
}
|
||
return this.$lang('file-preview.file-preview.title');
|
||
},
|
||
// 分享的真实标题(好友收到的卡片显示真实文件名,不截断)
|
||
shareTitle() {
|
||
const name = (this.fileName || '').trim();
|
||
if (name && !/^tmp_/i.test(name)) return name;
|
||
return this.$lang('file-preview.file-preview.title');
|
||
},
|
||
// 分享路径:只带文件三要素,不挂推荐码/关系
|
||
sharePath() {
|
||
const query = [];
|
||
if (this.fileName) query.push(`fileName=${encodeURIComponent(this.fileName)}`);
|
||
if (this.fileUrl) query.push(`fileUrl=${encodeURIComponent(this.fileUrl)}`);
|
||
if (this.fileType) query.push(`fileType=${encodeURIComponent(this.fileType)}`);
|
||
return `/pages_tool/file-preview/file-preview${query.length ? '?' + query.join('&') : ''}`;
|
||
},
|
||
themeColor() {
|
||
if (this.$shop && this.$shop.themeColor) {
|
||
return `--theme-color: ${this.$shop.themeColor};`;
|
||
}
|
||
return '';
|
||
}
|
||
},
|
||
onLoad(option) {
|
||
// 解析传递的参数
|
||
if (option.fileName) {
|
||
this.fileName = decodeURIComponent(option.fileName);
|
||
}
|
||
if (option.fileUrl) {
|
||
this.fileUrl = decodeURIComponent(option.fileUrl);
|
||
}
|
||
if (option.fileType) {
|
||
this.fileType = decodeURIComponent(option.fileType);
|
||
} else {
|
||
this.inferFileType();
|
||
}
|
||
// #ifdef MP-WEIXIN
|
||
const sys = uni.getSystemInfoSync();
|
||
this.statusBarHeight = sys.statusBarHeight || 20;
|
||
this.windowWidth = sys.windowWidth || 375;
|
||
// #endif
|
||
},
|
||
onReady() {
|
||
// 视频加载后手动触发一次播放占位(去掉 autoplay 防自动播放被系统拦截)
|
||
// #ifndef H5
|
||
if (this.fileType === 'video' && this.fileUrl) {
|
||
try {
|
||
const ctx = uni.createVideoContext('filePreviewVideo', this);
|
||
if (ctx && typeof ctx.play === 'function') { /* 留给用户点击,不自动播 */ }
|
||
} catch (e) {}
|
||
}
|
||
// #endif
|
||
},
|
||
// #ifdef MP-WEIXIN
|
||
// 微信小程序:右上角胶囊"分享给朋友" / button[open-type=share] 触发
|
||
onShareAppMessage() {
|
||
return {
|
||
title: this.shareTitle,
|
||
path: this.sharePath
|
||
};
|
||
},
|
||
// 微信小程序:分享到朋友圈
|
||
onShareTimeline() {
|
||
return {
|
||
title: this.shareTitle,
|
||
query: this.sharePath.split('?')[1] || ''
|
||
};
|
||
},
|
||
// #endif
|
||
methods: {
|
||
inferFileType() {
|
||
if (this.fileUrl) {
|
||
const clean = String(this.fileUrl).split('?')[0];
|
||
const ext = clean.split('.').pop().toLowerCase();
|
||
if (['pdf'].includes(ext)) {
|
||
this.fileType = 'pdf';
|
||
} else if (['doc', 'docx'].includes(ext)) {
|
||
this.fileType = 'word';
|
||
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
|
||
this.fileType = 'video';
|
||
}
|
||
} else if (this.fileName) {
|
||
const ext = this.fileName.split('.').pop().toLowerCase();
|
||
if (['pdf'].includes(ext)) {
|
||
this.fileType = 'pdf';
|
||
} else if (['doc', 'docx'].includes(ext)) {
|
||
this.fileType = 'word';
|
||
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) {
|
||
this.fileType = 'video';
|
||
}
|
||
}
|
||
},
|
||
goBack() {
|
||
const pages = getCurrentPages();
|
||
if (pages && pages.length > 1) {
|
||
uni.navigateBack();
|
||
} else {
|
||
uni.switchTab({ url: '/pages/index/index' }).catch(() => {
|
||
uni.navigateBack();
|
||
});
|
||
}
|
||
},
|
||
// #ifdef H5
|
||
// H5 端:新标签页打开文件(绕过 iframe X-Frame-Options/CORS 限制)
|
||
openInNewTab() {
|
||
if (!this.fileUrl) return;
|
||
try {
|
||
window.open(this.fileUrl, '_blank', 'noopener,noreferrer');
|
||
} catch (e) {
|
||
uni.showToast({ title: '打开失败,请稍后重试', icon: 'none' });
|
||
}
|
||
},
|
||
// H5 端:通过 <a download> 触发浏览器下载(最稳定的跨浏览器方案)
|
||
triggerDownload() {
|
||
if (!this.fileUrl) return;
|
||
try {
|
||
const link = document.createElement('a');
|
||
link.href = this.fileUrl;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener noreferrer';
|
||
if (this.fileName) {
|
||
// 仅提示浏览器用这个名字;受 CORS 影响,跨域文件时 download 属性可能被忽略,
|
||
// 此时浏览器会直接按服务器返回的文件名/直接显示,但不会丢失文件本身
|
||
link.download = this.fileName;
|
||
}
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
} catch (e) {
|
||
// 兜底:新标签页打开
|
||
this.openInNewTab();
|
||
}
|
||
}
|
||
// #endif
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.file-preview-container {
|
||
min-height: 100vh;
|
||
background-color: #f5f5f5;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
/* =================== 非 MP 端 header =================== */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 100rpx;
|
||
padding: 0 30rpx;
|
||
background-color: #fff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
flex-shrink: 0;
|
||
z-index: 10;
|
||
|
||
.nav-btn {
|
||
flex-shrink: 0;
|
||
width: 60rpx;
|
||
height: 60rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
background: transparent;
|
||
line-height: 1;
|
||
border: none;
|
||
margin: 0;
|
||
&::after { border: none; }
|
||
}
|
||
|
||
.back-btn {
|
||
.icon-back {
|
||
font-size: 36rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
|
||
.header-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
text-align: center;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
margin: 0 20rpx;
|
||
}
|
||
|
||
.share-btn {
|
||
.icon-fenxiang {
|
||
font-size: 34rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* =================== MP 端 header (cover-view 浮层) =================== */
|
||
/* #ifdef MP-WEIXIN */
|
||
.mp-header {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 9999;
|
||
background-color: #ffffff;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.mp-status-bar {
|
||
width: 100%;
|
||
}
|
||
|
||
.mp-header-inner {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
height: 88rpx;
|
||
padding: 0 20rpx;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.nav-btn {
|
||
flex-shrink: 0;
|
||
width: 64rpx;
|
||
height: 64rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
background: transparent;
|
||
line-height: 1;
|
||
border: none;
|
||
margin: 0;
|
||
&::after { border: none; }
|
||
}
|
||
|
||
.nav-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
display: block;
|
||
}
|
||
|
||
.header-title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
text-align: center;
|
||
margin: 0 16rpx;
|
||
/* cover-view 下 ellipsis 支持有限,JS 层(headerTitle computed)已经做了截断,这里再冗余防守 */
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
text-overflow: ellipsis;
|
||
}
|
||
}
|
||
/* #endif */
|
||
|
||
/* =================== 内容区 =================== */
|
||
.content {
|
||
flex: 1;
|
||
/* #ifdef MP-WEIXIN */
|
||
/* 给浮层 header(statusBar+88rpx)留位置,避免内容顶上去 */
|
||
padding-top: calc(var(--mp-header-h, 120rpx));
|
||
/* #endif */
|
||
padding-left: 20rpx;
|
||
padding-right: 20rpx;
|
||
padding-bottom: 20rpx;
|
||
box-sizing: border-box;
|
||
|
||
.file-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
background-color: #fff;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.error-message {
|
||
width: 100%;
|
||
height: 80vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* #ifdef H5 */
|
||
/* H5 兜底操作栏(PDF/Word 被 X-Frame-Options 拦截时显示)*/
|
||
.h5-action-bar {
|
||
padding: 24rpx 28rpx 20rpx;
|
||
background: linear-gradient(180deg, #fffbe6 0%, #ffffff 100%);
|
||
border-bottom: 1rpx solid #f0e9c8;
|
||
|
||
.h5-action-tip {
|
||
font-size: 26rpx;
|
||
color: #8a6d1b;
|
||
margin-bottom: 18rpx;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.h5-action-btns {
|
||
display: flex;
|
||
gap: 20rpx;
|
||
|
||
.h5-action-btn {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
line-height: 72rpx;
|
||
padding: 0;
|
||
font-size: 28rpx;
|
||
border-radius: 36rpx;
|
||
background: #f2f3f5;
|
||
color: #333;
|
||
border: 1rpx solid #e2e4e8;
|
||
margin: 0;
|
||
|
||
&::after { border: none; }
|
||
|
||
&.primary {
|
||
background: linear-gradient(90deg, #ff7e47 0%, #f73131 100%);
|
||
color: #fff;
|
||
border-color: transparent;
|
||
font-weight: 600;
|
||
box-shadow: 0 4rpx 12rpx rgba(247, 49, 49, 0.25);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/* #endif */
|
||
}
|
||
|
||
.video-viewer {
|
||
width: 100%;
|
||
min-height: 80vh;
|
||
background-color: #000;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
.video-player {
|
||
width: 100%;
|
||
height: 80vh;
|
||
}
|
||
|
||
.error-message {
|
||
width: 100%;
|
||
height: 80vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
background-color: #000;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|