hookehuyr

feat(离线模式): 添加弱网检测和离线预约码功能

实现弱网检测机制,当网络不可用时自动切换至离线模式
新增离线预约码页面和组件,支持本地缓存和二维码生成
添加网络状态监听和预约码数据预加载功能
更新文档说明离线模式的使用流程和实现细节
...@@ -15,6 +15,7 @@ declare module 'vue' { ...@@ -15,6 +15,7 @@ declare module 'vue' {
15 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem'] 15 NutFormItem: typeof import('@nutui/nutui-taro')['FormItem']
16 NutInput: typeof import('@nutui/nutui-taro')['Input'] 16 NutInput: typeof import('@nutui/nutui-taro')['Input']
17 NutPopup: typeof import('@nutui/nutui-taro')['Popup'] 17 NutPopup: typeof import('@nutui/nutui-taro')['Popup']
18 + OfflineQrCode: typeof import('./src/components/offlineQrCode.vue')['default']
18 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default'] 19 Picker: typeof import('./src/components/time-picker-data/picker.vue')['default']
19 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default'] 20 PosterBuilder: typeof import('./src/components/PosterBuilder/index.vue')['default']
20 QrCode: typeof import('./src/components/qrCode.vue')['default'] 21 QrCode: typeof import('./src/components/qrCode.vue')['default']
......
1 +1. 如果判断是没有网络或者长时间无法加载出页面, 判定进入弱网模式.
2 +2. 如果是弱网模式, 用户打开一个新页面, 文字描述提示用户网络不好, 进入离线模式, 中间有一个圆形按钮文字是预约码, 用户点击进入新的离线版预约版页面.
3 +3. 新做一个离线版预约码组件 仿照/components/qrCode.vue, 但是功能需要调整, 因为是离线状态, 所以刷新功能是要删除掉的, 对应qrcodeStatusAPI接口查询二维码相关功能也要删除掉, 不会显示二维码的状态, billPersonAPI接口相关功能也要删除掉, 因为不会有payId传进来. 也不会有轮询的功能. 也不会有查看预约记录的显示, 当有网情况下进入时就需要把qrcodeListAPI接口数据保存到本地, 如果判断是没有网络或者长时间无法加载出页面就要跳转到, 一个结构和内容类似于bookingCode的离线版新页面, 因为没有网络不能使用'/admin?m=srv&a=get_qrcode&key=' + item.qr_code的方式获取二维码,需要找一个生成二维码的库本地生成二维码.
4 +4. 现在接口没有数据, 先帮我mock数据, 数据格式和qrcodeListAPI接口返回的格式一致.
1 -2. 关键功能模块实现(微信小程序示例)
2 -以下是核心代码实现,基于微信小程序的 wx.getNetworkType 检测网络、wx.setStorageSync 本地存储、自有后端同步的方案:
3 -```javascript
4 -// pages/verify/verify.js
5 -// 下面是主要思路和实现, 需要修改的是扫描成功之后需要跳转到另一个页面显示核销成功/失败的提示
6 -// 1. 调用 scanCodeVerify 方法开始扫码核销
7 -// 2. 扫码成功后,调用 handleVerify 方法处理核销逻辑
8 -// 3. 在 handleVerify 方法中,先检测网络状态
9 -// 4. 如果有网,实时校验核销码并调用后端核销接口
10 -// 5. 如果无网,先校验本地存储中是否有该核销码的记录
11 -// 6. 如果有记录,说明之前核销过,提示用户已核销
12 -// 7. 如果无记录,提示用户无网络,无法核销
13 -// 8. 核销成功后,同步更新本地记录(可选)
14 -Page({
15 - /**
16 - * 扫码核销核心方法
17 - */
18 - scanCodeVerify() {
19 - // 1. 调起微信扫码API
20 - wx.scanCode({
21 - onlyFromCamera: true, // 仅允许从相机扫码
22 - scanType: ['qrCode'], // 仅识别二维码
23 - success: (res) => {
24 - const verifyCode = res.result; // 扫码获取的核销码
25 - this.handleVerify(verifyCode);
26 - },
27 - fail: (err) => {
28 - wx.showToast({ title: '扫码失败,请重试', icon: 'none' });
29 - console.error('扫码失败:', err);
30 - }
31 - });
32 - },
33 -
34 - /**
35 - * 处理核销逻辑(核心)
36 - * @param {string} verifyCode 核销码
37 - */
38 - handleVerify(verifyCode) {
39 - wx.showLoading({ title: '核销中...' });
40 -
41 - // 第一步:检测网络状态
42 - wx.getNetworkType({
43 - success: (networkRes) => {
44 - const networkType = networkRes.networkType;
45 - // 判断是否有网(wifi/4g/5g为有网,none/unknown为无网)
46 - if (['wifi', '4g', '5g', '3g', '2g'].includes(networkType)) {
47 - // 有网场景:实时校验+核销
48 - this.verifyOnline(verifyCode);
49 - } else {
50 - // 无网场景:本地校验+离线核销
51 - this.verifyOffline(verifyCode);
52 - }
53 - },
54 - fail: () => {
55 - // 网络检测失败,默认走离线逻辑
56 - this.verifyOffline(verifyCode);
57 - wx.hideLoading();
58 - }
59 - });
60 - },
61 -
62 - /**
63 - * 有网核销逻辑
64 - * @param {string} verifyCode 核销码
65 - */
66 - verifyOnline(verifyCode) {
67 - wx.request({
68 - url: 'https://你的服务器域名/api/verify', // 后端核销接口
69 - method: 'POST',
70 - data: { verifyCode },
71 - success: (res) => {
72 - wx.hideLoading();
73 - if (res.data.code === 200) {
74 - wx.showToast({ title: '核销成功', icon: 'success' });
75 - // 同步本地预约状态(可选)
76 - this.updateLocalVerifyStatus(verifyCode, true);
77 - } else {
78 - wx.showToast({ title: res.data.msg || '核销失败', icon: 'none' });
79 - }
80 - },
81 - fail: (err) => {
82 - wx.hideLoading();
83 - // 网络请求失败,降级到离线核销
84 - wx.showToast({ title: '网络不稳定,将使用离线核销', icon: 'none' });
85 - setTimeout(() => {
86 - this.verifyOffline(verifyCode);
87 - }, 1500);
88 - console.error('在线核销失败:', err);
89 - }
90 - });
91 - },
92 -
93 - /**
94 - * 无网核销逻辑
95 - * @param {string} verifyCode 核销码
96 - */
97 - verifyOffline(verifyCode) {
98 - wx.hideLoading();
99 - // 1. 读取本地缓存的预约数据(需提前同步到本地)
100 - const localAppointments = wx.getStorageSync('localAppointments') || [];
101 - // 2. 本地校验核销码是否有效
102 - const targetAppointment = localAppointments.find(item => item.verifyCode === verifyCode);
103 -
104 - if (!targetAppointment) {
105 - wx.showToast({ title: '未找到该预约记录', icon: 'none' });
106 - return;
107 - }
108 - if (targetAppointment.isVerified) {
109 - wx.showToast({ title: '该预约已核销', icon: 'none' });
110 - return;
111 - }
112 -
113 - // 3. 本地核销:更新本地状态+记录离线操作
114 - targetAppointment.isVerified = true;
115 - const offlineVerifyRecords = wx.getStorageSync('offlineVerifyRecords') || [];
116 - offlineVerifyRecords.push({
117 - verifyCode,
118 - appointmentId: targetAppointment.id,
119 - verifyTime: new Date().getTime(), // 核销时间戳
120 - status: 'pending' // 待同步
121 - });
122 -
123 - // 4. 保存本地修改
124 - wx.setStorageSync('localAppointments', localAppointments);
125 - wx.setStorageSync('offlineVerifyRecords', offlineVerifyRecords);
126 -
127 - wx.showToast({ title: '离线核销成功,网络恢复后自动同步', icon: 'success' });
128 - },
129 -
130 - /**
131 - * 网络恢复后同步离线核销记录
132 - */
133 - syncOfflineRecords() {
134 - // 监听网络状态变化
135 - wx.onNetworkStatusChange((res) => {
136 - if (res.isConnected) {
137 - const offlineRecords = wx.getStorageSync('offlineVerifyRecords') || [];
138 - if (offlineRecords.length === 0) return;
139 -
140 - // 批量同步离线记录到服务器
141 - wx.request({
142 - url: 'https://你的服务器域名/api/syncOfflineVerify',
143 - method: 'POST',
144 - data: { records: offlineRecords },
145 - success: (res) => {
146 - if (res.data.code === 200) {
147 - // 同步成功:清空离线记录
148 - wx.setStorageSync('offlineVerifyRecords', []);
149 - wx.showToast({ title: '离线核销记录已同步', icon: 'success' });
150 - }
151 - },
152 - fail: (err) => {
153 - console.error('离线记录同步失败:', err);
154 - }
155 - });
156 - }
157 - });
158 - },
159 -
160 - /**
161 - * 提前同步预约数据到本地(页面加载时执行)
162 - */
163 - syncAppointmentsToLocal() {
164 - // 页面初始化/小程序启动时,拉取预约数据到本地
165 - wx.request({
166 - url: 'https://你的服务器域名/api/getAppointments',
167 - success: (res) => {
168 - if (res.data.code === 200) {
169 - wx.setStorageSync('localAppointments', res.data.data);
170 - }
171 - },
172 - fail: () => {
173 - // 网络失败则使用上次缓存的本地数据
174 - console.log('使用本地缓存的预约数据');
175 - }
176 - });
177 - },
178 -
179 - /**
180 - * 更新本地核销状态(辅助方法)
181 - */
182 - updateLocalVerifyStatus(verifyCode, isVerified) {
183 - const localAppointments = wx.getStorageSync('localAppointments') || [];
184 - const index = localAppointments.findIndex(item => item.verifyCode === verifyCode);
185 - if (index > -1) {
186 - localAppointments[index].isVerified = isVerified;
187 - wx.setStorageSync('localAppointments', localAppointments);
188 - }
189 - },
190 -
191 - /**
192 - * 页面加载时初始化
193 - */
194 - onLoad(options) {
195 - // 1. 同步预约数据到本地
196 - this.syncAppointmentsToLocal();
197 - // 2. 监听网络状态,同步离线记录
198 - this.syncOfflineRecords();
199 - // 3. 主动检查一次是否有未同步的离线记录
200 - wx.getNetworkType({
201 - success: (res) => {
202 - if (['wifi', '4g', '5g'].includes(res.networkType)) {
203 - this.syncOfflineRecords();
204 - }
205 - }
206 - });
207 - }
208 -});
209 -```
210 -3. 配套设计要点
211 -- 本地数据预处理
212 - - 小程序启动 / 核销页面加载时,主动拉取当前门店 / 账号下的待核销预约数据,缓存到本地(wx.setStorageSync 或 wx.setStorage);
213 - - 本地数据需包含:预约 ID、核销码、用户信息、预约时间、核销状态等核心字段,避免冗余。
214 -- 防重复核销设计
215 - - 本地记录核销码的核销状态(isVerified),即使无网也能避免重复核销;
216 - - 服务器端需做最终校验,同步离线记录时若发现已核销,返回提示并更新本地状态。
217 -- 数据安全与容错
218 - - 本地存储的核销记录需加简单加密(如 base64 + 时间戳),防止篡改;
219 - - 离线记录同步时,服务器需校验核销码有效性、时间范围(如预约有效期),避免无效核销;
220 - - 小程序退出 / 重启后,保留本地缓存(微信小程序本地缓存默认长期有效,可设置过期时间)。
221 -- 用户体验优化
222 - - 无网核销时,明确提示 “离线核销成功,网络恢复后自动同步”;
223 - - 网络恢复后自动同步,无需用户手动操作;
224 - - 提供 “离线记录查询” 入口,方便查看未同步的核销记录。
225 -
226 -
227 -总结
228 - 核心逻辑:无网时依赖本地缓存的预约数据完成核销,记录离线操作;有网时实时核销,网络恢复后自动同步离线记录;
229 - 关键保障:提前同步待核销数据到本地、本地标记核销状态防重复、服务器端做最终校验;
230 - 体验优化:明确的网络状态提示、自动同步机制,减少用户感知网络差异。
...@@ -56,6 +56,7 @@ ...@@ -56,6 +56,7 @@
56 "axios-miniprogram": "^2.7.2", 56 "axios-miniprogram": "^2.7.2",
57 "dayjs": "^1.11.19", 57 "dayjs": "^1.11.19",
58 "pinia": "^3.0.3", 58 "pinia": "^3.0.3",
59 + "qrcode": "^1.5.4",
59 "qs": "^6.14.1", 60 "qs": "^6.14.1",
60 "taro-plugin-pinia": "^1.0.0", 61 "taro-plugin-pinia": "^1.0.0",
61 "vue": "^3.3.0", 62 "vue": "^3.3.0",
......
...@@ -68,6 +68,9 @@ importers: ...@@ -68,6 +68,9 @@ importers:
68 pinia: 68 pinia:
69 specifier: ^3.0.3 69 specifier: ^3.0.3
70 version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)) 70 version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
71 + qrcode:
72 + specifier: ^1.5.4
73 + version: 1.5.4
71 qs: 74 qs:
72 specifier: ^6.14.1 75 specifier: ^6.14.1
73 version: 6.14.1 76 version: 6.14.1
...@@ -2751,6 +2754,9 @@ packages: ...@@ -2751,6 +2754,9 @@ packages:
2751 resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} 2754 resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
2752 engines: {node: '>= 10'} 2755 engines: {node: '>= 10'}
2753 2756
2757 + cliui@6.0.0:
2758 + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
2759 +
2754 cliui@7.0.4: 2760 cliui@7.0.4:
2755 resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} 2761 resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
2756 2762
...@@ -3076,6 +3082,10 @@ packages: ...@@ -3076,6 +3082,10 @@ packages:
3076 supports-color: 3082 supports-color:
3077 optional: true 3083 optional: true
3078 3084
3085 + decamelize@1.2.0:
3086 + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
3087 + engines: {node: '>=0.10.0'}
3088 +
3079 decimal.js@10.6.0: 3089 decimal.js@10.6.0:
3080 resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} 3090 resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
3081 3091
...@@ -3186,6 +3196,9 @@ packages: ...@@ -3186,6 +3196,9 @@ packages:
3186 didyoumean@1.2.2: 3196 didyoumean@1.2.2:
3187 resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 3197 resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
3188 3198
3199 + dijkstrajs@1.0.3:
3200 + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
3201 +
3189 dingtalk-jsapi@2.15.6: 3202 dingtalk-jsapi@2.15.6:
3190 resolution: {integrity: sha512-804mFz2AFV/H9ysmo7dLqMjSGOQgREsgQIuep+Xg+yNQeQtnUOYntElEzlB798Sj/691e4mMKz9mtQ7v9qdjuA==} 3203 resolution: {integrity: sha512-804mFz2AFV/H9ysmo7dLqMjSGOQgREsgQIuep+Xg+yNQeQtnUOYntElEzlB798Sj/691e4mMKz9mtQ7v9qdjuA==}
3191 3204
...@@ -3617,6 +3630,10 @@ packages: ...@@ -3617,6 +3630,10 @@ packages:
3617 resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} 3630 resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==}
3618 engines: {node: '>=6'} 3631 engines: {node: '>=6'}
3619 3632
3633 + find-up@4.1.0:
3634 + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
3635 + engines: {node: '>=8'}
3636 +
3620 find-up@5.0.0: 3637 find-up@5.0.0:
3621 resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 3638 resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
3622 engines: {node: '>=10'} 3639 engines: {node: '>=10'}
...@@ -4512,6 +4529,10 @@ packages: ...@@ -4512,6 +4529,10 @@ packages:
4512 resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} 4529 resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==}
4513 engines: {node: '>=6'} 4530 engines: {node: '>=6'}
4514 4531
4532 + locate-path@5.0.0:
4533 + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
4534 + engines: {node: '>=8'}
4535 +
4515 locate-path@6.0.0: 4536 locate-path@6.0.0:
4516 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 4537 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
4517 engines: {node: '>=10'} 4538 engines: {node: '>=10'}
...@@ -4912,6 +4933,10 @@ packages: ...@@ -4912,6 +4933,10 @@ packages:
4912 resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} 4933 resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==}
4913 engines: {node: '>=6'} 4934 engines: {node: '>=6'}
4914 4935
4936 + p-locate@4.1.0:
4937 + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
4938 + engines: {node: '>=8'}
4939 +
4915 p-locate@5.0.0: 4940 p-locate@5.0.0:
4916 resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 4941 resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
4917 engines: {node: '>=10'} 4942 engines: {node: '>=10'}
...@@ -5084,6 +5109,10 @@ packages: ...@@ -5084,6 +5109,10 @@ packages:
5084 platform@1.3.6: 5109 platform@1.3.6:
5085 resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} 5110 resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
5086 5111
5112 + pngjs@5.0.0:
5113 + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
5114 + engines: {node: '>=10.13.0'}
5115 +
5087 possible-typed-array-names@1.1.0: 5116 possible-typed-array-names@1.1.0:
5088 resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} 5117 resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
5089 engines: {node: '>= 0.4'} 5118 engines: {node: '>= 0.4'}
...@@ -5611,6 +5640,11 @@ packages: ...@@ -5611,6 +5640,11 @@ packages:
5611 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 5640 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
5612 engines: {node: '>=6'} 5641 engines: {node: '>=6'}
5613 5642
5643 + qrcode@1.5.4:
5644 + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
5645 + engines: {node: '>=10.13.0'}
5646 + hasBin: true
5647 +
5614 qs@6.14.1: 5648 qs@6.14.1:
5615 resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==} 5649 resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
5616 engines: {node: '>=0.6'} 5650 engines: {node: '>=0.6'}
...@@ -5734,6 +5768,9 @@ packages: ...@@ -5734,6 +5768,9 @@ packages:
5734 resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} 5768 resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
5735 engines: {node: '>=0.10.0'} 5769 engines: {node: '>=0.10.0'}
5736 5770
5771 + require-main-filename@2.0.0:
5772 + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
5773 +
5737 requires-port@1.0.0: 5774 requires-port@1.0.0:
5738 resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} 5775 resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
5739 5776
...@@ -5928,6 +5965,9 @@ packages: ...@@ -5928,6 +5965,9 @@ packages:
5928 resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} 5965 resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
5929 engines: {node: '>= 0.8.0'} 5966 engines: {node: '>= 0.8.0'}
5930 5967
5968 + set-blocking@2.0.0:
5969 + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
5970 +
5931 set-function-length@1.2.2: 5971 set-function-length@1.2.2:
5932 resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} 5972 resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
5933 engines: {node: '>= 0.4'} 5973 engines: {node: '>= 0.4'}
...@@ -6655,6 +6695,9 @@ packages: ...@@ -6655,6 +6695,9 @@ packages:
6655 resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} 6695 resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
6656 engines: {node: '>= 0.4'} 6696 engines: {node: '>= 0.4'}
6657 6697
6698 + which-module@2.0.1:
6699 + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
6700 +
6658 which-typed-array@1.1.19: 6701 which-typed-array@1.1.19:
6659 resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} 6702 resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
6660 engines: {node: '>= 0.4'} 6703 engines: {node: '>= 0.4'}
...@@ -6715,6 +6758,9 @@ packages: ...@@ -6715,6 +6758,9 @@ packages:
6715 xxhashjs@0.2.2: 6758 xxhashjs@0.2.2:
6716 resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==} 6759 resolution: {integrity: sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==}
6717 6760
6761 + y18n@4.0.3:
6762 + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
6763 +
6718 y18n@5.0.8: 6764 y18n@5.0.8:
6719 resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 6765 resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
6720 engines: {node: '>=10'} 6766 engines: {node: '>=10'}
...@@ -6727,10 +6773,18 @@ packages: ...@@ -6727,10 +6773,18 @@ packages:
6727 engines: {node: '>= 14.6'} 6773 engines: {node: '>= 14.6'}
6728 hasBin: true 6774 hasBin: true
6729 6775
6776 + yargs-parser@18.1.3:
6777 + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
6778 + engines: {node: '>=6'}
6779 +
6730 yargs-parser@20.2.9: 6780 yargs-parser@20.2.9:
6731 resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} 6781 resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
6732 engines: {node: '>=10'} 6782 engines: {node: '>=10'}
6733 6783
6784 + yargs@15.4.1:
6785 + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
6786 + engines: {node: '>=8'}
6787 +
6734 yargs@16.2.0: 6788 yargs@16.2.0:
6735 resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} 6789 resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
6736 engines: {node: '>=10'} 6790 engines: {node: '>=10'}
...@@ -9724,6 +9778,12 @@ snapshots: ...@@ -9724,6 +9778,12 @@ snapshots:
9724 9778
9725 cli-width@3.0.0: {} 9779 cli-width@3.0.0: {}
9726 9780
9781 + cliui@6.0.0:
9782 + dependencies:
9783 + string-width: 4.2.3
9784 + strip-ansi: 6.0.1
9785 + wrap-ansi: 6.2.0
9786 +
9727 cliui@7.0.4: 9787 cliui@7.0.4:
9728 dependencies: 9788 dependencies:
9729 string-width: 4.2.3 9789 string-width: 4.2.3
...@@ -10065,6 +10125,8 @@ snapshots: ...@@ -10065,6 +10125,8 @@ snapshots:
10065 dependencies: 10125 dependencies:
10066 ms: 2.1.3 10126 ms: 2.1.3
10067 10127
10128 + decamelize@1.2.0: {}
10129 +
10068 decimal.js@10.6.0: {} 10130 decimal.js@10.6.0: {}
10069 10131
10070 decode-uri-component@0.2.2: {} 10132 decode-uri-component@0.2.2: {}
...@@ -10171,6 +10233,8 @@ snapshots: ...@@ -10171,6 +10233,8 @@ snapshots:
10171 10233
10172 didyoumean@1.2.2: {} 10234 didyoumean@1.2.2: {}
10173 10235
10236 + dijkstrajs@1.0.3: {}
10237 +
10174 dingtalk-jsapi@2.15.6: 10238 dingtalk-jsapi@2.15.6:
10175 dependencies: 10239 dependencies:
10176 promise-polyfill: 7.1.2 10240 promise-polyfill: 7.1.2
...@@ -10811,6 +10875,11 @@ snapshots: ...@@ -10811,6 +10875,11 @@ snapshots:
10811 dependencies: 10875 dependencies:
10812 locate-path: 3.0.0 10876 locate-path: 3.0.0
10813 10877
10878 + find-up@4.1.0:
10879 + dependencies:
10880 + locate-path: 5.0.0
10881 + path-exists: 4.0.0
10882 +
10814 find-up@5.0.0: 10883 find-up@5.0.0:
10815 dependencies: 10884 dependencies:
10816 locate-path: 6.0.0 10885 locate-path: 6.0.0
...@@ -11776,6 +11845,10 @@ snapshots: ...@@ -11776,6 +11845,10 @@ snapshots:
11776 p-locate: 3.0.0 11845 p-locate: 3.0.0
11777 path-exists: 3.0.0 11846 path-exists: 3.0.0
11778 11847
11848 + locate-path@5.0.0:
11849 + dependencies:
11850 + p-locate: 4.1.0
11851 +
11779 locate-path@6.0.0: 11852 locate-path@6.0.0:
11780 dependencies: 11853 dependencies:
11781 p-locate: 5.0.0 11854 p-locate: 5.0.0
...@@ -12149,6 +12222,10 @@ snapshots: ...@@ -12149,6 +12222,10 @@ snapshots:
12149 dependencies: 12222 dependencies:
12150 p-limit: 2.3.0 12223 p-limit: 2.3.0
12151 12224
12225 + p-locate@4.1.0:
12226 + dependencies:
12227 + p-limit: 2.3.0
12228 +
12152 p-locate@5.0.0: 12229 p-locate@5.0.0:
12153 dependencies: 12230 dependencies:
12154 p-limit: 3.1.0 12231 p-limit: 3.1.0
...@@ -12297,6 +12374,8 @@ snapshots: ...@@ -12297,6 +12374,8 @@ snapshots:
12297 12374
12298 platform@1.3.6: {} 12375 platform@1.3.6: {}
12299 12376
12377 + pngjs@5.0.0: {}
12378 +
12300 possible-typed-array-names@1.1.0: {} 12379 possible-typed-array-names@1.1.0: {}
12301 12380
12302 postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6): 12381 postcss-attribute-case-insensitive@7.0.1(postcss@8.5.6):
...@@ -12858,6 +12937,12 @@ snapshots: ...@@ -12858,6 +12937,12 @@ snapshots:
12858 12937
12859 punycode@2.3.1: {} 12938 punycode@2.3.1: {}
12860 12939
12940 + qrcode@1.5.4:
12941 + dependencies:
12942 + dijkstrajs: 1.0.3
12943 + pngjs: 5.0.0
12944 + yargs: 15.4.1
12945 +
12861 qs@6.14.1: 12946 qs@6.14.1:
12862 dependencies: 12947 dependencies:
12863 side-channel: 1.1.0 12948 side-channel: 1.1.0
...@@ -13004,6 +13089,8 @@ snapshots: ...@@ -13004,6 +13089,8 @@ snapshots:
13004 13089
13005 require-from-string@2.0.2: {} 13090 require-from-string@2.0.2: {}
13006 13091
13092 + require-main-filename@2.0.0: {}
13093 +
13007 requires-port@1.0.0: {} 13094 requires-port@1.0.0: {}
13008 13095
13009 resolve-from@4.0.0: {} 13096 resolve-from@4.0.0: {}
...@@ -13225,6 +13312,8 @@ snapshots: ...@@ -13225,6 +13312,8 @@ snapshots:
13225 transitivePeerDependencies: 13312 transitivePeerDependencies:
13226 - supports-color 13313 - supports-color
13227 13314
13315 + set-blocking@2.0.0: {}
13316 +
13228 set-function-length@1.2.2: 13317 set-function-length@1.2.2:
13229 dependencies: 13318 dependencies:
13230 define-data-property: 1.1.4 13319 define-data-property: 1.1.4
...@@ -14102,6 +14191,8 @@ snapshots: ...@@ -14102,6 +14191,8 @@ snapshots:
14102 is-weakmap: 2.0.2 14191 is-weakmap: 2.0.2
14103 is-weakset: 2.0.4 14192 is-weakset: 2.0.4
14104 14193
14194 + which-module@2.0.1: {}
14195 +
14105 which-typed-array@1.1.19: 14196 which-typed-array@1.1.19:
14106 dependencies: 14197 dependencies:
14107 available-typed-arrays: 1.0.7 14198 available-typed-arrays: 1.0.7
...@@ -14156,14 +14247,35 @@ snapshots: ...@@ -14156,14 +14247,35 @@ snapshots:
14156 dependencies: 14247 dependencies:
14157 cuint: 0.2.2 14248 cuint: 0.2.2
14158 14249
14250 + y18n@4.0.3: {}
14251 +
14159 y18n@5.0.8: {} 14252 y18n@5.0.8: {}
14160 14253
14161 yallist@3.1.1: {} 14254 yallist@3.1.1: {}
14162 14255
14163 yaml@2.8.2: {} 14256 yaml@2.8.2: {}
14164 14257
14258 + yargs-parser@18.1.3:
14259 + dependencies:
14260 + camelcase: 5.3.1
14261 + decamelize: 1.2.0
14262 +
14165 yargs-parser@20.2.9: {} 14263 yargs-parser@20.2.9: {}
14166 14264
14265 + yargs@15.4.1:
14266 + dependencies:
14267 + cliui: 6.0.0
14268 + decamelize: 1.2.0
14269 + find-up: 4.1.0
14270 + get-caller-file: 2.0.5
14271 + require-directory: 2.1.1
14272 + require-main-filename: 2.0.0
14273 + set-blocking: 2.0.0
14274 + string-width: 4.2.3
14275 + which-module: 2.0.1
14276 + y18n: 4.0.3
14277 + yargs-parser: 18.1.3
14278 +
14167 yargs@16.2.0: 14279 yargs@16.2.0:
14168 dependencies: 14280 dependencies:
14169 cliui: 7.0.4 14281 cliui: 7.0.4
......
...@@ -24,6 +24,8 @@ export default { ...@@ -24,6 +24,8 @@ export default {
24 'pages/visitorList/index', 24 'pages/visitorList/index',
25 'pages/volunteerLogin/index', 25 'pages/volunteerLogin/index',
26 'pages/verificationResult/index', 26 'pages/verificationResult/index',
27 + 'pages/weakNetwork/index',
28 + 'pages/offlineBookingCode/index',
27 ], 29 ],
28 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 30 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
29 { 31 {
......
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: 2025-06-28 11:04:17 4 + * @LastEditTime: 2026-01-07 20:54:02
5 - * @FilePath: /myApp/src/app.js 5 + * @FilePath: /xyxBooking-weapp/src/app.js
6 * @Description: 文件描述 6 * @Description: 文件描述
7 */ 7 */
8 import { createApp } from 'vue' 8 import { createApp } from 'vue'
9 import { createPinia } from 'pinia' 9 import { createPinia } from 'pinia'
10 import './app.less' 10 import './app.less'
11 import { saveCurrentPagePath, needAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' 11 import { saveCurrentPagePath, needAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect'
12 +import Taro from '@tarojs/taro'
13 +import { qrcodeListAPI } from '@/api/index'
14 +import { formatDatetime } from '@/utils/tools'
15 +
16 +const formatGroup = (data) => {
17 + let lastPayId = null;
18 + for (let i = 0; i < data.length; i++) {
19 + if (data[i].pay_id !== lastPayId) {
20 + data[i].sort = 1;
21 + lastPayId = data[i].pay_id;
22 + } else {
23 + data[i].sort = 0;
24 + }
25 + }
26 + return data;
27 +}
12 28
13 const App = createApp({ 29 const App = createApp({
14 // 对应 onLaunch 30 // 对应 onLaunch
...@@ -25,12 +41,54 @@ const App = createApp({ ...@@ -25,12 +41,54 @@ const App = createApp({
25 saveCurrentPagePath(full_path) 41 saveCurrentPagePath(full_path)
26 } 42 }
27 43
44 + const preloadQrData = async () => {
45 + try {
46 + const { code, data } = await qrcodeListAPI();
47 + if (code && data) {
48 + data.forEach(item => {
49 + item.qr_code_url = '/admin?m=srv&a=get_qrcode&key=' + item.qr_code;
50 + item.datetime = formatDatetime({ begin_time: item.begin_time, end_time: item.end_time })
51 + item.sort = 0;
52 + });
53 + const validData = data.filter(item => item.qr_code !== '');
54 + if (validData.length > 0) {
55 + const processed = formatGroup(validData);
56 + Taro.setStorageSync('OFFLINE_QR_DATA', processed);
57 + } else {
58 + Taro.removeStorageSync('OFFLINE_QR_DATA');
59 + }
60 + }
61 + } catch (e) {
62 + console.error('Preload QR failed', e);
63 + }
64 + };
65 +
66 + const checkNetworkAndPreload = () => {
67 + Taro.getNetworkType({
68 + success: (res) => {
69 + const isConnected = ['wifi', '4g', '5g', '3g'].includes(res.networkType);
70 + if (isConnected) {
71 + preloadQrData();
72 + }
73 + }
74 + });
75 + };
76 +
77 + Taro.onNetworkStatusChange((res) => {
78 + if (res.isConnected) {
79 + preloadQrData();
80 + }
81 + });
82 +
83 + checkNetworkAndPreload();
84 +
28 if (!needAuth()) return 85 if (!needAuth()) return
29 86
30 if (path === 'pages/auth/index') return 87 if (path === 'pages/auth/index') return
31 88
32 try { 89 try {
33 await silentAuth() 90 await silentAuth()
91 + checkNetworkAndPreload();
34 } catch (error) { 92 } catch (error) {
35 navigateToAuth(full_path || undefined) 93 navigateToAuth(full_path || undefined)
36 } 94 }
......
1 +<template>
2 + <view class="qr-code-page">
3 + <view v-if="userList.length" class="show-qrcode">
4 + <view class="qrcode-content">
5 + <view class="user-info">{{ userinfo.name }}&nbsp;{{ userinfo.id }}</view>
6 + <view class="user-qrcode">
7 + <view class="left" @tap="prevCode">
8 + <image :src="icon_1" />
9 + </view>
10 + <view class="center">
11 + <!-- 离线模式直接显示生成的 base64 图片 -->
12 + <image :src="currentQrCodeUrl" mode="aspectFit" />
13 + <!-- 离线模式不显示状态覆盖层,因为无法获取最新状态 -->
14 + </view>
15 + <view class="right" @tap="nextCode">
16 + <image :src="icon_2" />
17 + </view>
18 + </view>
19 + <view style="color: red; margin-top: 32rpx;">{{ userinfo.datetime }}</view>
20 + <view style="color: #999; font-size: 24rpx; margin-top: 10rpx;">(离线模式)</view>
21 + </view>
22 + <view class="user-list">
23 + <view
24 + @tap="selectUser(index)"
25 + v-for="(item, index) in userList"
26 + :key="index"
27 + :class="[
28 + 'user-item',
29 + select_index === index ? 'checked' : '',
30 + userList.length > 1 && item.sort ? 'border' : '',
31 + ]">
32 + {{ item.name }}
33 + </view>
34 + </view>
35 + </view>
36 + <view v-else class="no-qrcode">
37 + <image :src="icon_3" style="width: 320rpx; height: 320rpx;" />
38 + <view class="no-qrcode-title">本地无缓存预约记录</view>
39 + </view>
40 + </view>
41 +</template>
42 +
43 +<script setup>
44 +import { ref, computed, watch, onMounted } from 'vue'
45 +import Taro from '@tarojs/taro'
46 +import QRCode from 'qrcode'
47 +
48 +import icon_1 from '@/assets/images/左1@2x.png'
49 +import icon_2 from '@/assets/images/右1@2x.png'
50 +import icon_3 from '@/assets/images/暂无1@2x.png'
51 +
52 +const props = defineProps({
53 + list: {
54 + type: Array,
55 + default: () => []
56 + }
57 +});
58 +
59 +const select_index = ref(0);
60 +const userList = ref([]);
61 +const qrCodeImages = ref({}); // 存储生成的二维码图片 base64
62 +
63 +const prevCode = () => {
64 + select_index.value = select_index.value - 1;
65 + if (select_index.value < 0) {
66 + select_index.value = userList.value.length - 1;
67 + }
68 +};
69 +const nextCode = () => {
70 + select_index.value = select_index.value + 1;
71 + if (select_index.value > userList.value.length - 1) {
72 + select_index.value = 0;
73 + }
74 +};
75 +
76 +function replaceMiddleCharacters(inputString) {
77 + if (!inputString || inputString.length < 15) {
78 + return inputString;
79 + }
80 + const start = Math.floor((inputString.length - 8) / 2);
81 + const end = start + 8;
82 + const replacement = '*'.repeat(8);
83 + return inputString.substring(0, start) + replacement + inputString.substring(end);
84 +}
85 +
86 +const formatId = (id) => replaceMiddleCharacters(id);
87 +
88 +const userinfo = computed(() => {
89 + return {
90 + name: userList.value[select_index.value]?.name,
91 + id: formatId(userList.value[select_index.value]?.id_number),
92 + datetime: userList.value[select_index.value]?.datetime,
93 + };
94 +});
95 +
96 +const currentQrCodeUrl = computed(() => {
97 + const key = userList.value[select_index.value]?.qr_code;
98 + return qrCodeImages.value[key] || '';
99 +})
100 +
101 +const selectUser = (index) => {
102 + select_index.value = index;
103 +}
104 +
105 +const generateQrCodes = () => {
106 + for (const item of userList.value) {
107 + if (item.qr_code && !qrCodeImages.value[item.qr_code]) {
108 + try {
109 + // 使用 create + SVG 手动生成,避免 Taro 中 Canvas 依赖问题
110 + const qr = QRCode.create(item.qr_code, { errorCorrectionLevel: 'M' });
111 + const size = qr.modules.size;
112 + let d = '';
113 + for (let row = 0; row < size; row++) {
114 + for (let col = 0; col < size; col++) {
115 + if (qr.modules.get(col, row)) {
116 + d += `M${col},${row}h1v1h-1z`;
117 + }
118 + }
119 + }
120 + const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><path d="${d}" fill="#000"/></svg>`;
121 +
122 + // 转 Base64
123 + const buffer = new ArrayBuffer(svg.length);
124 + const view = new Uint8Array(buffer);
125 + for (let i = 0; i < svg.length; i++) {
126 + view[i] = svg.charCodeAt(i);
127 + }
128 + const b64 = Taro.arrayBufferToBase64(buffer);
129 +
130 + qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`;
131 + } catch (err) {
132 + console.error('QR Gen Error', err);
133 + }
134 + }
135 + }
136 +}
137 +
138 +onMounted(() => {
139 + if (props.list && props.list.length > 0) {
140 + userList.value = props.list;
141 + generateQrCodes();
142 + }
143 +});
144 +
145 +watch(() => props.list, (newVal) => {
146 + if (newVal && newVal.length > 0) {
147 + userList.value = newVal;
148 + generateQrCodes();
149 + }
150 +}, { deep: true });
151 +
152 +</script>
153 +
154 +<style lang="less">
155 +.qr-code-page {
156 + .qrcode-content {
157 + padding: 32rpx 0;
158 + display: flex;
159 + flex-direction: column;
160 + justify-content: center;
161 + align-items: center;
162 + background-color: #FFF;
163 + border-radius: 16rpx;
164 + box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.27);
165 +
166 + .user-info {
167 + color: #A6A6A6;
168 + font-size: 37rpx;
169 + margin-top: 16rpx;
170 + margin-bottom: 16rpx;
171 + }
172 + .user-qrcode {
173 + display: flex;
174 + align-items: center;
175 + .left {
176 + image {
177 + width: 56rpx; height: 56rpx; margin-right: 16rpx;
178 + }
179 + }
180 + .center {
181 + border: 2rpx solid #D1D1D1;
182 + border-radius: 40rpx;
183 + padding: 16rpx;
184 + position: relative;
185 + image {
186 + width: 480rpx; height: 480rpx;
187 + }
188 + }
189 + .right {
190 + image {
191 + width: 56rpx; height: 56rpx;
192 + margin-left: 16rpx;
193 + }
194 + }
195 + }
196 + }
197 + .user-list {
198 + display: flex;
199 + padding: 32rpx;
200 + align-items: center;
201 + flex-wrap: wrap;
202 + .user-item {
203 + position: relative;
204 + padding: 8rpx 16rpx;
205 + border: 2rpx solid #A67939;
206 + margin: 8rpx;
207 + border-radius: 10rpx;
208 + color: #A67939;
209 + &.checked {
210 + color: #FFF;
211 + background-color: #A67939;
212 + }
213 + &.border {
214 + margin-right: 16rpx;
215 + &::after {
216 + position: absolute;
217 + right: -16rpx;
218 + top: calc(50% - 16rpx);
219 + content: '';
220 + height: 32rpx;
221 + border-right: 2rpx solid #A67939;
222 + }
223 + }
224 + }
225 + }
226 +
227 + .no-qrcode {
228 + display: flex;
229 + justify-content: center;
230 + align-items: center;
231 + flex-direction: column;
232 + margin-bottom: 32rpx;
233 +
234 + .no-qrcode-title {
235 + color: #A67939;
236 + font-size: 34rpx;
237 + }
238 + }
239 +}
240 +</style>
1 <!-- 1 <!--
2 * @Date: 2024-01-16 10:06:47 2 * @Date: 2024-01-16 10:06:47
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-12-26 11:15:45 4 + * @LastEditTime: 2026-01-07 20:54:13
5 * @FilePath: /xyxBooking-weapp/src/components/qrCode.vue 5 * @FilePath: /xyxBooking-weapp/src/components/qrCode.vue
6 * @Description: 预约码卡组件 6 * @Description: 预约码卡组件
7 --> 7 -->
...@@ -168,7 +168,9 @@ const formatGroup = (data) => { ...@@ -168,7 +168,9 @@ const formatGroup = (data) => {
168 168
169 const init = async () => { 169 const init = async () => {
170 if (!props.type) { 170 if (!props.type) {
171 + try {
171 const { code, data } = await qrcodeListAPI(); 172 const { code, data } = await qrcodeListAPI();
173 +
172 if (code) { 174 if (code) {
173 data.forEach(item => { 175 data.forEach(item => {
174 item.qr_code_url = '/admin?m=srv&a=get_qrcode&key=' + item.qr_code; 176 item.qr_code_url = '/admin?m=srv&a=get_qrcode&key=' + item.qr_code;
...@@ -180,10 +182,17 @@ const init = async () => { ...@@ -180,10 +182,17 @@ const init = async () => {
180 182
181 if (validData.length > 0) { 183 if (validData.length > 0) {
182 userList.value = formatGroup(validData); 184 userList.value = formatGroup(validData);
185 + // 缓存数据供离线模式使用
186 + Taro.setStorageSync('OFFLINE_QR_DATA', userList.value);
183 refreshBtn(); 187 refreshBtn();
184 } else { 188 } else {
185 userList.value = []; 189 userList.value = [];
190 + // 清空缓存
191 + Taro.removeStorageSync('OFFLINE_QR_DATA');
192 + }
186 } 193 }
194 + } catch (err) {
195 + console.error('Fetch QR List Failed:', err);
187 } 196 }
188 } else { 197 } else {
189 if (props.payId) { 198 if (props.payId) {
......
1 <!-- 1 <!--
2 * @Date: 2024-01-16 10:06:47 2 * @Date: 2024-01-16 10:06:47
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-01-30 17:48:42 4 + * @LastEditTime: 2026-01-07 21:08:16
5 * @FilePath: /xyxBooking-weapp/src/pages/bookingCode/index.vue 5 * @FilePath: /xyxBooking-weapp/src/pages/bookingCode/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
34 34
35 <script setup> 35 <script setup>
36 import { ref } from 'vue' 36 import { ref } from 'vue'
37 -import Taro from '@tarojs/taro' 37 +import Taro, { useDidShow } from '@tarojs/taro'
38 import qrCode from '@/components/qrCode'; 38 import qrCode from '@/components/qrCode';
39 import { IconFont } from '@nutui/icons-vue-taro' 39 import { IconFont } from '@nutui/icons-vue-taro'
40 import icon_3 from '@/assets/images/首页01@2x.png' 40 import icon_3 from '@/assets/images/首页01@2x.png'
...@@ -44,6 +44,17 @@ import { useGo } from '@/hooks/useGo' ...@@ -44,6 +44,17 @@ import { useGo } from '@/hooks/useGo'
44 44
45 const go = useGo(); 45 const go = useGo();
46 46
47 +useDidShow(() => {
48 + Taro.getNetworkType({
49 + success: (res) => {
50 + const isConnected = ['wifi', '4g', '5g', '3g'].includes(res.networkType);
51 + if (!isConnected) {
52 + go('/pages/weakNetwork/index');
53 + }
54 + }
55 + });
56 +})
57 +
47 const toMy = () => { // 跳转到我的 58 const toMy = () => { // 跳转到我的
48 go('/pages/me/index'); 59 go('/pages/me/index');
49 } 60 }
......
1 +<template>
2 + <view class="offline-booking-code-page">
3 + <view class="header-tip">
4 + <IconFont name="tips" size="15" color="#A67939" />
5 + <text>您当前处于离线模式,仅展示本地缓存的预约码</text>
6 + </view>
7 +
8 + <view style="padding: 32rpx;">
9 + <offlineQrCode :list="qrList"></offlineQrCode>
10 + <view class="warning">
11 + <view>
12 + 温馨提示
13 + </view>
14 + <view style="margin-top: 16rpx;">一人一码,扫码或识别身份证成功后进入</view>
15 + </view>
16 + </view>
17 +
18 + <view class="action-area">
19 + <button class="home-btn" @tap="toHome">返回首页</button>
20 + </view>
21 + </view>
22 +</template>
23 +
24 +<script setup>
25 +import { ref, onMounted } from 'vue'
26 +import Taro from '@tarojs/taro'
27 +import offlineQrCode from '@/components/offlineQrCode';
28 +import { IconFont } from '@nutui/icons-vue-taro'
29 +import { useGo } from '@/hooks/useGo'
30 +
31 +const go = useGo();
32 +const qrList = ref([]);
33 +
34 +const toHome = () => {
35 + Taro.reLaunch({ url: '/pages/index/index' });
36 +}
37 +
38 +// Mock Data as per requirement
39 +const getMockData = () => {
40 + return [
41 + {
42 + name: '测试用户1',
43 + id_number: '110101199003078888',
44 + qr_code: 'OFFLINE_MOCK_QR_001',
45 + datetime: '2026-01-08 08:30-10:30',
46 + sort: 0
47 + },
48 + {
49 + name: '测试用户2',
50 + id_number: '110101199205126666',
51 + qr_code: 'OFFLINE_MOCK_QR_002',
52 + datetime: '2026-01-08 08:30-10:30',
53 + sort: 0
54 + }
55 + ];
56 +}
57 +
58 +onMounted(() => {
59 + try {
60 + const cachedData = Taro.getStorageSync('OFFLINE_QR_DATA');
61 + if (cachedData && cachedData.length > 0) {
62 + qrList.value = cachedData;
63 + } else {
64 + // Requirement 4: Mock data if no data
65 + console.log('No cached data found, using mock data');
66 + qrList.value = getMockData();
67 + }
68 + } catch (e) {
69 + console.error('Read storage failed', e);
70 + qrList.value = getMockData();
71 + }
72 +});
73 +
74 +</script>
75 +
76 +<style lang="less">
77 +.offline-booking-code-page {
78 + position: relative;
79 + min-height: 100vh;
80 + background-color: #F6F6F6;
81 +
82 + .header-tip {
83 + background-color: #FEF8E8;
84 + color: #A67939;
85 + padding: 20rpx 32rpx;
86 + font-size: 26rpx;
87 + display: flex;
88 + align-items: center;
89 +
90 + text {
91 + margin-left: 10rpx;
92 + }
93 + }
94 +
95 + .warning {
96 + text-align: center;
97 + color: #A67939;
98 + margin-top: 32rpx;
99 + }
100 +
101 + .action-area {
102 + position: fixed;
103 + bottom: 60rpx;
104 + left: 0;
105 + width: 100%;
106 + display: flex;
107 + justify-content: center;
108 +
109 + .home-btn {
110 + width: 600rpx;
111 + height: 88rpx;
112 + line-height: 88rpx;
113 + background: #fff;
114 + color: #A67939;
115 + border: 2rpx solid #A67939;
116 + border-radius: 44rpx;
117 + font-size: 32rpx;
118 + }
119 + }
120 +}
121 +</style>
1 +<template>
2 + <view class="weak-network-page">
3 + <view class="content">
4 + <view class="icon-wrapper">
5 + <IconFont name="mask-close" size="120" color="#ccc" />
6 + </view>
7 + <view class="title">网络连接不畅</view>
8 + <view class="desc">当前网络信号较弱,已自动为您切换至离线模式</view>
9 +
10 + <view class="offline-entry" @tap="toOfflineCode">
11 + <view class="circle-btn">
12 + <text>预约码</text>
13 + </view>
14 + </view>
15 +
16 + <view class="sub-action" @tap="retry">
17 + <text>尝试刷新重试</text>
18 + </view>
19 + </view>
20 + </view>
21 +</template>
22 +
23 +<script setup>
24 +import Taro from '@tarojs/taro'
25 +import { IconFont } from '@nutui/icons-vue-taro'
26 +import { useGo } from '@/hooks/useGo'
27 +
28 +const go = useGo();
29 +
30 +const toOfflineCode = () => {
31 + go('/pages/offlineBookingCode/index');
32 +}
33 +
34 +const retry = () => {
35 + // 尝试重新加载当前页或者是返回上一页重试
36 + // 这里简单做成返回首页
37 + Taro.reLaunch({ url: '/pages/index/index' });
38 +}
39 +</script>
40 +
41 +<style lang="less">
42 +.weak-network-page {
43 + min-height: 100vh;
44 + background-color: #fff;
45 + display: flex;
46 + flex-direction: column;
47 + align-items: center;
48 + justify-content: center;
49 +
50 + .content {
51 + display: flex;
52 + flex-direction: column;
53 + align-items: center;
54 + margin-top: -100rpx;
55 +
56 + .icon-wrapper {
57 + margin-bottom: 40rpx;
58 + }
59 +
60 + .title {
61 + font-size: 40rpx;
62 + color: #333;
63 + font-weight: bold;
64 + margin-bottom: 20rpx;
65 + }
66 +
67 + .desc {
68 + font-size: 28rpx;
69 + color: #999;
70 + margin-bottom: 80rpx;
71 + text-align: center;
72 + padding: 0 60rpx;
73 + }
74 +
75 + .offline-entry {
76 + margin-bottom: 60rpx;
77 + .circle-btn {
78 + width: 240rpx;
79 + height: 240rpx;
80 + border-radius: 50%;
81 + background: linear-gradient(135deg, #A67939 0%, #C69C5C 100%);
82 + display: flex;
83 + align-items: center;
84 + justify-content: center;
85 + box-shadow: 0 10rpx 30rpx rgba(166, 121, 57, 0.4);
86 +
87 + text {
88 + color: #fff;
89 + font-size: 40rpx;
90 + font-weight: bold;
91 + letter-spacing: 2rpx;
92 + }
93 +
94 + &:active {
95 + transform: scale(0.95);
96 + }
97 + }
98 + }
99 +
100 + .sub-action {
101 + padding: 20rpx;
102 + text {
103 + color: #A67939;
104 + font-size: 28rpx;
105 + text-decoration: underline;
106 + }
107 + }
108 + }
109 +}
110 +</style>