hookehuyr

refactor: 抽离通用工具函数并优化轮询生命周期管理

- 将身份证脱敏、状态映射等工具函数统一到 utils/tools.js
- 在 qrCode 组件暴露轮询启停方法,由页面生命周期控制
- 更新 README 中优化建议的完成状态
...@@ -139,7 +139,7 @@ src/ ...@@ -139,7 +139,7 @@ src/
139 ## 优化建议(下一步) 139 ## 优化建议(下一步)
140 140
141 - 补充测试:优先覆盖 request/authRedirect/offline cache 的关键边界 141 - 补充测试:优先覆盖 request/authRedirect/offline cache 的关键边界
142 -- 抽离通用工具:证件号脱敏、时间格式化、状态映射等统一放到 utils 142 +- 抽离通用工具:已完成(证件号脱敏与状态映射已统一到 utils)
143 -- 优化轮询策略:结合页面生命周期 onShow/onHide 控制轮询启停 143 +- 优化轮询策略:已完成(二维码轮询按页面生命周期启停)
144 - 资源优化:CDN 图片策略、分包策略、首屏关键资源预加载 144 - 资源优化:CDN 图片策略、分包策略、首屏关键资源预加载
145 - 构建性能:评估开启 Webpack 持久化缓存,缩短二次编译时间 145 - 构建性能:评估开启 Webpack 持久化缓存,缩短二次编译时间
......
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: 2026-01-20 15:48:13 4 + * @LastEditTime: 2026-01-24 14:12:30
5 * @FilePath: /xyxBooking-weapp/src/components/qrCode.vue 5 * @FilePath: /xyxBooking-weapp/src/components/qrCode.vue
6 * @Description: 预约码卡组件 6 * @Description: 预约码卡组件
7 --> 7 -->
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
18 <image :src="currentQrCodeUrl" mode="aspectFit" /> 18 <image :src="currentQrCodeUrl" mode="aspectFit" />
19 <view v-if="useStatus === STATUS_CODE.CANCELED || useStatus === STATUS_CODE.USED" class="qrcode-used"> 19 <view v-if="useStatus === STATUS_CODE.CANCELED || useStatus === STATUS_CODE.USED" class="qrcode-used">
20 <view class="overlay"></view> 20 <view class="overlay"></view>
21 - <text class="status-text">二维码{{ qr_code_status[useStatus] }}</text> 21 + <text class="status-text">二维码{{ get_qrcode_status_text(useStatus) }}</text>
22 </view> 22 </view>
23 </view> 23 </view>
24 <view class="right" @tap="nextCode"> 24 <view class="right" @tap="nextCode">
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
52 <script setup> 52 <script setup>
53 import { ref, computed, watch, onMounted, onUnmounted } from 'vue' 53 import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
54 import Taro from '@tarojs/taro' 54 import Taro from '@tarojs/taro'
55 -import { formatDatetime } from '@/utils/tools'; 55 +import { formatDatetime, mask_id_number, get_qrcode_status_text } from '@/utils/tools';
56 import { qrcodeListAPI, qrcodeStatusAPI, billPersonAPI } from '@/api/index' 56 import { qrcodeListAPI, qrcodeStatusAPI, billPersonAPI } from '@/api/index'
57 import { useGo } from '@/hooks/useGo' 57 import { useGo } from '@/hooks/useGo'
58 import BASE_URL from '@/utils/config'; 58 import BASE_URL from '@/utils/config';
...@@ -116,27 +116,7 @@ watch( ...@@ -116,27 +116,7 @@ watch(
116 { immediate: true } 116 { immediate: true }
117 ) 117 )
118 118
119 -/** 119 +const formatId = (id) => mask_id_number(id)
120 - * @description 身份证号脱敏:中间 8 位替换为 * 号
121 - * @param {string} inputString 原始身份证号
122 - * @returns {string} 脱敏后的身份证号
123 - */
124 -function replaceMiddleCharacters (inputString) {
125 - if (!inputString || inputString.length < 15) {
126 - return inputString;
127 - }
128 - const start = Math.floor((inputString.length - 8) / 2);
129 - const end = start + 8;
130 - const replacement = '*'.repeat(8);
131 - return inputString.substring(0, start) + replacement + inputString.substring(end);
132 -}
133 -
134 -/**
135 - * @description 格式化证件号展示(脱敏)
136 - * @param {string} id 原始证件号
137 - * @returns {string} 脱敏后的证件号
138 - */
139 -const formatId = (id) => replaceMiddleCharacters(id);
140 120
141 const userinfo = computed(() => { 121 const userinfo = computed(() => {
142 return { 122 return {
...@@ -156,13 +136,6 @@ const currentQrCodeUrl = computed(() => { ...@@ -156,13 +136,6 @@ const currentQrCodeUrl = computed(() => {
156 136
157 const useStatus = ref('0'); 137 const useStatus = ref('0');
158 138
159 -const qr_code_status = {
160 - '1': '未激活',
161 - '3': '待使用',
162 - '5': '被取消',
163 - '7': '已使用',
164 -};
165 -
166 const STATUS_CODE = { 139 const STATUS_CODE = {
167 APPLY: '1', 140 APPLY: '1',
168 SUCCESS: '3', 141 SUCCESS: '3',
...@@ -267,6 +240,7 @@ const init = async () => { ...@@ -267,6 +240,7 @@ const init = async () => {
267 240
268 onMounted(() => { 241 onMounted(() => {
269 init(); 242 init();
243 + start_polling();
270 }); 244 });
271 245
272 /** 246 /**
...@@ -285,13 +259,36 @@ const poll = async () => { ...@@ -285,13 +259,36 @@ const poll = async () => {
285 } 259 }
286 }; 260 };
287 261
288 -// 3 秒轮询一次,避免过于频繁 262 +const interval_id = ref(null)
289 -const intervalId = setInterval(poll, 3000); 263 +/**
264 + * @description 启动轮询
265 + * - 仅在当前选中用户存在时轮询
266 + * @returns {void} 无返回值
267 + */
268 +
269 +const start_polling = () => {
270 + if (interval_id.value) return
271 + interval_id.value = setInterval(poll, 3000)
272 +}
273 +
274 +/**
275 + * @description 停止轮询
276 + * - 组件卸载时调用,避免内存泄漏
277 + * @returns {void} 无返回值
278 + */
279 +
280 +const stop_polling = () => {
281 + if (!interval_id.value) return
282 + clearInterval(interval_id.value)
283 + interval_id.value = null
284 +}
290 285
291 onUnmounted(() => { 286 onUnmounted(() => {
292 - clearInterval(intervalId); 287 + stop_polling();
293 }); 288 });
294 289
290 +defineExpose({ start_polling, stop_polling })
291 +
295 /** 292 /**
296 * @description 跳转预约记录列表页 293 * @description 跳转预约记录列表页
297 * @returns {void} 无返回值 294 * @returns {void} 无返回值
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
8 <template> 8 <template>
9 <view class="booking-code-page"> 9 <view class="booking-code-page">
10 <view style="padding: 32rpx;"> 10 <view style="padding: 32rpx;">
11 - <qrCode></qrCode> 11 + <qrCode ref="qr_code_ref"></qrCode>
12 <view class="warning"> 12 <view class="warning">
13 <view style="display: flex; align-items: center; justify-content: center;"><IconFont name="tips" /><text style="margin-left: 10rpx;">温馨提示</text></view> 13 <view style="display: flex; align-items: center; justify-content: center;"><IconFont name="tips" /><text style="margin-left: 10rpx;">温馨提示</text></view>
14 <view style="margin-top: 16rpx;">一人一码,扫码或识别身份证成功后进入</view> 14 <view style="margin-top: 16rpx;">一人一码,扫码或识别身份证成功后进入</view>
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
27 27
28 <script setup> 28 <script setup>
29 import { ref } from 'vue' 29 import { ref } from 'vue'
30 -import Taro, { useDidShow } from '@tarojs/taro' 30 +import Taro, { useDidShow, useDidHide } from '@tarojs/taro'
31 import qrCode from '@/components/qrCode'; 31 import qrCode from '@/components/qrCode';
32 import { IconFont } from '@nutui/icons-vue-taro' 32 import { IconFont } from '@nutui/icons-vue-taro'
33 import indexNav from '@/components/indexNav.vue' 33 import indexNav from '@/components/indexNav.vue'
...@@ -39,7 +39,10 @@ import { has_offline_booking_cache } from '@/composables/useOfflineBookingCache' ...@@ -39,7 +39,10 @@ import { has_offline_booking_cache } from '@/composables/useOfflineBookingCache'
39 import { is_usable_network } from '@/utils/network' 39 import { is_usable_network } from '@/utils/network'
40 import { get_weak_network_modal_no_cache_options } from '@/utils/uiText' 40 import { get_weak_network_modal_no_cache_options } from '@/utils/uiText'
41 41
42 +const qr_code_ref = ref(null)
43 +
42 useDidShow(() => { 44 useDidShow(() => {
45 + qr_code_ref.value?.start_polling?.()
43 Taro.getNetworkType({ 46 Taro.getNetworkType({
44 success: async (res) => { 47 success: async (res) => {
45 const isConnected = is_usable_network(res.networkType); 48 const isConnected = is_usable_network(res.networkType);
...@@ -72,6 +75,10 @@ useDidShow(() => { ...@@ -72,6 +75,10 @@ useDidShow(() => {
72 }); 75 });
73 }) 76 })
74 77
78 +useDidHide(() => {
79 + qr_code_ref.value?.stop_polling?.()
80 +})
81 +
75 const toMy = () => { // 跳转到我的 82 const toMy = () => { // 跳转到我的
76 Taro.redirectTo({ 83 Taro.redirectTo({
77 url: '/pages/me/index' 84 url: '/pages/me/index'
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
7 --> 7 -->
8 <template> 8 <template>
9 <view class="booking-detail-page"> 9 <view class="booking-detail-page">
10 - <qrCode :status="qrCodeStatus" type="detail" :payId="pay_id"></qrCode> 10 + <qrCode ref="qr_code_ref" :status="qrCodeStatus" type="detail" :payId="pay_id"></qrCode>
11 <view v-if="billInfo.pay_id" class="detail-wrapper"> 11 <view v-if="billInfo.pay_id" class="detail-wrapper">
12 <view class="detail-item"> 12 <view class="detail-item">
13 <view>参访时间:</view> 13 <view>参访时间:</view>
...@@ -43,10 +43,10 @@ ...@@ -43,10 +43,10 @@
43 43
44 <script setup> 44 <script setup>
45 import { ref, computed } from 'vue' 45 import { ref, computed } from 'vue'
46 -import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro' 46 +import Taro, { useDidShow, useDidHide, useRouter as useTaroRouter } from '@tarojs/taro'
47 import qrCode from '@/components/qrCode'; 47 import qrCode from '@/components/qrCode';
48 import { billInfoAPI, icbcRefundAPI } from '@/api/index' 48 import { billInfoAPI, icbcRefundAPI } from '@/api/index'
49 -import { formatDatetime } from '@/utils/tools'; 49 +import { formatDatetime, get_bill_status_text } from '@/utils/tools';
50 import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache' 50 import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
51 51
52 const router = useTaroRouter(); 52 const router = useTaroRouter();
...@@ -54,6 +54,7 @@ const router = useTaroRouter(); ...@@ -54,6 +54,7 @@ const router = useTaroRouter();
54 const pay_id = ref(''); 54 const pay_id = ref('');
55 const qrCodeStatus = ref(''); 55 const qrCodeStatus = ref('');
56 const billInfo = ref({}); 56 const billInfo = ref({});
57 +const qr_code_ref = ref(null)
57 58
58 /** 59 /**
59 * @description 预约码状态枚举(与后端约定) 60 * @description 预约码状态枚举(与后端约定)
...@@ -74,14 +75,7 @@ const CodeStatus = { ...@@ -74,14 +75,7 @@ const CodeStatus = {
74 * @returns {string} 状态文案 75 * @returns {string} 状态文案
75 */ 76 */
76 const qrCodeStatusText = computed(() => { 77 const qrCodeStatusText = computed(() => {
77 - const status = billInfo.value?.status; 78 + return get_bill_status_text(billInfo.value?.status)
78 - switch (status) {
79 - case CodeStatus.SUCCESS: return '预约成功';
80 - case CodeStatus.CANCEL: return '已取消';
81 - case CodeStatus.USED: return '已使用';
82 - case CodeStatus.REFUNDING: return '退款中';
83 - default: return '未知状态';
84 - }
85 }) 79 })
86 80
87 /** 81 /**
...@@ -113,6 +107,7 @@ const cancelBooking = async () => { ...@@ -113,6 +107,7 @@ const cancelBooking = async () => {
113 } 107 }
114 108
115 useDidShow(async () => { 109 useDidShow(async () => {
110 + qr_code_ref.value?.start_polling?.()
116 pay_id.value = router.params.pay_id; 111 pay_id.value = router.params.pay_id;
117 if (pay_id.value) { 112 if (pay_id.value) {
118 const { code, data } = await billInfoAPI({ pay_id: pay_id.value }); 113 const { code, data } = await billInfoAPI({ pay_id: pay_id.value });
...@@ -123,6 +118,10 @@ useDidShow(async () => { ...@@ -123,6 +118,10 @@ useDidShow(async () => {
123 } 118 }
124 } 119 }
125 }) 120 })
121 +
122 +useDidHide(() => {
123 + qr_code_ref.value?.stop_polling?.()
124 +})
126 </script> 125 </script>
127 126
128 <style lang="less"> 127 <style lang="less">
......
...@@ -64,6 +64,7 @@ import icon_check1 from '@/assets/images/多选01@2x.png' ...@@ -64,6 +64,7 @@ import icon_check1 from '@/assets/images/多选01@2x.png'
64 import icon_check2 from '@/assets/images/多选02@2x.png' 64 import icon_check2 from '@/assets/images/多选02@2x.png'
65 import { personListAPI, addReserveAPI } from '@/api/index' 65 import { personListAPI, addReserveAPI } from '@/api/index'
66 import { wechat_pay } from '@/utils/wechatPay' 66 import { wechat_pay } from '@/utils/wechatPay'
67 +import { mask_id_number } from '@/utils/tools'
67 68
68 const router = useTaroRouter(); 69 const router = useTaroRouter();
69 const go = useGo(); 70 const go = useGo();
...@@ -74,34 +75,7 @@ const date = ref(''); ...@@ -74,34 +75,7 @@ const date = ref('');
74 const time = ref(''); 75 const time = ref('');
75 const price = ref(0); 76 const price = ref(0);
76 const period_type = ref(''); 77 const period_type = ref('');
77 - 78 +const formatId = (id) => mask_id_number(id)
78 -/**
79 - * @description 身份证号脱敏:中间 8 位替换为 *
80 - * @param {string} inputString 身份证号
81 - * @returns {string} 脱敏后的身份证号
82 - */
83 -function replaceMiddleCharacters(inputString) {
84 - if (!inputString || inputString.length < 15) {
85 - return inputString; // 字符串长度不足,不进行替换
86 - }
87 -
88 - const start = Math.floor((inputString.length - 8) / 2); // 开始替换的索引位置
89 - const end = start + 8; // 结束替换的索引位置
90 -
91 - const replacement = '*'.repeat(8); // 生成包含8个*号的字符串
92 -
93 - const replacedString = inputString.substring(0, start) + replacement + inputString.substring(end);
94 - return replacedString;
95 -}
96 -
97 -/**
98 - * @description 格式化身份证号展示
99 - * @param {string} id 身份证号
100 - * @returns {string} 脱敏后的身份证号
101 - */
102 -const formatId = (id) => {
103 - return replaceMiddleCharacters(id);
104 -};
105 79
106 /** 80 /**
107 * @description 当天预约标记 81 * @description 当天预约标记
......
...@@ -84,6 +84,7 @@ import { verifyTicketAPI, checkRedeemPermissionAPI } from '@/api/redeem' ...@@ -84,6 +84,7 @@ import { verifyTicketAPI, checkRedeemPermissionAPI } from '@/api/redeem'
84 import Taro, { useDidShow } from '@tarojs/taro' 84 import Taro, { useDidShow } from '@tarojs/taro'
85 import { mainStore } from '@/stores/main' 85 import { mainStore } from '@/stores/main'
86 import { useReplace } from '@/hooks/useGo' 86 import { useReplace } from '@/hooks/useGo'
87 +import { mask_id_number } from '@/utils/tools'
87 88
88 const router = useRouter() 89 const router = useRouter()
89 const verify_code = ref('') 90 const verify_code = ref('')
...@@ -93,18 +94,7 @@ const msg = ref('请点击下方按钮进行核销') ...@@ -93,18 +94,7 @@ const msg = ref('请点击下方按钮进行核销')
93 const store = mainStore() 94 const store = mainStore()
94 const replace = useReplace() 95 const replace = useReplace()
95 96
96 -// 身份证脱敏函数 97 +const formatIdNumber = (id) => mask_id_number(id, { keep_start: 6, keep_end: 4 })
97 -const formatIdNumber = (id) => {
98 - if (!id || id.length < 10) return id;
99 - // 保留前6位和后4位,中间用*替换
100 - // 或者根据需求:保留前3后4,中间4位?用户说“中间4位加*号”,通常指显示 110***1918 这种,或者 110101****1234
101 - // 按照常见隐私保护:保留前6位(地区)+出生年(4位)+ 后4位?
102 - // 用户原文:"身份证号码需要中间4位加*号" -> 这通常指隐藏中间部分,或者只隐藏具体的中间4位。
103 - // 标准脱敏通常是隐藏出生月日:1101011990****2918 (保留前10和后4)
104 - // 或者隐藏更彻底:110101********2918
105 - // 这里采用 110101********2918 (保留前6后4) 比较稳妥
106 - return id.replace(/^(.{6})(?:\d+)(.{4})$/, "$1********$2");
107 -}
108 98
109 const status_title = computed(() => { 99 const status_title = computed(() => {
110 if (verify_status.value === 'verifying') return '核销中' 100 if (verify_status.value === 'verifying') return '核销中'
......
...@@ -47,6 +47,7 @@ import indexNav from '@/components/indexNav.vue' ...@@ -47,6 +47,7 @@ import indexNav from '@/components/indexNav.vue'
47 import icon_3 from '@/assets/images/首页01@2x.png' 47 import icon_3 from '@/assets/images/首页01@2x.png'
48 import icon_4 from '@/assets/images/二维码icon.png' 48 import icon_4 from '@/assets/images/二维码icon.png'
49 import icon_5 from '@/assets/images/我的02@2x.png' 49 import icon_5 from '@/assets/images/我的02@2x.png'
50 +import { mask_id_number } from '@/utils/tools'
50 51
51 const go = useGo(); 52 const go = useGo();
52 53
...@@ -69,28 +70,7 @@ const on_nav_select = (key) => { ...@@ -69,28 +70,7 @@ const on_nav_select = (key) => {
69 } 70 }
70 71
71 const visitorList = ref([]); 72 const visitorList = ref([]);
72 - 73 +const formatId = (id) => mask_id_number(id)
73 -/**
74 - * @description 身份证号脱敏:中间 8 位替换为 * 号
75 - * @param {string} inputString 原始身份证号
76 - * @returns {string} 脱敏后的身份证号
77 - */
78 -function replaceMiddleCharacters (inputString) {
79 - if (!inputString || inputString.length < 15) {
80 - return inputString;
81 - }
82 - const start = Math.floor((inputString.length - 8) / 2);
83 - const end = start + 8;
84 - const replacement = '*'.repeat(8);
85 - return inputString.substring(0, start) + replacement + inputString.substring(end);
86 -}
87 -
88 -/**
89 - * @description 格式化证件号展示(脱敏)
90 - * @param {string} id 原始证件号
91 - * @returns {string} 脱敏后的证件号
92 - */
93 -const formatId = (id) => replaceMiddleCharacters(id);
94 74
95 /** 75 /**
96 * @description 加载参观者列表 76 * @description 加载参观者列表
......
...@@ -118,6 +118,71 @@ const formatDatetime = (data) => { ...@@ -118,6 +118,71 @@ const formatDatetime = (data) => {
118 }; 118 };
119 119
120 /** 120 /**
121 + * @description 证件号脱敏
122 + * @param {string} id_number 证件号
123 + * @param {Object} [options] 脱敏配置
124 + * @param {number} [options.keep_start] 保留前几位(传了则按“前后保留”模式脱敏)
125 + * @param {number} [options.keep_end] 保留后几位(传了则按“前后保留”模式脱敏)
126 + * @param {number} [options.mask_count=8] 中间替换为 * 的位数(默认 8)
127 + * @returns {string} 脱敏后的证件号
128 + */
129 +const mask_id_number = (id_number, options = {}) => {
130 + const raw = String(id_number || '')
131 + if (!raw) return ''
132 +
133 + const has_keep_start = Number.isFinite(options.keep_start)
134 + const has_keep_end = Number.isFinite(options.keep_end)
135 + const keep_start = has_keep_start ? options.keep_start : 0
136 + const keep_end = has_keep_end ? options.keep_end : 0
137 + const mask_count = Number.isFinite(options.mask_count) ? options.mask_count : 8
138 +
139 + if (has_keep_start && has_keep_end) {
140 + if (raw.length <= keep_start + keep_end) return raw
141 + const prefix = raw.slice(0, keep_start)
142 + const suffix = raw.slice(raw.length - keep_end)
143 + const middle_len = Math.max(1, raw.length - keep_start - keep_end)
144 + return `${prefix}${'*'.repeat(middle_len)}${suffix}`
145 + }
146 +
147 + if (raw.length < 15) return raw
148 +
149 + const safe_mask_count = Math.min(Math.max(1, mask_count), raw.length)
150 + const start = Math.floor((raw.length - safe_mask_count) / 2)
151 + const end = start + safe_mask_count
152 + if (start < 0 || end > raw.length) return raw
153 +
154 + return raw.substring(0, start) + '*'.repeat(safe_mask_count) + raw.substring(end)
155 +}
156 +
157 +/**
158 + * @description 二维码状态文案
159 + * @param {string|number} status 状态值
160 + * @returns {string} 状态文案
161 + */
162 +const get_qrcode_status_text = (status) => {
163 + const key = String(status || '')
164 + if (key === '1') return '未激活'
165 + if (key === '3') return '待使用'
166 + if (key === '5') return '被取消'
167 + if (key === '7') return '已使用'
168 + return '未知状态'
169 +}
170 +
171 +/**
172 + * @description 订单状态文案
173 + * @param {string|number} status 状态值
174 + * @returns {string} 状态文案
175 + */
176 +const get_bill_status_text = (status) => {
177 + const key = String(status || '')
178 + if (key === '3') return '预约成功'
179 + if (key === '5') return '已取消'
180 + if (key === '9') return '已使用'
181 + if (key === '11') return '退款中'
182 + return '未知状态'
183 +}
184 +
185 +/**
121 * @description 构建 API 请求 URL(带默认公共参数) 186 * @description 构建 API 请求 URL(带默认公共参数)
122 * @param {string} action 接口动作名称(例如:openid_wxapp) 187 * @param {string} action 接口动作名称(例如:openid_wxapp)
123 * @param {Object} [params={}] 额外 query 参数 188 * @param {Object} [params={}] 额外 query 参数
...@@ -133,4 +198,4 @@ const buildApiUrl = (action, params = {}) => { ...@@ -133,4 +198,4 @@ const buildApiUrl = (action, params = {}) => {
133 return `${BASE_URL}/srv/?${queryParams.toString()}` 198 return `${BASE_URL}/srv/?${queryParams.toString()}`
134 } 199 }
135 200
136 -export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, buildApiUrl }; 201 +export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, mask_id_number, get_qrcode_status_text, get_bill_status_text, buildApiUrl };
......