refactor: 抽离通用工具函数并优化轮询生命周期管理
- 将身份证脱敏、状态映射等工具函数统一到 utils/tools.js - 在 qrCode 组件暴露轮询启停方法,由页面生命周期控制 - 更新 README 中优化建议的完成状态
Showing
8 changed files
with
124 additions
and
112 deletions
| ... | @@ -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 }; | ... | ... |
-
Please register or login to post a comment