From b515b793b63625548219237bfbc7d40b2426b569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=B0=8F=E5=95=8A=E7=99=BD?= <2053890199@qq.com> Date: Sat, 15 Nov 2025 14:42:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=8C=E7=BB=B4?= =?UTF-8?q?=E7=A0=81=E5=92=8C=E5=8A=A0=E8=BD=BD=E7=BB=84=E4=BB=B6=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 qrcode.vue 依赖用于生成二维码 - 实现 Loading 组件,支持多种动画类型和配置 - 在首页表单中添加二维码显示功能 - 优化表单验证流程,添加加载状态管理 - 完善用户数据本地存储功能 --- package.json | 1 + pnpm-lock.yaml | 12 ++ src/components/Loading.vue | 333 +++++++++++++++++++++++++++++++++++++ src/components/loading.md | 304 +++++++++++++++++++++++++++++++++ src/components/loading.ts | 174 +++++++++++++++++++ src/pages/home/index.vue | 90 ++++++++-- 6 files changed, 903 insertions(+), 11 deletions(-) create mode 100644 src/components/Loading.vue create mode 100644 src/components/loading.md create mode 100644 src/components/loading.ts diff --git a/package.json b/package.json index 35d0e42..8b7ef5e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@tailwindcss/vite": "^4.1.17", "axios": "^1.13.2", "dayjs": "^1.11.19", + "qrcode.vue": "^3.6.0", "tailwindcss": "^4.1.17", "vue": "^3.5.24", "vue-router": "^4.6.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4264cfe..084c574 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: dayjs: specifier: ^1.11.19 version: 1.11.19 + qrcode.vue: + specifier: ^3.6.0 + version: 3.6.0(vue@3.5.24(typescript@5.9.3)) tailwindcss: specifier: ^4.1.17 version: 4.1.17 @@ -751,6 +754,11 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + qrcode.vue@3.6.0: + resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==} + peerDependencies: + vue: ^3.0.0 + rollup@4.53.2: resolution: {integrity: sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1406,6 +1414,10 @@ snapshots: proxy-from-env@1.1.0: {} + qrcode.vue@3.6.0(vue@3.5.24(typescript@5.9.3)): + dependencies: + vue: 3.5.24(typescript@5.9.3) + rollup@4.53.2: dependencies: '@types/estree': 1.0.8 diff --git a/src/components/Loading.vue b/src/components/Loading.vue new file mode 100644 index 0000000..bb90c88 --- /dev/null +++ b/src/components/Loading.vue @@ -0,0 +1,333 @@ + + + + + \ No newline at end of file diff --git a/src/components/loading.md b/src/components/loading.md new file mode 100644 index 0000000..24cf291 --- /dev/null +++ b/src/components/loading.md @@ -0,0 +1,304 @@ +# Loading 组件使用文档 + +## 组件介绍 + +Loading 组件是一个灵活的加载动画组件,支持多种动画类型、自定义大小、颜色等配置,可用于页面加载、数据请求等场景。 + +## 组件类型 + +- `spinner`:旋转圆环(默认) +- `dots`:点状加载动画 +- `pulse`:脉冲加载动画 +- `ring`:环形旋转动画 + +## 使用方式 + +### 方式一:组件方式使用 + +```vue + + + +``` + +### 方式二:编程方式使用 + +```vue + + + +``` + +## API 文档 + +### Loading 组件 Props + +| 属性名 | 类型 | 默认值 | 说明 | +|-------|------|-------|------| +| visible | boolean | false | 是否显示加载组件 | +| type | 'spinner' \| 'dots' \| 'pulse' \| 'ring' | 'spinner' | 加载动画类型 | +| size | number \| string | 40 | 加载动画大小 | +| color | string | '#1890ff' | 加载动画颜色 | +| fullscreen | boolean | true | 是否全屏显示 | +| text | string | '' | 加载文本 | +| background | string | 'rgba(255, 255, 255, 0.9)' | 遮罩层背景色(全屏模式下) | +| opacity | number | 1 | 遮罩层透明度(全屏模式下) | +| strokeWidth | number | 3 | 线条宽度(仅适用于spinner和ring类型) | +| strokeLinecap | 'round' \| 'butt' \| 'square' | 'round' | 线条末端样式(仅适用于spinner类型) | + +### loading.ts 函数API + +#### showLoading(options?: LoadingOptions): LoadingInstance + +创建并显示一个加载组件。 + +**参数**: +- options: LoadingOptions - 加载组件配置选项(与组件Props相同) + +**返回值**: +- LoadingInstance - 加载组件实例,包含 show()、hide()、close() 方法 + +#### hideLoading(): void + +隐藏当前活动的全屏加载组件。 + +#### showLoadingWithText(text: string, color?: string): LoadingInstance + +显示一个带有文本的全屏加载组件。 + +**参数**: +- text: string - 加载文本 +- color: string - 加载动画颜色,默认为 '#1890ff' + +**返回值**: +- LoadingInstance - 加载组件实例 + +#### showInlineLoading(size?: number, color?: string, type?: LoadingType): LoadingInstance + +显示一个小型内联加载组件。 + +**参数**: +- size: number - 加载组件大小,默认为 24 +- color: string - 加载动画颜色,默认为 '#1890ff' +- type: LoadingType - 加载动画类型,默认为 'spinner' + +**返回值**: +- LoadingInstance - 加载组件实例 + +### LoadingInstance 接口 + +| 方法名 | 说明 | +|-------|------| +| show() | 显示加载组件 | +| hide() | 隐藏加载组件并卸载 | +| close() | 同 hide(),隐藏加载组件并卸载 | + +## 示例代码 + +### 场景1:页面加载 + +```vue + + + +``` + +### 场景2:API请求加载 + +```vue + + + +``` + +### 场景3:提交表单时的加载 + +```vue + + + +``` + +## 注意事项 + +1. 当使用编程方式创建多个全屏加载组件时,最新创建的会替换之前的。 +2. 请确保在不需要加载动画时调用 hide() 方法来卸载组件,避免内存泄漏。 +3. 对于长时间运行的操作,建议添加加载文本以提高用户体验。 +4. 内联加载组件不会自动居中,需要根据具体场景调整其位置。 \ No newline at end of file diff --git a/src/components/loading.ts b/src/components/loading.ts new file mode 100644 index 0000000..c63eee6 --- /dev/null +++ b/src/components/loading.ts @@ -0,0 +1,174 @@ +import { createApp, h } from 'vue' +import Loading from './Loading.vue' + +// 加载组件配置接口 +export interface LoadingOptions { + // 加载动画类型 + type?: 'spinner' | 'dots' | 'pulse' | 'ring' + // 加载动画大小 + size?: number | string + // 加载动画颜色 + color?: string + // 是否全屏显示 + fullscreen?: boolean + // 加载文本 + text?: string + // 遮罩层背景色 + background?: string + // 遮罩层透明度 + opacity?: number + // 线条宽度(仅适用于spinner和ring类型) + strokeWidth?: number + // 线条末端样式(仅适用于spinner类型) + strokeLinecap?: 'round' | 'butt' | 'square' +} + +// Loading 实例接口 +interface LoadingInstance { + show: () => void + hide: () => void + close: () => void +} + +// 当前活动的Loading实例 +let activeInstance: LoadingInstance | null = null + +/** + * 创建并显示一个加载组件 + * @param options 加载组件配置选项 + * @returns 加载组件实例,可用于手动控制显示和隐藏 + */ +export function showLoading(options: LoadingOptions = {}): LoadingInstance { + // 如果已经有一个活动的全屏Loading实例,先隐藏它 + if (activeInstance && options.fullscreen !== false) { + activeInstance.hide() + } + + // 默认配置 + const defaultOptions: LoadingOptions = { + type: 'spinner', + size: 40, + color: '#1890ff', + fullscreen: true, + text: '', + background: 'rgba(0, 0, 0, 0.3)', + opacity: 1, + strokeWidth: 3, + strokeLinecap: 'round' + } + + // 合并配置 + const mergedOptions = { ...defaultOptions, ...options } + + // 创建一个容器元素 + const container = document.createElement('div') + + // 加载组件状态 + let visible = true + + // 创建应用实例 + const app = createApp({ + render() { + return h(Loading, { + visible, + type: mergedOptions.type, + size: mergedOptions.size, + color: mergedOptions.color, + fullscreen: mergedOptions.fullscreen, + text: mergedOptions.text, + background: mergedOptions.background, + opacity: mergedOptions.opacity, + strokeWidth: mergedOptions.strokeWidth, + strokeLinecap: mergedOptions.strokeLinecap + }) + } + }) + + // 隐藏加载组件 + function hideLoading() { + visible = false + // 卸载组件 + setTimeout(() => { + app.unmount() + if (container.parentNode) { + container.parentNode.removeChild(container) + } + // 如果当前实例是活动实例,清除活动实例引用 + if (activeInstance === instance) { + activeInstance = null + } + }, 300) + } + + // 显示加载组件(默认为显示状态) + function showLoadingComp() { + visible = true + } + + // 挂载组件 + app.mount(container) + document.body.appendChild(container) + + // 创建实例对象 + const instance: LoadingInstance = { + show: showLoadingComp, + hide: hideLoading, + close: hideLoading + } + + // 如果是全屏Loading,保存为活动实例 + if (mergedOptions.fullscreen) { + activeInstance = instance + } + + // 返回控制方法 + return instance +} + +/** + * 隐藏当前活动的全屏加载组件 + */ +export function hideLoading() { + if (activeInstance) { + activeInstance.hide() + activeInstance = null + } +} + +/** + * 显示一个带有文本的全屏加载组件 + * @param text 加载文本 + * @param color 加载动画颜色 + * @returns 加载组件实例 + */ +export function showLoadingWithText(text: string, color: string = '#1890ff'): LoadingInstance { + return showLoading({ + text, + color, + fullscreen: true + }) +} + +/** + * 显示一个小型内联加载组件 + * @param size 加载组件大小 + * @param color 加载动画颜色 + * @param type 加载动画类型 + * @returns 加载组件实例 + */ +export function showInlineLoading(size: number = 24, color: string = '#1890ff', type: 'spinner' | 'dots' | 'pulse' | 'ring' = 'spinner'): LoadingInstance { + return showLoading({ + size, + color, + type, + fullscreen: false + }) +} + +// 默认导出 +export default { + show: showLoading, + hide: hideLoading, + showWithText: showLoadingWithText, + showInline: showInlineLoading +} \ No newline at end of file diff --git a/src/pages/home/index.vue b/src/pages/home/index.vue index 44060b0..8e4c90c 100644 --- a/src/pages/home/index.vue +++ b/src/pages/home/index.vue @@ -50,7 +50,7 @@ -
+

结婚证信息:

@@ -69,15 +69,15 @@ class="w-full flex flex-col items-start border border-gray-200 rounded-lg p-5 bg-[#FAFAFA]">
结婚证字号: - {{ 11111 }} + {{ formData.marriageNo }}
男方姓名: - {{ 11111 }} + {{ formData.husbandName }}
女方姓名: - {{ 11111 }} + {{ formData.wifeName }}
@@ -88,6 +88,15 @@ 提交领取喜礼
+ +
+
+

请使用微信扫描下方二维码:

+
+
+ +
+
@@ -119,6 +128,8 @@ import message from '../../components/message'; import apiService from '../../services/apiService' import { ref, onMounted } from 'vue' import dayjs from 'dayjs' +import QrcodeVue from 'qrcode.vue' +import { showLoading, hideLoading } from '../../components/loading' const ocrUploadId = ref(); @@ -131,11 +142,13 @@ const formData = ref({ husbandName: '', // 男方姓名 wifeName: '', // 女方姓名 registerDate: '', // 登记日期 + qrCode: '', // 二维码 }) const activityInfo = ref({}); // 活动信息 onMounted(() => { // 检查活动是否已结束 + showLoading(); apiService.getCurrentActivity().then((response: any) => { if (response.data) { activityInfo.value = { @@ -147,11 +160,20 @@ onMounted(() => { if (new Date(response.data.activityEndTime) < new Date()) { message.error('活动已结束'); activityInfo.value.status = 2; + } else { + const user = JSON.parse(localStorage.getItem('userAs') || '{}'); + if (user.phone && user.smsCode) { + formData.value.phone = user.phone; + formData.value.smsCode = user.smsCode; + verifyCode(); + } } } }).catch((error) => { message.error('获取活动信息失败,请稍后重试') activityInfo.value.status = 3; + }).finally(() => { + hideLoading(); }) }) @@ -168,7 +190,7 @@ const sendVerificationCode = () => { message.error('请输入正确的手机号码') return } - + showLoading(); // 调用发送短信验证码接口 apiService.sendSms({ mobile: phone, @@ -176,6 +198,7 @@ const sendVerificationCode = () => { }).then(() => { formData.value.verifyCode = false; formData.value.smsCode = ''; + formData.value.qrCode = ''; // 开始倒计时 countdown.value = 60 @@ -196,6 +219,8 @@ const sendVerificationCode = () => { }).catch((error) => { console.error('发送验证码失败:', error) message.error('验证码发送失败,请稍后重试') + }).finally(() => { + hideLoading(); }) } @@ -208,16 +233,53 @@ const verifyCode = () => { return } + showLoading(); // 调用验证短信验证码接口 apiService.verifySms({ mobile: formData.value.phone, smsCode: smsCode, type: 3, - }).then(() => { + }).then((res) => { + localStorage.setItem('userAs', JSON.stringify({ + phone: formData.value.phone, + smsCode: formData.value.smsCode, + })); + formData.value.verifyCode = true; + + // 验证成功后重置倒计时 + countdown.value = 0; + + // 验证成功如果有二维码,保存到表单 + if (res.data.code) { + formData.value = { + ...formData.value, + qrCode: res.data.code, + } + } else { + formData.value = { + ...formData.value, + marriageNo: "", + husbandName: "", + wifeName: "", + registerDate: "", + qrCode: "" + } + } }).catch((error) => { - console.error('验证验证码失败:', error) - message.error('验证码验证失败,请重试') + localStorage.removeItem('userAs'); + + message.error('验证码验证失败,请重试'); + formData.value = { + ...formData.value, + marriageNo: "", + husbandName: "", + wifeName: "", + registerDate: "", + qrCode: "" + } + }).finally(() => { + hideLoading(); }) } @@ -236,6 +298,7 @@ const handleImageChange = () => { const formFormData = new FormData() formFormData.append('file', file) + showLoading(); apiService.uploadOcrImage(formFormData).then((response: any) => { apiService.parseOcrInfo({ mobile: formData.value.phone, @@ -254,10 +317,13 @@ const handleImageChange = () => { }).catch((error) => { message.error('解析OCR信息失败,请稍后重试') ocrUploadId.value && (ocrUploadId.value.value = ''); + }).finally(() => { + hideLoading(); }) }).catch((error) => { message.error('图片上传失败,请稍后重试') ocrUploadId.value && (ocrUploadId.value.value = ''); + hideLoading(); }) } @@ -270,16 +336,18 @@ const submitForm = () => { return } + showLoading(); apiService.receiveCheck({ marriageNo: formData.value.marriageNo, receiveName: formData.value.husbandName, receiveMobile: formData.value.phone, - code: formData.value.smsCode, smsCode: formData.value.smsCode, }).then((res: any) => { - console.log(res); + formData.value.qrCode = res.data.code; }).catch((error) => { - message.error('领取失败,请稍后重试') + message.error(error?.msg || '提交失败,请稍后重试') + }).finally(() => { + hideLoading(); }) }