refactor: 移除离线预约功能并添加授权模式开关
移除与离线预约相关的所有代码,包括缓存管理、轮询逻辑和弱网络提示模块 添加 ENABLE_AUTH_MODE 配置开关以控制授权功能的启用状态 简化网络超时处理逻辑,将弱网络弹窗改为 toast 提示 清理不再使用的示例页面和类型定义文件
Showing
11 changed files
with
59 additions
and
1083 deletions
No preview for this file type
components.d.ts
0 → 100644
| 1 | +/* eslint-disable */ | ||
| 2 | +/* prettier-ignore */ | ||
| 3 | +// @ts-nocheck | ||
| 4 | +// Generated by unplugin-vue-components | ||
| 5 | +// Read more: https://github.com/vuejs/core/pull/3399 | ||
| 6 | +export {} | ||
| 7 | + | ||
| 8 | +declare module 'vue' { | ||
| 9 | + export interface GlobalComponents { | ||
| 10 | + IndexNav: typeof import('./src/components/indexNav.vue')['default'] | ||
| 11 | + Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] | ||
| 12 | + PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] | ||
| 13 | + QrCode: typeof import('./src/components/qrCode.vue')['default'] | ||
| 14 | + QrCodeSearch: typeof import('./src/components/qrCodeSearch.vue')['default'] | ||
| 15 | + RouterLink: typeof import('vue-router')['RouterLink'] | ||
| 16 | + RouterView: typeof import('vue-router')['RouterView'] | ||
| 17 | + } | ||
| 18 | +} |
| ... | @@ -3,9 +3,8 @@ | ... | @@ -3,9 +3,8 @@ |
| 3 | * @Template: 在此定义您的业务 API 接口地址 | 3 | * @Template: 在此定义您的业务 API 接口地址 |
| 4 | */ | 4 | */ |
| 5 | 5 | ||
| 6 | -import { fn, fetch } from '@/api/fn'; | 6 | +// import { fn, fetch } from '@/api/fn'; |
| 7 | 7 | ||
| 8 | -const Api = {} | ||
| 9 | 8 | ||
| 10 | // ==================== 业务 API 接口示例 ==================== | 9 | // ==================== 业务 API 接口示例 ==================== |
| 11 | // 请根据实际业务需求修改或添加接口 | 10 | // 请根据实际业务需求修改或添加接口 | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2025-06-28 10:33:00 | 2 | * @Date: 2025-06-28 10:33:00 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-17 12:14:16 | 4 | + * @LastEditTime: 2026-01-29 18:29:57 |
| 5 | - * @FilePath: /xyxBooking-weapp/src/app.js | 5 | + * @FilePath: /manulife-weapp/src/app.js |
| 6 | * @Description: 应用入口文件 | 6 | * @Description: 应用入口文件 |
| 7 | */ | 7 | */ |
| 8 | import { createApp } from 'vue' | 8 | import { createApp } from 'vue' |
| ... | @@ -10,16 +10,6 @@ import { createPinia } from 'pinia' | ... | @@ -10,16 +10,6 @@ import { createPinia } from 'pinia' |
| 10 | import './utils/polyfill' | 10 | import './utils/polyfill' |
| 11 | import './app.less' | 11 | import './app.less' |
| 12 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' | 12 | import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' |
| 13 | -import Taro from '@tarojs/taro' | ||
| 14 | -import { refresh_offline_booking_cache, has_offline_booking_cache } from '@/composables/useOfflineBookingCache' | ||
| 15 | -import { is_usable_network, get_network_type } from '@/utils/network' | ||
| 16 | -import { enable_offline_booking_cache_polling } from '@/composables/useOfflineBookingCachePolling' | ||
| 17 | -import { weak_network_text, get_weak_network_modal_use_cache_options } from '@/utils/uiText' | ||
| 18 | - | ||
| 19 | -// 记录是否已展示过网络异常提示弹窗 | ||
| 20 | -let has_shown_network_modal = false | ||
| 21 | -// 记录上一次网络是否可用,用于识别“从可用变为不可用”的场景 | ||
| 22 | -let last_network_usable = null | ||
| 23 | 13 | ||
| 24 | const App = createApp({ | 14 | const App = createApp({ |
| 25 | // 对应 onLaunch | 15 | // 对应 onLaunch |
| ... | @@ -37,163 +27,8 @@ const App = createApp({ | ... | @@ -37,163 +27,8 @@ const App = createApp({ |
| 37 | saveCurrentPagePath(full_path) | 27 | saveCurrentPagePath(full_path) |
| 38 | } | 28 | } |
| 39 | 29 | ||
| 40 | - /** | 30 | + // 如果用户已授权,则不需要额外操作 |
| 41 | - * @description 预加载离线预约记录数据(列表+详情) | ||
| 42 | - * - 仅在有授权且网络可用时调用 | ||
| 43 | - * - 成功后将数据存储到本地缓存(key: OFFLINE_BOOKING_DATA) | ||
| 44 | - * @returns {Promise<void>} 无返回值 | ||
| 45 | - */ | ||
| 46 | - const preloadBookingData = async () => { | ||
| 47 | - try { | ||
| 48 | - await refresh_offline_booking_cache() | ||
| 49 | - } catch (e) { | ||
| 50 | - console.error('Preload booking cache failed', e) | ||
| 51 | - } | ||
| 52 | - } | ||
| 53 | - | ||
| 54 | - /** | ||
| 55 | - * @description 判断是否应该跳过网络异常提示弹窗 | ||
| 56 | - * - 仅在当前页面为离线预约列表/详情/核销码页时返回 true | ||
| 57 | - * @returns {boolean} true=跳过提示,false=不跳过 | ||
| 58 | - */ | ||
| 59 | - | ||
| 60 | - const should_skip_network_prompt = () => { | ||
| 61 | - const pages = Taro.getCurrentPages ? Taro.getCurrentPages() : [] | ||
| 62 | - const current_page = pages && pages.length ? pages[pages.length - 1] : null | ||
| 63 | - const current_route = String(current_page?.route || '') | ||
| 64 | - if (!current_route) return false | ||
| 65 | - if (current_route.includes('pages/offlineBookingList/index')) return true | ||
| 66 | - if (current_route.includes('pages/offlineBookingDetail/index')) return true | ||
| 67 | - if (current_route.includes('pages/offlineBookingCode/index')) return true | ||
| 68 | - return false | ||
| 69 | - } | ||
| 70 | - | ||
| 71 | - /** | ||
| 72 | - * @description 处理不良网络情况 | ||
| 73 | - * - 仅在当前页面未跳过提示弹窗时调用 | ||
| 74 | - * - 若有离线预约缓存,则展示弹窗询问是否使用缓存数据 | ||
| 75 | - * - 否则展示简单提示 toast | ||
| 76 | - * @param {string} network_type 网络类型(wifi/4g/5g/3g/none/unknown) | ||
| 77 | - * @returns {Promise<boolean>} true=需要中断后续启动流程,false=继续 | ||
| 78 | - */ | ||
| 79 | - | ||
| 80 | - const handle_bad_network = async (network_type) => { | ||
| 81 | - if (has_shown_network_modal) return false | ||
| 82 | - if (should_skip_network_prompt()) return false | ||
| 83 | - | ||
| 84 | - const is_none_network = network_type === 'none' | ||
| 85 | - const is_weak_network = !is_usable_network(network_type) | ||
| 86 | - if (!is_weak_network) return false | ||
| 87 | - | ||
| 88 | - has_shown_network_modal = true | ||
| 89 | - | ||
| 90 | - if (has_offline_booking_cache()) { | ||
| 91 | - try { | ||
| 92 | - const modal_res = await Taro.showModal(get_weak_network_modal_use_cache_options()) | ||
| 93 | - if (modal_res?.confirm) { | ||
| 94 | - await Taro.reLaunch({ url: '/pages/offlineBookingList/index' }) | ||
| 95 | - return true | ||
| 96 | - } | ||
| 97 | - } catch (e) { | ||
| 98 | - return is_none_network | ||
| 99 | - } | ||
| 100 | - } else { | ||
| 101 | - try { | ||
| 102 | - await Taro.showToast({ title: weak_network_text.toast_title, icon: 'none', duration: 2000 }) | ||
| 103 | - } catch (e) { | ||
| 104 | - return is_none_network | ||
| 105 | - } | ||
| 106 | - } | ||
| 107 | - | ||
| 108 | - return is_none_network | ||
| 109 | - } | ||
| 110 | - | ||
| 111 | - /** | ||
| 112 | - * 监听网络状态变化 | ||
| 113 | - * - 当网络连接且有授权时,预加载离线预约记录数据 | ||
| 114 | - */ | ||
| 115 | - Taro.onNetworkStatusChange((res) => { | ||
| 116 | - const is_connected = res?.isConnected !== false | ||
| 117 | - const network_type = res?.networkType || 'none' | ||
| 118 | - const network_usable = is_connected && is_usable_network(network_type) | ||
| 119 | - | ||
| 120 | - if (network_usable) { | ||
| 121 | - has_shown_network_modal = false | ||
| 122 | - last_network_usable = true | ||
| 123 | - if (hasAuth()) preloadBookingData() | ||
| 124 | - return | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | - const should_prompt = last_network_usable === true || last_network_usable === null | ||
| 128 | - last_network_usable = false | ||
| 129 | - if (should_prompt) { | ||
| 130 | - handle_bad_network(network_type) | ||
| 131 | - } | ||
| 132 | - return | ||
| 133 | - }) | ||
| 134 | - | ||
| 135 | - /** | ||
| 136 | - * @description 处理启动时的不良网络情况(只在启动阶段检查一次) | ||
| 137 | - * - 网络不可用且有离线缓存:询问是否进入离线模式 | ||
| 138 | - * - 网络不可用且无缓存:toast 提示网络不佳 | ||
| 139 | - * @returns {Promise<boolean>} true=进入离线模式并中断启动,false=继续启动 | ||
| 140 | - */ | ||
| 141 | - const handle_bad_network_on_launch = async () => { | ||
| 142 | - /** | ||
| 143 | - * 避免重复提示用户 | ||
| 144 | - * - 仅在首次启动时检查网络情况 | ||
| 145 | - * - 如果用户已展示过提示弹窗,则直接返回 false | ||
| 146 | - */ | ||
| 147 | - if (has_shown_network_modal) return false | ||
| 148 | - | ||
| 149 | - const network_type = await get_network_type() | ||
| 150 | - last_network_usable = is_usable_network(network_type) | ||
| 151 | - return handle_bad_network(network_type) | ||
| 152 | - } | ||
| 153 | - | ||
| 154 | - /** | ||
| 155 | - * @description 尝试在网络可用时预加载离线预约记录数据 | ||
| 156 | - * - 仅在有授权时调用 | ||
| 157 | - * @returns {void} 无返回值 | ||
| 158 | - */ | ||
| 159 | - const try_preload_when_online = () => { | ||
| 160 | - if (!hasAuth()) return | ||
| 161 | - Taro.getNetworkType({ | ||
| 162 | - success: (res) => { | ||
| 163 | - if (is_usable_network(res.networkType)) { | ||
| 164 | - preloadBookingData() | ||
| 165 | - } | ||
| 166 | - } | ||
| 167 | - }) | ||
| 168 | - } | ||
| 169 | - | ||
| 170 | - /** | ||
| 171 | - * @description 授权成功后的共用启动逻辑 | ||
| 172 | - * - 尝试在网络可用时预加载离线预约数据 | ||
| 173 | - * - 启动离线预约缓存轮询(会自行处理网络可用性与引用计数) | ||
| 174 | - * @returns {void} 无返回值 | ||
| 175 | - */ | ||
| 176 | - | ||
| 177 | - const bootstrap_after_auth = () => { | ||
| 178 | - try_preload_when_online() | ||
| 179 | - enable_offline_booking_cache_polling({ interval_ms: 2 * 1000 * 60 }) | ||
| 180 | - } | ||
| 181 | - | ||
| 182 | - // 处理在启动时出现的不良网络情况 | ||
| 183 | - const should_stop = await handle_bad_network_on_launch() | ||
| 184 | - // 如果用户选择进入离线模式,则直接返回 | ||
| 185 | - if (should_stop) return | ||
| 186 | - | ||
| 187 | - /** | ||
| 188 | - * 尝试在有授权时预加载离线预约记录数据 | ||
| 189 | - * - 若无授权,则尝试静默授权 | ||
| 190 | - * - 授权成功后调用 bootstrap_after_auth 启动共用逻辑 | ||
| 191 | - * - 授权失败则跳转至授权页面 | ||
| 192 | - */ | ||
| 193 | - | ||
| 194 | - // 如果用户已授权,则直接调用 bootstrap_after_auth 启动共用逻辑 | ||
| 195 | if (hasAuth()) { | 31 | if (hasAuth()) { |
| 196 | - bootstrap_after_auth() | ||
| 197 | return | 32 | return |
| 198 | } | 33 | } |
| 199 | 34 | ||
| ... | @@ -202,8 +37,6 @@ const App = createApp({ | ... | @@ -202,8 +37,6 @@ const App = createApp({ |
| 202 | try { | 37 | try { |
| 203 | // 尝试静默授权 | 38 | // 尝试静默授权 |
| 204 | await silentAuth() | 39 | await silentAuth() |
| 205 | - // 授权成功后调用 bootstrap_after_auth 启动共用逻辑 | ||
| 206 | - bootstrap_after_auth() | ||
| 207 | } catch (error) { | 40 | } catch (error) { |
| 208 | console.error('静默授权失败:', error) | 41 | console.error('静默授权失败:', error) |
| 209 | // 授权失败则跳转至授权页面 | 42 | // 授权失败则跳转至授权页面 | ... | ... |
| 1 | -/** | ||
| 2 | - * 刷新离线预约记录缓存 | ||
| 3 | - * - 仅在有授权且网络可用时调用 | ||
| 4 | - * - 成功后将数据存储到本地缓存(key: OFFLINE_BOOKING_DATA) | ||
| 5 | - * @param {boolean} force - 是否强制刷新,默认为 false | ||
| 6 | - * @returns {Promise<void>} | ||
| 7 | - */ | ||
| 8 | - | ||
| 9 | -import Taro from '@tarojs/taro' | ||
| 10 | -import { billOfflineAllAPI } from '@/api/index' | ||
| 11 | -import { hasAuth } from '@/utils/authRedirect' | ||
| 12 | -import { formatDatetime } from '@/utils/tools' | ||
| 13 | -import { is_usable_network, get_network_type } from '@/utils/network' | ||
| 14 | - | ||
| 15 | -export const OFFLINE_BOOKING_CACHE_KEY = 'OFFLINE_BOOKING_DATA' | ||
| 16 | - | ||
| 17 | -let refresh_promise = null | ||
| 18 | - | ||
| 19 | -/** | ||
| 20 | - * @description 兼容不同后端结构:从预约记录中提取可用数据载荷 | ||
| 21 | - * - 部分接口会把字段塞到 bill.list 对象里,这里做一次展开合并 | ||
| 22 | - * @param {Object} bill 原始预约记录 | ||
| 23 | - * @returns {Object} 扁平化后的预约记录对象 | ||
| 24 | - */ | ||
| 25 | -const extract_bill_payload = (bill) => { | ||
| 26 | - if (!bill) return {} | ||
| 27 | - | ||
| 28 | - const data = { ...bill } | ||
| 29 | - const list = data.list | ||
| 30 | - | ||
| 31 | - if (list && typeof list === 'object' && !Array.isArray(list)) { | ||
| 32 | - return { ...list, ...data } | ||
| 33 | - } | ||
| 34 | - | ||
| 35 | - return data | ||
| 36 | -} | ||
| 37 | - | ||
| 38 | -/** | ||
| 39 | - * @description 从预约记录中提取人员列表 | ||
| 40 | - * - 兼容不同字段名(person_list/bill_person_list/persons/qrcode_list/qr_list/detail_list) | ||
| 41 | - * - 保证返回数组类型 | ||
| 42 | - * @param {Object} bill 预约记录 | ||
| 43 | - * @returns {Array} 人员列表 | ||
| 44 | - */ | ||
| 45 | -const extract_person_list = (bill) => { | ||
| 46 | - if (!bill) return [] | ||
| 47 | - | ||
| 48 | - /** | ||
| 49 | - * 从预约记录中提取人员列表 | ||
| 50 | - * - 考虑不同字段名的情况(如 person_list, bill_person_list, persons, qrcode_list, qr_list, detail_list) | ||
| 51 | - * - 确保返回的是数组类型 | ||
| 52 | - */ | ||
| 53 | - const candidate = | ||
| 54 | - (Array.isArray(bill.list) ? bill.list : null) || | ||
| 55 | - (Array.isArray(bill?.list?.list) ? bill.list.list : null) || | ||
| 56 | - bill.person_list || | ||
| 57 | - bill.bill_person_list || | ||
| 58 | - bill.persons || | ||
| 59 | - bill.qrcode_list || | ||
| 60 | - bill.qr_list || | ||
| 61 | - bill.detail_list || | ||
| 62 | - [] | ||
| 63 | - | ||
| 64 | - return Array.isArray(candidate) ? candidate : [] | ||
| 65 | -} | ||
| 66 | - | ||
| 67 | -/** | ||
| 68 | - * @description 格式化预约记录项(统一字段与展示用时间) | ||
| 69 | - * @param {Object} item 原始预约记录项 | ||
| 70 | - * @returns {Object} 格式化后的预约记录项 | ||
| 71 | - */ | ||
| 72 | -const normalize_bill_item = (item) => { | ||
| 73 | - const data = extract_bill_payload(item) | ||
| 74 | - | ||
| 75 | - data.datetime = data.datetime || formatDatetime(data) | ||
| 76 | - data.booking_time = data.booking_time || data.datetime | ||
| 77 | - data.order_time = data.order_time || (data.created_time ? data.created_time.slice(0, -3) : '') | ||
| 78 | - | ||
| 79 | - if (!data.person_name) { | ||
| 80 | - const person_list = extract_person_list(item) | ||
| 81 | - const first = person_list[0] | ||
| 82 | - const name = first?.name || first?.person_name | ||
| 83 | - if (name) data.person_name = name | ||
| 84 | - } | ||
| 85 | - | ||
| 86 | - return data | ||
| 87 | -} | ||
| 88 | - | ||
| 89 | -/** | ||
| 90 | - * 获取离线预约记录缓存 | ||
| 91 | - * @returns {Array} 格式化后的预约记录项列表 | ||
| 92 | - */ | ||
| 93 | -export const get_offline_booking_cache = () => { | ||
| 94 | - try { | ||
| 95 | - const data = Taro.getStorageSync(OFFLINE_BOOKING_CACHE_KEY) | ||
| 96 | - return Array.isArray(data) ? data : [] | ||
| 97 | - } catch (e) { | ||
| 98 | - return [] | ||
| 99 | - } | ||
| 100 | -} | ||
| 101 | - | ||
| 102 | -/** | ||
| 103 | - * 检查是否存在离线预约记录缓存 | ||
| 104 | - * @returns {boolean} 是否存在缓存且非空 | ||
| 105 | - */ | ||
| 106 | -export const has_offline_booking_cache = () => { | ||
| 107 | - const list = get_offline_booking_cache() | ||
| 108 | - return Array.isArray(list) && list.length > 0 | ||
| 109 | -} | ||
| 110 | - | ||
| 111 | -/** | ||
| 112 | - * 根据支付ID获取离线预约记录 | ||
| 113 | - * @param {*} pay_id 支付ID | ||
| 114 | - * @returns {Object|null} 匹配的预约记录项或 null | ||
| 115 | - */ | ||
| 116 | -export const get_offline_booking_by_pay_id = (pay_id) => { | ||
| 117 | - const list = get_offline_booking_cache() | ||
| 118 | - const target_pay_id = String(pay_id || '') | ||
| 119 | - return list.find((item) => String(item?.pay_id || '') === target_pay_id) || null | ||
| 120 | -} | ||
| 121 | - | ||
| 122 | -/** | ||
| 123 | - * 获取预约记录中的人员列表 | ||
| 124 | - * @param {Object} bill - 预约记录项 | ||
| 125 | - * @returns {Array} 人员列表(包含姓名、身份证号、二维码等信息) | ||
| 126 | - */ | ||
| 127 | -export const get_offline_bill_person_list = (bill) => { | ||
| 128 | - return extract_person_list(bill) | ||
| 129 | -} | ||
| 130 | - | ||
| 131 | -/** | ||
| 132 | - * 构建预约记录中的二维码列表 | ||
| 133 | - * @param {Object} bill - 预约记录项 | ||
| 134 | - * @returns {Array} 二维码列表(包含姓名、身份证号、二维码、预约时间等信息) | ||
| 135 | - */ | ||
| 136 | -export const build_offline_qr_list = (bill) => { | ||
| 137 | - const list = get_offline_bill_person_list(bill) | ||
| 138 | - const datetime = bill?.datetime || formatDatetime(bill || {}) | ||
| 139 | - | ||
| 140 | - return list | ||
| 141 | - .filter((item) => item && (item.qr_code || item.qrcode || item.qrCode) && (item.qr_code || item.qrcode || item.qrCode) !== '') | ||
| 142 | - .map((item) => { | ||
| 143 | - const begin_time = item.begin_time || bill?.begin_time | ||
| 144 | - const end_time = item.end_time || bill?.end_time | ||
| 145 | - const qr_code = item.qr_code || item.qrcode || item.qrCode | ||
| 146 | - const name = item.name || item.person_name || item.real_name | ||
| 147 | - const id_number = item.id_number || item.idcard || item.idCard || item.id | ||
| 148 | - return { | ||
| 149 | - name, | ||
| 150 | - id_number, | ||
| 151 | - qr_code, | ||
| 152 | - begin_time, | ||
| 153 | - end_time, | ||
| 154 | - datetime: item.datetime || (begin_time && end_time ? formatDatetime({ begin_time, end_time }) : datetime), | ||
| 155 | - pay_id: bill?.pay_id, | ||
| 156 | - sort: 0, | ||
| 157 | - } | ||
| 158 | - }) | ||
| 159 | -} | ||
| 160 | - | ||
| 161 | -/** | ||
| 162 | - * 刷新离线预约记录缓存 | ||
| 163 | - * - 仅在有授权且网络可用时调用 | ||
| 164 | - * - 成功后将数据存储到本地缓存(key: OFFLINE_BOOKING_DATA) | ||
| 165 | - * @param {boolean} force - 是否强制刷新,默认为 false. force 参数的核心作用是控制是否忽略 “正在进行的缓存请求”, 管的是 “是否允许重复发起请求”,不管 “请求能不能成功执行缓存”。 | ||
| 166 | - * @returns 不同情况返回值不一样 | ||
| 167 | - * - 成功时包含格式化后的预约记录项列表 | ||
| 168 | - * - 失败时包含错误信息(如网络错误、授权失败等) | ||
| 169 | - */ | ||
| 170 | - | ||
| 171 | -export const refresh_offline_booking_cache = async ({ force = false } = {}) => { | ||
| 172 | - // 1. 检查是否有正在进行的刷新请求 | ||
| 173 | - // 2. 如果有,且 force 为 false,则直接返回该 Promise | ||
| 174 | - // 3. 如果没有,或 force 为 true,则继续执行刷新逻辑 | ||
| 175 | - // 4. 刷新完成后,将结果存储到本地缓存(key: OFFLINE_BOOKING_CACHE_KEY) | ||
| 176 | - // 5. 返回刷新结果 Promise | ||
| 177 | - | ||
| 178 | - if (!hasAuth()) return { code: 0, data: null, msg: '未授权' } | ||
| 179 | - | ||
| 180 | - if (refresh_promise && !force) return refresh_promise | ||
| 181 | - | ||
| 182 | - // 核心逻辑: | ||
| 183 | - // 1. 立刻触发异步逻辑,同时捕获 Promise 状态 | ||
| 184 | - // 2. 保证 refresh_promise 始终是 Promise 类型,适配 await | ||
| 185 | - // 3. 隔离作用域,避免变量污染 | ||
| 186 | - // 加 () 是为了 “让异步逻辑立刻跑起来”,并把 “跑的结果(Promise)” 存起来,供后续复用和等待。 | ||
| 187 | - refresh_promise = (async () => { | ||
| 188 | - const network_type = await get_network_type() | ||
| 189 | - if (!is_usable_network(network_type)) { | ||
| 190 | - return { code: 0, data: null, msg: '网络不可用' } | ||
| 191 | - } | ||
| 192 | - | ||
| 193 | - const { code, data, msg } = await billOfflineAllAPI() | ||
| 194 | - if (code && Array.isArray(data)) { | ||
| 195 | - // 过滤出状态为3(已完成)的记录 | ||
| 196 | - const normalized = data.map(normalize_bill_item).filter((item) => item && item.pay_id && item.status == 3) | ||
| 197 | - if (normalized.length > 0) { | ||
| 198 | - // TAG: 核心逻辑:将过滤后的记录存储到本地缓存 | ||
| 199 | - Taro.setStorageSync(OFFLINE_BOOKING_CACHE_KEY, normalized) | ||
| 200 | - } | ||
| 201 | - } | ||
| 202 | - return { code, data, msg } | ||
| 203 | - })() | ||
| 204 | - | ||
| 205 | - try { | ||
| 206 | - return await refresh_promise | ||
| 207 | - } finally { | ||
| 208 | - refresh_promise = null | ||
| 209 | - } | ||
| 210 | -} |
| 1 | -/** | ||
| 2 | - * @description: 轮询离线预约缓存 | ||
| 3 | - */ | ||
| 4 | - | ||
| 5 | -import Taro from '@tarojs/taro' | ||
| 6 | -import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache' | ||
| 7 | -import { get_network_type, is_usable_network } from '@/utils/network' | ||
| 8 | - | ||
| 9 | -/** | ||
| 10 | - * @description: 轮询状态 | ||
| 11 | - * @typedef {Object} PollingState | ||
| 12 | - * @property {Number} timer_id 轮询定时器id | ||
| 13 | - * @property {Boolean} running 是否正在轮询 | ||
| 14 | - * @property {Boolean} in_flight 是否正在刷新 | ||
| 15 | - * @property {Number} ref_count 引用计数 | ||
| 16 | - * @property {Boolean} app_enabled 是否启用应用 | ||
| 17 | - * @property {Object} last_options 最后一次选项 | ||
| 18 | - * @property {Boolean} network_usable 网络可用性 | ||
| 19 | - * @property {Boolean} has_network_listener 是否已注册网络监听器 | ||
| 20 | - * @property {Function} network_listener 网络监听器 | ||
| 21 | - * @property {Promise} network_listener_promise 网络监听器Promise | ||
| 22 | - * | ||
| 23 | - * 状态同步规则(app_enabled 与 ref_count): | ||
| 24 | - * - app_enabled = true 时,ref_count >= 1(至少有一个使用者) | ||
| 25 | - * - app_enabled = false 时,ref_count = 0(无使用者) | ||
| 26 | - * - enable_offline_booking_cache_polling 会设置 app_enabled = true | ||
| 27 | - * - disable_offline_booking_cache_polling 会设置 app_enabled = false | ||
| 28 | - * - acquire_polling_ref 会增加 ref_count | ||
| 29 | - * - release_polling_ref 会减少 ref_count,降为0时触发清理 | ||
| 30 | - */ | ||
| 31 | - | ||
| 32 | -/** @type {PollingState} */ | ||
| 33 | -const polling_state = { | ||
| 34 | - timer_id: null, // 轮询定时器id | ||
| 35 | - running: false, // 是否正在轮询 | ||
| 36 | - in_flight: false, // 是否正在刷新 | ||
| 37 | - ref_count: 0, // 引用计数 | ||
| 38 | - app_enabled: false, // 是否启用应用 | ||
| 39 | - last_options: null, // 最后一次选项 | ||
| 40 | - network_usable: null, // 网络可用性 | ||
| 41 | - has_network_listener: false, // 是否已注册网络监听器 | ||
| 42 | - network_listener: null, // 网络监听器 | ||
| 43 | - network_listener_promise: null, // 网络监听器Promise | ||
| 44 | -} | ||
| 45 | - | ||
| 46 | -/** | ||
| 47 | - * @description: 规范化选项参数(纯函数,无副作用) | ||
| 48 | - * @param {Object} options 选项 | ||
| 49 | - * @return {Object} 规范化后的选项 | ||
| 50 | - */ | ||
| 51 | -const normalize_options = (options) => { | ||
| 52 | - return options || {} | ||
| 53 | -} | ||
| 54 | - | ||
| 55 | -/** | ||
| 56 | - * @description: 保存最后一次选项(用于网络恢复时重启轮询) | ||
| 57 | - * @param {Object} options 选项 | ||
| 58 | - * @return {Object} 保存后的选项 | ||
| 59 | - */ | ||
| 60 | -const save_last_options = (options) => { | ||
| 61 | - if (options) polling_state.last_options = options | ||
| 62 | - return polling_state.last_options | ||
| 63 | -} | ||
| 64 | - | ||
| 65 | -/** | ||
| 66 | - * 这是异步编程中典型的飞行状态锁(In-Flight Lock) 模式,是异步防重的核心思维落地方式; | ||
| 67 | - * 核心逻辑:执行前 “上锁” 标记 → 执行异步操作 → 无论成败都 “解锁” 重置标记,从根源避免重复执行; | ||
| 68 | - * finally 块是关键保障:防止异步操作报错导致 “永久上锁”,确保后续调用能正常执行。 | ||
| 69 | - */ | ||
| 70 | - | ||
| 71 | -/** | ||
| 72 | - * @description: 刷新离线预约缓存一次 | ||
| 73 | - * @param {Object} options 选项 | ||
| 74 | - * @param {Boolean} options.force 是否强制刷新 | ||
| 75 | - */ | ||
| 76 | -const run_refresh_once = async (options) => { | ||
| 77 | - // 前置检查:不满足轮询条件时直接返回(网络不可用或无引用) | ||
| 78 | - if (!should_run_polling()) return | ||
| 79 | - // 核心防重复——如果正在刷新,直接返回 | ||
| 80 | - if (polling_state.in_flight) return | ||
| 81 | - // 标记为"正在刷新" | ||
| 82 | - polling_state.in_flight = true | ||
| 83 | - try { | ||
| 84 | - await refresh_offline_booking_cache({ force: !!options?.force }) | ||
| 85 | - } finally { | ||
| 86 | - // 刷新完成后,标记为"刷新完成" | ||
| 87 | - polling_state.in_flight = false | ||
| 88 | - } | ||
| 89 | -} | ||
| 90 | - | ||
| 91 | -/** | ||
| 92 | - * @description: 更新网络可用性 | ||
| 93 | - */ | ||
| 94 | - | ||
| 95 | -const update_network_usable = async () => { | ||
| 96 | - const type = await get_network_type() | ||
| 97 | - polling_state.network_usable = is_usable_network(type) | ||
| 98 | -} | ||
| 99 | - | ||
| 100 | -/** | ||
| 101 | - * @description: 判断是否需要运行轮询 | ||
| 102 | - * @return {Boolean} 是否需要运行轮询 | ||
| 103 | - * | ||
| 104 | - * 返回 false 的条件: | ||
| 105 | - * 1. ref_count <= 0:无使用者,无需轮询 | ||
| 106 | - * 2. network_usable === false:网络不可用,无需轮询 | ||
| 107 | - * 3. network_usable === null:网络状态未初始化,避免在无监听器时误判 | ||
| 108 | - * | ||
| 109 | - * 返回 true 的条件: | ||
| 110 | - * 1. ref_count > 0:至少有一个使用者 | ||
| 111 | - * 2. network_usable === true:网络可用 | ||
| 112 | - */ | ||
| 113 | -const should_run_polling = () => { | ||
| 114 | - if (polling_state.ref_count <= 0) return false | ||
| 115 | - if (polling_state.network_usable === false) return false | ||
| 116 | - if (polling_state.network_usable === null) return false | ||
| 117 | - return true | ||
| 118 | -} | ||
| 119 | - | ||
| 120 | -/** | ||
| 121 | - * @description: 确保网络监听器已注册 | ||
| 122 | - * @return {Promise<Boolean>} 是否注册成功(true=成功,false=失败) | ||
| 123 | - */ | ||
| 124 | -const ensure_network_listener = async () => { | ||
| 125 | - /** | ||
| 126 | - * 代码优先通过两个条件判断避免重复执行监听器逻辑 | ||
| 127 | - * 1. 有已注册的监听器直接返回 | ||
| 128 | - * 2. 有未完成的注册 Promise 则直接返回 | ||
| 129 | - */ | ||
| 130 | - | ||
| 131 | - if (polling_state.has_network_listener) { | ||
| 132 | - await update_network_usable() | ||
| 133 | - return true | ||
| 134 | - } | ||
| 135 | - | ||
| 136 | - if (polling_state.network_listener_promise) { | ||
| 137 | - await polling_state.network_listener_promise | ||
| 138 | - // 等待注册完成后检查是否成功 | ||
| 139 | - return polling_state.has_network_listener | ||
| 140 | - } | ||
| 141 | - | ||
| 142 | - // 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听) | ||
| 143 | - polling_state.network_listener_promise = (async () => { | ||
| 144 | - // 标记已注册网络监听器 | ||
| 145 | - polling_state.has_network_listener = true | ||
| 146 | - // 初始化时更新网络可用性 | ||
| 147 | - await update_network_usable() | ||
| 148 | - | ||
| 149 | - // 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行 | ||
| 150 | - polling_state.network_listener = (res) => { | ||
| 151 | - const is_connected = res?.isConnected !== false | ||
| 152 | - const type = res?.networkType || 'unknown' | ||
| 153 | - polling_state.network_usable = is_connected && is_usable_network(type) | ||
| 154 | - | ||
| 155 | - // 改进:不再主动停止轮询,由 run_refresh_once 中的 should_run_polling() 前置检查控制 | ||
| 156 | - // 优势: | ||
| 157 | - // 1. 避免网络恢复时需要额外的重启逻辑 | ||
| 158 | - // 2. 保持定时器稳定,避免频繁启动/停止 | ||
| 159 | - // 3. 网络不可用时,刷新操作会在 run_refresh_once 中被前置检查过滤掉 | ||
| 160 | - | ||
| 161 | - // 网络恢复时,确保轮询正在运行(处理之前因网络不可用而可能停止的轮询) | ||
| 162 | - if (polling_state.network_usable && should_run_polling()) { | ||
| 163 | - // 传入 restart: true,支持重启逻辑 | ||
| 164 | - // 使用 normalize_options 显式处理 null/undefined,语义更清晰 | ||
| 165 | - start_offline_booking_cache_polling({ | ||
| 166 | - ...normalize_options(polling_state.last_options), | ||
| 167 | - restart: true | ||
| 168 | - }) | ||
| 169 | - } | ||
| 170 | - } | ||
| 171 | - | ||
| 172 | - try { | ||
| 173 | - // 注册网络状态变化监听器 | ||
| 174 | - Taro.onNetworkStatusChange(polling_state.network_listener) | ||
| 175 | - } catch (e) { | ||
| 176 | - polling_state.has_network_listener = false | ||
| 177 | - polling_state.network_listener = null | ||
| 178 | - polling_state.network_usable = null | ||
| 179 | - console.error('注册网络监听失败:', e) | ||
| 180 | - } | ||
| 181 | - })() | ||
| 182 | - | ||
| 183 | - try { | ||
| 184 | - // 等待网络监听器初始化完成 | ||
| 185 | - await polling_state.network_listener_promise | ||
| 186 | - } finally { | ||
| 187 | - // 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净 | ||
| 188 | - polling_state.network_listener_promise = null | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - // 返回注册是否成功 | ||
| 192 | - return polling_state.has_network_listener | ||
| 193 | -} | ||
| 194 | - | ||
| 195 | -/** | ||
| 196 | - * @description: 注销网络监听器 | ||
| 197 | - * 涉及字段: | ||
| 198 | - * - has_network_listener:是否有注册网络监听器 | ||
| 199 | - * - ref_count:引用计数 | ||
| 200 | - * - network_listener:网络状态变化监听器 | ||
| 201 | - * - network_usable:网络可用性状态 | ||
| 202 | - */ | ||
| 203 | - | ||
| 204 | -const teardown_network_listener = () => { | ||
| 205 | - // 1. 前置校验:避免无效执行 | ||
| 206 | - // 如果没有注册网络监听器,直接返回 | ||
| 207 | - if (!polling_state.has_network_listener) return | ||
| 208 | - // 如果有引用计数,说明有其他地方在使用轮询,不能注销监听器 | ||
| 209 | - if (polling_state.ref_count > 0) return | ||
| 210 | - // 标记监听器已注销(核心状态更新) | ||
| 211 | - polling_state.has_network_listener = false | ||
| 212 | - // 解绑框架层面的监听器 | ||
| 213 | - if (polling_state.network_listener && typeof Taro.offNetworkStatusChange === 'function') { | ||
| 214 | - try { | ||
| 215 | - Taro.offNetworkStatusChange(polling_state.network_listener) | ||
| 216 | - } catch (e) { | ||
| 217 | - // 捕获解绑失败的异常(比如监听器已被手动解绑) | ||
| 218 | - console.warn('注销网络监听器失败:', e) | ||
| 219 | - } | ||
| 220 | - } | ||
| 221 | - // 手动清空本地引用(关键!无论解绑成功/失败都要做) | ||
| 222 | - // 注销后,清空网络监听器引用,确保后续调用能正常工作 | ||
| 223 | - polling_state.network_listener = null | ||
| 224 | - /** | ||
| 225 | - * 核心目的:清空 network_usable = null 是为了让状态和监听器的生命周期完全同步 —— 监听器注销后,其产生的网络状态也必须失效,避免 “无监听器却有状态” 的矛盾; | ||
| 226 | - * 关键作用:通过让 should_run_polling() 直接返回 false,杜绝基于过期状态启动轮询的可能; | ||
| 227 | - * 设计思维:体现了 “状态闭环” 的工程化思想 —— 任何状态都要有明确的产生、更新、销毁逻辑,不残留 “脏数据” 干扰后续流程。 | ||
| 228 | - */ | ||
| 229 | - // 清空网络可用性状态,确保后续判断逻辑能正常工作 | ||
| 230 | - // 清空衍生状态,避免脏数据 | ||
| 231 | - polling_state.network_usable = null | ||
| 232 | -} | ||
| 233 | - | ||
| 234 | -/** | ||
| 235 | - * @description: 启动离线预约缓存轮询 | ||
| 236 | - * @param {Object} options 选项 | ||
| 237 | - * @param {Number} options.interval_ms 轮询间隔,单位毫秒 | ||
| 238 | - * @param {Boolean} options.immediate 是否立即刷新一次 | ||
| 239 | - * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) | ||
| 240 | - * @param {Boolean} options.restart 是否为重启操作(网络恢复时调用) | ||
| 241 | - */ | ||
| 242 | -const start_offline_booking_cache_polling = (options) => { | ||
| 243 | - options = normalize_options(options) | ||
| 244 | - if (!should_run_polling()) return // 不满足轮询条件直接返回 | ||
| 245 | - | ||
| 246 | - const interval_ms = Number(options?.interval_ms || 60000) | ||
| 247 | - const is_restart = options?.restart === true | ||
| 248 | - | ||
| 249 | - // 改进:区分首次启动和重启的防重逻辑 | ||
| 250 | - // 首次启动时,如果已经在轮询则直接返回(防重复启动) | ||
| 251 | - // 重启时,需要清除旧定时器并重新建立(支持网络恢复时重启) | ||
| 252 | - if (polling_state.running && !is_restart) return | ||
| 253 | - | ||
| 254 | - // 如果是重启或定时器已存在,先清除旧定时器 | ||
| 255 | - if (is_restart && polling_state.timer_id) { | ||
| 256 | - clearInterval(polling_state.timer_id) | ||
| 257 | - polling_state.timer_id = null | ||
| 258 | - } | ||
| 259 | - | ||
| 260 | - polling_state.running = true // 标记为"正在轮询" | ||
| 261 | - | ||
| 262 | - // 立即刷新一次,确保轮询开始时数据是最新的 | ||
| 263 | - if (options?.immediate !== false) { | ||
| 264 | - run_refresh_once(options) | ||
| 265 | - } | ||
| 266 | - | ||
| 267 | - // 启动轮询定时器,按照指定间隔执行刷新操作 | ||
| 268 | - polling_state.timer_id = setInterval(() => { | ||
| 269 | - run_refresh_once(options) | ||
| 270 | - }, interval_ms) | ||
| 271 | -} | ||
| 272 | - | ||
| 273 | -/** | ||
| 274 | - * @description: 停止离线预约缓存轮询 | ||
| 275 | - */ | ||
| 276 | - | ||
| 277 | -const stop_offline_booking_cache_polling = () => { | ||
| 278 | - if (polling_state.timer_id) { | ||
| 279 | - clearInterval(polling_state.timer_id) | ||
| 280 | - polling_state.timer_id = null | ||
| 281 | - } | ||
| 282 | - polling_state.running = false | ||
| 283 | -} | ||
| 284 | - | ||
| 285 | -/** | ||
| 286 | - * 引用计数的核心作用 | ||
| 287 | - * 这两个函数实现了轮询功能的 “引用计数式资源管理”,本质是追踪有多少 “使用者 / 场景” 依赖这个轮询功能,从而决定是否启动 / 维持 / 停止轮询、注册 / 注销网络监听器, | ||
| 288 | - * 核心目的是: | ||
| 289 | - * - 避免轮询被重复启动、错误停止 | ||
| 290 | - * - 防止无使用者时仍占用资源(定时器、网络监听器) | ||
| 291 | - * - 保证多场景共用轮询时的逻辑一致性 | ||
| 292 | - */ | ||
| 293 | - | ||
| 294 | -/** | ||
| 295 | - * @description: 增加轮询引用计数 | ||
| 296 | - * 核心动作:将全局的 ref_count 加 1,代表 "又多了一个场景需要使用轮询功能"。 | ||
| 297 | - * @param {Object} options 选项 | ||
| 298 | - */ | ||
| 299 | -const acquire_polling_ref = (options) => { | ||
| 300 | - save_last_options(options) | ||
| 301 | - polling_state.ref_count += 1 | ||
| 302 | - // 改进:检查网络监听器注册结果,只有成功后才启动轮询 | ||
| 303 | - ensure_network_listener().then((success) => { | ||
| 304 | - if (success && polling_state.last_options) { | ||
| 305 | - start_offline_booking_cache_polling(polling_state.last_options) | ||
| 306 | - } | ||
| 307 | - }) | ||
| 308 | -} | ||
| 309 | - | ||
| 310 | -/** | ||
| 311 | - * @description: 减少轮询引用计数 | ||
| 312 | - * 核心动作:将 ref_count 减 1(且保证不会为负数),代表 “有一个场景不再需要轮询功能”。 | ||
| 313 | - */ | ||
| 314 | - | ||
| 315 | -const release_polling_ref = () => { | ||
| 316 | - polling_state.ref_count = Math.max(0, polling_state.ref_count - 1) | ||
| 317 | - if (polling_state.ref_count === 0) { | ||
| 318 | - // 引用计数降为0时,停止轮询并注销网络监听器 | ||
| 319 | - stop_offline_booking_cache_polling() | ||
| 320 | - teardown_network_listener() | ||
| 321 | - } | ||
| 322 | -} | ||
| 323 | - | ||
| 324 | -/** | ||
| 325 | - * @description: 启用离线预约缓存轮询 | ||
| 326 | - * @param {Object} options 选项 | ||
| 327 | - * @param {Number} options.interval_ms 轮询间隔,单位毫秒 | ||
| 328 | - * @param {Boolean} options.immediate 是否立即刷新一次 | ||
| 329 | - * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) | ||
| 330 | - */ | ||
| 331 | -export const enable_offline_booking_cache_polling = (options) => { | ||
| 332 | - save_last_options(options) | ||
| 333 | - /** | ||
| 334 | - * 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 "已启用但异常停止" 时能被主动恢复,而非被动等待网络变化; | ||
| 335 | - * 执行逻辑:先保证网络监听器(轮询的依赖)就绪,再尝试启动轮询,且利用 start_offline_booking_cache_polling 的幂等性避免重复; | ||
| 336 | - * 设计思维:体现了 "主动调用需即时生效" 的用户体验考量,以及 "依赖前置检查" 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。 | ||
| 337 | - */ | ||
| 338 | - if (polling_state.app_enabled) { | ||
| 339 | - ensure_network_listener().then((success) => { | ||
| 340 | - if (success && polling_state.last_options) { | ||
| 341 | - start_offline_booking_cache_polling(polling_state.last_options) | ||
| 342 | - } | ||
| 343 | - }) | ||
| 344 | - return | ||
| 345 | - } | ||
| 346 | - polling_state.app_enabled = true | ||
| 347 | - acquire_polling_ref(polling_state.last_options || {}) | ||
| 348 | -} | ||
| 349 | - | ||
| 350 | -/** | ||
| 351 | - * @description: 禁用离线预约缓存轮询 | ||
| 352 | - */ | ||
| 353 | - | ||
| 354 | -export const disable_offline_booking_cache_polling = () => { | ||
| 355 | - if (!polling_state.app_enabled) return | ||
| 356 | - polling_state.app_enabled = false | ||
| 357 | - release_polling_ref() | ||
| 358 | -} |
| 1 | -<template> | ||
| 2 | - <view class="api-demo"> | ||
| 3 | - <nut-cell-group> | ||
| 4 | - <nut-cell title="API 生成器演示" /> | ||
| 5 | - </nut-cell-group> | ||
| 6 | - | ||
| 7 | - <!-- 用户信息模块 --> | ||
| 8 | - <nut-cell-group title="用户模块"> | ||
| 9 | - <nut-cell> | ||
| 10 | - <template #title> | ||
| 11 | - <view>获取用户信息</view> | ||
| 12 | - </template> | ||
| 13 | - <template #link> | ||
| 14 | - <nut-button | ||
| 15 | - size="small" | ||
| 16 | - type="primary" | ||
| 17 | - @click="fetchUserInfo" | ||
| 18 | - :loading="userLoading" | ||
| 19 | - > | ||
| 20 | - 调用 | ||
| 21 | - </nut-button> | ||
| 22 | - </template> | ||
| 23 | - </nut-cell> | ||
| 24 | - | ||
| 25 | - <nut-cell v-if="userInfo" title="用户数据"> | ||
| 26 | - <view class="user-info"> | ||
| 27 | - <text>姓名: {{ userInfo?.name || '-' }}</text> | ||
| 28 | - <text>手机: {{ userInfo?.mobile || '-' }}</text> | ||
| 29 | - </view> | ||
| 30 | - </nut-cell> | ||
| 31 | - </nut-cell-group> | ||
| 32 | - | ||
| 33 | - <!-- 订单模块 --> | ||
| 34 | - <nut-cell-group title="订单模块"> | ||
| 35 | - <nut-cell> | ||
| 36 | - <template #title> | ||
| 37 | - <view>获取订单列表</view> | ||
| 38 | - </template> | ||
| 39 | - <template #link> | ||
| 40 | - <nut-button | ||
| 41 | - size="small" | ||
| 42 | - type="primary" | ||
| 43 | - @click="fetchOrderList" | ||
| 44 | - :loading="orderLoading" | ||
| 45 | - > | ||
| 46 | - 调用 | ||
| 47 | - </nut-button> | ||
| 48 | - </template> | ||
| 49 | - </nut-cell> | ||
| 50 | - | ||
| 51 | - <nut-cell v-if="orderList.length > 0" title="订单列表"> | ||
| 52 | - <view class="order-list"> | ||
| 53 | - <view | ||
| 54 | - v-for="order in orderList" | ||
| 55 | - :key="order.id" | ||
| 56 | - class="order-item" | ||
| 57 | - > | ||
| 58 | - <text>订单号: {{ order.order_no }}</text> | ||
| 59 | - <text>状态: {{ order.status }}</text> | ||
| 60 | - <text>金额: ¥{{ order.total_amount }}</text> | ||
| 61 | - </view> | ||
| 62 | - </view> | ||
| 63 | - </nut-cell> | ||
| 64 | - | ||
| 65 | - <nut-cell> | ||
| 66 | - <template #title> | ||
| 67 | - <view>获取订单详情</view> | ||
| 68 | - </template> | ||
| 69 | - <template #link> | ||
| 70 | - <nut-button | ||
| 71 | - size="small" | ||
| 72 | - type="primary" | ||
| 73 | - @click="fetchOrderDetail" | ||
| 74 | - :loading="detailLoading" | ||
| 75 | - > | ||
| 76 | - 调用 | ||
| 77 | - </nut-button> | ||
| 78 | - </template> | ||
| 79 | - </nut-cell> | ||
| 80 | - | ||
| 81 | - <nut-cell v-if="orderDetail" title="订单详情"> | ||
| 82 | - <view class="order-detail"> | ||
| 83 | - <text>订单号: {{ orderDetail.order_no }}</text> | ||
| 84 | - <text>商品数: {{ orderDetail.items?.length || 0 }}</text> | ||
| 85 | - </view> | ||
| 86 | - </nut-cell> | ||
| 87 | - </nut-cell-group> | ||
| 88 | - | ||
| 89 | - <!-- 使用说明 --> | ||
| 90 | - <nut-cell-group title="使用说明"> | ||
| 91 | - <nut-cell> | ||
| 92 | - <view class="instructions"> | ||
| 93 | - <text>1. 点击上方按钮调用 API</text> | ||
| 94 | - <text>2. API 文件位于 src/api/ 目录</text> | ||
| 95 | - <text>3. 由 docs/api-specs/ 文档自动生成</text> | ||
| 96 | - <text>4. 运行 pnpm api:generate 生成新 API</text> | ||
| 97 | - </view> | ||
| 98 | - </nut-cell> | ||
| 99 | - </nut-cell-group> | ||
| 100 | - </view> | ||
| 101 | -</template> | ||
| 102 | - | ||
| 103 | -<script setup> | ||
| 104 | -import { ref } from 'vue'; | ||
| 105 | -import Taro from '@tarojs/taro'; | ||
| 106 | -import { getUserInfoAPI } from '@/api/user'; | ||
| 107 | -import { getListAPI, getDetailAPI } from '@/api/order'; | ||
| 108 | - | ||
| 109 | -// 用户模块 | ||
| 110 | -const userInfo = ref(null); | ||
| 111 | -const userLoading = ref(false); | ||
| 112 | - | ||
| 113 | -// 订单模块 | ||
| 114 | -const orderList = ref([]); | ||
| 115 | -const orderLoading = ref(false); | ||
| 116 | -const orderDetail = ref(null); | ||
| 117 | -const detailLoading = ref(false); | ||
| 118 | - | ||
| 119 | -/** | ||
| 120 | - * 获取用户信息 | ||
| 121 | - */ | ||
| 122 | -const fetchUserInfo = async () => { | ||
| 123 | - userLoading.value = true; | ||
| 124 | - try { | ||
| 125 | - const result = await getUserInfoAPI(); | ||
| 126 | - | ||
| 127 | - if (result.code === 1) { | ||
| 128 | - userInfo.value = result.data.user; | ||
| 129 | - Taro.showToast({ | ||
| 130 | - title: '获取成功', | ||
| 131 | - icon: 'success', | ||
| 132 | - duration: 2000, | ||
| 133 | - }); | ||
| 134 | - } else { | ||
| 135 | - Taro.showToast({ | ||
| 136 | - title: result.msg || '获取失败', | ||
| 137 | - icon: 'error', | ||
| 138 | - duration: 2000, | ||
| 139 | - }); | ||
| 140 | - } | ||
| 141 | - } catch (error) { | ||
| 142 | - console.error('获取用户信息失败:', error); | ||
| 143 | - Taro.showToast({ | ||
| 144 | - title: '网络异常', | ||
| 145 | - icon: 'error', | ||
| 146 | - duration: 2000, | ||
| 147 | - }); | ||
| 148 | - } finally { | ||
| 149 | - userLoading.value = false; | ||
| 150 | - } | ||
| 151 | -}; | ||
| 152 | - | ||
| 153 | -/** | ||
| 154 | - * 获取订单列表 | ||
| 155 | - */ | ||
| 156 | -const fetchOrderList = async () => { | ||
| 157 | - orderLoading.value = true; | ||
| 158 | - try { | ||
| 159 | - const result = await getListAPI({ | ||
| 160 | - page: 1, | ||
| 161 | - pageSize: 10, | ||
| 162 | - }); | ||
| 163 | - | ||
| 164 | - if (result.code === 1) { | ||
| 165 | - orderList.value = result.data.list || []; | ||
| 166 | - Taro.showToast({ | ||
| 167 | - title: '获取成功', | ||
| 168 | - icon: 'success', | ||
| 169 | - duration: 2000, | ||
| 170 | - }); | ||
| 171 | - } else { | ||
| 172 | - Taro.showToast({ | ||
| 173 | - title: result.msg || '获取失败', | ||
| 174 | - icon: 'error', | ||
| 175 | - duration: 2000, | ||
| 176 | - }); | ||
| 177 | - } | ||
| 178 | - } catch (error) { | ||
| 179 | - console.error('获取订单列表失败:', error); | ||
| 180 | - Taro.showToast({ | ||
| 181 | - title: '网络异常', | ||
| 182 | - icon: 'error', | ||
| 183 | - duration: 2000, | ||
| 184 | - }); | ||
| 185 | - } finally { | ||
| 186 | - orderLoading.value = false; | ||
| 187 | - } | ||
| 188 | -}; | ||
| 189 | - | ||
| 190 | -/** | ||
| 191 | - * 获取订单详情 | ||
| 192 | - */ | ||
| 193 | -const fetchOrderDetail = async () => { | ||
| 194 | - detailLoading.value = true; | ||
| 195 | - try { | ||
| 196 | - const result = await getDetailAPI({ | ||
| 197 | - id: 123, // 示例订单ID | ||
| 198 | - }); | ||
| 199 | - | ||
| 200 | - if (result.code === 1) { | ||
| 201 | - orderDetail.value = result.data.order; | ||
| 202 | - Taro.showToast({ | ||
| 203 | - title: '获取成功', | ||
| 204 | - icon: 'success', | ||
| 205 | - duration: 2000, | ||
| 206 | - }); | ||
| 207 | - } else { | ||
| 208 | - Taro.showToast({ | ||
| 209 | - title: result.msg || '获取失败', | ||
| 210 | - icon: 'error', | ||
| 211 | - duration: 2000, | ||
| 212 | - }); | ||
| 213 | - } | ||
| 214 | - } catch (error) { | ||
| 215 | - console.error('获取订单详情失败:', error); | ||
| 216 | - Taro.showToast({ | ||
| 217 | - title: '网络异常', | ||
| 218 | - icon: 'error', | ||
| 219 | - duration: 2000, | ||
| 220 | - }); | ||
| 221 | - } finally { | ||
| 222 | - detailLoading.value = false; | ||
| 223 | - } | ||
| 224 | -}; | ||
| 225 | -</script> | ||
| 226 | - | ||
| 227 | -<style lang="less" scoped> | ||
| 228 | -.api-demo { | ||
| 229 | - padding: 20px; | ||
| 230 | - background-color: #f5f5f5; | ||
| 231 | - min-height: 100vh; | ||
| 232 | - | ||
| 233 | - .user-info, | ||
| 234 | - .order-list, | ||
| 235 | - .order-detail, | ||
| 236 | - .instructions { | ||
| 237 | - display: flex; | ||
| 238 | - flex-direction: column; | ||
| 239 | - gap: 10px; | ||
| 240 | - font-size: 14px; | ||
| 241 | - color: #666; | ||
| 242 | - | ||
| 243 | - text { | ||
| 244 | - display: block; | ||
| 245 | - } | ||
| 246 | - } | ||
| 247 | - | ||
| 248 | - .order-item { | ||
| 249 | - padding: 10px; | ||
| 250 | - background-color: #f9f9f9; | ||
| 251 | - border-radius: 4px; | ||
| 252 | - margin-bottom: 10px; | ||
| 253 | - } | ||
| 254 | -} | ||
| 255 | -</style> |
| 1 | import Taro from '@tarojs/taro' | 1 | import Taro from '@tarojs/taro' |
| 2 | import { routerStore } from '@/stores/router' | 2 | import { routerStore } from '@/stores/router' |
| 3 | import { buildApiUrl } from './tools' | 3 | import { buildApiUrl } from './tools' |
| 4 | +import { ENABLE_AUTH_MODE } from './config' | ||
| 4 | 5 | ||
| 5 | // 改进:添加全局状态变量注释 | 6 | // 改进:添加全局状态变量注释 |
| 6 | /** | 7 | /** |
| ... | @@ -58,6 +59,9 @@ export const saveCurrentPagePath = (custom_path) => { | ... | @@ -58,6 +59,9 @@ export const saveCurrentPagePath = (custom_path) => { |
| 58 | * @returns {boolean} true=已存在 sessionid,false=需要授权 | 59 | * @returns {boolean} true=已存在 sessionid,false=需要授权 |
| 59 | */ | 60 | */ |
| 60 | export const hasAuth = () => { | 61 | export const hasAuth = () => { |
| 62 | + // 如果禁用了授权模式,直接视为已授权 | ||
| 63 | + if (!ENABLE_AUTH_MODE) return true | ||
| 64 | + | ||
| 61 | try { | 65 | try { |
| 62 | const sessionid = Taro.getStorageSync('sessionid') | 66 | const sessionid = Taro.getStorageSync('sessionid') |
| 63 | return !!sessionid && sessionid !== '' | 67 | return !!sessionid && sessionid !== '' |
| ... | @@ -93,6 +97,11 @@ const extractCookie = (response) => { | ... | @@ -93,6 +97,11 @@ const extractCookie = (response) => { |
| 93 | * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果(会把 cookie 写入 storage 的 sessionid) | 97 | * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果(会把 cookie 写入 storage 的 sessionid) |
| 94 | */ | 98 | */ |
| 95 | export const refreshSession = async (options) => { | 99 | export const refreshSession = async (options) => { |
| 100 | + // 如果禁用了授权模式,直接返回模拟成功 | ||
| 101 | + if (!ENABLE_AUTH_MODE) { | ||
| 102 | + return { code: 1, msg: '授权模式已禁用', cookie: 'mock_session_id' } | ||
| 103 | + } | ||
| 104 | + | ||
| 96 | const show_loading = options?.show_loading !== false | 105 | const show_loading = options?.show_loading !== false |
| 97 | 106 | ||
| 98 | // 已有授权进行中时,直接复用同一个 Promise | 107 | // 已有授权进行中时,直接复用同一个 Promise |
| ... | @@ -254,6 +263,9 @@ const NAVIGATING_RESET_DELAY_MS = 300 | ... | @@ -254,6 +263,9 @@ const NAVIGATING_RESET_DELAY_MS = 300 |
| 254 | * @returns {Promise<void>} 无返回值 | 263 | * @returns {Promise<void>} 无返回值 |
| 255 | */ | 264 | */ |
| 256 | export const navigateToAuth = async (return_path) => { | 265 | export const navigateToAuth = async (return_path) => { |
| 266 | + // 如果禁用了授权模式,直接返回不跳转 | ||
| 267 | + if (!ENABLE_AUTH_MODE) return | ||
| 268 | + | ||
| 257 | const pages = Taro.getCurrentPages() | 269 | const pages = Taro.getCurrentPages() |
| 258 | const current_page = pages[pages.length - 1] | 270 | const current_page = pages[pages.length - 1] |
| 259 | const current_route = current_page?.route | 271 | const current_route = current_page?.route | ... | ... |
| 1 | /* | 1 | /* |
| 2 | + * @Date: 2026-01-29 10:48:35 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-01-29 17:56:45 | ||
| 5 | + * @FilePath: /manulife-weapp/src/utils/config.js | ||
| 6 | + * @Description: 文件描述 | ||
| 7 | + */ | ||
| 8 | +/* | ||
| 2 | * @Description: 服务器环境配置 | 9 | * @Description: 服务器环境配置 |
| 3 | * @Template: 请根据实际项目修改 BASE_URL 和 REQUEST_DEFAULT_PARAMS | 10 | * @Template: 请根据实际项目修改 BASE_URL 和 REQUEST_DEFAULT_PARAMS |
| 4 | */ | 11 | */ |
| ... | @@ -23,4 +30,11 @@ export const REQUEST_DEFAULT_PARAMS = { | ... | @@ -23,4 +30,11 @@ export const REQUEST_DEFAULT_PARAMS = { |
| 23 | client_name: 'YOUR_APP', // 🔧 修改为应用名称 | 30 | client_name: 'YOUR_APP', // 🔧 修改为应用名称 |
| 24 | } | 31 | } |
| 25 | 32 | ||
| 33 | +/** | ||
| 34 | + * @description 是否启用授权模式 | ||
| 35 | + * - true: 启用授权检查、自动跳转登录、401自动续期 | ||
| 36 | + * - false: 禁用所有授权相关功能(所有授权检查直接通过,不跳转登录页) | ||
| 37 | + */ | ||
| 38 | +export const ENABLE_AUTH_MODE = false | ||
| 39 | + | ||
| 26 | export default BASE_URL | 40 | export default BASE_URL | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2022-09-19 14:11:06 | 2 | * @Date: 2022-09-19 14:11:06 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-13 21:29:43 | 4 | + * @LastEditTime: 2026-01-29 18:35:55 |
| 5 | - * @FilePath: /xyxBooking-weapp/src/utils/request.js | 5 | + * @FilePath: /manulife-weapp/src/utils/request.js |
| 6 | * @Description: 简单axios封装,后续按实际处理 | 6 | * @Description: 简单axios封装,后续按实际处理 |
| 7 | */ | 7 | */ |
| 8 | // import axios from 'axios' | 8 | // import axios from 'axios' |
| ... | @@ -11,8 +11,6 @@ import Taro from '@tarojs/taro' | ... | @@ -11,8 +11,6 @@ import Taro from '@tarojs/taro' |
| 11 | // import qs from 'qs' | 11 | // import qs from 'qs' |
| 12 | // import { strExist } from './tools' | 12 | // import { strExist } from './tools' |
| 13 | import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect' | 13 | import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect' |
| 14 | -import { has_offline_booking_cache } from '@/composables/useOfflineBookingCache' | ||
| 15 | -import { get_weak_network_modal_no_cache_options } from '@/utils/uiText' | ||
| 16 | import { parseQueryString } from './tools' | 14 | import { parseQueryString } from './tools' |
| 17 | 15 | ||
| 18 | // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress'; | 16 | // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress'; |
| ... | @@ -136,34 +134,22 @@ const should_handle_bad_network = async (error) => { | ... | @@ -136,34 +134,22 @@ const should_handle_bad_network = async (error) => { |
| 136 | 134 | ||
| 137 | /** | 135 | /** |
| 138 | * @description 处理请求超时/弱网错误 | 136 | * @description 处理请求超时/弱网错误 |
| 139 | - * - 优先:若存在离线预约记录缓存,直接跳转离线预约列表页 | 137 | + * - 弹出弱网提示(统一文案由 uiText 管理) |
| 140 | - * - 否则:弹出弱网提示(统一文案由 uiText 管理) | ||
| 141 | * @returns {Promise<void>} 无返回值 | 138 | * @returns {Promise<void>} 无返回值 |
| 142 | */ | 139 | */ |
| 143 | const handle_request_timeout = async () => { | 140 | const handle_request_timeout = async () => { |
| 144 | if (has_shown_timeout_modal) return | 141 | if (has_shown_timeout_modal) return |
| 145 | has_shown_timeout_modal = true | 142 | has_shown_timeout_modal = true |
| 146 | 143 | ||
| 147 | - const pages = Taro.getCurrentPages ? Taro.getCurrentPages() : [] | 144 | + // 提示用户检查网络连接 |
| 148 | - const current_page = pages && pages.length ? pages[pages.length - 1] : null | ||
| 149 | - const current_route = current_page?.route || '' | ||
| 150 | - if (String(current_route).includes('pages/offlineBookingList/index')) return | ||
| 151 | - | ||
| 152 | - // 若有离线预约记录缓存,则跳转至离线预约列表页 | ||
| 153 | - if (has_offline_booking_cache()) { | ||
| 154 | - try { | ||
| 155 | - await Taro.reLaunch({ url: '/pages/offlineBookingList/index' }) | ||
| 156 | - } catch (e) { | ||
| 157 | - console.error('reLaunch offlineBookingList failed:', e) | ||
| 158 | - } | ||
| 159 | - return | ||
| 160 | - } | ||
| 161 | - | ||
| 162 | - // 否则提示用户检查网络连接 | ||
| 163 | try { | 145 | try { |
| 164 | - await Taro.showModal(get_weak_network_modal_no_cache_options()) | 146 | + await Taro.showToast({ |
| 147 | + title: '网络连接异常,请检查网络设置', | ||
| 148 | + icon: 'none', | ||
| 149 | + duration: 2000 | ||
| 150 | + }) | ||
| 165 | } catch (e) { | 151 | } catch (e) { |
| 166 | - console.error('show weak network modal failed:', e) | 152 | + console.error('show weak network toast failed:', e) |
| 167 | } | 153 | } |
| 168 | } | 154 | } |
| 169 | 155 | ||
| ... | @@ -239,7 +225,7 @@ service.interceptors.response.use( | ... | @@ -239,7 +225,7 @@ service.interceptors.response.use( |
| 239 | const res = response.data | 225 | const res = response.data |
| 240 | 226 | ||
| 241 | // 401 未授权处理 | 227 | // 401 未授权处理 |
| 242 | - if (res.code === 401) { | 228 | + if (res.code === 401 && ENABLE_AUTH_MODE) { |
| 243 | const config = response?.config || {} | 229 | const config = response?.config || {} |
| 244 | /** | 230 | /** |
| 245 | * 避免死循环/重复重试: | 231 | * 避免死循环/重复重试: | ... | ... |
src/utils/uiText.js
deleted
100644 → 0
| 1 | -/* | ||
| 2 | - * @Date: 2026-01-13 21:28:45 | ||
| 3 | - * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | - * @LastEditTime: 2026-01-15 19:24:57 | ||
| 5 | - * @FilePath: /xyxBooking-weapp/src/utils/uiText.js | ||
| 6 | - * @Description: 弱网络提示文本 | ||
| 7 | - */ | ||
| 8 | -/** | ||
| 9 | - * @description 弱网/断网统一文案 | ||
| 10 | - * - toast/modal/banner 等入口统一引用,避免多处硬编码 | ||
| 11 | - */ | ||
| 12 | -export const weak_network_text = { | ||
| 13 | - title: '网络连接不畅', | ||
| 14 | - toast_title: '网络连接不畅', | ||
| 15 | - banner_desc: '网络开小差啦!请检查网络设置,或更换位置后重新进入小程序~', | ||
| 16 | - offline_page_desc: '当前网络信号较弱,已自动为您切换至离线模式', | ||
| 17 | - modal_no_cache_content: '当前网络信号较弱,暂无法使用小程序,请检查网络设置,或更换位置后重新进入小程序~', | ||
| 18 | - modal_use_cache_content: '当前网络信号较弱,可使用已缓存的预约记录进入离线模式', | ||
| 19 | - modal_go_offline_records_content: '当前网络信号较弱,是否进入离线预约记录?', | ||
| 20 | - offline_mode_no_booking_toast: '当前为离线模式,无法预约', | ||
| 21 | - confirm_ok: '知道了', | ||
| 22 | - confirm_booking_records: '预约记录', | ||
| 23 | - confirm_offline_records: '离线记录', | ||
| 24 | -} | ||
| 25 | - | ||
| 26 | -/** | ||
| 27 | - * @description: 获取弱网络提示弹窗配置(无缓存) | ||
| 28 | - * @returns {Object} | ||
| 29 | - */ | ||
| 30 | -export const get_weak_network_modal_no_cache_options = () => { | ||
| 31 | - return { | ||
| 32 | - title: weak_network_text.title, | ||
| 33 | - content: weak_network_text.modal_no_cache_content, | ||
| 34 | - confirmText: weak_network_text.confirm_ok, | ||
| 35 | - showCancel: false, | ||
| 36 | - } | ||
| 37 | -} | ||
| 38 | - | ||
| 39 | -/** | ||
| 40 | - * @description: 获取弱网络提示弹窗配置(有缓存) | ||
| 41 | - * @returns {Object} | ||
| 42 | - */ | ||
| 43 | -export const get_weak_network_modal_use_cache_options = () => { | ||
| 44 | - return { | ||
| 45 | - title: weak_network_text.title, | ||
| 46 | - content: weak_network_text.modal_use_cache_content, | ||
| 47 | - confirmText: weak_network_text.confirm_booking_records, | ||
| 48 | - cancelText: weak_network_text.confirm_ok, | ||
| 49 | - } | ||
| 50 | -} | ||
| 51 | - | ||
| 52 | -/** | ||
| 53 | - * @description: 获取弱网络提示弹窗配置(进入离线预约记录) | ||
| 54 | - * @returns {Object} | ||
| 55 | - */ | ||
| 56 | -export const get_weak_network_modal_go_offline_records_options = () => { | ||
| 57 | - return { | ||
| 58 | - title: weak_network_text.title, | ||
| 59 | - content: weak_network_text.modal_go_offline_records_content, | ||
| 60 | - confirmText: weak_network_text.confirm_offline_records, | ||
| 61 | - cancelText: weak_network_text.confirm_ok, | ||
| 62 | - } | ||
| 63 | -} |
-
Please register or login to post a comment