| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- <template>
- <view class="wrapper" v-if="webviewUrl || value">
- <view class="close" @click="close">
- <uni-icons v-if="!$slots.close" class="close-icon" type="close" size="40" style="color: #999999;"></uni-icons>
- </view>
- <view class="wg-h5-preview-file-container" :style="containerStyle">
- <view class="webview">
- <!-- #ifdef H5 -->
- <iframe
- class="wg-h5-preview-file-iframe"
- :webview-styles="webviewStyles"
- :src="iframeUrl" />
- <!-- #endif -->
- </view>
- </view>
- </view>
- </template>
- <script>
- const urlMap = {
- word: "/static/hybrid/word.html?file=",
- pdf: "/static/hybrid/pdf/web/viewer.html?file=",
- excel: "/static/hybrid/excel.html?file=",
- txt: "/static/hybrid/txt.html?file=",
- pptx: "/static/hybrid/pptx.html?file=",
- };
- export default {
- name: "WgH5PreviewFile",
- props: {
- value: {
- type: String,
- required: true,
- },
- type: {
- type: String,
- required: true,
- validator(v) {
- return ["excel", "pdf", "word", "txt", "pptx"].includes(v);
- },
- },
- },
- computed: {
- iframeUrl() {
- return (
- this.webviewUrl || this.buildPreviewUrl(this.type, this.value)
- );
- },
- containerStyle() {
- // 动态设置容器的实际可视高度,避免 100vh 在移动端地址栏收起/展开导致的遮挡问题
- const style = {
- height:
- this.containerHeight > 0
- ? `${this.containerHeight}px`
- : "100vh",
- };
- // 当不支持 CSS env/constant(safe-area-inset-bottom) 时,用内联 padding 兜底
- if (!this.cssSafeAreaSupported && this.safeBottom > 0) {
- style.paddingBottom = `${this.safeBottom}px`;
- }
- return style;
- },
- },
- data() {
- return {
- safeBottom: 0,
- webviewUrl: null,
- webviewStyles: {
- zIndex: "9999",
- },
- // 实际可视区域高度(px)
- containerHeight: 0,
- // 是否支持 CSS 安全区变量
- cssSafeAreaSupported: false,
- };
- },
- methods: {
- preview({ url, type }) {
- this.webviewUrl = this.buildPreviewUrl(type, url);
- },
- buildPreviewUrl(fileType, url = "") {
- const relativePath = urlMap[fileType] || urlMap.word;
- const sanitizedPath = relativePath.replace(/^\//, "");
- const basePath = this.getPreviewBasePath();
- return `${basePath}${sanitizedPath}${encodeURIComponent(
- url || this.value
- )}&safeBottom=${this.safeBottom}`;
- },
- getPreviewBasePath() {
- if (typeof window !== "undefined") {
- const configBase =
- (window.__uniConfig &&
- window.__uniConfig.router &&
- window.__uniConfig.router.base) ||
- (window.__uniConfig && window.__uniConfig.publicPath);
- if (configBase && configBase !== "./" && configBase !== ".") {
- return configBase.endsWith("/")
- ? configBase
- : `${configBase}/`;
- }
- const pathname = window.location.pathname || "/";
- if (pathname === "/") {
- return "/";
- }
- if (pathname.endsWith("/")) {
- return pathname;
- }
- const lastSlashIndex = pathname.lastIndexOf("/");
- const lastSegment = pathname.slice(lastSlashIndex + 1);
- const looksLikeFile = lastSegment.includes(".");
- if (looksLikeFile) {
- return pathname.replace(/\/[^/]*$/, "/");
- }
- return `${pathname}/`;
- }
- return "/";
- },
- updateViewportHeight() {
- if (typeof window === "undefined") {
- this.containerHeight = 0;
- return;
- }
- let height = 0;
- if (
- window.visualViewport &&
- typeof window.visualViewport.height === "number"
- ) {
- height = Math.round(window.visualViewport.height);
- } else {
- height = window.innerHeight;
- }
- // 计算安全区底部像素,用于 iframe 参数传递与不支持 env 的兜底
- this.safeBottom = this.getSafeAreaInsetBottomPx();
- this.containerHeight = height;
- },
- addViewportListeners() {
- if (typeof window === "undefined") return;
- // 绑定一次,便于移除
- this._boundUpdateViewportHeight =
- this._boundUpdateViewportHeight ||
- this.updateViewportHeight.bind(this);
- window.addEventListener("resize", this._boundUpdateViewportHeight, {
- passive: true,
- });
- window.addEventListener(
- "orientationchange",
- this._boundUpdateViewportHeight,
- { passive: true }
- );
- if (window.visualViewport) {
- window.visualViewport.addEventListener(
- "resize",
- this._boundUpdateViewportHeight,
- { passive: true }
- );
- }
- },
- removeViewportListeners() {
- if (
- typeof window === "undefined" ||
- !this._boundUpdateViewportHeight
- )
- return;
- window.removeEventListener(
- "resize",
- this._boundUpdateViewportHeight
- );
- window.removeEventListener(
- "orientationchange",
- this._boundUpdateViewportHeight
- );
- if (window.visualViewport) {
- window.visualViewport.removeEventListener(
- "resize",
- this._boundUpdateViewportHeight
- );
- }
- },
- checkCssSafeAreaSupport() {
- if (typeof window === "undefined" || !window.CSS || !CSS.supports)
- return false;
- // 支持 env 或老版本 constant
- return (
- CSS.supports("padding-bottom: env(safe-area-inset-bottom)") ||
- CSS.supports("padding-bottom: constant(safe-area-inset-bottom)")
- );
- },
- getSafeAreaInsetBottomPx() {
- if (typeof window === "undefined" || !document || !document.body)
- return 0;
- let value = 0;
- try {
- const probe = document.createElement("div");
- probe.style.position = "absolute";
- probe.style.visibility = "hidden";
- probe.style.pointerEvents = "none";
- probe.style.paddingBottom = "env(safe-area-inset-bottom)";
- document.body.appendChild(probe);
- const v1 = parseFloat(
- window.getComputedStyle(probe).paddingBottom || "0"
- );
- document.body.removeChild(probe);
- if (v1 > 0) return Math.round(v1);
- // 尝试旧语法 constant()
- const probe2 = document.createElement("div");
- probe2.style.position = "absolute";
- probe2.style.visibility = "hidden";
- probe2.style.pointerEvents = "none";
- probe2.style.paddingBottom = "constant(safe-area-inset-bottom)";
- document.body.appendChild(probe2);
- const v2 = parseFloat(
- window.getComputedStyle(probe2).paddingBottom || "0"
- );
- document.body.removeChild(probe2);
- value = v2 > 0 ? Math.round(v2) : 0;
- } catch (e) {
- value = 0;
- }
- return value;
- },
- close() {
- this.webviewUrl = null;
- this.$emit('CloseDialog');
- // uni.navigateBack();
- },
- },
- mounted() {
- this.safeBottom = 0;
- this.cssSafeAreaSupported = this.checkCssSafeAreaSupport();
- this.updateViewportHeight();
- this.addViewportListeners();
- },
- beforeDestroy() {
- this.removeViewportListeners();
- },
- };
- </script>
- <style lang="scss">
- .wrapper {
- position: relative;
- .close {
- position: fixed;
- top: 44px;
- right: 20px;
- z-index: 9999;
- }
- .close-icon {
- width: 46px;
- height: 46px;
- }
- }
- iframe {
- z-index: 9999 !important;
- top: 0 !important;
- }
- .wg-h5-preview-file-iframe {
- width: 100vw;
- height: 100% !important;
- }
- .wg-h5-preview-file-container {
- position: fixed;
- left: 0px;
- bottom: 0;
- overflow: hidden;
- z-index: 998;
- width: 100vw;
- height: 100%;
- // iOS 刘海屏底部安全区
- padding-bottom: constant(safe-area-inset-bottom);
- padding-bottom: env(safe-area-inset-bottom);
- background-color: #fff;
- }
- .webview {
- height: 100% !important;
- }
- </style>
|