bridge.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. /**
  2. * H5与小程序通信桥接工具函数库
  3. * 提供统一的小程序原生功能调用接口
  4. */
  5. // 全局变量
  6. window.H5_PAGE_NAME = window.H5_PAGE_NAME || "default";
  7. /**
  8. * 设置当前页面名称
  9. * @param {string} pageName 页面名称
  10. */
  11. function setPageName(pageName) {
  12. window.H5_PAGE_NAME = pageName;
  13. }
  14. /**
  15. * 检查微信小程序环境
  16. * @returns {boolean} 是否在微信小程序环境
  17. */
  18. function checkEnvironment() {
  19. return typeof wx !== "undefined" && wx.miniProgram;
  20. }
  21. /**
  22. * 解析URL参数(兼容性处理)
  23. * @param {string} search URL查询字符串
  24. * @returns {Object} 参数对象
  25. */
  26. function parseUrlParams(search) {
  27. const params = {};
  28. if (search) {
  29. search
  30. .substring(1)
  31. .split("&")
  32. .forEach((param) => {
  33. const [key, value] = param.split("=");
  34. if (key && value) {
  35. params[decodeURIComponent(key)] = decodeURIComponent(value);
  36. }
  37. });
  38. }
  39. return params;
  40. }
  41. /**
  42. * 检查是否有返回的结果数据
  43. * @param {Function} onResult 结果处理回调
  44. */
  45. function checkResult(onResult) {
  46. const search = window.location.search;
  47. console.log("🔍 检查URL参数:", search);
  48. if (search) {
  49. const params = parseUrlParams(search);
  50. console.log("🔍 解析的参数:", params);
  51. if (params.result) {
  52. try {
  53. const resultData = JSON.parse(decodeURIComponent(params.result));
  54. console.log("🔍 解析的结果数据:", resultData);
  55. // 调用回调处理结果
  56. if (onResult) {
  57. onResult(resultData);
  58. }
  59. } catch (error) {
  60. console.error("解析结果数据失败:", error);
  61. }
  62. } else {
  63. console.log("🔍 URL中没有result参数");
  64. }
  65. } else {
  66. console.log("🔍 URL中没有查询参数");
  67. }
  68. }
  69. /**
  70. * 调用小程序原生功能
  71. * @param {string} action 操作类型
  72. * @param {string} message 消息描述
  73. * @param {Object} data 附加数据
  74. */
  75. function callNative(action, message = "", data = null) {
  76. if (!checkEnvironment()) {
  77. console.error("❌ 不在微信小程序环境");
  78. return;
  79. }
  80. const params = {
  81. action: action,
  82. message: message || "",
  83. source: window.H5_PAGE_NAME,
  84. };
  85. // 处理附加数据
  86. if (data && typeof data === "object") {
  87. try {
  88. const jsonString = JSON.stringify(data);
  89. params.data = encodeURIComponent(jsonString);
  90. console.log("📦 数据编码:", {
  91. original: data,
  92. json: jsonString,
  93. encoded: params.data,
  94. });
  95. } catch (error) {
  96. console.error("❌ JSON序列化失败:", error);
  97. return;
  98. }
  99. }
  100. // 构建URL参数
  101. const queryParts = [];
  102. Object.keys(params).forEach((key) => {
  103. if (params[key] !== undefined && params[key] !== null) {
  104. if (key === "data") {
  105. queryParts.push(`${key}=${params[key]}`);
  106. } else {
  107. queryParts.push(`${key}=${encodeURIComponent(params[key])}`);
  108. }
  109. }
  110. });
  111. const controllerUrl = `/pages/common/h5-controller?${queryParts.join("&")}`;
  112. console.log("🎛️ 跳转到控制器:", controllerUrl);
  113. // 调用微信小程序API,添加降级处理
  114. wx.miniProgram.navigateTo({
  115. url: controllerUrl,
  116. success: (res) => {
  117. console.log("✅ 成功跳转到控制器", res);
  118. },
  119. fail: (err) => {
  120. console.error("❌ 控制器调用失败,尝试降级方案:", err);
  121. // 降级方案:使用URL参数通信(兼容老版本webview)
  122. fallbackUrlCommunication(action, message, data);
  123. },
  124. });
  125. }
  126. /**
  127. * 降级通信方案:通过URL参数与webview通信
  128. * @param {string} action 操作类型
  129. * @param {string} message 消息描述
  130. * @param {Object} data 附加数据
  131. */
  132. function fallbackUrlCommunication(action, message = "", data = null) {
  133. console.log("🔄 使用降级通信方案");
  134. const params = {
  135. h5_action: action,
  136. h5_message: message,
  137. timestamp: Date.now(),
  138. };
  139. // 如果有额外数据,编码后添加
  140. if (data && typeof data === "object") {
  141. try {
  142. params.h5_data = encodeURIComponent(JSON.stringify(data));
  143. } catch (error) {
  144. console.error("❌ 数据编码失败:", error);
  145. }
  146. }
  147. // 保留原有的非指令参数
  148. const currentParams = parseUrlParams(window.location.search);
  149. Object.keys(currentParams).forEach((key) => {
  150. if (
  151. !key.startsWith("h5_") &&
  152. !key.startsWith("mp_") &&
  153. key !== "timestamp"
  154. ) {
  155. params[key] = currentParams[key];
  156. }
  157. });
  158. // 构建新URL
  159. const baseUrl = `${window.location.origin}${window.location.pathname}`;
  160. const queryParts = [];
  161. Object.keys(params).forEach((key) => {
  162. if (params[key] !== undefined && params[key] !== null) {
  163. queryParts.push(
  164. `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`
  165. );
  166. }
  167. });
  168. const url = `${baseUrl}?${queryParts.join("&")}`;
  169. console.log("🔄 降级跳转URL:", url);
  170. // 使用location.href触发webview的load事件
  171. window.location.href = url;
  172. }
  173. /**
  174. * 扫码
  175. * @param {string} message 消息描述
  176. * @returns {Promise} 调用结果
  177. */
  178. function scanCode(message = "扫描二维码") {
  179. return callNative("scanCode", message);
  180. }
  181. /**
  182. * 选择图片
  183. * @param {string} message 消息描述
  184. * @returns {Promise} 调用结果
  185. */
  186. function chooseImage(message = "选择图片") {
  187. return callNative("chooseImage", message);
  188. }
  189. /**
  190. * 拨打电话
  191. * @param {string} phoneNumber 电话号码
  192. * @param {string} message 消息描述
  193. * @returns {Promise} 调用结果
  194. */
  195. function makePhoneCall(phoneNumber, message = "拨打电话") {
  196. return callNative("makePhoneCall", message, { phoneNumber });
  197. }
  198. /**
  199. * 设置存储
  200. * @param {string} key 存储键
  201. * @param {*} value 存储值
  202. * @param {string} message 消息描述
  203. * @returns {Promise} 调用结果
  204. */
  205. function setStorage(key, value, message = "设置存储数据") {
  206. return callNative("setStorage", message, { key, value });
  207. }
  208. /**
  209. * 获取存储
  210. * @param {string} key 存储键
  211. * @param {string} message 消息描述
  212. * @returns {Promise} 调用结果
  213. */
  214. function getStorage(key, message = "获取存储数据") {
  215. return callNative("getStorage", message, { key });
  216. }
  217. /**
  218. * 返回小程序
  219. * @param {string} message 消息描述
  220. * @returns {Promise} 调用结果
  221. */
  222. function goBack(message = "返回小程序") {
  223. return callNative("goBack", message);
  224. }
  225. /**
  226. * 页面跳转
  227. * @param {string} page 目标页面
  228. * @param {Object} params 跳转参数
  229. */
  230. function navigateTo(page, params = {}) {
  231. const queryString =
  232. Object.keys(params).length > 0
  233. ? "?" +
  234. Object.keys(params)
  235. .map((key) => `${key}=${encodeURIComponent(params[key])}`)
  236. .join("&")
  237. : "";
  238. window.location.href = `./${page}.html${queryString}`;
  239. }
  240. /**
  241. * 保存数据到localStorage
  242. * @param {string} key 存储键
  243. * @param {*} data 存储数据
  244. */
  245. function saveData(key, data) {
  246. try {
  247. const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
  248. localStorage.setItem(storageKey, JSON.stringify(data));
  249. console.log("💾 数据已保存:", storageKey);
  250. } catch (error) {
  251. console.error("保存数据失败:", error);
  252. }
  253. }
  254. /**
  255. * 从localStorage加载数据
  256. * @param {string} key 存储键
  257. * @param {*} defaultValue 默认值
  258. * @returns {*} 加载的数据
  259. */
  260. function loadData(key, defaultValue = null) {
  261. try {
  262. const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
  263. const savedData = localStorage.getItem(storageKey);
  264. if (savedData) {
  265. return JSON.parse(savedData);
  266. }
  267. } catch (error) {
  268. console.error("加载数据失败:", error);
  269. }
  270. return defaultValue;
  271. }
  272. /**
  273. * 清除保存的数据
  274. * @param {string} key 存储键
  275. */
  276. function clearData(key) {
  277. try {
  278. const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
  279. localStorage.removeItem(storageKey);
  280. console.log("🗑️ 数据已清除:", storageKey);
  281. } catch (error) {
  282. console.error("清除数据失败:", error);
  283. }
  284. }
  285. /**
  286. * 默认的结果处理函数
  287. * @param {Object} data 结果数据
  288. */
  289. function defaultResultHandler(data) {
  290. const { action, success, result, error } = data;
  291. if (success) {
  292. console.log(`✅ ${action} 操作成功`, result);
  293. } else {
  294. console.error(`❌ ${action} 操作失败`, error);
  295. }
  296. }
  297. /**
  298. * 初始化页面(可选调用)
  299. * @param {string} pageName 页面名称
  300. * @param {Function} onResult 结果处理回调,不传则使用默认处理
  301. */
  302. function initPage(pageName, onResult) {
  303. setPageName(pageName);
  304. checkEnvironment();
  305. // 如果没有提供自定义处理,使用默认处理
  306. const resultHandler = onResult || defaultResultHandler;
  307. checkResult(resultHandler);
  308. }
  309. // ==================== UI工具函数 ====================
  310. /**
  311. * 显示Toast效果(H5页面UI)
  312. * @param {string} message Toast消息
  313. * @param {number} duration 显示时长(毫秒)
  314. * @param {string} type 类型:success|error|warning|info
  315. */
  316. function showToastEffect(message, duration = 2000, type = "success") {
  317. const colors = {
  318. success: "rgba(40, 167, 69, 0.9)",
  319. error: "rgba(220, 53, 69, 0.9)",
  320. warning: "rgba(255, 193, 7, 0.9)",
  321. info: "rgba(23, 162, 184, 0.9)",
  322. };
  323. const toast = document.createElement("div");
  324. toast.textContent = message;
  325. toast.style.cssText = `
  326. position: fixed;
  327. top: 50%;
  328. left: 50%;
  329. transform: translate(-50%, -50%);
  330. background: ${colors[type] || colors.success};
  331. color: white;
  332. padding: 12px 24px;
  333. border-radius: 8px;
  334. z-index: 9999;
  335. font-size: 14px;
  336. pointer-events: none;
  337. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
  338. `;
  339. document.body.appendChild(toast);
  340. setTimeout(() => {
  341. if (toast.parentNode) {
  342. toast.parentNode.removeChild(toast);
  343. }
  344. }, duration);
  345. }
  346. /**
  347. * 显示加载中效果
  348. * @param {string} message 加载消息
  349. * @returns {Function} 关闭加载的函数
  350. */
  351. function showLoading(message = "加载中...") {
  352. const loading = document.createElement("div");
  353. loading.innerHTML = `
  354. <div style="
  355. display: flex;
  356. align-items: center;
  357. justify-content: center;
  358. flex-direction: column;
  359. color: white;
  360. ">
  361. <div style="
  362. width: 30px;
  363. height: 30px;
  364. border: 3px solid rgba(255, 255, 255, 0.3);
  365. border-top: 3px solid white;
  366. border-radius: 50%;
  367. animation: spin 1s linear infinite;
  368. margin-bottom: 12px;
  369. "></div>
  370. <div>${message}</div>
  371. </div>
  372. `;
  373. loading.style.cssText = `
  374. position: fixed;
  375. top: 0;
  376. left: 0;
  377. width: 100%;
  378. height: 100%;
  379. background: rgba(0, 0, 0, 0.7);
  380. display: flex;
  381. align-items: center;
  382. justify-content: center;
  383. z-index: 10000;
  384. font-size: 14px;
  385. `;
  386. // 添加旋转动画
  387. if (!document.querySelector("#loading-style")) {
  388. const style = document.createElement("style");
  389. style.id = "loading-style";
  390. style.textContent = `
  391. @keyframes spin {
  392. 0% { transform: rotate(0deg); }
  393. 100% { transform: rotate(360deg); }
  394. }
  395. `;
  396. document.head.appendChild(style);
  397. }
  398. document.body.appendChild(loading);
  399. return function closeLoading() {
  400. if (loading.parentNode) {
  401. loading.parentNode.removeChild(loading);
  402. }
  403. };
  404. }
  405. /**
  406. * 格式化日期
  407. * @param {Date|string|number} date 日期
  408. * @param {string} format 格式
  409. * @returns {string} 格式化后的日期
  410. */
  411. function formatDate(date, format = "YYYY-MM-DD HH:mm:ss") {
  412. const d = new Date(date);
  413. const year = d.getFullYear();
  414. const month = String(d.getMonth() + 1).padStart(2, "0");
  415. const day = String(d.getDate()).padStart(2, "0");
  416. const hours = String(d.getHours()).padStart(2, "0");
  417. const minutes = String(d.getMinutes()).padStart(2, "0");
  418. const seconds = String(d.getSeconds()).padStart(2, "0");
  419. return format
  420. .replace("YYYY", year)
  421. .replace("MM", month)
  422. .replace("DD", day)
  423. .replace("HH", hours)
  424. .replace("mm", minutes)
  425. .replace("ss", seconds);
  426. }