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,85 @@
1
+ <template>
2
+ <div class="face-picture" v-loading="loading">
3
+ <img :src="src" width="100%" height="100%" />
4
+ <div class="delete-batn" @click="handleDelete">
5
+ <i class="el-icon-delete" style="size: 20px"></i>
6
+ </div>
7
+ </div>
8
+ </template>
9
+
10
+ <script>
11
+ import { deleteFaceApi } from "@/api/face";
12
+
13
+ export default {
14
+ props: {
15
+ row: Object,
16
+ },
17
+ data() {
18
+ return {
19
+ src: null,
20
+ loading: false,
21
+ };
22
+ },
23
+ mounted() {
24
+ this.loadFaceImage();
25
+ },
26
+ methods: {
27
+ handleDelete() {
28
+ // this.$emit("delete");
29
+ this.handleClearFaceInfo();
30
+ },
31
+ loadFaceImage() {
32
+ if (!this.row?.userId) {
33
+ console.warn("用户ID不存在,无法加载人脸信息");
34
+ return;
35
+ }
36
+ this.src = `/plugin/face/photo?userId=${this.row?.userId}`;
37
+ },
38
+ handleClearFaceInfo() {
39
+ this.$confirm("确定要清除人脸信息吗?", "提示", {
40
+ confirmButtonText: "确定",
41
+ cancelButtonText: "取消",
42
+ type: "warning",
43
+ })
44
+ .then(() => {
45
+ this.loading = true;
46
+ deleteFaceApi({ userId: this.row?.userId }).then(() => {
47
+ this.$emit("clear-face");
48
+ this.$message.success("人脸信息已清除");
49
+ }).finally(() => {
50
+ this.loading = false;
51
+ });
52
+ })
53
+ .catch(() => {});
54
+ },
55
+ },
56
+ };
57
+ </script>
58
+
59
+ <style lang="scss" scoped>
60
+ .face-picture {
61
+ position: relative;
62
+ width: 240px;
63
+ height: 240px;
64
+ background: #ffffff;
65
+ border: 1px dashed #bfbfbf;
66
+ border-radius: 10px;
67
+ }
68
+
69
+ .delete-batn {
70
+ position: absolute;
71
+ top: 0px;
72
+ right: 0px;
73
+ width: 24px;
74
+ background: #e5e5e7;
75
+ height: 24px;
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ cursor: pointer;
80
+ border-radius: 0 0 0 4px;
81
+ }
82
+ .delete-batn :hover {
83
+ background: #e5e5e7;
84
+ }
85
+ </style>
@@ -0,0 +1,336 @@
1
+ <template>
2
+ <div class="upload-picture">
3
+ <!-- 上传区域 -->
4
+ <el-upload
5
+ v-if="uploadedImages.length < maxImages"
6
+ class="avatar-uploader"
7
+ action=""
8
+ :auto-upload="false"
9
+ :show-file-list="false"
10
+ :on-change="handleFileSelect"
11
+ :before-upload="beforeAvatarUpload"
12
+ :multiple="false"
13
+ :disabled="uploading"
14
+ >
15
+ <div
16
+ class="upload-area flex-center align-center"
17
+ :class="{ uploading: uploading }"
18
+ >
19
+ <div class="upload-content">
20
+ <i
21
+ v-if="!uploading"
22
+ class="el-icon-plus"
23
+ style="font-size: 52px; color: #bfbfbf"
24
+ ></i>
25
+ <i
26
+ v-else
27
+ class="el-icon-loading"
28
+ style="font-size: 52px; color: #409eff"
29
+ ></i>
30
+ <div v-if="uploading" class="upload-text">正在上传中...</div>
31
+ </div>
32
+ </div>
33
+ </el-upload>
34
+
35
+ <!-- 达到最大上传数量提示 -->
36
+ <div v-else class="max-limit-tip flex-center align-center">
37
+ <div class="tip-content">
38
+ <i class="el-icon-warning" style="font-size: 32px; color: #f56c6c"></i>
39
+ <div class="tip-text">最多只能上传 {{ maxImages }} 张图片</div>
40
+ </div>
41
+ </div>
42
+ <div class="flex-center mt-12" style="width: 240px">
43
+ <el-button @click="h_back">返回</el-button>
44
+ </div>
45
+ </div>
46
+ </template>
47
+
48
+ <script>
49
+ import { addFaceApi } from "@/api/face";
50
+
51
+ export default {
52
+ name: "UploadPicture",
53
+ emits: ["images-change", "upload-success", "upload-error"],
54
+ props: {
55
+ row: Object,
56
+ },
57
+ data() {
58
+ return {
59
+ uploadedImages: [], // 存储已上传的图片
60
+ maxImages: 3, // 最大上传数量
61
+ uploading: false, // 上传状态
62
+ };
63
+ },
64
+ methods: {
65
+ h_back() {
66
+ this.$emit("back");
67
+ },
68
+ async handleFileSelect(file) {
69
+ // 当用户选择文件时触发
70
+ if (!file.raw) return;
71
+
72
+ // 验证文件
73
+ if (!this.validateFile(file.raw)) {
74
+ return;
75
+ }
76
+
77
+ const imageUrl = URL.createObjectURL(file.raw);
78
+
79
+ try {
80
+ // 调用后端接口上传图片
81
+ await this.uploadImageToServer(file.raw);
82
+
83
+ // 添加到已上传图片列表
84
+ this.uploadedImages.push({
85
+ url: imageUrl,
86
+ file: file.raw,
87
+ name: file.name,
88
+ size: file.size,
89
+ });
90
+
91
+ // 发射事件给父组件
92
+ this.$emit("images-change", this.uploadedImages);
93
+ this.$emit("upload-success", file.raw);
94
+
95
+ this.$message.success("图片上传成功!");
96
+ } catch (error) {
97
+ console.error("图片上传失败:", error);
98
+
99
+ // 释放内存中的URL
100
+ URL.revokeObjectURL(imageUrl);
101
+
102
+ // 发射错误事件
103
+ this.$emit("upload-error", error);
104
+
105
+ this.$message.error("图片上传失败,请重试!");
106
+ }
107
+ },
108
+
109
+ // 验证文件的方法
110
+ validateFile(file) {
111
+ // 检查是否正在上传
112
+ if (this.uploading) {
113
+ this.$message.warning("正在上传中,请稍候...");
114
+ return false;
115
+ }
116
+
117
+ // 检查文件格式
118
+ const isValidFormat =
119
+ file.type === "image/jpeg" ||
120
+ file.type === "image/jpg" ||
121
+ file.type === "image/png";
122
+
123
+ // 检查文件大小 (2MB)
124
+ const isLt2M = file.size / 1024 / 1024 < 2;
125
+
126
+ // 检查数量限制
127
+ const isUnderLimit = this.uploadedImages.length < this.maxImages;
128
+
129
+ // 验证用户信息
130
+ if (!this.row?.userId || !this.row?.row?.name) {
131
+ this.$message.error("用户信息不完整,无法上传图片!");
132
+ return false;
133
+ }
134
+
135
+ if (!isValidFormat) {
136
+ this.$message.error("上传图片只能是 JPG 或 PNG 格式!");
137
+ return false;
138
+ }
139
+
140
+ if (!isLt2M) {
141
+ this.$message.error("上传图片大小不能超过 2MB!");
142
+ return false;
143
+ }
144
+
145
+ if (!isUnderLimit) {
146
+ this.$message.error(`最多只能上传 ${this.maxImages} 张图片!`);
147
+ return false;
148
+ }
149
+
150
+ return true;
151
+ },
152
+
153
+ // 上传图片到服务器
154
+ async uploadImageToServer(file) {
155
+ this.uploading = true;
156
+
157
+ try {
158
+ // 创建FormData对象
159
+ const formData = new FormData();
160
+ formData.append("file", file);
161
+ formData.append("userId", this.row?.userId);
162
+ formData.append("userName", this.row?.row?.name);
163
+
164
+ // 调用API接口
165
+ const response = await addFaceApi(formData);
166
+ // this.$emit("upload-success", response);
167
+ return response;
168
+ } catch (error) {
169
+ console.error("服务器上传失败:", error);
170
+ throw error;
171
+ } finally {
172
+ this.uploading = false;
173
+ }
174
+ },
175
+
176
+ beforeAvatarUpload() {
177
+ // 阻止自动上传,我们在 handleFileSelect 中手动处理文件选择和上传
178
+ return false;
179
+ },
180
+
181
+ removeImage(index) {
182
+ // 释放内存中的 URL
183
+ URL.revokeObjectURL(this.uploadedImages[index].url);
184
+
185
+ // 从列表中移除
186
+ this.uploadedImages.splice(index, 1);
187
+
188
+ // 发射事件给父组件
189
+ this.$emit("images-change", this.uploadedImages);
190
+
191
+ this.$message.success("图片删除成功!");
192
+ },
193
+
194
+ // 获取已上传的图片列表 (供父组件调用)
195
+ getUploadedImages() {
196
+ return this.uploadedImages;
197
+ },
198
+
199
+ // 清空所有图片 (供父组件调用)
200
+ clearAllImages() {
201
+ // 释放所有 URL
202
+ this.uploadedImages.forEach((image) => {
203
+ URL.revokeObjectURL(image.url);
204
+ });
205
+
206
+ this.uploadedImages = [];
207
+ this.$emit("images-change", this.uploadedImages);
208
+ },
209
+ },
210
+
211
+ beforeUnmount() {
212
+ // 组件销毁时释放所有 URL
213
+ this.uploadedImages.forEach((image) => {
214
+ URL.revokeObjectURL(image.url);
215
+ });
216
+ },
217
+ };
218
+ </script>
219
+
220
+ <style lang="scss" scoped>
221
+ .upload-picture {
222
+ width: 100%;
223
+ max-width: 600px;
224
+ min-height: 240px;
225
+
226
+ .uploaded-images {
227
+ display: flex;
228
+ flex-wrap: wrap;
229
+ gap: 16px;
230
+ margin-bottom: 16px;
231
+
232
+ .image-item {
233
+ position: relative;
234
+ width: 180px;
235
+ height: 180px;
236
+ border-radius: 10px;
237
+ overflow: hidden;
238
+ border: 1px solid #e6e6e6;
239
+
240
+ img {
241
+ width: 100%;
242
+ height: 100%;
243
+ object-fit: cover;
244
+ }
245
+
246
+ .image-actions {
247
+ position: absolute;
248
+ top: 8px;
249
+ right: 8px;
250
+ background: rgba(0, 0, 0, 0.6);
251
+ border-radius: 50%;
252
+ width: 28px;
253
+ height: 28px;
254
+ display: flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ cursor: pointer;
258
+ transition: background-color 0.3s;
259
+
260
+ &:hover {
261
+ background: rgba(245, 108, 108, 0.8);
262
+ }
263
+
264
+ i {
265
+ color: white;
266
+ font-size: 14px;
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ .avatar-uploader {
273
+ .upload-area {
274
+ width: 240px;
275
+ height: 240px;
276
+ background: #ffffff;
277
+ border: 1px dashed #bfbfbf;
278
+ border-radius: 10px;
279
+ cursor: pointer;
280
+ transition: all 0.3s;
281
+
282
+ &:hover:not(.uploading) {
283
+ border-color: #409eff;
284
+ }
285
+
286
+ &.uploading {
287
+ border-color: #409eff;
288
+ background: #f0f9ff;
289
+ cursor: not-allowed;
290
+ }
291
+
292
+ .upload-content {
293
+ text-align: center;
294
+
295
+ .upload-text {
296
+ margin-top: 12px;
297
+ font-size: 14px;
298
+ color: #409eff;
299
+ font-weight: 500;
300
+ }
301
+ }
302
+ }
303
+ }
304
+
305
+ .max-limit-tip {
306
+ width: 240px;
307
+ height: 240px;
308
+ background: #f5f5f5;
309
+ border: 1px dashed #e6e6e6;
310
+ border-radius: 10px;
311
+
312
+ .tip-content {
313
+ text-align: center;
314
+
315
+ .tip-text {
316
+ margin-top: 12px;
317
+ font-size: 14px;
318
+ color: #f56c6c;
319
+ }
320
+ }
321
+ }
322
+ }
323
+
324
+ // Element UI 覆盖样式
325
+ ::v-deep .el-upload {
326
+ display: block;
327
+ }
328
+
329
+ ::v-deep .el-upload-dragger {
330
+ width: 100%;
331
+ height: 100%;
332
+ border: none;
333
+ border-radius: 10px;
334
+ background: transparent;
335
+ }
336
+ </style>
@@ -0,0 +1,242 @@
1
+ <template>
2
+ <!-- 正常功能区域 -->
3
+ <div class="flex-start">
4
+ <FacePicture
5
+ v-if="chooseType === 'face-info'"
6
+ class="mr-12"
7
+ :row="row"
8
+ @clear-face="handleClearFace"
9
+ />
10
+ <ChooseCameraOrPicture
11
+ v-if="!uploading && chooseType === ''"
12
+ @choose="handleChoose"
13
+ />
14
+ <UploadPicture
15
+ v-if="chooseType === 'picture'"
16
+ :row="row"
17
+ @back="handleBack"
18
+ @upload-success="handleUploadSuccess"
19
+ />
20
+ <FaceDetected
21
+ v-if="chooseType === 'camera' && !uploading"
22
+ @back="handleBack"
23
+ @detection-complete="handleDetectionComplete"
24
+ />
25
+
26
+ <!-- 上传中状态 -->
27
+ <div v-if="uploading" class="uploading-container">
28
+ <div class="uploading-content">
29
+ <i class="el-icon-loading" style="font-size: 48px; color: #409eff"></i>
30
+ <div class="uploading-text">正在上传人脸信息...</div>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </template>
35
+
36
+ <script>
37
+ import { existFaceApi, addFaceApi } from "@/api/face";
38
+ import ChooseCameraOrPicture from "./components/ChooseCameraOrPicture.vue";
39
+ import UploadPicture from "./components/UploadPicture.vue";
40
+ import FaceDetected from "./components/FaceDetected.vue";
41
+ import FacePicture from "./components/FacePicture.vue";
42
+
43
+ export default {
44
+ name: "FaceRegister",
45
+ props: {
46
+ row: Object,
47
+ },
48
+ components: {
49
+ ChooseCameraOrPicture,
50
+ UploadPicture,
51
+ FaceDetected,
52
+ FacePicture,
53
+ },
54
+ data() {
55
+ const userId = this.row?.userId;
56
+ return {
57
+ userId,
58
+ chooseType: "loading",
59
+ uploading: false, // 上传状态
60
+ userIdMissing: false, // 用户ID缺失状态
61
+ };
62
+ },
63
+ created() {
64
+ this.checkFaceInfo();
65
+ },
66
+ methods: {
67
+ checkFaceInfo() {
68
+ existFaceApi({ userId: this.userId }).then((res) => {
69
+ this.chooseType = res.data ? "face-info" : "";
70
+ });
71
+ },
72
+ handleBack() {
73
+ this.chooseType = "";
74
+ },
75
+ handleClose() {
76
+ // this.$router.back();
77
+ },
78
+ handleChoose(type) {
79
+ if (type === "camera") {
80
+ this.chooseType = "camera";
81
+ } else {
82
+ this.chooseType = "picture";
83
+ }
84
+ },
85
+ handleClearFace() {
86
+ this.checkFaceInfo();
87
+ },
88
+
89
+ handleRefresh() {
90
+ // 刷新页面
91
+ window.location.reload();
92
+ },
93
+ handleUploadSuccess() {
94
+ this.checkFaceInfo();
95
+ },
96
+ async handleDetectionComplete(data) {
97
+
98
+ // 检查用户ID是否存在
99
+ if (!this.userId) {
100
+ this.$message.error("用户信息不完整,无法进行人脸注册");
101
+ return;
102
+ }
103
+
104
+ // 检查是否有捕获的图片
105
+ if (!data.capturedImages || data.capturedImages.length === 0) {
106
+ this.$message.error("未检测到人脸图片");
107
+ return;
108
+ }
109
+
110
+ try {
111
+ this.uploading = true;
112
+ this.chooseType = "";
113
+
114
+ // 获取第一张捕获的图片
115
+ const capturedImage = data.capturedImages[0];
116
+ const base64Data = capturedImage.dataUrl;
117
+
118
+ // 将 base64 转换为 File 对象
119
+ const file = this.base64ToFile(base64Data, "face-detected.jpg");
120
+ // 创建 FormData
121
+ const formData = new FormData();
122
+ formData.append("file", file);
123
+ formData.append("userId", this.userId);
124
+ formData.append("userName", this.row?.row?.name || "");
125
+
126
+ // 调用上传接口
127
+ await addFaceApi(formData);
128
+
129
+ this.$message.success("人脸注册成功!");
130
+ this.checkFaceInfo();
131
+ } catch (error) {
132
+ console.error("人脸上传失败:", error);
133
+ this.chooseType = "";
134
+ } finally {
135
+ this.uploading = false;
136
+ }
137
+ },
138
+
139
+ // 将 base64 转换为 File 对象
140
+ base64ToFile(base64String, fileName) {
141
+ // 提取 base64 数据部分
142
+ const arr = base64String.split(",");
143
+ const mime = arr[0].match(/:(.*?);/)[1];
144
+ const bstr = atob(arr[1]);
145
+ let n = bstr.length;
146
+ const u8arr = new Uint8Array(n);
147
+
148
+ while (n--) {
149
+ u8arr[n] = bstr.charCodeAt(n);
150
+ }
151
+
152
+ return new File([u8arr], fileName, { type: mime });
153
+ },
154
+ },
155
+ };
156
+ </script>
157
+
158
+ <style lang="scss" scoped>
159
+ .dialog-header {
160
+ display: flex;
161
+ justify-content: space-between;
162
+ align-items: center;
163
+ padding: 16px 20px;
164
+ border-bottom: 1px solid #eee;
165
+ }
166
+ .title {
167
+ font-size: 18px;
168
+ font-weight: 500;
169
+ color: #333;
170
+ }
171
+ .close-icon {
172
+ width: 16px;
173
+ height: 16px;
174
+ cursor: pointer;
175
+ }
176
+ .dialog-content {
177
+ padding: 20px;
178
+ }
179
+
180
+ .uploading-container {
181
+ display: flex;
182
+ justify-content: center;
183
+ align-items: center;
184
+ width: 360px;
185
+ height: 360px;
186
+ background: #f0f9ff;
187
+ border-radius: 12px;
188
+ border: 2px dashed #409eff;
189
+ }
190
+
191
+ .uploading-content {
192
+ text-align: center;
193
+
194
+ .uploading-text {
195
+ margin-top: 16px;
196
+ font-size: 16px;
197
+ color: #409eff;
198
+ font-weight: 500;
199
+ }
200
+
201
+ i {
202
+ animation: rotate 2s linear infinite;
203
+ }
204
+ }
205
+
206
+ @keyframes rotate {
207
+ from {
208
+ transform: rotate(0deg);
209
+ }
210
+ to {
211
+ transform: rotate(360deg);
212
+ }
213
+ }
214
+
215
+ .error-container {
216
+ display: flex;
217
+ justify-content: center;
218
+ align-items: center;
219
+ width: 360px;
220
+ height: 360px;
221
+ background: #fef0f0;
222
+ border-radius: 12px;
223
+ border: 2px dashed #f56c6c;
224
+ }
225
+
226
+ .error-content {
227
+ text-align: center;
228
+
229
+ .error-text {
230
+ margin-top: 16px;
231
+ font-size: 18px;
232
+ color: #f56c6c;
233
+ font-weight: 600;
234
+ }
235
+
236
+ .error-desc {
237
+ margin-top: 8px;
238
+ font-size: 14px;
239
+ color: #999;
240
+ }
241
+ }
242
+ </style>