wg-h5-preview-file.vue 9.5 KB

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