docs: 完善项目注释与类型标注
- 为环境文件、常量、工具函数、API 模块等添加 JSDoc 注释 - 统一函数参数与返回值的类型标注 - 增强路由、状态管理、微信 JSSDK 等核心模块的可读性 - 修复动态路由 children 处理逻辑,避免生成非数组值 - 更新微信分享标题为动态年份,避免硬编码 - 重构防抖钩子,移除 lodash 依赖,减少打包体积
Showing
36 changed files
with
676 additions
and
153 deletions
| ... | @@ -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); | ... | ... |
| 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); | ... | ... |
| ... | @@ -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: '童声无界', | ... | ... |
| ... | @@ -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,7 +54,11 @@ const PAY_STATUS = { | ... | @@ -54,7 +54,11 @@ const PAY_STATUS = { |
| 54 | } | 54 | } |
| 55 | const pay_status = ref('0'); // 默认支付完成 | 55 | const pay_status = ref('0'); // 默认支付完成 |
| 56 | 56 | ||
| 57 | -//获取url中返回参数 | 57 | +/** |
| 58 | + * 获取 URL 查询参数 | ||
| 59 | + * @param {string} name 参数名 | ||
| 60 | + * @returns {string|null} 参数值 | ||
| 61 | + */ | ||
| 58 | function getQueryString(name) { | 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("&"); |
| ... | @@ -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,11 +247,16 @@ onMounted(async () => { | ... | @@ -210,11 +247,16 @@ 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 | +} |
| 214 | 251 | ||
| 215 | -watch( | 252 | +onMounted(init_from_query) |
| 216 | - () => $route.query?.result, | 253 | + |
| 217 | - async (next) => { | 254 | +/** |
| 255 | + * @description 监听路由参数变化,支持“同页面多次扫码/回传 result”场景 | ||
| 256 | + * @param {unknown} next 新的 result 参数 | ||
| 257 | + * @returns {Promise<void>} | ||
| 258 | + */ | ||
| 259 | +const watch_route_result = async (next) => { | ||
| 218 | const code = Array.isArray(next) ? next[0] : String(next || '') | 260 | const code = Array.isArray(next) ? next[0] : String(next || '') |
| 219 | if (!code) return | 261 | if (!code) return |
| 220 | if (verify_code.value === code) return | 262 | if (verify_code.value === code) return |
| ... | @@ -222,7 +264,11 @@ watch( | ... | @@ -222,7 +264,11 @@ watch( |
| 222 | if (!authed) return | 264 | if (!authed) return |
| 223 | manual_code.value = code | 265 | manual_code.value = code |
| 224 | await verify_ticket(code) | 266 | await verify_ticket(code) |
| 225 | - } | 267 | +} |
| 268 | + | ||
| 269 | +watch( | ||
| 270 | + () => $route.query?.result, | ||
| 271 | + watch_route_result | ||
| 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> | ... | ... |
-
Please register or login to post a comment