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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ text }}
+
+
+
+
+
+
+
+
\ 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();
})
}