index.vue 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <template>
  2. <view class="webview-wrapper">
  3. <!-- 加载状态提示 -->
  4. <view class="loading-mask" v-if="isLoading">
  5. <uni-loading-icon type="circle" size="24" color="#333"></uni-loading-icon>
  6. <text class="loading-txt">加载中...</text>
  7. </view>
  8. <web-view
  9. :src="h5Url"
  10. id="any-id"
  11. @load="onWebViewLoad"
  12. @error="onWebViewError"
  13. @message="onWebViewMessage"
  14. ></web-view>
  15. </view>
  16. </template>
  17. <script setup>
  18. import { ref, onUnmounted } from "vue";
  19. import { onLoad } from "@dcloudio/uni-app";
  20. import { H5_BASE_URL, TOKENNAME, WHITELIST } from "@/config/app";
  21. import { useAppStore } from "@/stores/app";
  22. import { toLogin, checkLogin } from "@/libs/login";
  23. const h5Url = ref("");
  24. const appStore = useAppStore();
  25. const isLoading = ref(true);
  26. const errorMsg = ref("");
  27. onUnmounted(() => {
  28. isLoading.value = false;
  29. });
  30. onLoad((query) => {
  31. try {
  32. const { path = "/", ...otherParams } = query;
  33. let normalizedPath = normalizeH5Path(path);
  34. const token = appStore.tokenComputed;
  35. if (!token && !WHITELIST.includes(normalizedPath)) {
  36. uni.showToast({
  37. title: "登录已失效,请重新登录",
  38. icon: "none",
  39. duration: 1500,
  40. });
  41. setTimeout(() => {
  42. toLogin();
  43. }, 1500);
  44. return;
  45. }
  46. if (!normalizedPath) {
  47. uni.showToast({
  48. title: "页面路径无效,默认跳转首页",
  49. icon: "none",
  50. duration: 1500,
  51. });
  52. normalizedPath = "/";
  53. }
  54. // 参数处理
  55. const params = {
  56. ...parseComplexParams(otherParams),
  57. [TOKENNAME]: token,
  58. };
  59. const queryString = buildQueryString(params);
  60. h5Url.value = `${H5_BASE_URL}/#${normalizedPath}${
  61. queryString ? `?${queryString}` : ""
  62. }`;
  63. console.log("WebView 加载地址:", h5Url.value);
  64. } catch (err) {
  65. console.error("WebView 初始化失败:", err);
  66. uni.showToast({ title: "页面加载异常", icon: "none", duration: 1500 });
  67. setTimeout(() => uni.navigateBack(), 1500);
  68. }
  69. });
  70. // 标准化H5路径
  71. const normalizeH5Path = (path) => {
  72. if (typeof path !== "string") {
  73. console.warn("H5路径格式错误:非字符串类型");
  74. return null;
  75. }
  76. // URL解码
  77. let decodedPath;
  78. try {
  79. decodedPath = decodeURIComponent(path);
  80. } catch (err) {
  81. console.error("H5路径解码失败(非法编码格式):", err, "原始路径:", path);
  82. return null;
  83. }
  84. const purePath = decodedPath.split(/[?#]/)[0];
  85. const pathReg = /^\/[a-zA-Z0-9\/:\.\-_\u4e00-\u9fa5]*$/;
  86. const isValid = pathReg.test(purePath);
  87. if (!isValid) {
  88. console.warn("H5路径格式非法(含不允许字符):", purePath);
  89. return null;
  90. }
  91. // 第五步:最终处理(去除末尾多余的/,避免重复路径如/path// → /path)
  92. const normalizedPath = purePath.replace(/\/+$/, "") || "/";
  93. return normalizedPath;
  94. };
  95. /**
  96. * 解析复杂参数
  97. * @param {object} params - 原始参数
  98. * @returns {object} 处理后的参数
  99. */
  100. const parseComplexParams = (params) => {
  101. const result = {};
  102. Object.entries(params).forEach(([key, value]) => {
  103. if (value === undefined || value === null) return;
  104. if (Array.isArray(value)) {
  105. value.forEach((item) => {
  106. const encodedItem = encodeURIComponent(item);
  107. if (result[key]) {
  108. result[key] = `${result[key]},${encodedItem}`;
  109. } else {
  110. result[key] = encodedItem;
  111. }
  112. });
  113. } else if (typeof value === "object") {
  114. result[key] = encodeURIComponent(JSON.stringify(value));
  115. }
  116. // 基础类型直接保留
  117. else {
  118. result[key] = value;
  119. }
  120. });
  121. return result;
  122. };
  123. /**
  124. * 构建编码后的查询参数串
  125. * @param {object} params - 处理后的参数
  126. * @returns {string} 编码后的查询串
  127. */
  128. const buildQueryString = (params) => {
  129. return Object.entries(params)
  130. .filter(
  131. ([_, value]) => value !== undefined && value !== null && value !== ""
  132. )
  133. .map(
  134. ([key, value]) =>
  135. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  136. )
  137. .join("&");
  138. };
  139. /**
  140. * web-view 加载成功
  141. */
  142. const onWebViewLoad = () => {
  143. isLoading.value = false; // 隐藏加载中
  144. };
  145. /**
  146. * web-view 加载失败
  147. * @param {object} err - 错误信息
  148. */
  149. const onWebViewError = (err) => {
  150. isLoading.value = false;
  151. errorMsg.value = `页面加载失败:${err.detail.errMsg}`;
  152. uni.showToast({ title: errorMsg.value, icon: "none", duration: 2000 });
  153. console.error("WebView 加载错误:", err);
  154. setTimeout(() => uni.navigateBack(), 1500);
  155. };
  156. /**
  157. * 接收 H5 发送的消息
  158. * @param {object} e - 消息事件
  159. */
  160. const onWebViewMessage = (e) => {
  161. const h5Msg = e.detail.data[0]; // H5 发送的消息格式为 { data: [消息体] }
  162. console.log("接收 H5 消息:", h5Msg);
  163. // 示例:H5 触发「返回小程序」
  164. if (h5Msg.type === "navigateBack") {
  165. uni.navigateBack({ delta: h5Msg.delta || 1 });
  166. }
  167. if (h5Msg.type === "refreshToken") {
  168. appStore.refreshToken().then((newToken) => {
  169. const webview = uni.createSelectorQuery().select("#any-id");
  170. webview
  171. .context((res) => {
  172. res.context.postMessage({
  173. data: { type: "newToken", token: newToken },
  174. });
  175. })
  176. .exec();
  177. });
  178. }
  179. };
  180. </script>
  181. <style scoped>
  182. .webview-wrapper {
  183. width: 100vw;
  184. height: 100vh;
  185. position: relative;
  186. }
  187. .loading-mask {
  188. position: absolute;
  189. top: 0;
  190. left: 0;
  191. width: 100%;
  192. height: 100%;
  193. background: rgba(255, 255, 255, 0.8);
  194. display: flex;
  195. flex-direction: column;
  196. justify-content: center;
  197. align-items: center;
  198. z-index: 999;
  199. }
  200. .loading-txt {
  201. margin-top: 16rpx;
  202. font-size: 28rpx;
  203. color: #666;
  204. }
  205. </style>