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. console.log(H5_BASE_URL)
  61. h5Url.value = `${H5_BASE_URL}/#${normalizedPath}${
  62. queryString ? `?${queryString}` : ""
  63. }`;
  64. console.log("WebView 加载地址:", h5Url.value);
  65. } catch (err) {
  66. console.error("WebView 初始化失败:", err);
  67. uni.showToast({ title: "页面加载异常", icon: "none", duration: 1500 });
  68. setTimeout(() => uni.navigateBack(), 1500);
  69. }
  70. });
  71. // 标准化H5路径
  72. const normalizeH5Path = (path) => {
  73. if (typeof path !== "string") {
  74. console.warn("H5路径格式错误:非字符串类型");
  75. return null;
  76. }
  77. // URL解码
  78. let decodedPath;
  79. try {
  80. decodedPath = decodeURIComponent(path);
  81. } catch (err) {
  82. console.error("H5路径解码失败(非法编码格式):", err, "原始路径:", path);
  83. return null;
  84. }
  85. const purePath = decodedPath.split(/[?#]/)[0];
  86. const pathReg = /^\/[a-zA-Z0-9\/:\.\-_\u4e00-\u9fa5]*$/;
  87. const isValid = pathReg.test(purePath);
  88. if (!isValid) {
  89. console.warn("H5路径格式非法(含不允许字符):", purePath);
  90. return null;
  91. }
  92. // 第五步:最终处理(去除末尾多余的/,避免重复路径如/path// → /path)
  93. const normalizedPath = purePath.replace(/\/+$/, "") || "/";
  94. return normalizedPath;
  95. };
  96. /**
  97. * 解析复杂参数
  98. * @param {object} params - 原始参数
  99. * @returns {object} 处理后的参数
  100. */
  101. const parseComplexParams = (params) => {
  102. const result = {};
  103. Object.entries(params).forEach(([key, value]) => {
  104. if (value === undefined || value === null) return;
  105. if (Array.isArray(value)) {
  106. value.forEach((item) => {
  107. const encodedItem = encodeURIComponent(item);
  108. if (result[key]) {
  109. result[key] = `${result[key]},${encodedItem}`;
  110. } else {
  111. result[key] = encodedItem;
  112. }
  113. });
  114. } else if (typeof value === "object") {
  115. result[key] = encodeURIComponent(JSON.stringify(value));
  116. }
  117. // 基础类型直接保留
  118. else {
  119. result[key] = value;
  120. }
  121. });
  122. return result;
  123. };
  124. /**
  125. * 构建编码后的查询参数串
  126. * @param {object} params - 处理后的参数
  127. * @returns {string} 编码后的查询串
  128. */
  129. const buildQueryString = (params) => {
  130. return Object.entries(params)
  131. .filter(
  132. ([_, value]) => value !== undefined && value !== null && value !== ""
  133. )
  134. .map(
  135. ([key, value]) =>
  136. `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
  137. )
  138. .join("&");
  139. };
  140. /**
  141. * web-view 加载成功
  142. */
  143. const onWebViewLoad = () => {
  144. isLoading.value = false; // 隐藏加载中
  145. };
  146. /**
  147. * web-view 加载失败
  148. * @param {object} err - 错误信息
  149. */
  150. const onWebViewError = (err) => {
  151. isLoading.value = false;
  152. errorMsg.value = `页面加载失败:${err.detail.errMsg}`;
  153. uni.showToast({ title: errorMsg.value, icon: "none", duration: 2000 });
  154. console.error("WebView 加载错误:", err);
  155. setTimeout(() => uni.navigateBack(), 1500);
  156. };
  157. /**
  158. * 接收 H5 发送的消息
  159. * @param {object} e - 消息事件
  160. */
  161. const onWebViewMessage = (e) => {
  162. const h5Msg = e.detail.data[0]; // H5 发送的消息格式为 { data: [消息体] }
  163. console.log("接收 H5 消息:", h5Msg);
  164. // 示例:H5 触发「返回小程序」
  165. if (h5Msg.type === "navigateBack") {
  166. uni.navigateBack({ delta: h5Msg.delta || 1 });
  167. }
  168. if (h5Msg.type === "refreshToken") {
  169. appStore.refreshToken().then((newToken) => {
  170. const webview = uni.createSelectorQuery().select("#any-id");
  171. webview
  172. .context((res) => {
  173. res.context.postMessage({
  174. data: { type: "newToken", token: newToken },
  175. });
  176. })
  177. .exec();
  178. });
  179. }
  180. };
  181. </script>
  182. <style scoped>
  183. .webview-wrapper {
  184. width: 100vw;
  185. height: 100vh;
  186. position: relative;
  187. }
  188. .loading-mask {
  189. position: absolute;
  190. top: 0;
  191. left: 0;
  192. width: 100%;
  193. height: 100%;
  194. background: rgba(255, 255, 255, 0.8);
  195. display: flex;
  196. flex-direction: column;
  197. justify-content: center;
  198. align-items: center;
  199. z-index: 999;
  200. }
  201. .loading-txt {
  202. margin-top: 16rpx;
  203. font-size: 28rpx;
  204. color: #666;
  205. }
  206. </style>