af-mobile-client-vue3 1.0.91 → 1.0.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ <script setup lang="ts">
2
+ import { nextTick, onMounted, onUnmounted, ref } from 'vue'
3
+ import { Icon as VanIcon } from 'vant'
4
+ import { startScanAnimation, stopScanAnimation } from './startScanAnimation'
5
+
6
+ // 扫码框的引用
7
+ const scannerRef = ref<HTMLDivElement>()
8
+ // 扫码动画的引用
9
+ const scanLineRef = ref<HTMLDivElement>()
10
+ // 闪光灯状态
11
+ const isLightOn = ref(false)
12
+
13
+ // 初始化扫描动画
14
+ async function initScanAnimation() {
15
+ // 确保元素已渲染完成
16
+ await nextTick()
17
+ // 添加延迟确保元素尺寸已计算完成
18
+ setTimeout(() => {
19
+ if (scannerRef.value && scanLineRef.value) {
20
+ // 设置初始位置
21
+ scanLineRef.value.style.top = '0px'
22
+ startScanAnimation(scanLineRef, scannerRef)
23
+ }
24
+ }, 300)
25
+ }
26
+
27
+ // 切换闪光灯
28
+ function toggleLight() {
29
+ isLightOn.value = !isLightOn.value
30
+ }
31
+
32
+ onMounted(() => {
33
+ initScanAnimation()
34
+ })
35
+
36
+ onUnmounted(() => {
37
+ stopScanAnimation()
38
+ })
39
+ </script>
40
+
41
+ <template>
42
+ <div class="qr-scanner">
43
+ <div class="scanner-container">
44
+ <div class="scanner-bg">
45
+ <div ref="scannerRef" class="scanner-box">
46
+ <div class="corner-lt" />
47
+ <div class="corner-rt" />
48
+ <div class="corner-lb" />
49
+ <div class="corner-rb" />
50
+ <div ref="scanLineRef" class="scan-line" />
51
+ <div class="light-btn" @click="toggleLight">
52
+ <VanIcon :name="isLightOn ? 'bulb-o' : 'bulb'" class="light-icon" />
53
+ </div>
54
+ </div>
55
+ <div class="scanner-tip">
56
+ 将二维码放入框内,即可自动扫描
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <style scoped lang="less">
64
+ .qr-scanner {
65
+ width: 100%;
66
+ height: 100%;
67
+ background-color: #fff;
68
+ display: flex;
69
+ flex-direction: column;
70
+ border-radius: 8px;
71
+ overflow: hidden;
72
+
73
+ .scanner-container {
74
+ flex: 1;
75
+ display: flex;
76
+ flex-direction: column;
77
+ align-items: center;
78
+ justify-content: center;
79
+ padding: 20px;
80
+ background-color: #fff;
81
+
82
+ .scanner-bg {
83
+ width: 320px;
84
+ height: 320px;
85
+ background-color: #f0f0f0;
86
+ border-radius: 8px;
87
+ padding: 20px;
88
+ display: flex;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ justify-content: flex-start;
92
+ padding-top: 40px;
93
+ }
94
+
95
+ .scanner-box {
96
+ width: 200px;
97
+ height: 200px;
98
+ position: relative;
99
+ background-color: transparent;
100
+ border: 1px solid #1989fa;
101
+ border-radius: 0;
102
+ box-sizing: border-box;
103
+ margin: 0;
104
+
105
+ &::before {
106
+ content: '';
107
+ position: absolute;
108
+ top: 0;
109
+ left: 0;
110
+ right: 0;
111
+ bottom: 0;
112
+ border: none;
113
+ z-index: 1;
114
+ }
115
+
116
+ .corner-lt,
117
+ .corner-rt,
118
+ .corner-lb,
119
+ .corner-rb {
120
+ position: absolute;
121
+ width: 24px;
122
+ height: 24px;
123
+ border-color: #1989fa;
124
+ border-style: solid;
125
+ z-index: 2;
126
+ margin: 0;
127
+ }
128
+
129
+ .corner-lt {
130
+ top: -3px;
131
+ left: -3px;
132
+ border-width: 3px 0 0 3px;
133
+ border-radius: 0;
134
+ margin: -8px 0 0 -8px;
135
+ }
136
+
137
+ .corner-rt {
138
+ top: -3px;
139
+ right: -3px;
140
+ border-width: 3px 3px 0 0;
141
+ border-radius: 0;
142
+ margin: -8px -8px 0 0;
143
+ }
144
+
145
+ .corner-lb {
146
+ bottom: -3px;
147
+ left: -3px;
148
+ border-width: 0 0 3px 3px;
149
+ border-radius: 0;
150
+ margin: 0 0 -8px -8px;
151
+ }
152
+
153
+ .corner-rb {
154
+ bottom: -3px;
155
+ right: -3px;
156
+ border-width: 0 3px 3px 0;
157
+ border-radius: 0;
158
+ margin: 0 -8px -8px 0;
159
+ }
160
+
161
+ .scan-line {
162
+ position: absolute;
163
+ left: 0;
164
+ top: 0;
165
+ width: 100%;
166
+ height: 2px;
167
+ background: linear-gradient(to right, transparent, #1989fa, transparent);
168
+ box-shadow: 0 0 4px #1989fa;
169
+ z-index: 3;
170
+ }
171
+
172
+ .light-btn {
173
+ position: absolute;
174
+ right: -20px;
175
+ bottom: -20px;
176
+ width: 40px;
177
+ height: 40px;
178
+ border-radius: 50%;
179
+ background-color: rgba(44, 44, 44, 0.5);
180
+ display: flex;
181
+ align-items: center;
182
+ justify-content: center;
183
+ cursor: pointer;
184
+ z-index: 3;
185
+
186
+ .light-icon {
187
+ font-size: 20px;
188
+ color: #fff;
189
+ }
190
+
191
+ &:active {
192
+ opacity: 0.8;
193
+ }
194
+ }
195
+ }
196
+
197
+ .scanner-tip {
198
+ color: #666;
199
+ font-size: 14px;
200
+ text-align: center;
201
+ margin-top: 30px;
202
+ margin-bottom: 0;
203
+ width: 100%;
204
+ }
205
+ }
206
+ }
207
+ </style>
@@ -0,0 +1,53 @@
1
+ import type { Ref } from 'vue'
2
+
3
+ let animationTimer: ReturnType<typeof setInterval> | null = null
4
+
5
+ // 开始扫码动画
6
+ export function startScanAnimation(scanLineRef: Ref<HTMLDivElement | undefined>, scannerRef: Ref<HTMLDivElement | undefined>) {
7
+ // 先停止之前的动画
8
+ stopScanAnimation()
9
+
10
+ if (!scanLineRef.value || !scannerRef.value)
11
+ return
12
+
13
+ // 确保扫描线初始位置正确
14
+ scanLineRef.value.style.top = '0px'
15
+
16
+ let direction = 'down'
17
+ let position = 0
18
+ const speed = 2
19
+ const maxPosition = scannerRef.value.offsetHeight || 200
20
+
21
+ // 确保maxPosition不为0
22
+ if (maxPosition <= 10) {
23
+ // 如果容器高度获取失败,使用一个默认延迟后再尝试
24
+ setTimeout(() => {
25
+ startScanAnimation(scanLineRef, scannerRef)
26
+ }, 500)
27
+ return
28
+ }
29
+
30
+ animationTimer = setInterval(() => {
31
+ if (direction === 'down') {
32
+ position += speed
33
+ if (position >= maxPosition - 2)
34
+ direction = 'up'
35
+ }
36
+ else {
37
+ position -= speed
38
+ if (position <= 0)
39
+ direction = 'down'
40
+ }
41
+ if (scanLineRef.value)
42
+ scanLineRef.value.style.top = `${position}px`
43
+ }, 20)
44
+
45
+ return animationTimer
46
+ }
47
+
48
+ export function stopScanAnimation() {
49
+ if (animationTimer) {
50
+ clearInterval(animationTimer)
51
+ animationTimer = null
52
+ }
53
+ }
@@ -0,0 +1,119 @@
1
+ <script setup lang="ts">
2
+ import { ref } from 'vue'
3
+ import { Icon as VanIcon } from 'vant'
4
+
5
+ defineEmits(['close'])
6
+
7
+ const isScanning = ref(true)
8
+ </script>
9
+
10
+ <template>
11
+ <div class="vpn-recognition">
12
+ <div class="scanner-container">
13
+ <div class="scanner-bg">
14
+ <div class="scan-circle">
15
+ <VanIcon name="idcard" size="60" color="rgb(59,130,246)"/>
16
+ </div>
17
+ <div class="scan-ripple" v-if="isScanning" />
18
+ <div class="scan-tip">
19
+ 准备读取NFC
20
+ </div>
21
+ </div>
22
+ <div class="scan-subtitle">
23
+ 请将手机靠近NFC标签进行读取
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </template>
28
+
29
+ <style scoped lang="less">
30
+ .vpn-recognition {
31
+ width: 100%;
32
+ height: 100%;
33
+ background-color: #fff;
34
+ display: flex;
35
+ flex-direction: column;
36
+ border-radius: 8px;
37
+ overflow: hidden;
38
+
39
+ .scanner-container {
40
+ flex: 1;
41
+ display: flex;
42
+ flex-direction: column;
43
+ align-items: center;
44
+ justify-content: center;
45
+ padding: 20px;
46
+ background-color: #fff;
47
+
48
+ .scanner-bg {
49
+ width: 320px;
50
+ height: 320px;
51
+ background-color: #f0f0f0;
52
+ border-radius: 8px;
53
+ padding: 20px;
54
+ display: flex;
55
+ flex-direction: column;
56
+ align-items: center;
57
+ justify-content: flex-start;
58
+ padding-top: 40px;
59
+ position: relative;
60
+ }
61
+
62
+ .scan-circle {
63
+ width: 100px;
64
+ height: 100px;
65
+ border-radius: 50%;
66
+ background-color: rgba(25, 137, 250, 0.1);
67
+ display: flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ margin-bottom: 20px;
71
+ position: relative;
72
+ z-index: 2;
73
+ }
74
+
75
+ .scan-ripple {
76
+ position: absolute;
77
+ top: 35%;
78
+ left: 50%;
79
+ transform: translate(-50%, -50%);
80
+ width: 120px;
81
+ height: 120px;
82
+ background-color: rgba(25, 137, 250, 0.2);
83
+ border-radius: 50%;
84
+ animation: ripple 3s infinite;
85
+ z-index: 1;
86
+ }
87
+
88
+ .scan-tip {
89
+ color: #666;
90
+ font-size: 16px;
91
+ text-align: center;
92
+ margin-top: 30px;
93
+ margin-bottom: 8px;
94
+ width: 100%;
95
+ font-weight: normal;
96
+ }
97
+
98
+ .scan-subtitle {
99
+ font-size: 14px;
100
+ color: #969799;
101
+ text-align: center;
102
+ width: 100%;
103
+ font-weight: bold;
104
+ margin-top: 7%;
105
+ }
106
+ }
107
+ }
108
+
109
+ @keyframes ripple {
110
+ 0% {
111
+ transform: translate(-50%, -50%) scale(1);
112
+ opacity: 0.5;
113
+ }
114
+ 100% {
115
+ transform: translate(-50%, -50%) scale(2);
116
+ opacity: 0;
117
+ }
118
+ }
119
+ </style>