leadal-auth 0.0.1

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.
Files changed (91) hide show
  1. package/README.md +83 -0
  2. package/babel.config.js +5 -0
  3. package/jsconfig.json +19 -0
  4. package/ld-auth/demo.html +1 -0
  5. package/ld-auth/ld-auth.common.js +44730 -0
  6. package/ld-auth/ld-auth.css +1 -0
  7. package/ld-auth/ld-auth.umd.js +44730 -0
  8. package/ld-auth/ld-auth.umd.min.js +55 -0
  9. package/package.json +58 -0
  10. package/public/favicon.ico +0 -0
  11. package/public/index.html +17 -0
  12. package/public/models/age_gender_model-shard1 +0 -0
  13. package/public/models/age_gender_model-weights_manifest.json +1 -0
  14. package/public/models/face_expression_model-shard1 +0 -0
  15. package/public/models/face_expression_model-weights_manifest.json +1 -0
  16. package/public/models/face_landmark_68_model-shard1 +0 -0
  17. package/public/models/face_landmark_68_model-weights_manifest.json +1 -0
  18. package/public/models/face_landmark_68_tiny_model-shard1 +0 -0
  19. package/public/models/face_landmark_68_tiny_model-weights_manifest.json +1 -0
  20. package/public/models/face_recognition_model-shard1 +0 -0
  21. package/public/models/face_recognition_model-shard2 +6 -0
  22. package/public/models/face_recognition_model-weights_manifest.json +1 -0
  23. package/public/models/mtcnn_model-shard1 +0 -0
  24. package/public/models/mtcnn_model-weights_manifest.json +1 -0
  25. package/public/models/ssd_mobilenetv1_model-shard1 +0 -0
  26. package/public/models/ssd_mobilenetv1_model-shard2 +145 -0
  27. package/public/models/ssd_mobilenetv1_model-weights_manifest.json +1 -0
  28. package/public/models/tiny_face_detector_model-shard1 +0 -0
  29. package/public/models/tiny_face_detector_model-weights_manifest.json +1 -0
  30. package/src/App.vue +19 -0
  31. package/src/api/card.js +58 -0
  32. package/src/api/face.js +37 -0
  33. package/src/api/finger.js +64 -0
  34. package/src/api/index.js +100 -0
  35. package/src/assets/BIN.png +0 -0
  36. package/src/assets/CLOSE.svg +11 -0
  37. package/src/assets/blue-left.png +0 -0
  38. package/src/assets/blue-right.png +0 -0
  39. package/src/assets/finger-ready.png +0 -0
  40. package/src/assets/finger-select.png +0 -0
  41. package/src/assets/finger-status-1-last.png +0 -0
  42. package/src/assets/finger-status-1.gif +0 -0
  43. package/src/assets/finger-status-2-last.png +0 -0
  44. package/src/assets/finger-status-2.gif +0 -0
  45. package/src/assets/finger-status-3-last.png +0 -0
  46. package/src/assets/finger-status-3.gif +0 -0
  47. package/src/assets/finger-status-compeleted.png +0 -0
  48. package/src/assets/finger-status-start.png +0 -0
  49. package/src/assets/icon-camera.png +0 -0
  50. package/src/assets/icon-picture.png +0 -0
  51. package/src/assets/icon-success.png +0 -0
  52. package/src/assets/img-camera.png +0 -0
  53. package/src/assets/img-card.png +0 -0
  54. package/src/assets/img-loading.png +0 -0
  55. package/src/assets/left.png +0 -0
  56. package/src/assets/logo.png +0 -0
  57. package/src/assets/right.png +0 -0
  58. package/src/assets/ukey1.png +0 -0
  59. package/src/assets/ukey2.png +0 -0
  60. package/src/assets//346/214/207/347/272/271/350/257/206/345/210/2531.png +0 -0
  61. package/src/assets//346/214/207/347/272/271/350/257/206/345/210/2532.png +0 -0
  62. package/src/components/auth-com.vue +100 -0
  63. package/src/components/card-register/components/CardTable.vue +94 -0
  64. package/src/components/card-register/components/RegisterDialog.vue +137 -0
  65. package/src/components/card-register/index.vue +110 -0
  66. package/src/components/edit-user-dialog.vue +141 -0
  67. package/src/components/empty.vue +13 -0
  68. package/src/components/face-register/components/ChooseCameraOrPicture.vue +59 -0
  69. package/src/components/face-register/components/FaceDetected.vue +543 -0
  70. package/src/components/face-register/components/FaceInfo.vue +171 -0
  71. package/src/components/face-register/components/FacePicture.vue +85 -0
  72. package/src/components/face-register/components/UploadPicture.vue +336 -0
  73. package/src/components/face-register/index.vue +242 -0
  74. package/src/components/finger-register/index.vue +685 -0
  75. package/src/components/organ-tree.vue +211 -0
  76. package/src/components/tree-select.vue +131 -0
  77. package/src/components/user-drawer.vue +147 -0
  78. package/src/components/user-info.vue +272 -0
  79. package/src/components/user-table.vue +405 -0
  80. package/src/main.js +26 -0
  81. package/src/package/auth-manage/index.vue +461 -0
  82. package/src/package/index.js +22 -0
  83. package/src/store/index.js +39 -0
  84. package/src/styles/common.scss +183 -0
  85. package/src/styles/index.scss +38 -0
  86. package/src/utils/dict.js +47 -0
  87. package/src/utils/event-bus.js +6 -0
  88. package/src/utils/request-auth.js +64 -0
  89. package/src/utils/request.js +64 -0
  90. package/src/utils/websocket.js +282 -0
  91. package/vue.config.js +43 -0
@@ -0,0 +1,543 @@
1
+ <template>
2
+ <div class="face-detection">
3
+ <div style="margin-bottom: 32px; margin-top: 12px; text-align: center">
4
+ 人脸识别中,请进入识别范围内
5
+ </div>
6
+ <div class="face-detection-container flex-center align-center">
7
+ <!-- 摄像头预览区域 -->
8
+ <div class="camera-container" :class="{ detecting: isDetecting }">
9
+ <video
10
+ ref="video"
11
+ :width="videoWidth"
12
+ :height="videoHeight"
13
+ autoplay
14
+ muted
15
+ playsinline
16
+ @loadedmetadata="onVideoLoaded"
17
+ style="border-radius: 50%; object-fit: cover"
18
+ ></video>
19
+
20
+ <!-- 人脸检测框 -->
21
+ <canvas
22
+ ref="overlay"
23
+ :width="videoWidth"
24
+ :height="videoHeight"
25
+ class="overlay-canvas"
26
+ ></canvas>
27
+
28
+ <!-- 检测结果预览 -->
29
+ <div v-if="capturedImages.length > 0" class="results">
30
+ <div class="captured-image-container">
31
+ <img
32
+ :src="capturedImages[0].dataUrl"
33
+ :width="videoWidth"
34
+ :height="videoHeight"
35
+ alt="检测到的人脸"
36
+ class="captured-face-image"
37
+ />
38
+ </div>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ <div class="flex-center mt-12 w100">
43
+ <el-button @click="h_back">返回</el-button>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <script>
49
+ import * as faceapi from "face-api.js";
50
+
51
+ export default {
52
+ name: "FaceDetection",
53
+
54
+ emits: ["face-detected", "face-lost", "error", "detection-complete"],
55
+
56
+ props: {
57
+ videoWidth: {
58
+ type: Number,
59
+ default: 240,
60
+ },
61
+ videoHeight: {
62
+ type: Number,
63
+ default: 240,
64
+ },
65
+ minConfidence: {
66
+ type: Number,
67
+ default: 0.5,
68
+ },
69
+ captureInterval: {
70
+ type: Number,
71
+ default: 2000, // 2秒检测一次
72
+ },
73
+ maxCaptures: {
74
+ type: Number,
75
+ default: 1, // 只保存1张图片
76
+ },
77
+ modelPath: {
78
+ type: String,
79
+ default: "/models", // face-api.js模型文件路径
80
+ },
81
+ },
82
+
83
+ data() {
84
+ return {
85
+ loading: false,
86
+ modelsLoaded: false,
87
+ cameraStarted: false,
88
+ cameraReady: false,
89
+ isDetecting: false,
90
+ stream: null,
91
+ detectionInterval: null,
92
+ capturedImages: [],
93
+ lastCaptureTime: 0,
94
+ };
95
+ },
96
+
97
+ async mounted() {
98
+ console.log("mounted______");
99
+
100
+ await this.loadModels();
101
+ // 自动开始检测流程
102
+ await this.autoStartDetection();
103
+ },
104
+
105
+ beforeDestroy() {
106
+ console.log("结束,清理摄像头");
107
+
108
+ this.cleanup();
109
+ },
110
+
111
+ methods: {
112
+ h_back() {
113
+ this.$emit("back");
114
+ },
115
+ async loadModels() {
116
+ try {
117
+ this.loading = true;
118
+
119
+ // 加载face-api.js模型
120
+ await Promise.all([
121
+ faceapi.nets.tinyFaceDetector.loadFromUri(this.modelPath),
122
+ faceapi.nets.faceLandmark68Net.loadFromUri(this.modelPath),
123
+ faceapi.nets.faceRecognitionNet.loadFromUri(this.modelPath),
124
+ ]);
125
+
126
+ this.modelsLoaded = true;
127
+ console.log("人脸检测模型加载完成");
128
+ } catch (error) {
129
+ // console.error("模型加载失败:", error);
130
+ this.$emit("error", { type: "model-load-error", error });
131
+ } finally {
132
+ this.loading = false;
133
+ }
134
+ },
135
+
136
+ async autoStartDetection() {
137
+ if (!this.modelsLoaded) return;
138
+
139
+ try {
140
+ await this.startCamera();
141
+ // 等待摄像头准备好后自动开始检测
142
+ setTimeout(() => {
143
+ if (this.cameraReady) {
144
+ this.startDetection();
145
+ }
146
+ }, 1000);
147
+ } catch (error) {
148
+ console.error("自动开始检测失败:", error);
149
+ }
150
+ },
151
+
152
+ async startCamera() {
153
+ try {
154
+ this.stream = await navigator.mediaDevices.getUserMedia({
155
+ video: {
156
+ width: this.videoWidth,
157
+ height: this.videoHeight,
158
+ facingMode: "user",
159
+ },
160
+ });
161
+
162
+ this.$refs.video.srcObject = this.stream;
163
+ this.cameraStarted = true;
164
+ } catch (error) {
165
+ console.error("摄像头启动失败:", error);
166
+ this.$emit("error", { type: "camera-error", error });
167
+ }
168
+ },
169
+
170
+ onVideoLoaded() {
171
+ this.cameraReady = true;
172
+ console.log("摄像头准备就绪");
173
+
174
+ // 如果模型已加载且还未开始检测,则自动开始检测
175
+ if (this.modelsLoaded && !this.isDetecting) {
176
+ setTimeout(() => {
177
+ this.startDetection();
178
+ }, 500);
179
+ }
180
+ },
181
+
182
+ stopCamera() {
183
+ this.stopDetection();
184
+
185
+ if (this.stream) {
186
+ this.stream.getTracks().forEach((track) => track.stop());
187
+ this.stream = null;
188
+ }
189
+
190
+ this.cameraStarted = false;
191
+ this.cameraReady = false;
192
+ },
193
+
194
+ startDetection() {
195
+ if (!this.modelsLoaded || !this.cameraReady) return;
196
+
197
+ this.isDetecting = true;
198
+ this.detectionInterval = setInterval(() => {
199
+ this.detectFaces();
200
+ }, 100); // 每100ms检测一次
201
+ },
202
+
203
+ stopDetection() {
204
+ this.isDetecting = false;
205
+ if (this.detectionInterval) {
206
+ clearInterval(this.detectionInterval);
207
+ this.detectionInterval = null;
208
+ }
209
+
210
+ // 清空检测框
211
+ const canvas = this.$refs.overlay;
212
+ const ctx = canvas.getContext("2d");
213
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
214
+ },
215
+
216
+ toggleDetection() {
217
+ if (this.isDetecting) {
218
+ this.stopDetection();
219
+ } else {
220
+ this.startDetection();
221
+ }
222
+ },
223
+
224
+ async detectFaces() {
225
+ const video = this.$refs.video;
226
+ const canvas = this.$refs.overlay;
227
+
228
+ if (!video || !canvas) return;
229
+
230
+ try {
231
+ const detections = await faceapi
232
+ .detectAllFaces(video, new faceapi.TinyFaceDetectorOptions())
233
+ .withFaceLandmarks()
234
+ .withFaceDescriptors();
235
+
236
+ // 清空之前的检测框
237
+ const ctx = canvas.getContext("2d");
238
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
239
+
240
+ if (detections.length > 0) {
241
+ // 绘制检测框
242
+ detections.forEach((detection) => {
243
+ const { x, y, width, height } = detection.detection.box;
244
+
245
+ ctx.strokeStyle = "#00ff00";
246
+ ctx.lineWidth = 2;
247
+ ctx.strokeRect(x, y, width, height);
248
+
249
+ // 显示置信度
250
+ ctx.fillStyle = "#00ff00";
251
+ ctx.font = "16px Arial";
252
+ ctx.fillText(
253
+ `${(detection.detection.score * 100).toFixed(1)}%`,
254
+ x,
255
+ y - 10
256
+ );
257
+ });
258
+
259
+ // 检查是否需要捕获图片
260
+ const now = Date.now();
261
+ if (now - this.lastCaptureTime > this.captureInterval) {
262
+ this.captureHighConfidenceFaces(detections);
263
+ this.lastCaptureTime = now;
264
+ }
265
+
266
+ this.$emit("face-detected", detections);
267
+
268
+ // 检测到人脸后停止检测并关闭摄像头
269
+ this.onFaceDetectedComplete(detections);
270
+ } else {
271
+ this.$emit("face-lost");
272
+ }
273
+ } catch (error) {
274
+ console.error("人脸检测失败:", error);
275
+ this.$emit("error", { type: "detection-error", error });
276
+ }
277
+ },
278
+
279
+ captureHighConfidenceFaces(detections) {
280
+ const video = this.$refs.video;
281
+
282
+ detections.forEach((detection) => {
283
+ if (detection.detection.score >= this.minConfidence) {
284
+ const canvas = document.createElement("canvas");
285
+ const ctx = canvas.getContext("2d");
286
+
287
+ const { x, y, width, height } = detection.detection.box;
288
+
289
+ // 扩展捕获区域以包含头发
290
+ const expandRatio = 1; // 向上扩展30%的高度
291
+ const sideExpandRatio = 1; // 左右各扩展15%的宽度
292
+ const bottomExpandRatio = 1; // 向下扩展10%的高度
293
+
294
+ // 计算扩展后的区域
295
+ const expandedHeight = height * (1 + expandRatio + bottomExpandRatio);
296
+ const expandedWidth = width * (1 + sideExpandRatio * 2);
297
+ const expandedX = Math.max(0, x - width * sideExpandRatio);
298
+ const expandedY = Math.max(0, y - height * expandRatio);
299
+
300
+ // 确保不超出视频边界
301
+ const videoWidth = video.videoWidth || this.videoWidth;
302
+ const videoHeight = video.videoHeight || this.videoHeight;
303
+
304
+ const finalX = Math.max(0, expandedX);
305
+ const finalY = Math.max(0, expandedY);
306
+ const finalWidth = Math.min(expandedWidth, videoWidth - finalX);
307
+ const finalHeight = Math.min(expandedHeight, videoHeight - finalY);
308
+
309
+ // 设置画布大小
310
+ canvas.width = finalWidth;
311
+ canvas.height = finalHeight;
312
+
313
+ // 绘制扩展后的人脸区域
314
+ ctx.drawImage(
315
+ video,
316
+ finalX,
317
+ finalY,
318
+ finalWidth,
319
+ finalHeight,
320
+ 0,
321
+ 0,
322
+ finalWidth,
323
+ finalHeight
324
+ );
325
+
326
+ // 转换为base64
327
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.8);
328
+
329
+ // 保存图片
330
+ this.capturedImages.unshift({
331
+ dataUrl,
332
+ confidence: detection.detection.score,
333
+ timestamp: Date.now(),
334
+ box: detection.detection.box,
335
+ expandedBox: {
336
+ x: finalX,
337
+ y: finalY,
338
+ width: finalWidth,
339
+ height: finalHeight,
340
+ },
341
+ });
342
+
343
+ // 限制保存数量
344
+ if (this.capturedImages.length > this.maxCaptures) {
345
+ this.capturedImages = this.capturedImages.slice(
346
+ 0,
347
+ this.maxCaptures
348
+ );
349
+ }
350
+
351
+ console.log("捕获到高置信度人脸:", detection.detection.score);
352
+ }
353
+ });
354
+ },
355
+
356
+ async copyToClipboard(dataUrl) {
357
+ try {
358
+ await navigator.clipboard.writeText(dataUrl);
359
+ // 这里可以添加成功提示
360
+ console.log("Base64已复制到剪贴板");
361
+ } catch (error) {
362
+ console.error("复制失败:", error);
363
+ }
364
+ },
365
+
366
+ onFaceDetectedComplete(detections) {
367
+ // 检测到人脸后的处理逻辑
368
+ console.log("检测到人脸,正在停止检测...");
369
+
370
+ // 停止检测和摄像头
371
+ this.stopDetection();
372
+ this.stopCamera();
373
+
374
+ // 可以在这里添加其他后续处理
375
+ this.$emit("detection-complete", {
376
+ detections,
377
+ capturedImages: this.capturedImages,
378
+ });
379
+ },
380
+
381
+ cleanup() {
382
+ this.stopCamera();
383
+ this.capturedImages = [];
384
+ },
385
+
386
+ /**
387
+ * 重新开始认证流程
388
+ * 用于认证失败后重新进行人脸检测
389
+ */
390
+ async restartAuthentication() {
391
+ try {
392
+ console.log("开始重新认证...");
393
+
394
+ // 清理之前的状态
395
+ this.cleanup();
396
+
397
+ // 重置状态
398
+ this.capturedImages = [];
399
+ this.lastCaptureTime = 0;
400
+
401
+ // 如果模型未加载,先加载模型
402
+ if (!this.modelsLoaded) {
403
+ await this.loadModels();
404
+ }
405
+
406
+ // 重新开始检测流程
407
+ await this.autoStartDetection();
408
+
409
+ console.log("重新认证流程已启动");
410
+
411
+ // 发射重新认证开始事件
412
+ this.$emit("restart-authentication");
413
+ } catch (error) {
414
+ console.error("重新认证启动失败:", error);
415
+ this.$emit("error", { type: "restart-error", error });
416
+ }
417
+ },
418
+ },
419
+ };
420
+ </script>
421
+
422
+ <style scoped>
423
+ .face-detection {
424
+ width: 500px;
425
+ height: 480px;
426
+ border-radius: 8px;
427
+ border: 1px solid #e5e5e7;
428
+ }
429
+
430
+ .face-detection-container {
431
+ margin: 0 auto;
432
+ width: 360px;
433
+ height: 360px;
434
+ background: #fff;
435
+ border-radius: 12px;
436
+ background: url("@/assets/img-camera.png") no-repeat center center;
437
+ background-size: 100% 100%;
438
+ }
439
+
440
+ .camera-container {
441
+ position: relative;
442
+ display: inline-block;
443
+ border: 2px solid #ddd;
444
+ border-radius: 50%;
445
+ overflow: hidden;
446
+ background: #000;
447
+ width: 240px;
448
+ height: 240px;
449
+ }
450
+
451
+ .camera-container.detecting {
452
+ border-color: #00ff00;
453
+ box-shadow: 0 0 20px rgba(0, 255, 0, 0.3);
454
+ }
455
+
456
+ .overlay-canvas {
457
+ position: absolute;
458
+ top: 0;
459
+ left: 0;
460
+ pointer-events: none;
461
+ border-radius: 50%;
462
+ }
463
+
464
+ .status-overlay {
465
+ position: absolute;
466
+ bottom: 10px;
467
+ left: 50%;
468
+ transform: translateX(-50%);
469
+ background: rgba(0, 0, 0, 0.8);
470
+ color: white;
471
+ padding: 8px 16px;
472
+ border-radius: 20px;
473
+ font-size: 14px;
474
+ }
475
+
476
+ .status-item {
477
+ display: flex;
478
+ align-items: center;
479
+ gap: 8px;
480
+ }
481
+
482
+ .spinner {
483
+ width: 16px;
484
+ height: 16px;
485
+ border: 2px solid #333;
486
+ border-top: 2px solid #fff;
487
+ border-radius: 50%;
488
+ animation: spin 1s linear infinite;
489
+ }
490
+
491
+ .pulse {
492
+ width: 12px;
493
+ height: 12px;
494
+ background: #00ff00;
495
+ border-radius: 50%;
496
+ animation: pulse 1s ease-in-out infinite;
497
+ }
498
+
499
+ @keyframes spin {
500
+ 0% {
501
+ transform: rotate(0deg);
502
+ }
503
+ 100% {
504
+ transform: rotate(360deg);
505
+ }
506
+ }
507
+
508
+ @keyframes pulse {
509
+ 0%,
510
+ 100% {
511
+ opacity: 1;
512
+ }
513
+ 50% {
514
+ opacity: 0.5;
515
+ }
516
+ }
517
+
518
+ .results {
519
+ position: absolute;
520
+ top: 0;
521
+ left: 0;
522
+ display: flex;
523
+ justify-content: center;
524
+ align-items: center;
525
+ }
526
+
527
+ .captured-image-container {
528
+ display: flex;
529
+ justify-content: center;
530
+ align-items: center;
531
+ }
532
+
533
+ .captured-face-image {
534
+ border-radius: 50%;
535
+ object-fit: cover;
536
+ }
537
+
538
+ @media (max-width: 768px) {
539
+ .face-detection-container {
540
+ padding: 10px;
541
+ }
542
+ }
543
+ </style>
@@ -0,0 +1,171 @@
1
+ <template>
2
+ <div class="face-info">
3
+ <div v-if="src" class="image-container">
4
+ <img
5
+ :src="src"
6
+ @error="handleImageError"
7
+ @load="handleImageLoad"
8
+ alt="用户人脸信息"
9
+ />
10
+ </div>
11
+ <div v-else-if="loading" class="loading-container">
12
+ <i class="el-icon-loading" style="font-size: 48px; color: #409eff"></i>
13
+ <div>加载中...</div>
14
+ </div>
15
+ <div v-else class="no-image-container">
16
+ <i class="el-icon-picture" style="font-size: 48px; color: #ccc"></i>
17
+ <div>暂无人脸信息</div>
18
+ </div>
19
+ <el-button
20
+ type="primary"
21
+ style="margin-top: 48px; height: 42px; font-size: 16px"
22
+ :disabled="!src"
23
+ @click="handleClearFaceInfo"
24
+ round
25
+ >
26
+ 清除人脸信息
27
+ </el-button>
28
+ </div>
29
+ </template>
30
+
31
+ <script>
32
+ import { mapGetters } from "vuex";
33
+ import { deleteFaceApi } from "@/api/face";
34
+ export default {
35
+ name: "FaceInfo",
36
+
37
+ emits: ["clear-face"],
38
+
39
+ data() {
40
+ return {
41
+ src: null,
42
+ loading: false,
43
+ };
44
+ },
45
+
46
+ computed: {
47
+ ...mapGetters(["userId"]),
48
+ },
49
+
50
+ mounted() {
51
+ this.loadFaceImage();
52
+ },
53
+
54
+ methods: {
55
+ loadFaceImage() {
56
+ if (!this.userId) {
57
+ console.warn("用户ID不存在,无法加载人脸信息");
58
+ return;
59
+ }
60
+
61
+ this.loading = true;
62
+ this.src = `/plugin/face/photo?userId=${this.userId}`;
63
+ },
64
+
65
+ handleImageLoad() {
66
+ this.loading = false;
67
+ console.log("人脸图片加载成功");
68
+ },
69
+
70
+ handleImageError() {
71
+ this.loading = false;
72
+ this.src = null;
73
+ console.warn("人脸图片加载失败");
74
+ },
75
+
76
+ handleClearFaceInfo() {
77
+ this.$confirm("确定要清除人脸信息吗?", "提示", {
78
+ confirmButtonText: "确定",
79
+ cancelButtonText: "取消",
80
+ type: "warning",
81
+ })
82
+ .then(() => {
83
+ deleteFaceApi({ userId: this.userId }).then(() => {
84
+ this.$emit("clear-face");
85
+ this.$message.success("人脸信息已清除");
86
+ });
87
+ })
88
+ .catch(() => {});
89
+ },
90
+
91
+ // 刷新人脸图片 (供父组件调用)
92
+ refresh() {
93
+ this.loadFaceImage();
94
+ },
95
+ },
96
+ };
97
+ </script>
98
+
99
+ <style lang="scss" scoped>
100
+ .face-info {
101
+ width: 100%;
102
+ height: 100%;
103
+ display: flex;
104
+ flex-direction: column;
105
+ align-items: center;
106
+ justify-content: center;
107
+ padding: 20px;
108
+
109
+ .image-container {
110
+ display: flex;
111
+ justify-content: center;
112
+ align-items: center;
113
+
114
+ img {
115
+ width: 200px;
116
+ height: 240px;
117
+ border: 1px solid #1346ee;
118
+ border-radius: 10px;
119
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
120
+ transition: transform 0.3s ease;
121
+ object-fit: cover;
122
+
123
+ &:hover {
124
+ transform: scale(1.02);
125
+ }
126
+ }
127
+ }
128
+
129
+ .loading-container,
130
+ .no-image-container {
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: center;
134
+ justify-content: center;
135
+ width: 240px;
136
+ height: 240px;
137
+ background: #fff;
138
+ border-radius: 10px;
139
+ border: 2px dashed #e6e6e6;
140
+ text-align: center;
141
+
142
+ div {
143
+ margin-top: 16px;
144
+ font-size: 14px;
145
+ color: #666;
146
+ }
147
+ }
148
+
149
+ .loading-container {
150
+ border-color: #409eff;
151
+ background: #f0f9ff;
152
+
153
+ div {
154
+ color: #409eff;
155
+ }
156
+
157
+ i {
158
+ animation: rotate 2s linear infinite;
159
+ }
160
+ }
161
+ }
162
+
163
+ @keyframes rotate {
164
+ from {
165
+ transform: rotate(0deg);
166
+ }
167
+ to {
168
+ transform: rotate(360deg);
169
+ }
170
+ }
171
+ </style>