wg-h5-preview-file.vue 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. <template>
  2. <view class="wrapper" v-if="webviewUrl || value">
  3. <view class="close" @click="close">
  4. <uni-icons v-if="!$slots.close" class="close-icon" type="close" size="40" style="color: #999999;"></uni-icons>
  5. </view>
  6. <view class="wg-h5-preview-file-container" :style="containerStyle">
  7. <view class="webview">
  8. <!-- #ifdef H5 -->
  9. <iframe
  10. class="wg-h5-preview-file-iframe"
  11. :webview-styles="webviewStyles"
  12. :src="iframeUrl" />
  13. <!-- #endif -->
  14. </view>
  15. </view>
  16. </view>
  17. </template>
  18. <script>
  19. const urlMap = {
  20. word: "/static/hybrid/word.html?file=",
  21. pdf: "/static/hybrid/pdf/web/viewer.html?file=",
  22. excel: "/static/hybrid/excel.html?file=",
  23. txt: "/static/hybrid/txt.html?file=",
  24. pptx: "/static/hybrid/pptx.html?file=",
  25. };
  26. export default {
  27. name: "WgH5PreviewFile",
  28. // props: {
  29. // value: {
  30. // type: String,
  31. // required: true,
  32. // },
  33. // type: {
  34. // type: String,
  35. // required: true,
  36. // validator(v) {
  37. // return ["excel", "pdf", "word", "txt", "pptx"].includes(v);
  38. // },
  39. // },
  40. // },
  41. computed: {
  42. iframeUrl() {
  43. return (
  44. this.webviewUrl || this.buildPreviewUrl(this.type, this.value)
  45. );
  46. },
  47. containerStyle() {
  48. // 动态设置容器的实际可视高度,避免 100vh 在移动端地址栏收起/展开导致的遮挡问题
  49. const style = {
  50. height:
  51. this.containerHeight > 0
  52. ? `${this.containerHeight}px`
  53. : "100vh",
  54. };
  55. // 当不支持 CSS env/constant(safe-area-inset-bottom) 时,用内联 padding 兜底
  56. if (!this.cssSafeAreaSupported && this.safeBottom > 0) {
  57. style.paddingBottom = `${this.safeBottom}px`;
  58. }
  59. return style;
  60. },
  61. },
  62. data() {
  63. return {
  64. safeBottom: 0,
  65. webviewUrl: null,
  66. webviewStyles: {
  67. zIndex: "9999",
  68. },
  69. // 实际可视区域高度(px)
  70. containerHeight: 0,
  71. // 是否支持 CSS 安全区变量
  72. cssSafeAreaSupported: false,
  73. value:'',
  74. type:''
  75. };
  76. },
  77. methods: {
  78. preview({ url, type }) {
  79. this.value = url;
  80. this.type = type;
  81. this.webviewUrl = this.buildPreviewUrl(type, url);
  82. },
  83. buildPreviewUrl(fileType, url = "") {
  84. const relativePath = urlMap[fileType] || urlMap.word;
  85. const sanitizedPath = relativePath.replace(/^\//, "");
  86. const basePath = this.getPreviewBasePath();
  87. return `${basePath}${sanitizedPath}${encodeURIComponent(
  88. url || this.value
  89. )}&safeBottom=${this.safeBottom}`;
  90. },
  91. getPreviewBasePath() {
  92. if (typeof window !== "undefined") {
  93. const configBase =
  94. (window.__uniConfig &&
  95. window.__uniConfig.router &&
  96. window.__uniConfig.router.base) ||
  97. (window.__uniConfig && window.__uniConfig.publicPath);
  98. if (configBase && configBase !== "./" && configBase !== ".") {
  99. return configBase.endsWith("/")
  100. ? configBase
  101. : `${configBase}/`;
  102. }
  103. const pathname = window.location.pathname || "/";
  104. if (pathname === "/") {
  105. return "/";
  106. }
  107. if (pathname.endsWith("/")) {
  108. return pathname;
  109. }
  110. const lastSlashIndex = pathname.lastIndexOf("/");
  111. const lastSegment = pathname.slice(lastSlashIndex + 1);
  112. const looksLikeFile = lastSegment.includes(".");
  113. if (looksLikeFile) {
  114. return pathname.replace(/\/[^/]*$/, "/");
  115. }
  116. return `${pathname}/`;
  117. }
  118. return "/";
  119. },
  120. updateViewportHeight() {
  121. if (typeof window === "undefined") {
  122. this.containerHeight = 0;
  123. return;
  124. }
  125. let height = 0;
  126. if (
  127. window.visualViewport &&
  128. typeof window.visualViewport.height === "number"
  129. ) {
  130. height = Math.round(window.visualViewport.height);
  131. } else {
  132. height = window.innerHeight;
  133. }
  134. // 计算安全区底部像素,用于 iframe 参数传递与不支持 env 的兜底
  135. this.safeBottom = this.getSafeAreaInsetBottomPx();
  136. this.containerHeight = height;
  137. },
  138. addViewportListeners() {
  139. if (typeof window === "undefined") return;
  140. // 绑定一次,便于移除
  141. this._boundUpdateViewportHeight =
  142. this._boundUpdateViewportHeight ||
  143. this.updateViewportHeight.bind(this);
  144. window.addEventListener("resize", this._boundUpdateViewportHeight, {
  145. passive: true,
  146. });
  147. window.addEventListener(
  148. "orientationchange",
  149. this._boundUpdateViewportHeight,
  150. { passive: true }
  151. );
  152. if (window.visualViewport) {
  153. window.visualViewport.addEventListener(
  154. "resize",
  155. this._boundUpdateViewportHeight,
  156. { passive: true }
  157. );
  158. }
  159. },
  160. removeViewportListeners() {
  161. if (
  162. typeof window === "undefined" ||
  163. !this._boundUpdateViewportHeight
  164. )
  165. return;
  166. window.removeEventListener(
  167. "resize",
  168. this._boundUpdateViewportHeight
  169. );
  170. window.removeEventListener(
  171. "orientationchange",
  172. this._boundUpdateViewportHeight
  173. );
  174. if (window.visualViewport) {
  175. window.visualViewport.removeEventListener(
  176. "resize",
  177. this._boundUpdateViewportHeight
  178. );
  179. }
  180. },
  181. checkCssSafeAreaSupport() {
  182. if (typeof window === "undefined" || !window.CSS || !CSS.supports)
  183. return false;
  184. // 支持 env 或老版本 constant
  185. return (
  186. CSS.supports("padding-bottom: env(safe-area-inset-bottom)") ||
  187. CSS.supports("padding-bottom: constant(safe-area-inset-bottom)")
  188. );
  189. },
  190. getSafeAreaInsetBottomPx() {
  191. if (typeof window === "undefined" || !document || !document.body)
  192. return 0;
  193. let value = 0;
  194. try {
  195. const probe = document.createElement("div");
  196. probe.style.position = "absolute";
  197. probe.style.visibility = "hidden";
  198. probe.style.pointerEvents = "none";
  199. probe.style.paddingBottom = "env(safe-area-inset-bottom)";
  200. document.body.appendChild(probe);
  201. const v1 = parseFloat(
  202. window.getComputedStyle(probe).paddingBottom || "0"
  203. );
  204. document.body.removeChild(probe);
  205. if (v1 > 0) return Math.round(v1);
  206. // 尝试旧语法 constant()
  207. const probe2 = document.createElement("div");
  208. probe2.style.position = "absolute";
  209. probe2.style.visibility = "hidden";
  210. probe2.style.pointerEvents = "none";
  211. probe2.style.paddingBottom = "constant(safe-area-inset-bottom)";
  212. document.body.appendChild(probe2);
  213. const v2 = parseFloat(
  214. window.getComputedStyle(probe2).paddingBottom || "0"
  215. );
  216. document.body.removeChild(probe2);
  217. value = v2 > 0 ? Math.round(v2) : 0;
  218. } catch (e) {
  219. value = 0;
  220. }
  221. return value;
  222. },
  223. close() {
  224. this.webviewUrl = null;
  225. this.value = null;
  226. // uni.navigateBack();
  227. },
  228. },
  229. mounted() {
  230. this.safeBottom = 0;
  231. this.cssSafeAreaSupported = this.checkCssSafeAreaSupport();
  232. this.updateViewportHeight();
  233. this.addViewportListeners();
  234. },
  235. beforeDestroy() {
  236. this.removeViewportListeners();
  237. },
  238. };
  239. </script>
  240. <style lang="scss">
  241. .wrapper {
  242. position: relative;
  243. .close {
  244. position: fixed;
  245. top: 44px;
  246. right: 20px;
  247. z-index: 9999;
  248. }
  249. .close-icon {
  250. width: 46px;
  251. height: 46px;
  252. }
  253. }
  254. iframe {
  255. z-index: 9999 !important;
  256. top: 0 !important;
  257. }
  258. .wg-h5-preview-file-iframe {
  259. width: 100vw;
  260. height: 100% !important;
  261. border: 0;
  262. }
  263. .wg-h5-preview-file-container {
  264. position: fixed;
  265. left: 0px;
  266. bottom: 0;
  267. overflow: hidden;
  268. z-index: 998;
  269. width: 100vw;
  270. height: 100%;
  271. // iOS 刘海屏底部安全区
  272. padding-bottom: constant(safe-area-inset-bottom);
  273. padding-bottom: env(safe-area-inset-bottom);
  274. background-color: #fff;
  275. }
  276. .webview {
  277. height: 100% !important;
  278. }
  279. </style>