You need to sign in or sign up before continuing.
hookehuyr

docs: 完善项目注释与类型标注

- 为环境文件、常量、工具函数、API 模块等添加 JSDoc 注释
- 统一函数参数与返回值的类型标注
- 增强路由、状态管理、微信 JSSDK 等核心模块的可读性
- 修复动态路由 children 处理逻辑,避免生成非数组值
- 更新微信分享标题为动态年份,避免硬编码
- 重构防抖钩子,移除 lodash 依赖,减少打包体积
1 +###
2 + # @Date: 2026-01-22 10:52:04
3 + # @LastEditors: hookehuyr hookehuyr@gmail.com
4 + # @LastEditTime: 2026-01-24 13:15:00
5 + # @FilePath: /xysBooking/.env.development
6 + # @Description: 开发环境配置文件
7 +###
1 # 资源公共路径 8 # 资源公共路径
2 VITE_BASE = / 9 VITE_BASE = /
3 10
......
...@@ -40,6 +40,12 @@ setToastDefaultOptions({ ...@@ -40,6 +40,12 @@ setToastDefaultOptions({
40 className: 'zIndex' 40 className: 'zIndex'
41 }); 41 });
42 42
43 +/**
44 + * 初始化微信 JSSDK(全局只执行一次)
45 + * - 拉取后端签名参数
46 + * - 调用 wx.config 并等待 wx.ready
47 + * @returns {Promise<boolean>} 是否初始化成功
48 + */
43 const init_wx_global = async () => { 49 const init_wx_global = async () => {
44 try { 50 try {
45 const cfg_res = await wxJsAPI() 51 const cfg_res = await wxJsAPI()
...@@ -64,6 +70,7 @@ const init_wx_global = async () => { ...@@ -64,6 +70,7 @@ const init_wx_global = async () => {
64 } 70 }
65 71
66 onMounted(async () => { 72 onMounted(async () => {
73 + // 避免重复初始化:把初始化 Promise 挂在 window 上复用
67 if (!window.__wx_ready_promise) { 74 if (!window.__wx_ready_promise) {
68 window.__wx_ready_promise = init_wx_global() 75 window.__wx_ready_promise = init_wx_global()
69 } 76 }
......
...@@ -14,34 +14,31 @@ const Api = { ...@@ -14,34 +14,31 @@ const Api = {
14 } 14 }
15 15
16 /** 16 /**
17 - * @description: 发送验证码 17 + * 发送短信验证码
18 - * @param {*} phone 手机号码 18 + * @param {{ phone: string }} params 请求参数
19 - * @returns 19 + * @returns {Promise<Object|false>} 统一返回(成功为后端对象,失败为 false)
20 */ 20 */
21 export const smsAPI = (params) => fn(fetch.post(Api.SMS, params)); 21 export const smsAPI = (params) => fn(fetch.post(Api.SMS, params));
22 22
23 /** 23 /**
24 - * @description: 获取七牛token 24 + * 获取七牛上传 token
25 - * @param {*} filename 文件名 25 + * @param {{ filename: string, file?: string }} params 请求参数(file 可选,用于部分后端校验)
26 - * @param {*} file 图片base64 26 + * @returns {Promise<Object|false>} 统一返回(成功为后端对象,失败为 false)
27 - * @returns
28 */ 27 */
29 export const qiniuTokenAPI = (params) => fn(fetch.stringifyPost(Api.TOKEN, params)); 28 export const qiniuTokenAPI = (params) => fn(fetch.stringifyPost(Api.TOKEN, params));
30 29
31 /** 30 /**
32 - * @description: 上传七牛 31 + * 上传到七牛(第三方接口,返回结构与业务接口不同)
33 - * @param {*} 32 + * @param {string} url 七牛上传地址
34 - * @returns 33 + * @param {any} data 上传 body(通常是 FormData)
34 + * @param {Object} config axios 配置
35 + * @returns {Promise<Object|false>} 成功返回七牛数据,失败返回 false
35 */ 36 */
36 export const qiniuUploadAPI = (url, data, config) => uploadFn(fetch.basePost(url, data, config)); 37 export const qiniuUploadAPI = (url, data, config) => uploadFn(fetch.basePost(url, data, config));
37 38
38 /** 39 /**
39 - * @description: 保存图片 40 + * 保存七牛文件(通知后端落库/生成访问地址等)
40 - * @param {*} format 41 + * @param {{ format: string, hash: string, height?: number, width?: number, filekey: string }} params 请求参数
41 - * @param {*} hash 42 + * @returns {Promise<Object|false>} 统一返回(成功为后端对象,失败为 false)
42 - * @param {*} height
43 - * @param {*} width
44 - * @param {*} filekey
45 - * @returns
46 */ 43 */
47 export const saveFileAPI = (params) => fn(fetch.stringifyPost(Api.SAVE_FILE, params)); 44 export const saveFileAPI = (params) => fn(fetch.stringifyPost(Api.SAVE_FILE, params));
......
...@@ -3,16 +3,19 @@ ...@@ -3,16 +3,19 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2024-01-25 10:03:20 4 * @LastEditTime: 2024-01-25 10:03:20
5 * @FilePath: /xysBooking/src/api/fn.js 5 * @FilePath: /xysBooking/src/api/fn.js
6 - * @Description: 文件描述 6 + * @Description: API 统一返回处理与 GET/POST 统一封装
7 */ 7 */
8 import axios from '@/utils/axios'; 8 import axios from '@/utils/axios';
9 -import { showSuccessToast, showFailToast, showToast } from 'vant'; 9 +import { showFailToast, showToast } from 'vant';
10 import qs from 'Qs' 10 import qs from 'Qs'
11 11
12 /** 12 /**
13 - * 网络请求功能函数 13 + * 统一处理后端接口返回
14 - * @param {*} api 请求axios接口 14 + * - 约定后端返回结构:{ code, data, msg, show }
15 - * @returns 请求成功后,获取数据 15 + * - code === 1 视为成功,原样返回 res.data
16 + * - 失败时根据 show 决定是否弹出 Toast
17 + * @param {Promise<import('axios').AxiosResponse>} api axios 请求 Promise
18 + * @returns {Promise<Object|false>} 成功返回后端对象,失败返回 false
16 */ 19 */
17 export const fn = (api) => { 20 export const fn = (api) => {
18 return api 21 return api
...@@ -37,9 +40,11 @@ export const fn = (api) => { ...@@ -37,9 +40,11 @@ export const fn = (api) => {
37 } 40 }
38 41
39 /** 42 /**
40 - * 七牛返回格式 43 + * 七牛上传接口返回处理
41 - * @param {*} api 44 + * - 七牛成功时常见表现为 res.statusText === 'OK'
42 - * @returns 45 + * - 与业务接口不同,失败提示使用 showFailToast
46 + * @param {Promise<import('axios').AxiosResponse>} api axios 请求 Promise
47 + * @returns {Promise<Object|false>} 成功返回 res.data,失败返回 false
43 */ 48 */
44 export const uploadFn = (api) => { 49 export const uploadFn = (api) => {
45 return api 50 return api
...@@ -62,18 +67,46 @@ export const uploadFn = (api) => { ...@@ -62,18 +67,46 @@ export const uploadFn = (api) => {
62 } 67 }
63 68
64 /** 69 /**
65 - * 统一 GET/POST 不同传参形式 70 + * 统一封装 GET/POST 的传参形式
71 + * - get:以 { params } 形式传递查询参数
72 + * - post:以 JSON 形式传递 body
73 + * - stringifyPost:以 x-www-form-urlencoded 形式传递 body(兼容部分 PHP 接口)
66 */ 74 */
67 export const fetch = { 75 export const fetch = {
76 + /**
77 + * GET 请求封装
78 + * @param {string} api 接口地址
79 + * @param {Object} params 查询参数
80 + * @returns {Promise<import('axios').AxiosResponse>} axios Promise
81 + */
68 get: function (api, params) { 82 get: function (api, params) {
69 return axios.get(api, { params }) 83 return axios.get(api, { params })
70 }, 84 },
85 + /**
86 + * POST 请求封装(JSON body)
87 + * @param {string} api 接口地址
88 + * @param {Object} params 请求体
89 + * @returns {Promise<import('axios').AxiosResponse>} axios Promise
90 + */
71 post: function (api, params) { 91 post: function (api, params) {
72 return axios.post(api, params) 92 return axios.post(api, params)
73 }, 93 },
94 + /**
95 + * POST 请求封装(urlencoded body)
96 + * @param {string} api 接口地址
97 + * @param {Object} params 请求体
98 + * @returns {Promise<import('axios').AxiosResponse>} axios Promise
99 + */
74 stringifyPost: function (api, params) { 100 stringifyPost: function (api, params) {
75 return axios.post(api, qs.stringify(params)) 101 return axios.post(api, qs.stringify(params))
76 }, 102 },
103 + /**
104 + * 透传 POST(用于七牛等第三方上传)
105 + * @param {string} url 完整 URL
106 + * @param {any} data body 数据
107 + * @param {Object} config axios 配置
108 + * @returns {Promise<import('axios').AxiosResponse>} axios Promise
109 + */
77 basePost: function (url, data, config) { 110 basePost: function (url, data, config) {
78 return axios.post(url, data, config) 111 return axios.post(url, data, config)
79 } 112 }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 * @LastEditTime: 2022-06-14 14:47:01 5 * @LastEditTime: 2022-06-14 14:47:01
6 * @FilePath: /tswj/src/api/wx/config.js 6 * @FilePath: /tswj/src/api/wx/config.js
7 - * @Description: 7 + * @Description: 微信 JSSDK 配置相关接口
8 */ 8 */
9 import { fn, fetch } from '@/api/fn'; 9 import { fn, fetch } from '@/api/fn';
10 10
...@@ -13,8 +13,9 @@ const Api = { ...@@ -13,8 +13,9 @@ const Api = {
13 } 13 }
14 14
15 /** 15 /**
16 - * @description 获取微信CONFIG配置文件 16 + * 获取微信 JSSDK config 所需参数
17 - * @param {*} url 17 + * - 返回数据通常包含 appId/timestamp/nonceStr/signature 等
18 - * @returns {*} cfg 18 + * @param {{ url?: string }} params 请求参数(部分后端会用 url 参与签名)
19 + * @returns {Promise<Object|false>} 统一返回(成功为后端对象,失败为 false)
19 */ 20 */
20 export const wxJsAPI = (params) => fn(fetch.get(Api.WX_JSAPI, params)); 21 export const wxJsAPI = (params) => fn(fetch.get(Api.WX_JSAPI, params));
......
...@@ -3,7 +3,12 @@ ...@@ -3,7 +3,12 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2022-06-13 14:27:21 4 * @LastEditTime: 2022-06-13 14:27:21
5 * @FilePath: /tswj/src/api/wx/jsApiList.js 5 * @FilePath: /tswj/src/api/wx/jsApiList.js
6 - * @Description: 文件描述 6 + * @Description: 微信 JSSDK 需要注册的能力列表
7 + */
8 +/**
9 + * 微信 JSSDK 需要注册的 jsApiList
10 + * - 用于 wx.config({ jsApiList }) 申请对应能力
11 + * @type {string[]}
7 */ 12 */
8 export const apiList = [ 13 export const apiList = [
9 "updateAppMessageShareData", 14 "updateAppMessageShareData",
......
1 /** 1 /**
2 - * 判断多行省略文本 2 + * @Description: 通用 DOM 工具
3 - * @param {*} id 目标dom标签 3 + */
4 - * @returns 4 +
5 +/**
6 + * 判断指定元素是否发生了多行溢出(常用于判断是否需要“展开/收起”)
7 + * @param {string} id 目标 DOM 的 id
8 + * @returns {boolean} 是否溢出
5 */ 9 */
6 const hasEllipsis = (id) => { 10 const hasEllipsis = (id) => {
7 let oDiv = document.getElementById(id); 11 let oDiv = document.getElementById(id);
...@@ -14,4 +18,4 @@ const hasEllipsis = (id) => { ...@@ -14,4 +18,4 @@ const hasEllipsis = (id) => {
14 18
15 export default { 19 export default {
16 hasEllipsis 20 hasEllipsis
17 -}
...\ No newline at end of file ...\ No newline at end of file
21 +}
......
1 import { ref } from 'vue' 1 import { ref } from 'vue'
2 -import { useBrowserLocation, useEventListener, useTitle, useUrlSearchParams, useWindowScroll, logicAnd } from '@vueuse/core' 2 +// import { useBrowserLocation, useEventListener, useTitle, useUrlSearchParams, useWindowScroll, logicAnd } from '@vueuse/core'
3 +
4 +/**
5 + * @Description: vueuse 能力测试/示例(开发调试用)
6 + */
7 +
8 +/**
9 + * vueuse 示例函数(当前未接入业务逻辑)
10 + * @returns {void}
11 + */
3 export const fn = () => { 12 export const fn = () => {
4 // const location = useBrowserLocation() 13 // const location = useBrowserLocation()
5 // console.warn(location.value); 14 // console.warn(location.value);
6 15
7 - // useEventListener(window, 'scroll', (evt) => { 16 + // useEventListener(window, 'scroll', (evt) => {
8 // const { x, y } = useWindowScroll() 17 // const { x, y } = useWindowScroll()
9 // // console.warn(x.value); 18 // // console.warn(x.value);
10 // console.warn(y.value); 19 // console.warn(y.value);
...@@ -16,5 +25,5 @@ export const fn = () => { ...@@ -16,5 +25,5 @@ export const fn = () => {
16 const b = ref(true) 25 const b = ref(true)
17 const flag = a.value && b.value 26 const flag = a.value && b.value
18 console.warn(flag); 27 console.warn(flag);
19 - 28 +
20 } 29 }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2023-06-21 15:58:52 4 * @LastEditTime: 2023-06-21 15:58:52
5 * @FilePath: /huizhu/src/composables/useLogin.js 5 * @FilePath: /huizhu/src/composables/useLogin.js
6 - * @Description: 文件描述 6 + * @Description: 登录流程封装(验证码发送、表单校验、提交登录)
7 */ 7 */
8 import { bLoginAPI } from '@/api/B/login' 8 import { bLoginAPI } from '@/api/B/login'
9 import { useRouter } from 'vue-router' 9 import { useRouter } from 'vue-router'
...@@ -13,11 +13,20 @@ import { useCountDown } from '@vant/use'; ...@@ -13,11 +13,20 @@ import { useCountDown } from '@vant/use';
13 import { smsAPI } from '@/api/common' 13 import { smsAPI } from '@/api/common'
14 import { showSuccessToast, showFailToast } from 'vant'; 14 import { showSuccessToast, showFailToast } from 'vant';
15 15
16 +/**
17 + * 登录逻辑组合式函数
18 + * - 主要用于 B 端登录(历史代码)
19 + * @returns {Object} 页面所需的响应式状态与方法集合
20 + */
16 export const useLogin = () => { 21 export const useLogin = () => {
17 const phone = ref(''); 22 const phone = ref('');
18 const code = ref('') 23 const code = ref('')
19 24
20 const refForm = ref(null); 25 const refForm = ref(null);
26 + /**
27 + * 校验表单并触发提交
28 + * @returns {void}
29 + */
21 const validateForm = () => { 30 const validateForm = () => {
22 const valid = refForm.value.validate(); 31 const valid = refForm.value.validate();
23 valid 32 valid
...@@ -41,7 +50,8 @@ export const useLogin = () => { ...@@ -41,7 +50,8 @@ export const useLogin = () => {
41 /** 50 /**
42 * 手机号码校验 51 * 手机号码校验
43 * 函数返回 true 表示校验通过,false 表示不通过 52 * 函数返回 true 表示校验通过,false 表示不通过
44 - * @param {*} val 53 + * @param {string} val 输入值
54 + * @returns {boolean} 是否校验通过
45 */ 55 */
46 const sms_disabled = ref(false); 56 const sms_disabled = ref(false);
47 const phoneValidator = (val) => { 57 const phoneValidator = (val) => {
...@@ -62,9 +72,18 @@ export const useLogin = () => { ...@@ -62,9 +72,18 @@ export const useLogin = () => {
62 */ 72 */
63 const keyboard_show = ref(false); 73 const keyboard_show = ref(false);
64 const refPhone = ref(null) 74 const refPhone = ref(null)
75 + /**
76 + * 弹出数字键盘
77 + * @returns {void}
78 + */
65 const showKeyboard = () => { // 弹出数字弹框 79 const showKeyboard = () => { // 弹出数字弹框
66 keyboard_show.value = true; 80 keyboard_show.value = true;
67 }; 81 };
82 + /**
83 + * 数字键盘失焦回调
84 + * - 关闭键盘并触发手机号校验
85 + * @returns {void}
86 + */
68 const keyboardBlur = () => { // 数字键盘失焦回调 87 const keyboardBlur = () => { // 数字键盘失焦回调
69 keyboard_show.value = false; 88 keyboard_show.value = false;
70 refPhone.value.validate(); 89 refPhone.value.validate();
...@@ -80,6 +99,11 @@ export const useLogin = () => { ...@@ -80,6 +99,11 @@ export const useLogin = () => {
80 } 99 }
81 }); 100 });
82 101
102 + /**
103 + * 发送验证码
104 + * - 启动倒计时,避免频繁触发
105 + * @returns {Promise<void>}
106 + */
83 const sendCode = async () => { // 发送验证码 107 const sendCode = async () => { // 发送验证码
84 countDown.start(); 108 countDown.start();
85 // 验证码接口 109 // 验证码接口
...@@ -90,12 +114,17 @@ export const useLogin = () => { ...@@ -90,12 +114,17 @@ export const useLogin = () => {
90 }; 114 };
91 115
92 // 过滤输入的数字 只能四位 116 // 过滤输入的数字 只能四位
117 + /**
118 + * 限制验证码输入长度(最多 4 位)
119 + * @param {string} value 输入值
120 + * @returns {string} 截断后的值
121 + */
93 const smsFormatter = (value) => value.substring(0, 4); 122 const smsFormatter = (value) => value.substring(0, 4);
94 123
95 /** 124 /**
96 * 用户登录 125 * 用户登录
97 - * @param {*} phone 126 + * @param {{ phone: string, code: string }} values 表单值
98 - * @param {*} pin 127 + * @returns {Promise<void>}
99 */ 128 */
100 const $router = useRouter(); 129 const $router = useRouter();
101 const onSubmit = async (values) => { 130 const onSubmit = async (values) => {
......
1 /* 1 /*
2 * @Date: 2022-06-13 17:42:32 2 * @Date: 2022-06-13 17:42:32
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2024-02-05 19:05:56 4 + * @LastEditTime: 2026-01-24 13:09:52
5 * @FilePath: /xysBooking/src/composables/useShare.js 5 * @FilePath: /xysBooking/src/composables/useShare.js
6 - * @Description: 文件描述 6 + * @Description: 微信分享能力封装
7 */ 7 */
8 import wx from 'weixin-js-sdk'; 8 import wx from 'weixin-js-sdk';
9 // import { Toast } from 'vant'; 9 // import { Toast } from 'vant';
10 10
11 /** 11 /**
12 - * @description: 微信分享功能 12 + * 配置当前页面的微信分享信息
13 - * @param {*} title 标题 13 + * - 依赖:页面已完成 wx.config 且 wx.ready
14 - * @param {*} desc 描述 14 + * @param {{ title?: string, desc?: string, imgUrl?: string }} options 分享配置
15 - * @param {*} imgUrl 图标 15 + * @returns {void}
16 - * @return {*}
17 */ 16 */
18 -export const sharePage = ({ title = '西园寺2024年春节入寺预约', desc = '除夕21点至初五17点', imgUrl = 'https://cdn.ipadbiz.cn/xys/booking/logo_s.jpg'}) => { 17 +const current_year = new Date().getFullYear();
18 +export const sharePage = ({ title = `西园寺${current_year}年春节入寺预约`, desc = '除夕21点至初五17点', imgUrl = 'https://cdn.ipadbiz.cn/xys/booking/logo_s.jpg'}) => {
19 const shareData = { 19 const shareData = {
20 title, // 分享标题 20 title, // 分享标题
21 desc, // 分享描述 21 desc, // 分享描述
......
...@@ -2,12 +2,15 @@ ...@@ -2,12 +2,15 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2022-05-25 18:34:17 3 * @Date: 2022-05-25 18:34:17
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2023-06-14 09:55:42 5 + * @LastEditTime: 2026-01-24 12:50:43
6 - * @FilePath: /huizhu/src/constant.js 6 + * @FilePath: /xysBooking/src/constant.js
7 - * @Description: 7 + * @Description: 项目常量集合(颜色/枚举等)
8 */ 8 */
9 9
10 -// 颜色变量 10 +/**
11 + * 主题颜色变量
12 + * @type {{ baseColor: string, baseFontColor: string }}
13 + */
11 export const styleColor = { 14 export const styleColor = {
12 baseColor: '#11D2B1', 15 baseColor: '#11D2B1',
13 baseFontColor: '#FFFFFF' 16 baseFontColor: '#FFFFFF'
......
...@@ -4,16 +4,18 @@ import { provide, inject } from "vue"; ...@@ -4,16 +4,18 @@ import { provide, inject } from "vue";
4 4
5 /** 5 /**
6 * 创建全局变量 6 * 创建全局变量
7 - * @param {*} context 7 + * - 基于 provide/inject,适合在组件树内部共享上下文
8 - * @param {*} key 8 + * @param {any} context 要注入的上下文对象
9 + * @param {any} key 注入 key(建议使用 Symbol)
10 + * @returns {void}
9 */ 11 */
10 export function createContext(context, key) { 12 export function createContext(context, key) {
11 provide(key, context) 13 provide(key, context)
12 } 14 }
13 /** 15 /**
14 * 使用全局变量 16 * 使用全局变量
15 - * @param {*} key 17 + * @param {any} key 注入 key
16 - * @returns 18 + * @returns {any} 注入的上下文对象
17 */ 19 */
18 export function useContext(key) { 20 export function useContext(key) {
19 return inject(key) 21 return inject(key)
......
...@@ -4,16 +4,58 @@ ...@@ -4,16 +4,58 @@
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 * @LastEditTime: 2024-01-30 15:26:57 5 * @LastEditTime: 2024-01-30 15:26:57
6 * @FilePath: /xysBooking/src/hooks/useDebounce.js 6 * @FilePath: /xysBooking/src/hooks/useDebounce.js
7 - * @Description: 7 + * @Description: 防抖工具(避免高频触发导致请求/渲染过多)
8 */ 8 */
9 // import _ from 'lodash'; 9 // import _ from 'lodash';
10 /** 10 /**
11 - * 封装lodash防抖 11 + * 防抖函数
12 - * @param {*} fn 执行函数 12 + * - 默认:leading=true、trailing=false(更适合“搜索框输入后立即请求一次”的场景)
13 - * @param {*} timestamp 执行间隔 13 + * - 说明:此实现不依赖 lodash,避免引入额外依赖与打包体积波动
14 - * @param {*} options 函数配置 - 在延迟开始前调用,在延迟结束后不调用 14 + * @param {Function} fn 需要防抖执行的函数
15 - * @returns 返回新的 debounced(防抖动)函数。 15 + * @param {number} timestamp 防抖间隔(毫秒)
16 + * @param {{ leading?: boolean, trailing?: boolean }} options 配置项
17 + * @returns {Function} 防抖后的函数
16 */ 18 */
17 export const useDebounce = (fn, timestamp = 500, options = { leading: true, trailing: false }) => { 19 export const useDebounce = (fn, timestamp = 500, options = { leading: true, trailing: false }) => {
18 - // return _.debounce(fn, timestamp, options); 20 + const cfg = options || {};
21 + const leading = !!cfg.leading;
22 + const trailing = !!cfg.trailing;
23 +
24 + let timer = null;
25 + let last_args = null;
26 + let last_this = null;
27 +
28 + const run_trailing = () => {
29 + // 结束一次防抖窗口:仅在 trailing=true 且窗口内有新入参时执行
30 + if (trailing && last_args) {
31 + fn.apply(last_this, last_args);
32 + }
33 + last_args = null;
34 + last_this = null;
35 + };
36 +
37 + const clear_timer = () => {
38 + if (timer) {
39 + clearTimeout(timer);
40 + timer = null;
41 + }
42 + };
43 +
44 + return function (...args) {
45 + last_args = args;
46 + last_this = this;
47 +
48 + // 如果当前不在防抖窗口内,且允许 leading,则立即执行一次
49 + if (!timer && leading) {
50 + fn.apply(last_this, last_args);
51 + last_args = null;
52 + last_this = null;
53 + }
54 +
55 + clear_timer();
56 + timer = setTimeout(() => {
57 + timer = null;
58 + run_trailing();
59 + }, timestamp);
60 + }
19 } 61 }
......
1 /** 1 /**
2 - * @description 封装简化滚动查询列表执行流程 2 + * 封装简化滚动查询列表执行流程
3 - * @param {*} data 接口返回列表数据 3 + * - 统一拼接列表、去重、更新加载状态与空数据状态
4 - * @param {*} list 自定义列表 4 + * @param {Array<any>} data 接口返回的列表数据
5 - * @param {*} offset 5 + * @param {{ value: Array<any> }} list 需要渲染的列表(ref)
6 - * @param {*} loading 6 + * @param {{ value: number }} offset 已加载数量(ref)
7 - * @param {*} finished 7 + * @param {{ value: boolean }} loading 加载状态(ref)
8 - * @param {*} finishedTextStatus 8 + * @param {{ value: boolean }} finished 是否全部加载完成(ref)
9 - * @param {*} emptyStatus 9 + * @param {{ value: boolean }} finishedTextStatus 是否显示“没有更多了”(ref)
10 + * @param {{ value: boolean }} emptyStatus 是否为空列表(ref)
11 + * @returns {void}
10 */ 12 */
11 // import _ from 'lodash' 13 // import _ from 'lodash'
12 14
13 export const flowFn = (data, list, offset, loading, finished, finishedTextStatus, emptyStatus) => { 15 export const flowFn = (data, list, offset, loading, finished, finishedTextStatus, emptyStatus) => {
14 - // list.value = _.concat(list.value, data); 16 + const next_list = Array.isArray(data) ? data : [];
15 - // list.value = _.uniqBy(list.value, 'id'); 17 +
16 - // offset.value = list.value.length; 18 + // 合并数据
17 - // loading.value = false; 19 + const merged = (Array.isArray(list.value) ? list.value : []).concat(next_list);
18 - // // 数据全部加载完成 20 +
19 - // if (!data.length) { 21 + // 尝试按 id 去重(如果每条数据都具备 id)
20 - // // 加载状态结束 22 + const can_dedupe_by_id = merged.length > 0 && merged.every(item => item && typeof item === 'object' && 'id' in item);
21 - // finished.value = true; 23 + const deduped = can_dedupe_by_id
22 - // } 24 + ? Array.from(new Map(merged.map(item => [item.id, item])).values())
23 - // // 空数据提示 25 + : merged;
24 - // if (!list.value.length) { 26 +
25 - // finishedTextStatus.value = false; 27 + list.value = deduped;
26 - // } 28 + offset.value = deduped.length;
27 - // emptyStatus.value = Object.is(list.value.length, 0); 29 + loading.value = false;
30 +
31 + // 数据全部加载完成:本次没有拉到数据
32 + if (!next_list.length) {
33 + finished.value = true;
34 + }
35 +
36 + // 空数据提示:列表为空时不显示“没有更多了”
37 + if (!deduped.length) {
38 + finishedTextStatus.value = false;
39 + }
40 +
41 + emptyStatus.value = Object.is(deduped.length, 0);
28 } 42 }
......
1 import { useRouter } from 'vue-router'; 1 import { useRouter } from 'vue-router';
2 2
3 /** 3 /**
4 - * 封装路由跳转方便行内调用 4 + * 获取路由跳转方法(push)
5 - * @returns 5 + * @returns {(path: string, query?: Object) => void} 跳转函数
6 */ 6 */
7 export function useGo () { 7 export function useGo () {
8 let router = useRouter() 8 let router = useRouter()
9 + /**
10 + * 路由跳转(push)
11 + * @param {string} path 路径
12 + * @param {Object} query query 参数
13 + * @returns {void}
14 + */
9 function go (path, query) { 15 function go (path, query) {
10 router.push({ 16 router.push({
11 path: path, 17 path: path,
...@@ -15,8 +21,18 @@ export function useGo () { ...@@ -15,8 +21,18 @@ export function useGo () {
15 return go 21 return go
16 } 22 }
17 23
24 +/**
25 + * 获取路由跳转方法(replace)
26 + * @returns {(path: string, query?: Object) => void} 替换跳转函数
27 + */
18 export function useReplace () { 28 export function useReplace () {
19 let router = useRouter() 29 let router = useRouter()
30 + /**
31 + * 路由跳转(replace)
32 + * @param {string} path 路径
33 + * @param {Object} query query 参数
34 + * @returns {void}
35 + */
20 function replace (path, query) { 36 function replace (path, query) {
21 router.replace({ 37 router.replace({
22 path: path, 38 path: path,
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 * @LastEditTime: 2024-01-23 13:14:47 5 * @LastEditTime: 2024-01-23 13:14:47
6 * @FilePath: /xysBooking/src/main.js 6 * @FilePath: /xysBooking/src/main.js
7 - * @Description: 7 + * @Description: 项目入口(创建应用、注册路由/Pinia、注册 Vant 组件、挂载全局 http)
8 */ 8 */
9 import { createApp } from 'vue'; 9 import { createApp } from 'vue';
10 import { 10 import {
...@@ -56,7 +56,8 @@ import '@vant/touch-emulator'; ...@@ -56,7 +56,8 @@ import '@vant/touch-emulator';
56 const pinia = createPinia(); 56 const pinia = createPinia();
57 const app = createApp(App); 57 const app = createApp(App);
58 58
59 -app.config.globalProperties.$http = axios; // 关键语句 59 +// 统一挂载 http 实例:页面里可以通过 this.$http 使用(Options API 场景)
60 +app.config.globalProperties.$http = axios;
60 61
61 app 62 app
62 .use(pinia) 63 .use(pinia)
......
...@@ -5,6 +5,11 @@ ...@@ -5,6 +5,11 @@
5 * @FilePath: /git/xysBooking/src/route.js 5 * @FilePath: /git/xysBooking/src/route.js
6 * @Description: 路由列表 6 * @Description: 路由列表
7 */ 7 */
8 +/**
9 + * 静态路由表
10 + * - 每个页面的 meta.title 用于设置 document.title
11 + * @type {Array<Object>}
12 + */
8 export default [ 13 export default [
9 { 14 {
10 path: '/', 15 path: '/',
...@@ -19,8 +24,8 @@ export default [ ...@@ -19,8 +24,8 @@ export default [
19 meta: { 24 meta: {
20 title: '预约须知', 25 title: '预约须知',
21 }, 26 },
22 - //路由的独享守卫 27 + // 路由独享守卫(预留)
23 - beforeEnter: (to,from,next) => { 28 + beforeEnter: (to, from, next) => {
24 // console.warn(to, from); 29 // console.warn(to, from);
25 next(); 30 next();
26 } 31 }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2022-06-29 21:36:59 4 * @LastEditTime: 2022-06-29 21:36:59
5 * @FilePath: /tswj/src/router.js 5 * @FilePath: /tswj/src/router.js
6 - * @Description: 文件描述 6 + * @Description: 路由入口(静态路由 + 动态路由注入)
7 */ 7 */
8 import { createRouter, createWebHashHistory } from 'vue-router'; 8 import { createRouter, createWebHashHistory } from 'vue-router';
9 import RootRoute from './route.js'; 9 import RootRoute from './route.js';
...@@ -15,6 +15,7 @@ import generateRoutes from './utils/generateRoute' ...@@ -15,6 +15,7 @@ import generateRoutes from './utils/generateRoute'
15 * 把项目独有的路由配置到相应的路径,默认路由文件只放公用部分 15 * 把项目独有的路由配置到相应的路径,默认路由文件只放公用部分
16 * 但是 vue 文件内容还是要事先准备好 16 * 但是 vue 文件内容还是要事先准备好
17 */ 17 */
18 +// Vite: 扫描并一次性引入模块路由(按需可拆分成多文件维护)
18 const modules = import.meta.globEager('@/router/routes/modules/**/*.js'); // Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块 19 const modules = import.meta.globEager('@/router/routes/modules/**/*.js'); // Vite 支持使用特殊的 import.meta.glob 函数从文件系统导入多个模块
19 const routeModuleList = []; 20 const routeModuleList = [];
20 21
...@@ -26,6 +27,7 @@ Object.keys(modules).forEach((key) => { ...@@ -26,6 +27,7 @@ Object.keys(modules).forEach((key) => {
26 27
27 // 创建路由实例并传递 `routes` 配置 28 // 创建路由实例并传递 `routes` 配置
28 const router = createRouter({ 29 const router = createRouter({
30 + // hash 模式:兼容微信环境与静态资源部署路径
29 history: createWebHashHistory('/index.html'), 31 history: createWebHashHistory('/index.html'),
30 routes: [...RootRoute, ...routeModuleList] 32 routes: [...RootRoute, ...routeModuleList]
31 }); 33 });
...@@ -34,13 +36,22 @@ const router = createRouter({ ...@@ -34,13 +36,22 @@ const router = createRouter({
34 /** 36 /**
35 * generateRoute 负责把后台返回数据拼接成项目需要的路由结构,动态添加到路由表里面 37 * generateRoute 负责把后台返回数据拼接成项目需要的路由结构,动态添加到路由表里面
36 */ 38 */
39 +/**
40 + * 动态路由注入守卫
41 + * - 以 404 页面作为中转:首次进入时先落到 404,再把动态路由 addRoute 进来后重定向回目标页面
42 + * @param {any} to 目标路由
43 + * @param {any} from 来源路由
44 + * @param {Function} next 放行函数
45 + * @returns {void}
46 + */
37 router.beforeEach((to, from, next) => { 47 router.beforeEach((to, from, next) => {
38 // 使用404为中转页面,避免动态路由没有渲染出来,控制台报警告问题 48 // 使用404为中转页面,避免动态路由没有渲染出来,控制台报警告问题
39 if (to.path == '/404' && to.redirectedFrom != undefined) { 49 if (to.path == '/404' && to.redirectedFrom != undefined) {
40 // 模拟异步操作 50 // 模拟异步操作
41 setTimeout(() => { 51 setTimeout(() => {
42 if (!asyncRoutesArr.length) return; // 没有动态路由避免报错 52 if (!asyncRoutesArr.length) return; // 没有动态路由避免报错
43 - const arr = generateRoutes(asyncRoutesArr); // 在路由守卫处生成,避免有子路由时刷新白屏问题。 53 + // 在路由守卫处生成,避免有子路由时刷新白屏问题
54 + const arr = generateRoutes(asyncRoutesArr);
44 arr.forEach(item => { 55 arr.forEach(item => {
45 router.addRoute(item) // 新增路由 56 router.addRoute(item) // 新增路由
46 }) 57 })
......
...@@ -3,24 +3,35 @@ ...@@ -3,24 +3,35 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2024-01-30 15:26:30 4 * @LastEditTime: 2024-01-30 15:26:30
5 * @FilePath: /xysBooking/src/store/index.js 5 * @FilePath: /xysBooking/src/store/index.js
6 - * @Description: 文件描述 6 + * @Description: Pinia 主仓库(全局状态与页面缓存控制)
7 */ 7 */
8 import { defineStore } from 'pinia'; 8 import { defineStore } from 'pinia';
9 // import { testStore } from './test'; // 另一个store 9 // import { testStore } from './test'; // 另一个store
10 // import _ from 'lodash'; 10 // import _ from 'lodash';
11 import { useRouter } from 'vue-router' 11 import { useRouter } from 'vue-router'
12 12
13 +/**
14 + * 主状态仓库
15 + * - auth:授权状态
16 + * - keepPages:用于 keep-alive include 的缓存页面列表
17 + * - appUserInfo:缓存预约人信息(跨页面复用)
18 + * @returns {Function} useStore 方法(调用后获取 store 实例)
19 + */
13 export const mainStore = defineStore('main', { 20 export const mainStore = defineStore('main', {
14 state: () => { 21 state: () => {
15 return { 22 return {
16 msg: 'Hello world', 23 msg: 'Hello world',
17 count: 0, 24 count: 0,
18 - auth: false, 25 + auth: false, // 是否已完成授权
19 - keepPages: ['default'], // 很坑爹,空值全部都缓存 26 + keepPages: ['default'], // keep-alive include 为空会全部缓存,这里用默认占位值兜底
20 appUserInfo: [], // 缓存预约人信息 27 appUserInfo: [], // 缓存预约人信息
21 }; 28 };
22 }, 29 },
23 getters: { 30 getters: {
31 + /**
32 + * 获取缓存页面列表(用于 keep-alive include)
33 + * @returns {string[]} 缓存页面 name 列表
34 + */
24 getKeepPages () { 35 getKeepPages () {
25 return this.keepPages 36 return this.keepPages
26 }, 37 },
...@@ -29,12 +40,27 @@ export const mainStore = defineStore('main', { ...@@ -29,12 +40,27 @@ export const mainStore = defineStore('main', {
29 // } 40 // }
30 }, 41 },
31 actions: { 42 actions: {
43 + /**
44 + * 修改授权状态
45 + * @param {boolean} state 授权状态
46 + * @returns {void}
47 + */
32 changeState (state) { 48 changeState (state) {
33 this.auth = state; 49 this.auth = state;
34 }, 50 },
51 + /**
52 + * 清空所有缓存页
53 + * - 用一个不存在的值覆盖,避免 include 为空导致“全页面缓存”
54 + * @returns {void}
55 + */
35 changeKeepPages () { // 清空所有缓存,用一个不存在的值覆盖 56 changeKeepPages () { // 清空所有缓存,用一个不存在的值覆盖
36 this.keepPages = ['default']; 57 this.keepPages = ['default'];
37 }, 58 },
59 + /**
60 + * 把当前页面加入缓存列表
61 + * - 依赖路由 meta.name 作为 keep-alive include 的 key
62 + * @returns {void}
63 + */
38 keepThisPage () { // 新增缓存页 64 keepThisPage () { // 新增缓存页
39 const $router = useRouter(); 65 const $router = useRouter();
40 const page = $router.currentRoute.value.meta.name; 66 const page = $router.currentRoute.value.meta.name;
...@@ -45,6 +71,11 @@ export const mainStore = defineStore('main', { ...@@ -45,6 +71,11 @@ export const mainStore = defineStore('main', {
45 // const page = $router.currentRoute.value.meta.name; 71 // const page = $router.currentRoute.value.meta.name;
46 // _.remove(this.keepPages, item => item === page) 72 // _.remove(this.keepPages, item => item === page)
47 }, 73 },
74 + /**
75 + * 缓存预约人信息
76 + * @param {Array<any>} info 预约人信息
77 + * @returns {void}
78 + */
48 changeUserInfo (info) { 79 changeUserInfo (info) {
49 this.appUserInfo = info; 80 this.appUserInfo = info;
50 } 81 }
......
...@@ -2,9 +2,9 @@ ...@@ -2,9 +2,9 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2022-05-28 10:17:40 3 * @Date: 2022-05-28 10:17:40
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2024-02-06 09:44:43 5 + * @LastEditTime: 2026-01-24 12:41:18
6 * @FilePath: /xysBooking/src/utils/axios.js 6 * @FilePath: /xysBooking/src/utils/axios.js
7 - * @Description: 7 + * @Description: axios 实例与拦截器(统一参数、统一鉴权跳转、统一错误提示入口)
8 */ 8 */
9 import axios from 'axios'; 9 import axios from 'axios';
10 import router from '@/router'; 10 import router from '@/router';
...@@ -12,13 +12,18 @@ import router from '@/router'; ...@@ -12,13 +12,18 @@ import router from '@/router';
12 // import { strExist } from '@/utils/tools' 12 // import { strExist } from '@/utils/tools'
13 // import { parseQueryString } from '@/utils/tools' 13 // import { parseQueryString } from '@/utils/tools'
14 14
15 +// 设置所有请求默认携带的公共参数
15 axios.defaults.params = { 16 axios.defaults.params = {
16 f: 'reserve', 17 f: 'reserve',
17 client_name: '智慧西园寺', 18 client_name: '智慧西园寺',
18 }; 19 };
19 20
20 /** 21 /**
21 - * @description 请求拦截器 22 + * 请求拦截器
23 + * - GET 默认追加时间戳,避免浏览器/中间层缓存导致数据不更新
24 + * - 统一补齐 config.params,保证公共参数与业务参数可以并存
25 + * @param {import('axios').InternalAxiosRequestConfig} config axios 请求配置
26 + * @returns {import('axios').InternalAxiosRequestConfig} 处理后的请求配置
22 */ 27 */
23 axios.interceptors.request.use( 28 axios.interceptors.request.use(
24 config => { 29 config => {
...@@ -36,12 +41,17 @@ axios.interceptors.request.use( ...@@ -36,12 +41,17 @@ axios.interceptors.request.use(
36 return config; 41 return config;
37 }, 42 },
38 error => { 43 error => {
39 - // 请求错误处理 44 + // 请求错误处理(如:参数序列化异常、网络层拦截等)
40 return Promise.reject(error); 45 return Promise.reject(error);
41 }); 46 });
42 47
43 /** 48 /**
44 - * @description 响应拦截器 49 + * 响应拦截器
50 + * - 约定后端返回 { code, data, msg, show } 结构
51 + * - code === 401 时,进行授权页跳转(避免未授权接口一直报错)
52 + * - 部分业务错误提示需要静默(show === false 或命中特定 msg)
53 + * @param {import('axios').AxiosResponse} response axios 响应对象
54 + * @returns {import('axios').AxiosResponse} 原样返回响应(由上层 fn 统一处理 code/msg)
45 */ 55 */
46 axios.interceptors.response.use( 56 axios.interceptors.response.use(
47 response => { 57 response => {
...@@ -51,6 +61,7 @@ axios.interceptors.response.use( ...@@ -51,6 +61,7 @@ axios.interceptors.response.use(
51 // // C/B 授权拼接头特殊标识,openid_x 61 // // C/B 授权拼接头特殊标识,openid_x
52 // let prefixAPI = router?.currentRoute.value.href?.indexOf('business') > 0 ? 'b' : 'c'; 62 // let prefixAPI = router?.currentRoute.value.href?.indexOf('business') > 0 ? 'b' : 'c';
53 if (response.data.code === 401) { 63 if (response.data.code === 401) {
64 + // 未授权时不弹 Toast,统一跳转到授权页
54 response.data.show = false; 65 response.data.show = false;
55 const request_params = response?.config?.params || {}; 66 const request_params = response?.config?.params || {};
56 const is_redeem_admin = request_params?.f === 'reserve_admin'; 67 const is_redeem_admin = request_params?.f === 'reserve_admin';
...@@ -60,6 +71,7 @@ axios.interceptors.response.use( ...@@ -60,6 +71,7 @@ axios.interceptors.response.use(
60 } 71 }
61 } 72 }
62 if (['预约ID不存在'].includes(response.data.msg)) { 73 if (['预约ID不存在'].includes(response.data.msg)) {
74 + // 这类错误属于流程中“可预期异常”,不提示用户,交给页面自行兜底
63 response.data.show = false; 75 response.data.show = false;
64 } 76 }
65 // // 拦截B端未登录情况 77 // // 拦截B端未登录情况
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2024-01-30 15:19:10 4 * @LastEditTime: 2024-01-30 15:19:10
5 * @FilePath: /xysBooking/src/utils/generatePackage.js 5 * @FilePath: /xysBooking/src/utils/generatePackage.js
6 - * @Description: 文件描述 6 + * @Description: 统一导出常用依赖(减少页面重复 import)
7 */ 7 */
8 import Cookies from 'js-cookie' 8 import Cookies from 'js-cookie'
9 // import $ from 'jquery' 9 // import $ from 'jquery'
...@@ -15,6 +15,7 @@ import { Toast, Dialog } from 'vant'; ...@@ -15,6 +15,7 @@ import { Toast, Dialog } from 'vant';
15 import { wxInfo, hasEllipsis } from '@/utils/tools'; 15 import { wxInfo, hasEllipsis } from '@/utils/tools';
16 import { useTitle } from '@vueuse/core' 16 import { useTitle } from '@vueuse/core'
17 17
18 +// TAG: 这里集中导出“项目高频使用”的依赖,页面按需引入即可
18 export { 19 export {
19 Cookies, 20 Cookies,
20 // $, 21 // $,
......
...@@ -3,13 +3,13 @@ ...@@ -3,13 +3,13 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2022-06-29 17:00:15 4 * @LastEditTime: 2022-06-29 17:00:15
5 * @FilePath: /tswj/src/utils/generateRoute.js 5 * @FilePath: /tswj/src/utils/generateRoute.js
6 - * @Description: 文件描述 6 + * @Description: 动态路由组装(把后端路由数据转换成 vue-router 结构)
7 */ 7 */
8 8
9 /** 9 /**
10 - * 根据后台返回的路径,生成页面的组件模版 10 + * 根据后端返回的 component 字段,生成对应页面的动态 import
11 - * @param {*} component 11 + * @param {string} component views 下的页面文件名(不含 .vue)
12 - * @returns 模版地址 12 + * @returns {() => Promise<any>} 组件加载函数
13 */ 13 */
14 function loadView(component) { 14 function loadView(component) {
15 return () => import(`../views/${component}.vue`) 15 return () => import(`../views/${component}.vue`)
...@@ -17,7 +17,8 @@ function loadView(component) { ...@@ -17,7 +17,8 @@ function loadView(component) {
17 17
18 /** 18 /**
19 * 生成路由结构 19 * 生成路由结构
20 - * @param {*} routes 20 + * @param {Array<Object>} routes 后端路由数组
21 + * @returns {Array<Object>} vue-router routes 数组
21 */ 22 */
22 const generateRoutes = (routes) => { 23 const generateRoutes = (routes) => {
23 const arr = [] 24 const arr = []
...@@ -39,7 +40,8 @@ const generateRoutes = (routes) => { ...@@ -39,7 +40,8 @@ const generateRoutes = (routes) => {
39 router.component = loadView(component) 40 router.component = loadView(component)
40 keepAlive && (router.keepAlive = keepAlive) 41 keepAlive && (router.keepAlive = keepAlive)
41 meta && (router.meta = meta) 42 meta && (router.meta = meta)
42 - router.children = !Array.isArray(children) || generateRoutes(children); 43 + // children 不是数组时,统一置空,避免生成 true 导致路由结构异常
44 + router.children = Array.isArray(children) ? generateRoutes(children) : [];
43 arr.push(router) 45 arr.push(router)
44 }) 46 })
45 return arr 47 return arr
......
1 import wx from 'weixin-js-sdk' 1 import wx from 'weixin-js-sdk'
2 import axios from '@/utils/axios'; 2 import axios from '@/utils/axios';
3 3
4 +/**
5 + * 微信分享配置(历史代码,当前主要用于调试/预留)
6 + * @param {{ name?: string }} to 目标路由信息(通常来自 vue-router)
7 + * @returns {void}
8 + */
4 const fn = (to) => { 9 const fn = (to) => {
5 - // 路由名 10 + // 路由名(从 hash 中截取)
6 let ruleName = location.href.split('#/')[1].split('?')[0]; 11 let ruleName = location.href.split('#/')[1].split('?')[0];
7 12
13 + // 分享图标
8 const icon = 'https://cdn.lifeat.cn/webappgroup/betterLifelogo.png' 14 const icon = 'https://cdn.lifeat.cn/webappgroup/betterLifelogo.png'
9 15
16 + // 分享文案映射表
10 const shareInfoMap = { 17 const shareInfoMap = {
11 'client/index': { 18 'client/index': {
12 title: '童声无界', 19 title: '童声无界',
...@@ -82,4 +89,4 @@ export default fn ...@@ -82,4 +89,4 @@ export default fn
82 // }).catch(err => { 89 // }).catch(err => {
83 // console.info('err:', err) 90 // console.info('err:', err)
84 // }) 91 // })
85 -// }
...\ No newline at end of file ...\ No newline at end of file
92 +// }
......
...@@ -3,18 +3,22 @@ ...@@ -3,18 +3,22 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2024-01-30 15:43:33 4 * @LastEditTime: 2024-01-30 15:43:33
5 * @FilePath: /xysBooking/src/utils/tools.js 5 * @FilePath: /xysBooking/src/utils/tools.js
6 - * @Description: 文件描述 6 + * @Description: 通用工具函数(时间格式化、终端判断、URL 解析等)
7 */ 7 */
8 import dayjs from 'dayjs'; 8 import dayjs from 'dayjs';
9 9
10 -// 格式化时间 10 +/**
11 + * 格式化时间(默认到分钟)
12 + * @param {string|number|Date} date 时间入参
13 + * @returns {string} 格式化后的时间字符串:YYYY-MM-DD HH:mm
14 + */
11 const formatDate = (date) => { 15 const formatDate = (date) => {
12 return dayjs(date).format('YYYY-MM-DD HH:mm'); 16 return dayjs(date).format('YYYY-MM-DD HH:mm');
13 }; 17 };
14 18
15 /** 19 /**
16 - * @description 判断浏览器属于平台 20 + * 判断当前运行环境(Android/iOS/微信)
17 - * @returns 21 + * @returns {{ isAndroid: boolean, isiOS: boolean, isTable: boolean }} 终端信息
18 */ 22 */
19 const wxInfo = () => { 23 const wxInfo = () => {
20 let u = navigator.userAgent; 24 let u = navigator.userAgent;
...@@ -30,9 +34,9 @@ const wxInfo = () => { ...@@ -30,9 +34,9 @@ const wxInfo = () => {
30 }; 34 };
31 35
32 /** 36 /**
33 - * @description 判断多行省略文本 37 + * 判断指定元素是否发生了多行溢出(常用于判断是否需要“展开/收起”)
34 - * @param {*} id 目标dom标签 38 + * @param {string} id 目标 DOM 的 id
35 - * @returns 39 + * @returns {boolean} 是否溢出
36 */ 40 */
37 const hasEllipsis = (id) => { 41 const hasEllipsis = (id) => {
38 let oDiv = document.getElementById(id); 42 let oDiv = document.getElementById(id);
...@@ -44,9 +48,9 @@ const hasEllipsis = (id) => { ...@@ -44,9 +48,9 @@ const hasEllipsis = (id) => {
44 } 48 }
45 49
46 /** 50 /**
47 - * @description 解析URL参数 51 + * 解析 URL 查询参数
48 - * @param {*} url 52 + * @param {string} url 完整 URL(包含 ?query)
49 - * @returns 53 + * @returns {Record<string, string>} 解析后的键值对
50 */ 54 */
51 const parseQueryString = url => { 55 const parseQueryString = url => {
52 var json = {}; 56 var json = {};
...@@ -71,9 +75,21 @@ const strExist = (array, str) => { ...@@ -71,9 +75,21 @@ const strExist = (array, str) => {
71 return exist.length > 0 75 return exist.length > 0
72 } 76 }
73 77
78 +/**
79 + * 格式化预约时段显示文本
80 + * - 兼容后端返回的 ISO 字符串(带 Z / +08:00)以及空格分隔等形式
81 + * - 若结束时间恰好为次日 00:00,则展示为 24:00(更符合“营业到 24:00”的直觉)
82 + * @param {{ begin_time?: string, end_time?: string }} data 接口返回的时段对象
83 + * @returns {string} 形如:YYYY-MM-DD HH:mm-HH:mm
84 + */
74 const formatDatetime = (data) => { 85 const formatDatetime = (data) => {
75 if (!data || !data.begin_time || !data.end_time) return ''; 86 if (!data || !data.begin_time || !data.end_time) return '';
76 87
88 + /**
89 + * 规范化时间字符串,尽量喂给 dayjs 可解析格式
90 + * @param {string} timeStr 原始时间字符串
91 + * @returns {string} 规范化后的字符串
92 + */
77 const normalize = (timeStr) => { 93 const normalize = (timeStr) => {
78 if (!timeStr) return ''; 94 if (!timeStr) return '';
79 let clean = timeStr.split('+')[0]; 95 let clean = timeStr.split('+')[0];
......
...@@ -3,15 +3,19 @@ ...@@ -3,15 +3,19 @@
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 * @LastEditTime: 2024-02-06 13:04:25 4 * @LastEditTime: 2024-02-06 13:04:25
5 * @FilePath: /xysBooking/src/utils/versionUpdater.js 5 * @FilePath: /xysBooking/src/utils/versionUpdater.js
6 - * @Description: 6 + * @Description: 轮询检测静态资源变更(用于提示用户刷新页面)
7 */ 7 */
8 /* eslint-disable */ 8 /* eslint-disable */
9 /** 9 /**
10 - * @description: 版本更新检查 10 + * 版本更新检查器
11 - * @param {*} time 阈值 11 + * - 思路:定时拉取 index.html,对比其中 <script> 标签内容是否发生变化
12 - * @return {*} 12 + * - 适用:Vite 构建的产物文件名带 hash,更新后 script src 会变化
13 + * @class
13 */ 14 */
14 export class Updater { 15 export class Updater {
16 + /**
17 + * @param {{ time?: number }} options 配置项
18 + */
15 constructor(options = {}) { 19 constructor(options = {}) {
16 this.oldScript = []; 20 this.oldScript = [];
17 this.newScript = []; 21 this.newScript = [];
...@@ -20,45 +24,75 @@ export class Updater { ...@@ -20,45 +24,75 @@ export class Updater {
20 this.timing(options.time); //轮询 24 this.timing(options.time); //轮询
21 } 25 }
22 26
27 + /**
28 + * 初始化:读取当前 html 并记录 script 标签快照
29 + * @returns {Promise<void>}
30 + */
23 async init() { 31 async init() {
24 const html = await this.getHtml(); 32 const html = await this.getHtml();
25 this.oldScript = this.parserScript(html); 33 this.oldScript = this.parserScript(html);
26 } 34 }
27 35
36 + /**
37 + * 拉取 index.html 文本内容
38 + * @returns {Promise<string>} html 文本
39 + */
28 async getHtml() { 40 async getHtml() {
29 // TAG: html的位置需要动态修改 41 // TAG: html的位置需要动态修改
30 const html = await fetch(import.meta.env.VITE_BASE).then((res) => res.text()); //读取index html 42 const html = await fetch(import.meta.env.VITE_BASE).then((res) => res.text()); //读取index html
31 return html; 43 return html;
32 } 44 }
33 45
46 + /**
47 + * 解析 html 中的 script 标签字符串数组
48 + * @param {string} html html 文本
49 + * @returns {string[]|null} script 标签数组(match 可能返回 null)
50 + */
34 parserScript(html) { 51 parserScript(html) {
35 const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi); //script正则 52 const reg = new RegExp(/<script(?:\s+[^>]*)?>(.*?)<\/script\s*>/gi); //script正则
36 return html.match(reg); //匹配script标签 53 return html.match(reg); //匹配script标签
37 } 54 }
38 55
39 - //发布订阅通知 56 + /**
57 + * 订阅事件
58 + * @param {'no-update'|'update'|string} key 事件名
59 + * @param {Function} fn 回调函数
60 + * @returns {Updater} 当前实例,便于链式调用
61 + */
40 on(key, fn) { 62 on(key, fn) {
41 (this.dispatch[key] || (this.dispatch[key] = [])).push(fn); 63 (this.dispatch[key] || (this.dispatch[key] = [])).push(fn);
42 return this; 64 return this;
43 } 65 }
44 66
67 + /**
68 + * 对比两次 script 标签快照
69 + * @param {string[]|null} oldArr 旧快照
70 + * @param {string[]|null} newArr 新快照
71 + * @returns {void}
72 + */
45 compare(oldArr, newArr) { 73 compare(oldArr, newArr) {
46 - const base = oldArr.length; 74 + // 兼容 match 返回 null 的场景,避免 compare 触发运行时异常
75 + const safeOldArr = Array.isArray(oldArr) ? oldArr : [];
76 + const safeNewArr = Array.isArray(newArr) ? newArr : [];
77 + const base = safeOldArr.length;
47 // 去重 78 // 去重
48 - const arr = Array.from(new Set(oldArr.concat(newArr))); 79 + const arr = Array.from(new Set(safeOldArr.concat(safeNewArr)));
49 //如果新旧length 一样无更新 80 //如果新旧length 一样无更新
50 if (arr.length === base) { 81 if (arr.length === base) {
51 - this.dispatch['no-update'].forEach((fn) => { 82 + const fns = Array.isArray(this.dispatch['no-update']) ? this.dispatch['no-update'] : [];
52 - fn(); 83 + fns.forEach((fn) => fn());
53 - });
54 } else { 84 } else {
55 //否则通知更新 85 //否则通知更新
56 - this.dispatch['update'].forEach((fn) => { 86 + const fns = Array.isArray(this.dispatch['update']) ? this.dispatch['update'] : [];
57 - fn(); 87 + fns.forEach((fn) => fn());
58 - });
59 } 88 }
60 } 89 }
61 90
91 + /**
92 + * 开始轮询
93 + * @param {number} time 轮询间隔(毫秒)
94 + * @returns {void}
95 + */
62 timing(time = 10000) { 96 timing(time = 10000) {
63 //轮询 97 //轮询
64 setInterval(async () => { 98 setInterval(async () => {
......
...@@ -72,6 +72,10 @@ const idCode = ref(''); ...@@ -72,6 +72,10 @@ const idCode = ref('');
72 const show_error = ref(false); 72 const show_error = ref(false);
73 const error_message = ref(''); 73 const error_message = ref('');
74 74
75 +/**
76 + * 校验姓名
77 + * @returns {boolean} 是否通过
78 + */
75 const checkUsername = () => { // 检查用户名是否为空 79 const checkUsername = () => { // 检查用户名是否为空
76 let flag = true; 80 let flag = true;
77 if (!username.value) { 81 if (!username.value) {
...@@ -82,6 +86,12 @@ const checkUsername = () => { // 检查用户名是否为空 ...@@ -82,6 +86,12 @@ const checkUsername = () => { // 检查用户名是否为空
82 return flag; 86 return flag;
83 } 87 }
84 88
89 +/**
90 + * 校验证件号
91 + * - 身份证:18 位使用 cin 校验;15 位不校验(兼容老证件)
92 + * - 其他证件:仅做非空校验
93 + * @returns {boolean} 是否通过
94 + */
85 const checkIdCode = () => { // 检查身份证号是否为空 95 const checkIdCode = () => { // 检查身份证号是否为空
86 let flag = true; 96 let flag = true;
87 if (!idCode.value) { 97 if (!idCode.value) {
...@@ -103,6 +113,10 @@ const checkIdCode = () => { // 检查身份证号是否为空 ...@@ -103,6 +113,10 @@ const checkIdCode = () => { // 检查身份证号是否为空
103 return flag; 113 return flag;
104 } 114 }
105 115
116 +/**
117 + * 保存参观者信息
118 + * @returns {Promise<void>}
119 + */
106 const addVisitor = async () => { 120 const addVisitor = async () => {
107 // 保存用户信息 121 // 保存用户信息
108 if (checkUsername() && checkIdCode()) { 122 if (checkUsername() && checkIdCode()) {
...@@ -117,6 +131,10 @@ const addVisitor = async () => { ...@@ -117,6 +131,10 @@ const addVisitor = async () => {
117 } 131 }
118 132
119 const showPicker = ref(false); 133 const showPicker = ref(false);
134 +/**
135 + * 打开/关闭证件类型选择器
136 + * @returns {void}
137 + */
120 const idTypeChange = () => { 138 const idTypeChange = () => {
121 showPicker.value = !showPicker.value; 139 showPicker.value = !showPicker.value;
122 } 140 }
...@@ -128,6 +146,11 @@ const columns = [ ...@@ -128,6 +146,11 @@ const columns = [
128 const fieldValue = ref('身份证'); 146 const fieldValue = ref('身份证');
129 const id_type = ref(1); 147 const id_type = ref(1);
130 148
149 +/**
150 + * 证件类型选择回调
151 + * @param {{ selectedOptions: Array<{ text: string, value: number }> }} payload 选择结果
152 + * @returns {void}
153 + */
131 const onConfirm = ({ selectedOptions }) => { // 切换类型回调 154 const onConfirm = ({ selectedOptions }) => { // 切换类型回调
132 showPicker.value = false; 155 showPicker.value = false;
133 // 156 //
......
...@@ -229,6 +229,13 @@ const infinity_tips_text = computed(() => { ...@@ -229,6 +229,13 @@ const infinity_tips_text = computed(() => {
229 return '暂未开启预约'; 229 return '暂未开启预约';
230 }); 230 });
231 231
232 +/**
233 + * 选择时间段
234 + * - 仅余量大于 0 或“不限量”时允许选择
235 + * @param {any} item 时间段数据
236 + * @param {number} index 时间段下标
237 + * @returns {void}
238 + */
232 const chooseTime = (item, index) => { // 选择时间段回调 239 const chooseTime = (item, index) => { // 选择时间段回调
233 if (item.rest_qty || item.rest_qty === QtyStatus.INFINITY) { // 余量等于-1为不限制数量 240 if (item.rest_qty || item.rest_qty === QtyStatus.INFINITY) { // 余量等于-1为不限制数量
234 checked_time.value = index; 241 checked_time.value = index;
...@@ -237,14 +244,22 @@ const chooseTime = (item, index) => { // 选择时间段回调 ...@@ -237,14 +244,22 @@ const chooseTime = (item, index) => { // 选择时间段回调
237 }; 244 };
238 245
239 /** 246 /**
240 - * @description: 数量状态 247 + * 数量状态
241 - * @return {object} {FULL: 0, INFINITY: -1 } 248 + * - FULL:无余量
249 + * - INFINITY:不限量
250 + * @type {{ FULL: number, INFINITY: number }}
242 */ 251 */
243 const QtyStatus = { 252 const QtyStatus = {
244 FULL: 0, // 无余量 253 FULL: 0, // 无余量
245 INFINITY: -1, // 无限制 254 INFINITY: -1, // 无限制
246 } 255 }
247 256
257 +/**
258 + * 选择日期
259 + * - 可约时查询对应日期的时间段列表
260 + * @param {string} date 日期(YYYY-MM-DD)
261 + * @returns {Promise<void>}
262 + */
248 const chooseDay = async (date) => { // 点击日期回调 263 const chooseDay = async (date) => { // 点击日期回调
249 if (!date) return; 264 if (!date) return;
250 const info = findDatesInfo(date); 265 const info = findDatesInfo(date);
...@@ -264,6 +279,10 @@ const chooseDay = async (date) => { // 点击日期回调 ...@@ -264,6 +279,10 @@ const chooseDay = async (date) => { // 点击日期回调
264 }; 279 };
265 280
266 const showPicker = ref(false); 281 const showPicker = ref(false);
282 +/**
283 + * 打开月份选择器
284 + * @returns {void}
285 + */
267 const chooseDate = () => { 286 const chooseDate = () => {
268 showPicker.value = true; 287 showPicker.value = true;
269 } 288 }
...@@ -275,6 +294,13 @@ const minDate = new Date(); ...@@ -275,6 +294,13 @@ const minDate = new Date();
275 const maxDate = new Date(2050, 11, 1); 294 const maxDate = new Date(2050, 11, 1);
276 const currentDateText = ref((raw_date.getMonth() + 1).toString().padStart(2, '0')); 295 const currentDateText = ref((raw_date.getMonth() + 1).toString().padStart(2, '0'));
277 296
297 +/**
298 + * 月份选择确认回调
299 + * - 重置当前选择的日期/时段
300 + * - 拉取该月可预约日期列表
301 + * @param {{ selectedValues: number[], selectedOptions: any[] }} payload 选择结果
302 + * @returns {Promise<void>}
303 + */
278 const onConfirm = async ({ selectedValues, selectedOptions }) => { // 选择日期回调 304 const onConfirm = async ({ selectedValues, selectedOptions }) => { // 选择日期回调
279 showPicker.value = false; 305 showPicker.value = false;
280 currentDateText.value = selectedValues[1].toString(); 306 currentDateText.value = selectedValues[1].toString();
...@@ -297,12 +323,21 @@ const onConfirm = async ({ selectedValues, selectedOptions }) => { // 选择日 ...@@ -297,12 +323,21 @@ const onConfirm = async ({ selectedValues, selectedOptions }) => { // 选择日
297 } 323 }
298 } 324 }
299 325
326 +/**
327 + * 月份选择取消回调
328 + * @returns {void}
329 + */
300 const onCancel = () => { 330 const onCancel = () => {
301 showPicker.value = false; 331 showPicker.value = false;
302 } 332 }
303 333
304 const show_error = ref(false); 334 const show_error = ref(false);
305 const error_message = ref(''); 335 const error_message = ref('');
336 +/**
337 + * 下一步:进入提交页
338 + * - 需要先选择日期与时间段
339 + * @returns {void}
340 + */
306 const nextBtn = () => { 341 const nextBtn = () => {
307 if (!checked_day.value || checked_time.value === -1) { 342 if (!checked_day.value || checked_time.value === -1) {
308 show_error.value = true; 343 show_error.value = true;
......
...@@ -49,6 +49,7 @@ const finished = ref(false); ...@@ -49,6 +49,7 @@ const finished = ref(false);
49 const finishedTextStatus = ref(false); 49 const finishedTextStatus = ref(false);
50 50
51 onMounted(async () => { 51 onMounted(async () => {
52 + // 初始化第一页数据
52 const { code, data } = await billListAPI({ page: page.value, row_num: limit.value }); 53 const { code, data } = await billListAPI({ page: page.value, row_num: limit.value });
53 if (code) { 54 if (code) {
54 // 格式化数据 55 // 格式化数据
...@@ -60,6 +61,11 @@ onMounted(async () => { ...@@ -60,6 +61,11 @@ onMounted(async () => {
60 } 61 }
61 }); 62 });
62 63
64 +/**
65 + * 列表触底加载
66 + * - van-list 触发 @load
67 + * @returns {Promise<void>}
68 + */
63 const onLoad = async () => { 69 const onLoad = async () => {
64 page.value++; 70 page.value++;
65 const { code, data } = await billListAPI({ page: page.value, row_num: limit.value }); 71 const { code, data } = await billListAPI({ page: page.value, row_num: limit.value });
......
...@@ -36,8 +36,8 @@ ...@@ -36,8 +36,8 @@
36 <script setup> 36 <script setup>
37 import { ref } from 'vue' 37 import { ref } from 'vue'
38 import { useRoute, useRouter } from 'vue-router' 38 import { useRoute, useRouter } from 'vue-router'
39 -import { onAuthBillInfoAPI, icbcOrderQryAPI } from '@/api/index' 39 +import { onAuthBillInfoAPI } from '@/api/index'
40 -import { Cookies, axios, storeToRefs, mainStore, Toast, useTitle } from '@/utils/generatePackage.js' 40 +import { useTitle } from '@/utils/generatePackage.js'
41 //import { } from '@/utils/generateModules.js' 41 //import { } from '@/utils/generateModules.js'
42 //import { } from '@/utils/generateIcons.js' 42 //import { } from '@/utils/generateIcons.js'
43 //import { } from '@/composables' 43 //import { } from '@/composables'
...@@ -54,8 +54,12 @@ const PAY_STATUS = { ...@@ -54,8 +54,12 @@ const PAY_STATUS = {
54 } 54 }
55 const pay_status = ref('0'); // 默认支付完成 55 const pay_status = ref('0'); // 默认支付完成
56 56
57 -//获取url中返回参数 57 +/**
58 -function getQueryString(name) { 58 + * 获取 URL 查询参数
59 + * @param {string} name 参数名
60 + * @returns {string|null} 参数值
61 + */
62 +function getQueryString(name) {
59 var query = window.location.search.substring(1); 63 var query = window.location.search.substring(1);
60 var vars = query.split("&"); 64 var vars = query.split("&");
61 for (var i = 0; i < vars.length; i++) { 65 for (var i = 0; i < vars.length; i++) {
...@@ -72,6 +76,12 @@ function getQueryString(name) { ...@@ -72,6 +76,12 @@ function getQueryString(name) {
72 // const out_trade_no = '110286610147000542401290012506'; 76 // const out_trade_no = '110286610147000542401290012506';
73 const out_trade_no = $route.query.out_trade_no; 77 const out_trade_no = $route.query.out_trade_no;
74 78
79 +/**
80 + * 支付回跳处理
81 + * - 读取订单信息并渲染
82 + * - 通知上层支付 iframe:页面已准备完成(自定义协议)
83 + * @returns {Promise<void>}
84 + */
75 const callback = async () => { 85 const callback = async () => {
76 // 获取订单详情 86 // 获取订单详情
77 const { code:code_pay, data:data_pay } = await onAuthBillInfoAPI({ order_id: out_trade_no }); 87 const { code:code_pay, data:data_pay } = await onAuthBillInfoAPI({ order_id: out_trade_no });
...@@ -86,6 +96,10 @@ const callback = async () => { ...@@ -86,6 +96,10 @@ const callback = async () => {
86 } 96 }
87 } 97 }
88 98
99 +/**
100 + * 通知支付 iframe 跳出回到商户页面(自定义协议)
101 + * @returns {void}
102 + */
89 const returnMerchant = () => { 103 const returnMerchant = () => {
90 var mchData = { 104 var mchData = {
91 action: 'jumpOut', 105 action: 'jumpOut',
...@@ -95,6 +109,7 @@ const returnMerchant = () => { ...@@ -95,6 +109,7 @@ const returnMerchant = () => {
95 var postData = JSON.stringify(mchData) 109 var postData = JSON.stringify(mchData)
96 top.postMessage(postData, "*") 110 top.postMessage(postData, "*")
97 } 111 }
112 +// 页面加载后立即拉取订单信息(支付回跳场景)
98 callback(); 113 callback();
99 114
100 onMounted(async () => { 115 onMounted(async () => {
......
...@@ -82,8 +82,10 @@ const toHome = () => { // 跳转到首页 ...@@ -82,8 +82,10 @@ const toHome = () => { // 跳转到首页
82 const visitorList = ref([]); 82 const visitorList = ref([]);
83 83
84 /** 84 /**
85 - * 生成15位身份证号中间8位替换为*号 85 + * 生成脱敏后的证件号
86 - * @param {*} inputString 86 + * - 仅对长度 >= 15 的字符串进行处理中间 8 位替换
87 + * @param {string} inputString 证件号
88 + * @returns {string} 脱敏后的证件号
87 */ 89 */
88 function replaceMiddleCharacters(inputString) { 90 function replaceMiddleCharacters(inputString) {
89 if (inputString.length < 15) { 91 if (inputString.length < 15) {
...@@ -99,10 +101,20 @@ function replaceMiddleCharacters(inputString) { ...@@ -99,10 +101,20 @@ function replaceMiddleCharacters(inputString) {
99 return replacedString; 101 return replacedString;
100 } 102 }
101 103
104 +/**
105 + * 格式化证件号显示(脱敏)
106 + * @param {string} id 证件号
107 + * @returns {string} 脱敏后的证件号
108 + */
102 const formatId = (id) => { 109 const formatId = (id) => {
103 return replaceMiddleCharacters(id); 110 return replaceMiddleCharacters(id);
104 }; 111 };
105 112
113 +/**
114 + * 删除参观者
115 + * @param {any} item 参观者信息
116 + * @returns {void}
117 + */
106 const removeItem = (item) => { 118 const removeItem = (item) => {
107 showConfirmDialog({ 119 showConfirmDialog({
108 title: '温馨提示', 120 title: '温馨提示',
...@@ -124,6 +136,7 @@ const removeItem = (item) => { ...@@ -124,6 +136,7 @@ const removeItem = (item) => {
124 } 136 }
125 137
126 onMounted(async () => { 138 onMounted(async () => {
139 + // 初始化参观者列表
127 const { code, data } = await personListAPI(); 140 const { code, data } = await personListAPI();
128 if (code) { 141 if (code) {
129 visitorList.value = data; 142 visitorList.value = data;
......
...@@ -82,6 +82,12 @@ const id_number = ref(''); ...@@ -82,6 +82,12 @@ const id_number = ref('');
82 const show_error = ref(false); 82 const show_error = ref(false);
83 const error_message = ref(''); 83 const error_message = ref('');
84 84
85 +/**
86 + * 校验证件号
87 + * - 身份证:18 位使用 cin 校验;15 位不校验(兼容老证件)
88 + * - 其他证件:仅做非空校验
89 + * @returns {boolean} 是否通过
90 + */
85 const checkIdCode = () => { // 检查身份证号是否为空 91 const checkIdCode = () => { // 检查身份证号是否为空
86 let flag = true; 92 let flag = true;
87 if (!idCode.value) { 93 if (!idCode.value) {
...@@ -103,6 +109,11 @@ const checkIdCode = () => { // 检查身份证号是否为空 ...@@ -103,6 +109,11 @@ const checkIdCode = () => { // 检查身份证号是否为空
103 return flag; 109 return flag;
104 } 110 }
105 111
112 +/**
113 + * 进入查询结果页
114 + * - 当前实现仅切换展示组件,由组件内部发起查询
115 + * @returns {Promise<void>}
116 + */
106 const searchBtn = async () => { 117 const searchBtn = async () => {
107 // 查询用户信息 118 // 查询用户信息
108 if (checkIdCode()) { 119 if (checkIdCode()) {
...@@ -111,14 +122,26 @@ const searchBtn = async () => { ...@@ -111,14 +122,26 @@ const searchBtn = async () => {
111 idCode.value = '' 122 idCode.value = ''
112 } 123 }
113 } 124 }
125 +/**
126 + * 返回查询输入页
127 + * @returns {void}
128 + */
114 const goBack = () => { 129 const goBack = () => {
115 is_search.value = false; 130 is_search.value = false;
116 } 131 }
132 +/**
133 + * 返回首页
134 + * @returns {void}
135 + */
117 const goToHome = () => { 136 const goToHome = () => {
118 go('/') 137 go('/')
119 } 138 }
120 139
121 const showPicker = ref(false); 140 const showPicker = ref(false);
141 +/**
142 + * 打开/关闭证件类型选择器
143 + * @returns {void}
144 + */
122 const idTypeChange = () => { 145 const idTypeChange = () => {
123 showPicker.value = !showPicker.value; 146 showPicker.value = !showPicker.value;
124 } 147 }
...@@ -130,6 +153,11 @@ const columns = [ ...@@ -130,6 +153,11 @@ const columns = [
130 const fieldValue = ref('身份证'); 153 const fieldValue = ref('身份证');
131 const id_type = ref(1); 154 const id_type = ref(1);
132 155
156 +/**
157 + * 证件类型选择回调
158 + * @param {{ selectedOptions: Array<{ text: string, value: number }> }} payload 选择结果
159 + * @returns {void}
160 + */
133 const onConfirm = ({ selectedOptions }) => { // 切换类型回调 161 const onConfirm = ({ selectedOptions }) => { // 切换类型回调
134 showPicker.value = false; 162 showPicker.value = false;
135 fieldValue.value = selectedOptions[0].text; 163 fieldValue.value = selectedOptions[0].text;
......
...@@ -67,8 +67,10 @@ const go = useGo(); ...@@ -67,8 +67,10 @@ const go = useGo();
67 const visitorList = ref([]); 67 const visitorList = ref([]);
68 68
69 /** 69 /**
70 - * 生成15位身份证号中间8位替换为*号 70 + * 生成脱敏后的证件号
71 - * @param {*} inputString 71 + * - 仅对长度 >= 15 的字符串进行处理中间 8 位替换
72 + * @param {string} inputString 证件号
73 + * @returns {string} 脱敏后的证件号
72 */ 74 */
73 function replaceMiddleCharacters(inputString) { 75 function replaceMiddleCharacters(inputString) {
74 if (inputString.length < 15) { 76 if (inputString.length < 15) {
...@@ -84,6 +86,11 @@ function replaceMiddleCharacters(inputString) { ...@@ -84,6 +86,11 @@ function replaceMiddleCharacters(inputString) {
84 return replacedString; 86 return replacedString;
85 } 87 }
86 88
89 +/**
90 + * 格式化证件号显示(脱敏)
91 + * @param {string} id 证件号
92 + * @returns {string} 脱敏后的证件号
93 + */
87 const formatId = (id) => { 94 const formatId = (id) => {
88 return replaceMiddleCharacters(id); 95 return replaceMiddleCharacters(id);
89 }; 96 };
...@@ -93,6 +100,12 @@ const RESERVE_STATUS = { ...@@ -93,6 +100,12 @@ const RESERVE_STATUS = {
93 } 100 }
94 101
95 const checked_visitors = ref([]); 102 const checked_visitors = ref([]);
103 +/**
104 + * 勾选/取消勾选参观者
105 + * - 已预约的参观者不允许再次勾选
106 + * @param {any} item 参观者信息
107 + * @returns {void}
108 + */
96 const addVisitor = (item) => { 109 const addVisitor = (item) => {
97 if (item.is_reserve === RESERVE_STATUS.ENABLE) { // 今天已经预约 110 if (item.is_reserve === RESERVE_STATUS.ENABLE) { // 今天已经预约
98 showToast('已预约过参观,请不要重复预约') 111 showToast('已预约过参观,请不要重复预约')
...@@ -114,13 +127,27 @@ const total = computed(() => { ...@@ -114,13 +127,27 @@ const total = computed(() => {
114 return price * checked_visitors.value.length; 127 return price * checked_visitors.value.length;
115 }) 128 })
116 129
130 +/**
131 + * 返回预约时段选择页
132 + * @returns {void}
133 + */
117 const goToBooking = () => { 134 const goToBooking = () => {
118 go('/booking'); 135 go('/booking');
119 } 136 }
137 +/**
138 + * 跳转到参观者管理页
139 + * @returns {void}
140 + */
120 const goToVisitor = () => { 141 const goToVisitor = () => {
121 go('/addVisitor'); 142 go('/addVisitor');
122 } 143 }
123 144
145 +/**
146 + * 提交订单并跳转支付
147 + * - 免费订单直接进入成功页
148 + * - 需要支付则跳转到工行支付页面
149 + * @returns {Promise<void>}
150 + */
124 const submitBtn = async () => { 151 const submitBtn = async () => {
125 if (!checked_visitors.value.length) { 152 if (!checked_visitors.value.length) {
126 showToast('请先添加参观者') 153 showToast('请先添加参观者')
...@@ -144,6 +171,7 @@ const submitBtn = async () => { ...@@ -144,6 +171,7 @@ const submitBtn = async () => {
144 } 171 }
145 172
146 onMounted(async () => { 173 onMounted(async () => {
174 + // 初始化参观者列表,并标记“是否已预约”
147 const { code, data } = await personListAPI({ reserve_date: date, begin_time: time.split('-')[0], end_time: time.split('-')[1], period_type }); 175 const { code, data } = await personListAPI({ reserve_date: date, begin_time: time.split('-')[0], end_time: time.split('-')[1], period_type });
148 if (code) { 176 if (code) {
149 visitorList.value = data; 177 visitorList.value = data;
......
...@@ -104,14 +104,27 @@ const status_icon_color = computed(() => { ...@@ -104,14 +104,27 @@ const status_icon_color = computed(() => {
104 return '#A67939' 104 return '#A67939'
105 }) 105 })
106 106
107 +/**
108 + * @description 对身份证号做脱敏展示(仅用于前端展示)
109 + * @param {string} id 证件号码
110 + * @returns {string} 脱敏后的证件号码
111 + */
107 const format_id_number = (id) => { 112 const format_id_number = (id) => {
108 if (!id || typeof id !== 'string' || id.length < 10) return id 113 if (!id || typeof id !== 'string' || id.length < 10) return id
109 return id.replace(/^(.{6})(?:\d+)(.{4})$/, '$1********$2') 114 return id.replace(/^(.{6})(?:\d+)(.{4})$/, '$1********$2')
110 } 115 }
111 116
117 +/**
118 + * @description 统一扫码结果格式
119 + * - 条形码可能返回 "codeType,codeValue" 格式,这里取最后一段
120 + * - 二维码可能是 URL,尝试从 query 里解析 qr_code
121 + * @param {unknown} raw 原始扫码结果
122 + * @returns {string} 解析后的核销码(失败返回空字符串)
123 + */
112 const normalize_scan_result = (raw) => { 124 const normalize_scan_result = (raw) => {
113 if (!raw) return '' 125 if (!raw) return ''
114 const text = String(raw) 126 const text = String(raw)
127 + // 部分机型扫描条形码会返回 "CODE_128,123456" 这种格式
115 const barcode_split = text.split(',') 128 const barcode_split = text.split(',')
116 const candidate = barcode_split.length > 1 ? barcode_split[barcode_split.length - 1] : text 129 const candidate = barcode_split.length > 1 ? barcode_split[barcode_split.length - 1] : text
117 if (candidate.includes('qr_code=')) { 130 if (candidate.includes('qr_code=')) {
...@@ -119,6 +132,7 @@ const normalize_scan_result = (raw) => { ...@@ -119,6 +132,7 @@ const normalize_scan_result = (raw) => {
119 const url = new URL(candidate) 132 const url = new URL(candidate)
120 return url.searchParams.get('qr_code') || candidate 133 return url.searchParams.get('qr_code') || candidate
121 } catch (e) { 134 } catch (e) {
135 + // 兼容不完整 URL 或低版本浏览器不支持 URL 构造的情况
122 const match = candidate.match(/(?:\?|&)qr_code=([^&]+)/) 136 const match = candidate.match(/(?:\?|&)qr_code=([^&]+)/)
123 if (match && match[1]) return decodeURIComponent(match[1]) 137 if (match && match[1]) return decodeURIComponent(match[1])
124 } 138 }
...@@ -126,6 +140,10 @@ const normalize_scan_result = (raw) => { ...@@ -126,6 +140,10 @@ const normalize_scan_result = (raw) => {
126 return candidate 140 return candidate
127 } 141 }
128 142
143 +/**
144 + * @description 校验当前账号是否有核销权限,无权限时重定向到义工登录
145 + * @returns {Promise<boolean>} 是否具备核销权限
146 + */
129 const ensure_permission = async () => { 147 const ensure_permission = async () => {
130 const permission_res = await checkRedeemPermissionAPI() 148 const permission_res = await checkRedeemPermissionAPI()
131 if (!permission_res || permission_res?.code !== 1) { 149 if (!permission_res || permission_res?.code !== 1) {
...@@ -140,6 +158,11 @@ const ensure_permission = async () => { ...@@ -140,6 +158,11 @@ const ensure_permission = async () => {
140 return true 158 return true
141 } 159 }
142 160
161 +/**
162 + * @description 核销预约码
163 + * @param {string} code 扫码结果或手动输入内容
164 + * @returns {Promise<void>}
165 + */
143 const verify_ticket = async (code) => { 166 const verify_ticket = async (code) => {
144 const normalized = normalize_scan_result(code) 167 const normalized = normalize_scan_result(code)
145 if (!normalized) return 168 if (!normalized) return
...@@ -162,7 +185,12 @@ const verify_ticket = async (code) => { ...@@ -162,7 +185,12 @@ const verify_ticket = async (code) => {
162 verify_info.value = {} 185 verify_info.value = {}
163 } 186 }
164 187
188 +/**
189 + * @description 在微信环境内调起 JSSDK 扫码
190 + * @returns {Promise<string>} 扫码原始结果(失败返回空字符串)
191 + */
165 const scan_in_wechat = async () => { 192 const scan_in_wechat = async () => {
193 + // 依赖 App.vue 全局初始化的 Promise(wx.ready / wx.error 都会 resolve)
166 const ok = await window.__wx_ready_promise 194 const ok = await window.__wx_ready_promise
167 if (!ok) return '' 195 if (!ok) return ''
168 return new Promise((resolve) => { 196 return new Promise((resolve) => {
...@@ -176,10 +204,15 @@ const scan_in_wechat = async () => { ...@@ -176,10 +204,15 @@ const scan_in_wechat = async () => {
176 }) 204 })
177 } 205 }
178 206
207 +/**
208 + * @description 点击按钮:先做权限校验,再根据环境选择“微信扫码”或“手动核销”
209 + * @returns {Promise<void>}
210 + */
179 const start_scan_and_verify = async () => { 211 const start_scan_and_verify = async () => {
180 const authed = await ensure_permission() 212 const authed = await ensure_permission()
181 if (!authed) return 213 if (!authed) return
182 214
215 + // 这里使用 wxInfo 的环境判断结果,避免在非微信环境误调用 JSSDK
183 const in_wechat = wxInfo().isTable === true 216 const in_wechat = wxInfo().isTable === true
184 if (in_wechat) { 217 if (in_wechat) {
185 const result = await scan_in_wechat() 218 const result = await scan_in_wechat()
...@@ -201,7 +234,11 @@ const start_scan_and_verify = async () => { ...@@ -201,7 +234,11 @@ const start_scan_and_verify = async () => {
201 showToast('请在微信内扫码,或手动输入预约码') 234 showToast('请在微信内扫码,或手动输入预约码')
202 } 235 }
203 236
204 -onMounted(async () => { 237 +/**
238 + * @description 页面初始化:读取 URL 参数并自动核销(适配“扫码后跳转页面”场景)
239 + * @returns {Promise<void>}
240 + */
241 +const init_from_query = async () => {
205 const authed = await ensure_permission() 242 const authed = await ensure_permission()
206 if (!authed) return 243 if (!authed) return
207 const code = $route.query?.result || $route.query?.qr_code || '' 244 const code = $route.query?.result || $route.query?.qr_code || ''
...@@ -210,19 +247,28 @@ onMounted(async () => { ...@@ -210,19 +247,28 @@ onMounted(async () => {
210 manual_code.value = str_code 247 manual_code.value = str_code
211 await verify_ticket(str_code) 248 await verify_ticket(str_code)
212 } 249 }
213 -}) 250 +}
251 +
252 +onMounted(init_from_query)
253 +
254 +/**
255 + * @description 监听路由参数变化,支持“同页面多次扫码/回传 result”场景
256 + * @param {unknown} next 新的 result 参数
257 + * @returns {Promise<void>}
258 + */
259 +const watch_route_result = async (next) => {
260 + const code = Array.isArray(next) ? next[0] : String(next || '')
261 + if (!code) return
262 + if (verify_code.value === code) return
263 + const authed = await ensure_permission()
264 + if (!authed) return
265 + manual_code.value = code
266 + await verify_ticket(code)
267 +}
214 268
215 watch( 269 watch(
216 () => $route.query?.result, 270 () => $route.query?.result,
217 - async (next) => { 271 + watch_route_result
218 - const code = Array.isArray(next) ? next[0] : String(next || '')
219 - if (!code) return
220 - if (verify_code.value === code) return
221 - const authed = await ensure_permission()
222 - if (!authed) return
223 - manual_code.value = code
224 - await verify_ticket(code)
225 - }
226 ) 272 )
227 </script> 273 </script>
228 274
......
...@@ -66,8 +66,10 @@ const toHome = () => { // 跳转到首页 ...@@ -66,8 +66,10 @@ const toHome = () => { // 跳转到首页
66 const visitorList = ref([]); 66 const visitorList = ref([]);
67 67
68 /** 68 /**
69 - * 生成15位身份证号中间8位替换为*号 69 + * 生成脱敏后的证件号
70 - * @param {*} inputString 70 + * - 仅对长度 >= 15 的字符串进行处理中间 8 位替换
71 + * @param {string} inputString 证件号
72 + * @returns {string} 脱敏后的证件号
71 */ 73 */
72 function replaceMiddleCharacters(inputString) { 74 function replaceMiddleCharacters(inputString) {
73 if (inputString.length < 15) { 75 if (inputString.length < 15) {
...@@ -83,10 +85,20 @@ function replaceMiddleCharacters(inputString) { ...@@ -83,10 +85,20 @@ function replaceMiddleCharacters(inputString) {
83 return replacedString; 85 return replacedString;
84 } 86 }
85 87
88 +/**
89 + * 格式化证件号显示(脱敏)
90 + * @param {string} id 证件号
91 + * @returns {string} 脱敏后的证件号
92 + */
86 const formatId = (id) => { 93 const formatId = (id) => {
87 return replaceMiddleCharacters(id); 94 return replaceMiddleCharacters(id);
88 }; 95 };
89 96
97 +/**
98 + * 删除参观者
99 + * @param {any} item 参观者信息
100 + * @returns {void}
101 + */
90 const removeItem = (item) => { 102 const removeItem = (item) => {
91 showConfirmDialog({ 103 showConfirmDialog({
92 title: '温馨提示', 104 title: '温馨提示',
...@@ -108,6 +120,7 @@ const removeItem = (item) => { ...@@ -108,6 +120,7 @@ const removeItem = (item) => {
108 } 120 }
109 121
110 onMounted(async () => { 122 onMounted(async () => {
123 + // 初始化参观者列表
111 const { code, data } = await personListAPI(); 124 const { code, data } = await personListAPI();
112 if (code) { 125 if (code) {
113 visitorList.value = data; 126 visitorList.value = data;
......
...@@ -33,6 +33,10 @@ const username = ref('') ...@@ -33,6 +33,10 @@ const username = ref('')
33 const password = ref('') 33 const password = ref('')
34 const loading = ref(false) 34 const loading = ref(false)
35 35
36 +/**
37 + * @description 进入页面时先做权限校验,已有核销权限则直接跳转核销页
38 + * @returns {Promise<void>}
39 + */
36 const check_permission_and_redirect = async () => { 40 const check_permission_and_redirect = async () => {
37 const permission_res = await checkRedeemPermissionAPI() 41 const permission_res = await checkRedeemPermissionAPI()
38 if (!permission_res) return 42 if (!permission_res) return
...@@ -42,10 +46,15 @@ const check_permission_and_redirect = async () => { ...@@ -42,10 +46,15 @@ const check_permission_and_redirect = async () => {
42 } 46 }
43 } 47 }
44 48
45 -onMounted(() => { 49 +onMounted(check_permission_and_redirect)
46 - check_permission_and_redirect()
47 -})
48 50
51 +/**
52 + * @description 义工登录
53 + * - 先登录获取会话/权限凭证
54 + * - 再调用权限校验接口,确认具备核销权限
55 + * - 成功后跳转核销页
56 + * @returns {Promise<void>}
57 + */
49 const handle_login = async () => { 58 const handle_login = async () => {
50 if (!username.value || !password.value) { 59 if (!username.value || !password.value) {
51 showToast('请输入账号密码') 60 showToast('请输入账号密码')
...@@ -61,6 +70,7 @@ const handle_login = async () => { ...@@ -61,6 +70,7 @@ const handle_login = async () => {
61 return 70 return
62 } 71 }
63 72
73 + // 登录成功后做一次权限校验,避免“登录成功但无核销权限”的误导
64 const permission_res = await checkRedeemPermissionAPI() 74 const permission_res = await checkRedeemPermissionAPI()
65 if (!permission_res || permission_res?.code !== 1) { 75 if (!permission_res || permission_res?.code !== 1) {
66 showToast(permission_res?.msg || '权限校验失败') 76 showToast(permission_res?.msg || '权限校验失败')
...@@ -71,6 +81,7 @@ const handle_login = async () => { ...@@ -71,6 +81,7 @@ const handle_login = async () => {
71 81
72 if (permission_res?.data?.can_redeem === true) { 82 if (permission_res?.data?.can_redeem === true) {
73 showSuccessToast(permission_res?.msg || login_res?.msg || '登录成功') 83 showSuccessToast(permission_res?.msg || login_res?.msg || '登录成功')
84 + // 延迟跳转,确保用户能看到成功提示
74 setTimeout(() => $router.replace({ path: '/verificationResult' }), 800) 85 setTimeout(() => $router.replace({ path: '/verificationResult' }), 800)
75 return 86 return
76 } 87 }
......
...@@ -57,12 +57,21 @@ const wx_ready_text = computed(() => { ...@@ -57,12 +57,21 @@ const wx_ready_text = computed(() => {
57 return '未检测' 57 return '未检测'
58 }) 58 })
59 59
60 +/**
61 + * @description 确保微信 JSSDK 已完成初始化(依赖 App.vue 注入的全局 Promise)
62 + * @returns {Promise<boolean>} 是否已就绪
63 + */
60 const ensure_wx_ready = async () => { 64 const ensure_wx_ready = async () => {
65 + // App.vue 在启动时会初始化 wx.config,并把 ready 结果写到 window.__wx_ready_promise
61 if (!window.__wx_ready_promise) return false 66 if (!window.__wx_ready_promise) return false
62 const ok = await window.__wx_ready_promise 67 const ok = await window.__wx_ready_promise
63 return ok === true 68 return ok === true
64 } 69 }
65 70
71 +/**
72 + * @description 调起微信扫码并展示结果(仅微信环境可用)
73 + * @returns {Promise<void>}
74 + */
66 const start_scan = async () => { 75 const start_scan = async () => {
67 if (!in_wechat.value) { 76 if (!in_wechat.value) {
68 showToast('请在微信内打开该页面') 77 showToast('请在微信内打开该页面')
...@@ -78,6 +87,7 @@ const start_scan = async () => { ...@@ -78,6 +87,7 @@ const start_scan = async () => {
78 showToast('wx 初始化失败') 87 showToast('wx 初始化失败')
79 return 88 return
80 } 89 }
90 + // JSSDK 扫码:needResult=1 表示直接拿到结果文本,不跳转微信扫码结果页
81 const result = await new Promise((resolve) => { 91 const result = await new Promise((resolve) => {
82 wx.scanQRCode({ 92 wx.scanQRCode({
83 needResult: 1, 93 needResult: 1,
...@@ -94,13 +104,19 @@ const start_scan = async () => { ...@@ -94,13 +104,19 @@ const start_scan = async () => {
94 } 104 }
95 } 105 }
96 106
97 -onMounted(async () => { 107 +/**
108 + * @description 页面初始化:同步展示“是否在微信内”和“JSSDK 是否就绪”
109 + * @returns {Promise<void>}
110 + */
111 +const init_scan_test_page = async () => {
98 if (!in_wechat.value) { 112 if (!in_wechat.value) {
99 wx_ready.value = false 113 wx_ready.value = false
100 return 114 return
101 } 115 }
102 wx_ready.value = await ensure_wx_ready() 116 wx_ready.value = await ensure_wx_ready()
103 -}) 117 +}
118 +
119 +onMounted(init_scan_test_page)
104 </script> 120 </script>
105 121
106 <style lang="less" scoped> 122 <style lang="less" scoped>
......