hookehuyr

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

- 将身份证脱敏、状态映射等工具函数统一到 utils/tools.js
- 在 qrCode 组件暴露轮询启停方法,由页面生命周期控制
- 更新 README 中优化建议的完成状态
......@@ -139,7 +139,7 @@ src/
## 优化建议(下一步)
- 补充测试:优先覆盖 request/authRedirect/offline cache 的关键边界
- 抽离通用工具:证件号脱敏、时间格式化、状态映射等统一放到 utils
- 优化轮询策略:结合页面生命周期 onShow/onHide 控制轮询启停
- 抽离通用工具:已完成(证件号脱敏与状态映射已统一到 utils)
- 优化轮询策略:已完成(二维码轮询按页面生命周期启停)
- 资源优化:CDN 图片策略、分包策略、首屏关键资源预加载
- 构建性能:评估开启 Webpack 持久化缓存,缩短二次编译时间
......
<!--
* @Date: 2024-01-16 10:06:47
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-20 15:48:13
* @LastEditTime: 2026-01-24 14:12:30
* @FilePath: /xyxBooking-weapp/src/components/qrCode.vue
* @Description: 预约码卡组件
-->
......@@ -18,7 +18,7 @@
<image :src="currentQrCodeUrl" mode="aspectFit" />
<view v-if="useStatus === STATUS_CODE.CANCELED || useStatus === STATUS_CODE.USED" class="qrcode-used">
<view class="overlay"></view>
<text class="status-text">二维码{{ qr_code_status[useStatus] }}</text>
<text class="status-text">二维码{{ get_qrcode_status_text(useStatus) }}</text>
</view>
</view>
<view class="right" @tap="nextCode">
......@@ -52,7 +52,7 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
import { formatDatetime } from '@/utils/tools';
import { formatDatetime, mask_id_number, get_qrcode_status_text } from '@/utils/tools';
import { qrcodeListAPI, qrcodeStatusAPI, billPersonAPI } from '@/api/index'
import { useGo } from '@/hooks/useGo'
import BASE_URL from '@/utils/config';
......@@ -116,27 +116,7 @@ watch(
{ immediate: true }
)
/**
* @description 身份证号脱敏:中间 8 位替换为 * 号
* @param {string} inputString 原始身份证号
* @returns {string} 脱敏后的身份证号
*/
function replaceMiddleCharacters (inputString) {
if (!inputString || inputString.length < 15) {
return inputString;
}
const start = Math.floor((inputString.length - 8) / 2);
const end = start + 8;
const replacement = '*'.repeat(8);
return inputString.substring(0, start) + replacement + inputString.substring(end);
}
/**
* @description 格式化证件号展示(脱敏)
* @param {string} id 原始证件号
* @returns {string} 脱敏后的证件号
*/
const formatId = (id) => replaceMiddleCharacters(id);
const formatId = (id) => mask_id_number(id)
const userinfo = computed(() => {
return {
......@@ -156,13 +136,6 @@ const currentQrCodeUrl = computed(() => {
const useStatus = ref('0');
const qr_code_status = {
'1': '未激活',
'3': '待使用',
'5': '被取消',
'7': '已使用',
};
const STATUS_CODE = {
APPLY: '1',
SUCCESS: '3',
......@@ -267,6 +240,7 @@ const init = async () => {
onMounted(() => {
init();
start_polling();
});
/**
......@@ -285,13 +259,36 @@ const poll = async () => {
}
};
// 3 秒轮询一次,避免过于频繁
const intervalId = setInterval(poll, 3000);
const interval_id = ref(null)
/**
* @description 启动轮询
* - 仅在当前选中用户存在时轮询
* @returns {void} 无返回值
*/
const start_polling = () => {
if (interval_id.value) return
interval_id.value = setInterval(poll, 3000)
}
/**
* @description 停止轮询
* - 组件卸载时调用,避免内存泄漏
* @returns {void} 无返回值
*/
const stop_polling = () => {
if (!interval_id.value) return
clearInterval(interval_id.value)
interval_id.value = null
}
onUnmounted(() => {
clearInterval(intervalId);
stop_polling();
});
defineExpose({ start_polling, stop_polling })
/**
* @description 跳转预约记录列表页
* @returns {void} 无返回值
......
......@@ -8,7 +8,7 @@
<template>
<view class="booking-code-page">
<view style="padding: 32rpx;">
<qrCode></qrCode>
<qrCode ref="qr_code_ref"></qrCode>
<view class="warning">
<view style="display: flex; align-items: center; justify-content: center;"><IconFont name="tips" /><text style="margin-left: 10rpx;">温馨提示</text></view>
<view style="margin-top: 16rpx;">一人一码,扫码或识别身份证成功后进入</view>
......@@ -27,7 +27,7 @@
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import Taro, { useDidShow, useDidHide } from '@tarojs/taro'
import qrCode from '@/components/qrCode';
import { IconFont } from '@nutui/icons-vue-taro'
import indexNav from '@/components/indexNav.vue'
......@@ -39,7 +39,10 @@ import { has_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import { is_usable_network } from '@/utils/network'
import { get_weak_network_modal_no_cache_options } from '@/utils/uiText'
const qr_code_ref = ref(null)
useDidShow(() => {
qr_code_ref.value?.start_polling?.()
Taro.getNetworkType({
success: async (res) => {
const isConnected = is_usable_network(res.networkType);
......@@ -72,6 +75,10 @@ useDidShow(() => {
});
})
useDidHide(() => {
qr_code_ref.value?.stop_polling?.()
})
const toMy = () => { // 跳转到我的
Taro.redirectTo({
url: '/pages/me/index'
......
......@@ -7,7 +7,7 @@
-->
<template>
<view class="booking-detail-page">
<qrCode :status="qrCodeStatus" type="detail" :payId="pay_id"></qrCode>
<qrCode ref="qr_code_ref" :status="qrCodeStatus" type="detail" :payId="pay_id"></qrCode>
<view v-if="billInfo.pay_id" class="detail-wrapper">
<view class="detail-item">
<view>参访时间:</view>
......@@ -43,10 +43,10 @@
<script setup>
import { ref, computed } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import Taro, { useDidShow, useDidHide, useRouter as useTaroRouter } from '@tarojs/taro'
import qrCode from '@/components/qrCode';
import { billInfoAPI, icbcRefundAPI } from '@/api/index'
import { formatDatetime } from '@/utils/tools';
import { formatDatetime, get_bill_status_text } from '@/utils/tools';
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
const router = useTaroRouter();
......@@ -54,6 +54,7 @@ const router = useTaroRouter();
const pay_id = ref('');
const qrCodeStatus = ref('');
const billInfo = ref({});
const qr_code_ref = ref(null)
/**
* @description 预约码状态枚举(与后端约定)
......@@ -74,14 +75,7 @@ const CodeStatus = {
* @returns {string} 状态文案
*/
const qrCodeStatusText = computed(() => {
const status = billInfo.value?.status;
switch (status) {
case CodeStatus.SUCCESS: return '预约成功';
case CodeStatus.CANCEL: return '已取消';
case CodeStatus.USED: return '已使用';
case CodeStatus.REFUNDING: return '退款中';
default: return '未知状态';
}
return get_bill_status_text(billInfo.value?.status)
})
/**
......@@ -113,6 +107,7 @@ const cancelBooking = async () => {
}
useDidShow(async () => {
qr_code_ref.value?.start_polling?.()
pay_id.value = router.params.pay_id;
if (pay_id.value) {
const { code, data } = await billInfoAPI({ pay_id: pay_id.value });
......@@ -123,6 +118,10 @@ useDidShow(async () => {
}
}
})
useDidHide(() => {
qr_code_ref.value?.stop_polling?.()
})
</script>
<style lang="less">
......
......@@ -64,6 +64,7 @@ import icon_check1 from '@/assets/images/多选01@2x.png'
import icon_check2 from '@/assets/images/多选02@2x.png'
import { personListAPI, addReserveAPI } from '@/api/index'
import { wechat_pay } from '@/utils/wechatPay'
import { mask_id_number } from '@/utils/tools'
const router = useTaroRouter();
const go = useGo();
......@@ -74,34 +75,7 @@ const date = ref('');
const time = ref('');
const price = ref(0);
const period_type = ref('');
/**
* @description 身份证号脱敏:中间 8 位替换为 *
* @param {string} inputString 身份证号
* @returns {string} 脱敏后的身份证号
*/
function replaceMiddleCharacters(inputString) {
if (!inputString || inputString.length < 15) {
return inputString; // 字符串长度不足,不进行替换
}
const start = Math.floor((inputString.length - 8) / 2); // 开始替换的索引位置
const end = start + 8; // 结束替换的索引位置
const replacement = '*'.repeat(8); // 生成包含8个*号的字符串
const replacedString = inputString.substring(0, start) + replacement + inputString.substring(end);
return replacedString;
}
/**
* @description 格式化身份证号展示
* @param {string} id 身份证号
* @returns {string} 脱敏后的身份证号
*/
const formatId = (id) => {
return replaceMiddleCharacters(id);
};
const formatId = (id) => mask_id_number(id)
/**
* @description 当天预约标记
......
......@@ -84,6 +84,7 @@ import { verifyTicketAPI, checkRedeemPermissionAPI } from '@/api/redeem'
import Taro, { useDidShow } from '@tarojs/taro'
import { mainStore } from '@/stores/main'
import { useReplace } from '@/hooks/useGo'
import { mask_id_number } from '@/utils/tools'
const router = useRouter()
const verify_code = ref('')
......@@ -93,18 +94,7 @@ const msg = ref('请点击下方按钮进行核销')
const store = mainStore()
const replace = useReplace()
// 身份证脱敏函数
const formatIdNumber = (id) => {
if (!id || id.length < 10) return id;
// 保留前6位和后4位,中间用*替换
// 或者根据需求:保留前3后4,中间4位?用户说“中间4位加*号”,通常指显示 110***1918 这种,或者 110101****1234
// 按照常见隐私保护:保留前6位(地区)+出生年(4位)+ 后4位?
// 用户原文:"身份证号码需要中间4位加*号" -> 这通常指隐藏中间部分,或者只隐藏具体的中间4位。
// 标准脱敏通常是隐藏出生月日:1101011990****2918 (保留前10和后4)
// 或者隐藏更彻底:110101********2918
// 这里采用 110101********2918 (保留前6后4) 比较稳妥
return id.replace(/^(.{6})(?:\d+)(.{4})$/, "$1********$2");
}
const formatIdNumber = (id) => mask_id_number(id, { keep_start: 6, keep_end: 4 })
const status_title = computed(() => {
if (verify_status.value === 'verifying') return '核销中'
......
......@@ -47,6 +47,7 @@ import indexNav from '@/components/indexNav.vue'
import icon_3 from '@/assets/images/首页01@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的02@2x.png'
import { mask_id_number } from '@/utils/tools'
const go = useGo();
......@@ -69,28 +70,7 @@ const on_nav_select = (key) => {
}
const visitorList = ref([]);
/**
* @description 身份证号脱敏:中间 8 位替换为 * 号
* @param {string} inputString 原始身份证号
* @returns {string} 脱敏后的身份证号
*/
function replaceMiddleCharacters (inputString) {
if (!inputString || inputString.length < 15) {
return inputString;
}
const start = Math.floor((inputString.length - 8) / 2);
const end = start + 8;
const replacement = '*'.repeat(8);
return inputString.substring(0, start) + replacement + inputString.substring(end);
}
/**
* @description 格式化证件号展示(脱敏)
* @param {string} id 原始证件号
* @returns {string} 脱敏后的证件号
*/
const formatId = (id) => replaceMiddleCharacters(id);
const formatId = (id) => mask_id_number(id)
/**
* @description 加载参观者列表
......
......@@ -118,6 +118,71 @@ const formatDatetime = (data) => {
};
/**
* @description 证件号脱敏
* @param {string} id_number 证件号
* @param {Object} [options] 脱敏配置
* @param {number} [options.keep_start] 保留前几位(传了则按“前后保留”模式脱敏)
* @param {number} [options.keep_end] 保留后几位(传了则按“前后保留”模式脱敏)
* @param {number} [options.mask_count=8] 中间替换为 * 的位数(默认 8)
* @returns {string} 脱敏后的证件号
*/
const mask_id_number = (id_number, options = {}) => {
const raw = String(id_number || '')
if (!raw) return ''
const has_keep_start = Number.isFinite(options.keep_start)
const has_keep_end = Number.isFinite(options.keep_end)
const keep_start = has_keep_start ? options.keep_start : 0
const keep_end = has_keep_end ? options.keep_end : 0
const mask_count = Number.isFinite(options.mask_count) ? options.mask_count : 8
if (has_keep_start && has_keep_end) {
if (raw.length <= keep_start + keep_end) return raw
const prefix = raw.slice(0, keep_start)
const suffix = raw.slice(raw.length - keep_end)
const middle_len = Math.max(1, raw.length - keep_start - keep_end)
return `${prefix}${'*'.repeat(middle_len)}${suffix}`
}
if (raw.length < 15) return raw
const safe_mask_count = Math.min(Math.max(1, mask_count), raw.length)
const start = Math.floor((raw.length - safe_mask_count) / 2)
const end = start + safe_mask_count
if (start < 0 || end > raw.length) return raw
return raw.substring(0, start) + '*'.repeat(safe_mask_count) + raw.substring(end)
}
/**
* @description 二维码状态文案
* @param {string|number} status 状态值
* @returns {string} 状态文案
*/
const get_qrcode_status_text = (status) => {
const key = String(status || '')
if (key === '1') return '未激活'
if (key === '3') return '待使用'
if (key === '5') return '被取消'
if (key === '7') return '已使用'
return '未知状态'
}
/**
* @description 订单状态文案
* @param {string|number} status 状态值
* @returns {string} 状态文案
*/
const get_bill_status_text = (status) => {
const key = String(status || '')
if (key === '3') return '预约成功'
if (key === '5') return '已取消'
if (key === '9') return '已使用'
if (key === '11') return '退款中'
return '未知状态'
}
/**
* @description 构建 API 请求 URL(带默认公共参数)
* @param {string} action 接口动作名称(例如:openid_wxapp)
* @param {Object} [params={}] 额外 query 参数
......@@ -133,4 +198,4 @@ const buildApiUrl = (action, params = {}) => {
return `${BASE_URL}/srv/?${queryParams.toString()}`
}
export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, buildApiUrl };
export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, mask_id_number, get_qrcode_status_text, get_bill_status_text, buildApiUrl };
......