feat: 添加二维码和加载组件功能
- 新增 qrcode.vue 依赖用于生成二维码 - 实现 Loading 组件,支持多种动画类型和配置 - 在首页表单中添加二维码显示功能 - 优化表单验证流程,添加加载状态管理 - 完善用户数据本地存储功能
This commit is contained in:
parent
0d3462a8f5
commit
b515b793b6
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="loading-container"
|
||||
:class="{
|
||||
'loading-container-fullscreen': fullscreen,
|
||||
'loading-container-inline': !fullscreen
|
||||
}"
|
||||
:style="containerStyle"
|
||||
>
|
||||
<div class="loading-spinner-wrapper" :style="spinnerWrapperStyle">
|
||||
<!-- 旋转圆环样式 -->
|
||||
<div v-if="type === 'spinner'" class="loading-spinner" :style="spinnerStyle">
|
||||
<svg class="loading-svg" viewBox="0 0 50 50" :style="svgStyle">
|
||||
<circle
|
||||
class="loading-path"
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
:stroke-width="strokeWidth"
|
||||
:stroke-linecap="strokeLinecap"
|
||||
stroke-dasharray="94.2 94.2"
|
||||
stroke-dashoffset="94.2"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 点状加载样式 -->
|
||||
<div v-else-if="type === 'dots'" class="loading-dots" :style="dotsContainerStyle">
|
||||
<div
|
||||
v-for="n in 3"
|
||||
:key="n"
|
||||
class="loading-dot"
|
||||
:style="[
|
||||
dotStyle,
|
||||
{
|
||||
animationDelay: `${n * 0.2}s`
|
||||
}
|
||||
]"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- 脉冲样式 -->
|
||||
<div v-else-if="type === 'pulse'" class="loading-pulse" :style="pulseStyle"></div>
|
||||
|
||||
<!-- 环形样式 -->
|
||||
<div v-else-if="type === 'ring'" class="loading-ring" :style="ringStyle">
|
||||
<div class="loading-ring-circle" :style="ringCircleStyle"></div>
|
||||
</div>
|
||||
|
||||
<!-- 默认样式(旋转圆环) -->
|
||||
<div v-else class="loading-spinner" :style="spinnerStyle">
|
||||
<svg class="loading-svg" viewBox="0 0 50 50" :style="svgStyle">
|
||||
<circle
|
||||
class="loading-path"
|
||||
cx="25"
|
||||
cy="25"
|
||||
r="20"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
:stroke-width="strokeWidth"
|
||||
:stroke-linecap="strokeLinecap"
|
||||
stroke-dasharray="94.2 94.2"
|
||||
stroke-dashoffset="94.2"
|
||||
></circle>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- 加载文本 -->
|
||||
<div v-if="text" class="loading-text" :style="textStyle">
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
// 定义加载动画类型
|
||||
type LoadingType = 'spinner' | 'dots' | 'pulse' | 'ring'
|
||||
|
||||
// Props 定义
|
||||
const props = defineProps<{
|
||||
// 是否显示加载组件
|
||||
visible: boolean
|
||||
// 加载动画类型
|
||||
type?: LoadingType
|
||||
// 加载动画大小
|
||||
size?: number | string
|
||||
// 加载动画颜色
|
||||
color?: string
|
||||
// 是否全屏显示
|
||||
fullscreen?: boolean
|
||||
// 加载文本
|
||||
text?: string
|
||||
// 遮罩层背景色
|
||||
background?: string
|
||||
// 遮罩层透明度
|
||||
opacity?: number
|
||||
// 线条宽度(仅适用于spinner和ring类型)
|
||||
strokeWidth?: number
|
||||
// 线条末端样式(仅适用于spinner类型)
|
||||
strokeLinecap?: 'round' | 'butt' | 'square'
|
||||
}>()
|
||||
|
||||
// 计算容器样式
|
||||
const containerStyle = computed(() => {
|
||||
const style: any = {}
|
||||
if (props.fullscreen) {
|
||||
style.backgroundColor = props.background || 'rgba(255, 255, 255, 0.9)'
|
||||
style.opacity = props.opacity ?? 1
|
||||
}
|
||||
return style
|
||||
})
|
||||
|
||||
// 计算spinner包装器样式
|
||||
const spinnerWrapperStyle = computed(() => {
|
||||
return {
|
||||
color: props.color || '#1890ff'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算spinner样式
|
||||
const spinnerStyle = computed(() => {
|
||||
return {
|
||||
width: typeof props.size === 'number' ? `${props.size}px` : props.size || '40px',
|
||||
height: typeof props.size === 'number' ? `${props.size}px` : props.size || '40px'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算svg样式
|
||||
const svgStyle = computed(() => {
|
||||
return {
|
||||
animationDuration: '1.4s'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算线条宽度
|
||||
const strokeWidth = computed(() => {
|
||||
return props.strokeWidth || 3
|
||||
})
|
||||
|
||||
// 计算线条末端样式
|
||||
const strokeLinecap = computed(() => {
|
||||
return props.strokeLinecap || 'round'
|
||||
})
|
||||
|
||||
// 计算点状加载容器样式
|
||||
const dotsContainerStyle = computed(() => {
|
||||
return {
|
||||
gap: typeof props.size === 'number' ? `${props.size / 6}px` : '6px'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算点状加载样式
|
||||
const dotStyle = computed(() => {
|
||||
const dotSize = typeof props.size === 'number' ? props.size / 5 : 8
|
||||
return {
|
||||
width: `${dotSize}px`,
|
||||
height: `${dotSize}px`,
|
||||
backgroundColor: props.color || '#1890ff'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算脉冲样式
|
||||
const pulseStyle = computed(() => {
|
||||
const pulseSize = typeof props.size === 'number' ? props.size : 40
|
||||
return {
|
||||
width: `${pulseSize}px`,
|
||||
height: `${pulseSize}px`,
|
||||
backgroundColor: props.color || '#1890ff'
|
||||
}
|
||||
})
|
||||
|
||||
// 计算环形样式
|
||||
const ringStyle = computed(() => {
|
||||
const ringSize = typeof props.size === 'number' ? props.size : 40
|
||||
return {
|
||||
width: `${ringSize}px`,
|
||||
height: `${ringSize}px`
|
||||
}
|
||||
})
|
||||
|
||||
// 计算环形内部圆样式
|
||||
const ringCircleStyle = computed(() => {
|
||||
const circleBorderWidth = props.strokeWidth || 3
|
||||
return {
|
||||
borderWidth: `${circleBorderWidth}px`,
|
||||
borderColor: `${props.color || '#1890ff'} transparent ${props.color || '#1890ff'} transparent`
|
||||
}
|
||||
})
|
||||
|
||||
// 计算文本样式
|
||||
const textStyle = computed(() => {
|
||||
return {
|
||||
marginTop: typeof props.size === 'number' ? `${props.size / 4}px` : '10px',
|
||||
color: props.color || '#666'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全屏容器样式 */
|
||||
.loading-container-fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 内联容器样式 */
|
||||
.loading-container-inline {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 旋转圆环样式 */
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-svg {
|
||||
animation: rotate 1.4s linear infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.loading-path {
|
||||
animation: dash 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 点状加载样式 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loading-dot {
|
||||
border-radius: 50%;
|
||||
animation: bounce 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 脉冲样式 */
|
||||
.loading-pulse {
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 环形样式 */
|
||||
.loading-ring {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading-ring-circle {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-style: solid;
|
||||
border-radius: 50%;
|
||||
animation: spin 1.4s linear infinite;
|
||||
}
|
||||
|
||||
/* 加载文本样式 */
|
||||
.loading-text {
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 动画定义 */
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dashoffset: 94.2;
|
||||
}
|
||||
50% {
|
||||
stroke-dashoffset: 23.55;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 94.2;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0);
|
||||
opacity: 0.5;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(0.3);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
# Loading 组件使用文档
|
||||
|
||||
## 组件介绍
|
||||
|
||||
Loading 组件是一个灵活的加载动画组件,支持多种动画类型、自定义大小、颜色等配置,可用于页面加载、数据请求等场景。
|
||||
|
||||
## 组件类型
|
||||
|
||||
- `spinner`:旋转圆环(默认)
|
||||
- `dots`:点状加载动画
|
||||
- `pulse`:脉冲加载动画
|
||||
- `ring`:环形旋转动画
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式一:组件方式使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<!-- 基本使用 -->
|
||||
<Loading :visible="loadingVisible" />
|
||||
|
||||
<!-- 自定义配置 -->
|
||||
<Loading
|
||||
:visible="loadingVisible"
|
||||
type="dots"
|
||||
size="60"
|
||||
color="#1890ff"
|
||||
text="加载中..."
|
||||
:fullscreen="true"
|
||||
background="rgba(0, 0, 0, 0.7)"
|
||||
:opacity="0.8"
|
||||
:strokeWidth="4"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
<button @click="showLoading">显示加载</button>
|
||||
<button @click="hideLoading">隐藏加载</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Loading from './Loading.vue'
|
||||
|
||||
const loadingVisible = ref(false)
|
||||
|
||||
function showLoading() {
|
||||
loadingVisible.value = true
|
||||
}
|
||||
|
||||
function hideLoading() {
|
||||
loadingVisible.value = false
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 方式二:编程方式使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<button @click="showFullscreenLoading">显示全屏加载</button>
|
||||
<button @click="showLoadingWithText">显示带文本的加载</button>
|
||||
<button @click="showInlineLoading">显示内联加载</button>
|
||||
<button @click="hideAllLoading">隐藏所有加载</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { showLoading, hideLoading, showLoadingWithText, showInlineLoading } from './loading'
|
||||
import type { LoadingInstance } from './loading'
|
||||
|
||||
// 保存加载实例
|
||||
let loadingInstance: LoadingInstance | null = null
|
||||
|
||||
// 显示全屏加载
|
||||
function showFullscreenLoading() {
|
||||
loadingInstance = showLoading({
|
||||
type: 'spinner',
|
||||
size: 50,
|
||||
color: '#1890ff',
|
||||
fullscreen: true,
|
||||
background: 'rgba(255, 255, 255, 0.9)'
|
||||
})
|
||||
}
|
||||
|
||||
// 显示带文本的加载
|
||||
function showLoadingWithText() {
|
||||
loadingInstance = showLoadingWithText('数据加载中,请稍候...', '#52c41a')
|
||||
}
|
||||
|
||||
// 显示内联加载
|
||||
function showInlineLoading() {
|
||||
loadingInstance = showInlineLoading(32, '#fa8c16', 'dots')
|
||||
}
|
||||
|
||||
// 隐藏所有加载
|
||||
function hideAllLoading() {
|
||||
// 方式1:隐藏特定实例
|
||||
if (loadingInstance) {
|
||||
loadingInstance.hide()
|
||||
loadingInstance = null
|
||||
}
|
||||
|
||||
// 方式2:隐藏当前活动的全屏加载(如果有的话)
|
||||
hideLoading()
|
||||
}
|
||||
|
||||
// 模拟异步请求
|
||||
function mockAsyncRequest() {
|
||||
const loading = showLoadingWithText('处理中...')
|
||||
|
||||
setTimeout(() => {
|
||||
// 请求完成后隐藏加载
|
||||
loading.hide()
|
||||
console.log('请求完成')
|
||||
}, 3000)
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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
|
||||
<template>
|
||||
<div v-if="!loading">
|
||||
<!-- 页面内容 -->
|
||||
</div>
|
||||
<Loading v-else :visible="loading" text="页面加载中..." />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import Loading from './components/Loading.vue'
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
// 模拟页面数据加载
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 2000)
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### 场景2:API请求加载
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<button @click="fetchData" :disabled="loading">获取数据</button>
|
||||
<div v-if="data">
|
||||
<!-- 数据展示 -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { showLoading, hideLoading } from './components/loading'
|
||||
|
||||
const loading = ref(false)
|
||||
const data = ref(null)
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 显示加载
|
||||
const loadingInstance = showLoadingWithText('数据请求中...')
|
||||
|
||||
// 模拟API请求
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
// 获取数据
|
||||
data.value = { /* 数据内容 */ }
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 隐藏加载
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 场景3:提交表单时的加载
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<form @submit.prevent="submitForm">
|
||||
<!-- 表单内容 -->
|
||||
<button type="submit" :disabled="submitting">
|
||||
{{ submitting ? '提交中...' : '提交' }}
|
||||
<Loading
|
||||
v-if="submitting"
|
||||
:visible="true"
|
||||
:fullscreen="false"
|
||||
type="spinner"
|
||||
:size="16"
|
||||
style="display: inline-block; margin-left: 8px;"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import Loading from './components/Loading.vue'
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
async function submitForm() {
|
||||
submitting.value = true
|
||||
|
||||
try {
|
||||
// 模拟表单提交
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
console.log('表单提交成功')
|
||||
} catch (error) {
|
||||
console.error('表单提交失败:', error)
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 当使用编程方式创建多个全屏加载组件时,最新创建的会替换之前的。
|
||||
2. 请确保在不需要加载动画时调用 hide() 方法来卸载组件,避免内存泄漏。
|
||||
3. 对于长时间运行的操作,建议添加加载文本以提高用户体验。
|
||||
4. 内联加载组件不会自动居中,需要根据具体场景调整其位置。
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.verifyCode">
|
||||
<div v-if="formData.verifyCode && !formData.qrCode">
|
||||
<!-- 结婚证信息 -->
|
||||
<div class="mb-6">
|
||||
<h3 class="text-base text-gray-800 mb-3 font-medium">结婚证信息:</h3>
|
||||
|
|
@ -69,15 +69,15 @@
|
|||
class="w-full flex flex-col items-start border border-gray-200 rounded-lg p-5 bg-[#FAFAFA]">
|
||||
<div>
|
||||
<span class="inline-block w-25 text-right">结婚证字号:</span>
|
||||
<span class="pl-2">{{ 11111 }}</span>
|
||||
<span class="pl-2">{{ formData.marriageNo }}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="inline-block w-25 text-right">男方姓名:</span>
|
||||
<span class="pl-2">{{ 11111 }}</span>
|
||||
<span class="pl-2">{{ formData.husbandName }}</span>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<span class="inline-block w-25 text-right">女方姓名:</span>
|
||||
<span class="pl-2">{{ 11111 }}</span>
|
||||
<span class="pl-2">{{ formData.wifeName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -88,6 +88,15 @@
|
|||
提交领取喜礼
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="formData.qrCode">
|
||||
<div>
|
||||
<h3 class="text-base text-gray-800 mb-3 font-medium">请使用微信扫描下方二维码:</h3>
|
||||
</div>
|
||||
<div class="mt-4 flex justify-center items-center">
|
||||
<qrcode-vue :value="formData.qrCode" :size="200" level="H" render-as="canvas" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="activityInfo.status === 2">
|
||||
|
|
@ -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<HTMLInputElement>();
|
||||
|
||||
|
|
@ -131,11 +142,13 @@ const formData = ref({
|
|||
husbandName: '', // 男方姓名
|
||||
wifeName: '', // 女方姓名
|
||||
registerDate: '', // 登记日期
|
||||
qrCode: '', // 二维码
|
||||
})
|
||||
|
||||
const activityInfo = ref<any>({}); // 活动信息
|
||||
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();
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue