gridsum-vue3-pc 1.0.0

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 (57) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSE +21 -0
  3. package/README.md +88 -0
  4. package/bin/create-vue3-pc.mjs +545 -0
  5. package/package.json +68 -0
  6. package/template/base/.dockerignore +12 -0
  7. package/template/base/.env +5 -0
  8. package/template/base/.env.production +5 -0
  9. package/template/base/.eslintrc.cjs +22 -0
  10. package/template/base/.husky/commit-msg +1 -0
  11. package/template/base/.husky/pre-commit +1 -0
  12. package/template/base/.lintstagedrc +7 -0
  13. package/template/base/.prettierrc +5 -0
  14. package/template/base/.stylelintrc.cjs +6 -0
  15. package/template/base/.vscode/settings.json +26 -0
  16. package/template/base/CHANGELOG.md +6 -0
  17. package/template/base/Dockerfile +19 -0
  18. package/template/base/README.md +87 -0
  19. package/template/base/commitlint.config.cjs +1 -0
  20. package/template/base/index.html +15 -0
  21. package/template/base/mock/user.js +393 -0
  22. package/template/base/nginx.conf +27 -0
  23. package/template/base/package.json +47 -0
  24. package/template/base/public/favicon.svg +9 -0
  25. package/template/base/public/logo.svg +9 -0
  26. package/template/base/src/App.vue +20 -0
  27. package/template/base/src/assets/index.css +83 -0
  28. package/template/base/src/assets/logo.png +0 -0
  29. package/template/base/src/components/LanguageSwitch.vue +65 -0
  30. package/template/base/src/components/basic-layout.vue +484 -0
  31. package/template/base/src/composables/useCrud.ts +172 -0
  32. package/template/base/src/env.d.ts +28 -0
  33. package/template/base/src/env.ts +24 -0
  34. package/template/base/src/locales/en.json +153 -0
  35. package/template/base/src/locales/index.ts +32 -0
  36. package/template/base/src/locales/zh.json +153 -0
  37. package/template/base/src/main.ts +27 -0
  38. package/template/base/src/router/index.ts +91 -0
  39. package/template/base/src/services/http.ts +64 -0
  40. package/template/base/src/services/user.ts +23 -0
  41. package/template/base/src/store/modules/user.ts +45 -0
  42. package/template/base/src/views/Admin.vue +326 -0
  43. package/template/base/src/views/Home.vue +382 -0
  44. package/template/base/src/views/Login.vue +1252 -0
  45. package/template/base/src/views/Role.vue +269 -0
  46. package/template/base/src/views/User.vue +332 -0
  47. package/template/base/src/views/error/Forbidden.vue +62 -0
  48. package/template/base/src/views/error/NotFound.vue +60 -0
  49. package/template/base/src/views/error/ServerError.vue +62 -0
  50. package/template/base/tests/e2e/example.spec.ts +7 -0
  51. package/template/base/tests/unit/user.test.ts +15 -0
  52. package/template/base/vite.config.ts +52 -0
  53. package/template/cicd-github/.github/workflows/ci.yml +123 -0
  54. package/template/cicd-gitlab/.gitlab-ci.yml +103 -0
  55. package/template/cicd-jenkins/Jenkinsfile +107 -0
  56. package/template/ts/shims-vue.d.ts +5 -0
  57. package/template/ts/tsconfig.json +23 -0
@@ -0,0 +1,1252 @@
1
+ <template>
2
+ <div class="login-page">
3
+ <div class="login-card">
4
+ <div class="left-panel">
5
+ <div class="logo">
6
+ <img src="/logo.svg" width="28" height="28" alt="logo" />
7
+ <span>{{ title }}</span>
8
+ </div>
9
+ <div class="characters-wrapper">
10
+ <div class="characters-scene" id="characters-scene">
11
+ <div class="character char-purple" id="char-purple" ref="purpleRef">
12
+ <div class="eyes" id="purple-eyes" style="left:45px;top:40px;gap:28px">
13
+ <div class="eyeball" id="purple-eye-l" style="width:18px;height:18px">
14
+ <div class="pupil" id="purple-pupil-l" style="width:7px;height:7px"></div>
15
+ </div>
16
+ <div class="eyeball" id="purple-eye-r" style="width:18px;height:18px">
17
+ <div class="pupil" id="purple-pupil-r" style="width:7px;height:7px"></div>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ <div class="character char-black" id="char-black" ref="blackRef">
22
+ <div class="eyes" id="black-eyes" style="left:26px;top:32px;gap:20px">
23
+ <div class="eyeball" id="black-eye-l" style="width:16px;height:16px">
24
+ <div class="pupil" id="black-pupil-l" style="width:6px;height:6px"></div>
25
+ </div>
26
+ <div class="eyeball" id="black-eye-r" style="width:16px;height:16px">
27
+ <div class="pupil" id="black-pupil-r" style="width:6px;height:6px"></div>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ <div class="character char-orange" id="char-orange" ref="orangeRef">
32
+ <div class="eyes" id="orange-eyes" style="left:82px;top:90px;gap:28px">
33
+ <div class="bare-pupil" id="orange-pupil-l"></div>
34
+ <div class="bare-pupil" id="orange-pupil-r"></div>
35
+ </div>
36
+ <div class="orange-mouth" id="orange-mouth" style="left:90px;top:120px"></div>
37
+ </div>
38
+ <div class="character char-yellow" id="char-yellow" ref="yellowRef">
39
+ <div class="eyes" id="yellow-eyes" style="left:52px;top:40px;gap:20px">
40
+ <div class="bare-pupil" id="yellow-pupil-l"></div>
41
+ <div class="bare-pupil" id="yellow-pupil-r"></div>
42
+ </div>
43
+ <div class="yellow-mouth" id="yellow-mouth" style="left:40px;top:88px"></div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ <div class="footer-links">
48
+ <a href="#">Privacy Policy</a>
49
+ <a href="#">Terms of Service</a>
50
+ <a href="#">Contact</a>
51
+ </div>
52
+ </div>
53
+ <div class="right-panel">
54
+ <div class="toolbar">
55
+ <LanguageSwitch />
56
+ <el-tooltip :content="isDark ? $t('layout.lightMode') : $t('layout.darkMode')" placement="bottom">
57
+ <el-button text class="toolbar-btn" @click="toggleDark">
58
+ <el-icon><Moon v-if="!isDark" /><Sunny v-else /></el-icon>
59
+ </el-button>
60
+ </el-tooltip>
61
+ </div>
62
+ <div class="form-container" :class="{ shake: shaking }">
63
+ <div class="sparkle-icon">
64
+ <svg viewBox="0 0 24 24" fill="currentColor">
65
+ <path d="M12 2L13.5 9H10.5L12 2Z" />
66
+ <path d="M12 22L10.5 15H13.5L12 22Z" />
67
+ <path d="M2 12L9 10.5V13.5L2 12Z" />
68
+ <path d="M22 12L15 13.5V10.5L22 12Z" />
69
+ </svg>
70
+ </div>
71
+ <div class="form-header">
72
+ <h1>{{ title }}</h1>
73
+ <p>{{ $t('user.welcome') }}</p>
74
+ </div>
75
+ <el-form
76
+ ref="formRef"
77
+ :model="form"
78
+ :rules="rules"
79
+ @submit.prevent="handleLogin"
80
+ class="login-form"
81
+ >
82
+ <el-form-item prop="username">
83
+ <el-input
84
+ v-model="form.username"
85
+ :placeholder="$t('user.usernamePlaceholder')"
86
+ size="large"
87
+ :prefix-icon="User"
88
+ :disabled="cooldown > 0"
89
+ autocomplete="username"
90
+ clearable
91
+ id="email-input"
92
+ @focus="onEmailFocus"
93
+ @blur="onEmailBlur"
94
+ @input="updateCharacters"
95
+ />
96
+ </el-form-item>
97
+ <el-form-item prop="password">
98
+ <el-input
99
+ v-model="form.password"
100
+ :type="showPassword ? 'text' : 'password'"
101
+ :placeholder="$t('user.passwordPlaceholder')"
102
+ size="large"
103
+ :prefix-icon="Lock"
104
+ :disabled="cooldown > 0"
105
+ autocomplete="current-password"
106
+ id="password-input"
107
+ @focus="onPasswordFocus"
108
+ @blur="onPasswordBlur"
109
+ @input="updateCharacters"
110
+ >
111
+ <template #suffix>
112
+ <span class="toggle-password" @click="togglePassword" id="toggle-password">
113
+ <svg
114
+ v-if="!showPassword"
115
+ width="20" height="20" viewBox="0 0 24 24"
116
+ fill="none" stroke="currentColor" stroke-width="2"
117
+ stroke-linecap="round" stroke-linejoin="round"
118
+ >
119
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
120
+ <circle cx="12" cy="12" r="3" />
121
+ </svg>
122
+ <svg
123
+ v-else
124
+ width="20" height="20" viewBox="0 0 24 24"
125
+ fill="none" stroke="currentColor" stroke-width="2"
126
+ stroke-linecap="round" stroke-linejoin="round"
127
+ >
128
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24" />
129
+ <line x1="1" y1="1" x2="23" y2="23" />
130
+ </svg>
131
+ </span>
132
+ </template>
133
+ </el-input>
134
+ </el-form-item>
135
+ <el-form-item prop="captcha" v-if="showCaptcha">
136
+ <div class="captcha-row">
137
+ <el-input
138
+ v-model="form.captcha"
139
+ :placeholder="$t('user.captchaPlaceholder')"
140
+ size="large"
141
+ style="flex:1"
142
+ />
143
+ <div class="captcha-box" @click="generateCaptcha" v-html="captchaSvg" />
144
+ </div>
145
+ </el-form-item>
146
+ <el-form-item>
147
+ <el-checkbox v-model="rememberMe">{{ $t('user.rememberMe') }}</el-checkbox>
148
+ </el-form-item>
149
+ <el-form-item>
150
+ <el-button
151
+ type="primary"
152
+ size="large"
153
+ :loading="loading"
154
+ :disabled="cooldown > 0"
155
+ native-type="submit"
156
+ class="login-btn"
157
+ >
158
+ {{ cooldown > 0 ? `${$t('user.retryAfter')} ${cooldown}s` : loading ? $t('user.loggingIn') : $t('user.loginBtn') }}
159
+ </el-button>
160
+ </el-form-item>
161
+ </el-form>
162
+ <div class="form-actions">
163
+ <span class="demo-hint">{{ $t('user.demoHint') }}</span>
164
+ <el-button text size="small" @click="resetForm" class="reset-btn">
165
+ {{ $t('user.resetForm') }}
166
+ </el-button>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ </div>
172
+ </template>
173
+
174
+ <script setup lang="ts">
175
+ import { reactive, ref, onMounted, onUnmounted } from 'vue';
176
+ import { useRouter } from 'vue-router';
177
+ import { useI18n } from 'vue-i18n';
178
+ import { ElMessage } from 'element-plus';
179
+ import { User, Lock, Moon, Sunny } from '@element-plus/icons-vue';
180
+ import { useUserStore } from '@/store/modules/user';
181
+ import { login } from '@/services/user';
182
+ import LanguageSwitch from '@/components/LanguageSwitch.vue';
183
+ import type { FormInstance, FormRules } from 'element-plus';
184
+
185
+ const router = useRouter();
186
+ const userStore = useUserStore();
187
+ const { t } = useI18n();
188
+ const title = import.meta.env.VITE_APP_TITLE || 'Vue3 PC Template';
189
+ const REMEMBER_KEY = 'remembered_user';
190
+
191
+ const formRef = ref<FormInstance>();
192
+
193
+ const form = reactive({
194
+ username: '',
195
+ password: '',
196
+ captcha: '',
197
+ });
198
+
199
+ const rules: FormRules = {
200
+ username: [
201
+ { required: true, message: () => t('user.requiredUsername'), trigger: 'blur' },
202
+ ],
203
+ password: [
204
+ { required: true, message: () => t('user.requiredPassword'), trigger: 'blur' },
205
+ { min: 3, message: () => t('user.passwordTooShort'), trigger: 'blur' },
206
+ ],
207
+ captcha: [
208
+ { required: true, message: () => t('user.requiredCaptcha'), trigger: 'blur' },
209
+ ],
210
+ };
211
+
212
+ const loading = ref(false);
213
+ const rememberMe = ref(false);
214
+ const showCaptcha = ref(false);
215
+ const captchaText = ref('');
216
+ const captchaSvg = ref('');
217
+ const shaking = ref(false);
218
+ const cooldown = ref(0);
219
+ const showPassword = ref(false);
220
+ const isDark = ref(false);
221
+ let failCount = 0;
222
+
223
+ const toggleDark = () => {
224
+ isDark.value = !isDark.value;
225
+ document.documentElement.classList.toggle('dark', isDark.value);
226
+ localStorage.setItem('theme', isDark.value ? 'dark' : 'light');
227
+ };
228
+
229
+ // Character animation state
230
+ const purpleRef = ref<HTMLElement>();
231
+ const blackRef = ref<HTMLElement>();
232
+ const orangeRef = ref<HTMLElement>();
233
+ const yellowRef = ref<HTMLElement>();
234
+
235
+ let mouseX = 0;
236
+ let mouseY = 0;
237
+ let isTyping = false;
238
+ let isLookingAtEachOther = false;
239
+ let isPurpleBlinking = false;
240
+ let isBlackBlinking = false;
241
+ let isPurplePeeking = false;
242
+ let isPasswordFocused = false;
243
+ let isLoginError = false;
244
+ let typingTimer: ReturnType<typeof setTimeout> | null = null;
245
+ let errorRecoverTimer: ReturnType<typeof setTimeout> | null = null;
246
+ let peekTimer: ReturnType<typeof setTimeout> | null = null;
247
+
248
+ const generateCaptcha = () => {
249
+ const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
250
+ const text = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
251
+ captchaText.value = text;
252
+
253
+ const colors = ['#667eea', '#764ba2', '#e74c3c', '#2ecc71', '#f39c12'];
254
+ const noise = Array.from({ length: 3 }, () => {
255
+ const p = () => Math.random();
256
+ return `<line x1="${p() * 120}" y1="${p() * 40}" x2="${p() * 120}" y2="${p() * 40}" stroke="${colors[Math.floor(Math.random() * colors.length)]}" stroke-width="1" opacity="0.4"/>`;
257
+ }).join('');
258
+ const dots = Array.from({ length: 20 }, () =>
259
+ `<circle cx="${Math.random() * 120}" cy="${Math.random() * 40}" r="1.5" fill="${colors[Math.floor(Math.random() * colors.length)]}" opacity="0.5"/>`
260
+ ).join('');
261
+ const letters = text.split('').map((c, i) => `<tspan fill="${colors[i % colors.length]}">${c}</tspan>`).join('');
262
+
263
+ const isDarkMode = document.documentElement.classList.contains('dark');
264
+ const bgFill = isDarkMode ? '#1a1a2e' : '#f0f2f5';
265
+ const textFill = isDarkMode ? '#e8e8f0' : '#333';
266
+ captchaSvg.value = `<svg xmlns="http://www.w3.org/2000/svg" width="120" height="40" viewBox="0 0 120 40">
267
+ <rect width="120" height="40" fill="${bgFill}" rx="4"/>${noise}${dots}
268
+ <text x="60" y="28" text-anchor="middle" font-size="20" font-weight="bold" font-family="Arial" fill="${textFill}" letter-spacing="4">${letters}</text>
269
+ </svg>`;
270
+ };
271
+
272
+ const startCooldown = () => {
273
+ cooldown.value = 15;
274
+ const timer = setInterval(() => {
275
+ cooldown.value--;
276
+ if (cooldown.value <= 0) {
277
+ clearInterval(timer);
278
+ }
279
+ }, 1000);
280
+ };
281
+
282
+ const triggerShake = () => {
283
+ shaking.value = true;
284
+ setTimeout(() => { shaking.value = false; }, 500);
285
+ };
286
+
287
+ const handleLogin = async () => {
288
+ const valid = await formRef.value?.validate().catch(() => false);
289
+ if (!valid) return;
290
+
291
+ if (showCaptcha.value && form.captcha.toUpperCase() !== captchaText.value) {
292
+ ElMessage.error(t('user.captchaError'));
293
+ generateCaptcha();
294
+ form.captcha = '';
295
+ return;
296
+ }
297
+
298
+ loading.value = true;
299
+ try {
300
+ const res = await login(form.username, form.password);
301
+ userStore.setUserInfo(res.data);
302
+ if (rememberMe.value) {
303
+ localStorage.setItem(REMEMBER_KEY, JSON.stringify({ username: form.username }));
304
+ } else {
305
+ localStorage.removeItem(REMEMBER_KEY);
306
+ }
307
+ ElMessage.success(t('user.loginSuccess'));
308
+ router.push('/');
309
+ } catch (err: any) {
310
+ triggerLoginError();
311
+ triggerShake();
312
+ failCount++;
313
+ if (failCount >= 3) {
314
+ showCaptcha.value = true;
315
+ generateCaptcha();
316
+ }
317
+ if (failCount >= 5) {
318
+ startCooldown();
319
+ }
320
+ ElMessage.error(err?.response?.data?.message || err?.message || t('user.loginFail'));
321
+ } finally {
322
+ loading.value = false;
323
+ }
324
+ };
325
+
326
+ const resetForm = () => {
327
+ formRef.value?.resetFields();
328
+ form.captcha = '';
329
+ failCount = 0;
330
+ showCaptcha.value = false;
331
+ cooldown.value = 0;
332
+ };
333
+
334
+ // ============= CHARACTER ANIMATION =============
335
+
336
+ const $ = (id: string) => document.getElementById(id) as HTMLElement;
337
+
338
+ const calcPosition = (el: HTMLElement) => {
339
+ const rect = el.getBoundingClientRect();
340
+ const cx = rect.left + rect.width / 2;
341
+ const cy = rect.top + rect.height / 3;
342
+ const dx = mouseX - cx;
343
+ const dy = mouseY - cy;
344
+ const faceX = Math.max(-15, Math.min(15, dx / 20));
345
+ const faceY = Math.max(-10, Math.min(10, dy / 30));
346
+ const bodySkew = Math.max(-6, Math.min(6, -dx / 120));
347
+ return { faceX, faceY, bodySkew };
348
+ };
349
+
350
+ const calcPupilOffset = (el: HTMLElement, maxDist: number) => {
351
+ const rect = el.getBoundingClientRect();
352
+ const cx = rect.left + rect.width / 2;
353
+ const cy = rect.top + rect.height / 2;
354
+ const dx = mouseX - cx;
355
+ const dy = mouseY - cy;
356
+ const dist = Math.min(Math.sqrt(dx * dx + dy * dy), maxDist);
357
+ const angle = Math.atan2(dy, dx);
358
+ return { x: Math.cos(angle) * dist, y: Math.sin(angle) * dist };
359
+ };
360
+
361
+ const updateCharacters = () => {
362
+ const purple = $('char-purple');
363
+ const black = $('char-black');
364
+ const orange = $('char-orange');
365
+ const yellow = $('char-yellow');
366
+
367
+ const purplePos = calcPosition(purple);
368
+ const blackPos = calcPosition(black);
369
+ const orangePos = calcPosition(orange);
370
+ const yellowPos = calcPosition(yellow);
371
+
372
+ const pwdLen = form.password.length;
373
+ const isShowingPwd = pwdLen > 0 && showPassword.value;
374
+ const isLookingAway = isPasswordFocused && !showPassword.value;
375
+
376
+ // ---- Purple body ----
377
+ if (isShowingPwd) {
378
+ purple.style.transform = 'skewX(0deg)';
379
+ purple.style.height = '370px';
380
+ } else if (isLookingAway) {
381
+ purple.style.transform = 'skewX(-14deg) translateX(-20px)';
382
+ purple.style.height = '410px';
383
+ } else if (isTyping) {
384
+ purple.style.transform = `skewX(${(purplePos.bodySkew || 0) - 12}deg) translateX(40px)`;
385
+ purple.style.height = '410px';
386
+ } else {
387
+ purple.style.transform = `skewX(${purplePos.bodySkew}deg)`;
388
+ purple.style.height = '370px';
389
+ }
390
+
391
+ // Purple eyes
392
+ const purpleEyes = $('purple-eyes');
393
+ const purpleEyeL = $('purple-eye-l');
394
+ const purpleEyeR = $('purple-eye-r');
395
+ const purplePupilL = $('purple-pupil-l');
396
+ const purplePupilR = $('purple-pupil-r');
397
+
398
+ purpleEyeL.style.height = isPurpleBlinking ? '2px' : '18px';
399
+ purpleEyeR.style.height = isPurpleBlinking ? '2px' : '18px';
400
+
401
+ if (isLoginError) {
402
+ purpleEyes.style.left = '30px';
403
+ purpleEyes.style.top = '55px';
404
+ purplePupilL.style.transform = 'translate(-3px, 4px)';
405
+ purplePupilR.style.transform = 'translate(-3px, 4px)';
406
+ } else if (isLookingAway) {
407
+ purpleEyes.style.left = '20px';
408
+ purpleEyes.style.top = '25px';
409
+ purplePupilL.style.transform = 'translate(-5px, -5px)';
410
+ purplePupilR.style.transform = 'translate(-5px, -5px)';
411
+ } else if (isShowingPwd) {
412
+ purpleEyes.style.left = '20px';
413
+ purpleEyes.style.top = '35px';
414
+ const px = isPurplePeeking ? 4 : -4;
415
+ const py = isPurplePeeking ? 5 : -4;
416
+ purplePupilL.style.transform = `translate(${px}px, ${py}px)`;
417
+ purplePupilR.style.transform = `translate(${px}px, ${py}px)`;
418
+ } else if (isLookingAtEachOther) {
419
+ purpleEyes.style.left = '55px';
420
+ purpleEyes.style.top = '65px';
421
+ purplePupilL.style.transform = 'translate(3px, 4px)';
422
+ purplePupilR.style.transform = 'translate(3px, 4px)';
423
+ } else {
424
+ purpleEyes.style.left = 45 + purplePos.faceX + 'px';
425
+ purpleEyes.style.top = 40 + purplePos.faceY + 'px';
426
+ const po = calcPupilOffset(purpleEyeL, 5);
427
+ purplePupilL.style.transform = `translate(${po.x}px, ${po.y}px)`;
428
+ purplePupilR.style.transform = `translate(${po.x}px, ${po.y}px)`;
429
+ }
430
+
431
+ // ---- Black body ----
432
+ if (isShowingPwd) {
433
+ black.style.transform = 'skewX(0deg)';
434
+ } else if (isLookingAway) {
435
+ black.style.transform = 'skewX(12deg) translateX(-10px)';
436
+ } else if (isLookingAtEachOther) {
437
+ black.style.transform = `skewX(${(blackPos.bodySkew || 0) * 1.5 + 10}deg) translateX(20px)`;
438
+ } else if (isTyping) {
439
+ black.style.transform = `skewX(${(blackPos.bodySkew || 0) * 1.5}deg)`;
440
+ } else {
441
+ black.style.transform = `skewX(${blackPos.bodySkew}deg)`;
442
+ }
443
+
444
+ // Black eyes
445
+ const blackEyes = $('black-eyes');
446
+ const blackEyeL = $('black-eye-l');
447
+ const blackEyeR = $('black-eye-r');
448
+ const blackPupilL = $('black-pupil-l');
449
+ const blackPupilR = $('black-pupil-r');
450
+
451
+ blackEyeL.style.height = isBlackBlinking ? '2px' : '16px';
452
+ blackEyeR.style.height = isBlackBlinking ? '2px' : '16px';
453
+
454
+ if (isLoginError) {
455
+ blackEyes.style.left = '15px';
456
+ blackEyes.style.top = '40px';
457
+ blackPupilL.style.transform = 'translate(-3px, 4px)';
458
+ blackPupilR.style.transform = 'translate(-3px, 4px)';
459
+ } else if (isLookingAway) {
460
+ blackEyes.style.left = '10px';
461
+ blackEyes.style.top = '20px';
462
+ blackPupilL.style.transform = 'translate(-4px, -5px)';
463
+ blackPupilR.style.transform = 'translate(-4px, -5px)';
464
+ } else if (isShowingPwd) {
465
+ blackEyes.style.left = '10px';
466
+ blackEyes.style.top = '28px';
467
+ blackPupilL.style.transform = 'translate(-4px, -4px)';
468
+ blackPupilR.style.transform = 'translate(-4px, -4px)';
469
+ } else if (isLookingAtEachOther) {
470
+ blackEyes.style.left = '32px';
471
+ blackEyes.style.top = '12px';
472
+ blackPupilL.style.transform = 'translate(0px, -4px)';
473
+ blackPupilR.style.transform = 'translate(0px, -4px)';
474
+ } else {
475
+ blackEyes.style.left = 26 + blackPos.faceX + 'px';
476
+ blackEyes.style.top = 32 + blackPos.faceY + 'px';
477
+ const bo = calcPupilOffset(blackEyeL, 4);
478
+ blackPupilL.style.transform = `translate(${bo.x}px, ${bo.y}px)`;
479
+ blackPupilR.style.transform = `translate(${bo.x}px, ${bo.y}px)`;
480
+ }
481
+
482
+ // ---- Orange body ----
483
+ if (isShowingPwd) {
484
+ orange.style.transform = 'skewX(0deg)';
485
+ } else {
486
+ orange.style.transform = `skewX(${orangePos.bodySkew}deg)`;
487
+ }
488
+
489
+ const orangeEyes = $('orange-eyes');
490
+ const orangePupilL = $('orange-pupil-l');
491
+ const orangePupilR = $('orange-pupil-r');
492
+ const orangeMouth = $('orange-mouth');
493
+
494
+ if (isLoginError) {
495
+ orangeMouth.style.left = 80 + orangePos.faceX + 'px';
496
+ orangeMouth.style.top = '130px';
497
+ orangeEyes.style.left = '60px';
498
+ orangeEyes.style.top = '95px';
499
+ orangePupilL.style.transform = 'translate(-3px, 4px)';
500
+ orangePupilR.style.transform = 'translate(-3px, 4px)';
501
+ } else if (isLookingAway) {
502
+ orangeEyes.style.left = '50px';
503
+ orangeEyes.style.top = '75px';
504
+ orangePupilL.style.transform = 'translate(-5px, -5px)';
505
+ orangePupilR.style.transform = 'translate(-5px, -5px)';
506
+ } else if (isShowingPwd) {
507
+ orangeEyes.style.left = '50px';
508
+ orangeEyes.style.top = '85px';
509
+ orangePupilL.style.transform = 'translate(-5px, -4px)';
510
+ orangePupilR.style.transform = 'translate(-5px, -4px)';
511
+ } else {
512
+ orangeEyes.style.left = 82 + orangePos.faceX + 'px';
513
+ orangeEyes.style.top = 90 + orangePos.faceY + 'px';
514
+ const oo = calcPupilOffset(orangePupilL, 5);
515
+ orangePupilL.style.transform = `translate(${oo.x}px, ${oo.y}px)`;
516
+ orangePupilR.style.transform = `translate(${oo.x}px, ${oo.y}px)`;
517
+ }
518
+
519
+ // ---- Yellow body ----
520
+ if (isShowingPwd) {
521
+ yellow.style.transform = 'skewX(0deg)';
522
+ } else {
523
+ yellow.style.transform = `skewX(${yellowPos.bodySkew}deg)`;
524
+ }
525
+
526
+ const yellowEyes = $('yellow-eyes');
527
+ const yellowPupilL = $('yellow-pupil-l');
528
+ const yellowPupilR = $('yellow-pupil-r');
529
+ const yellowMouth = $('yellow-mouth');
530
+
531
+ if (isLoginError) {
532
+ yellowEyes.style.left = '35px';
533
+ yellowEyes.style.top = '45px';
534
+ yellowPupilL.style.transform = 'translate(-3px, 4px)';
535
+ yellowPupilR.style.transform = 'translate(-3px, 4px)';
536
+ yellowMouth.style.left = '30px';
537
+ yellowMouth.style.top = '92px';
538
+ yellowMouth.style.transform = 'rotate(-8deg)';
539
+ } else if (isLookingAway) {
540
+ yellowEyes.style.left = '20px';
541
+ yellowEyes.style.top = '30px';
542
+ yellowPupilL.style.transform = 'translate(-5px, -5px)';
543
+ yellowPupilR.style.transform = 'translate(-5px, -5px)';
544
+ yellowMouth.style.left = '15px';
545
+ yellowMouth.style.top = '78px';
546
+ yellowMouth.style.transform = 'rotate(0deg)';
547
+ } else if (isShowingPwd) {
548
+ yellowEyes.style.left = '20px';
549
+ yellowEyes.style.top = '35px';
550
+ yellowPupilL.style.transform = 'translate(-5px, -4px)';
551
+ yellowPupilR.style.transform = 'translate(-5px, -4px)';
552
+ yellowMouth.style.left = '10px';
553
+ yellowMouth.style.top = '88px';
554
+ yellowMouth.style.transform = 'rotate(0deg)';
555
+ } else {
556
+ yellowEyes.style.left = 52 + yellowPos.faceX + 'px';
557
+ yellowEyes.style.top = 40 + yellowPos.faceY + 'px';
558
+ const yo = calcPupilOffset(yellowPupilL, 5);
559
+ yellowPupilL.style.transform = `translate(${yo.x}px, ${yo.y}px)`;
560
+ yellowPupilR.style.transform = `translate(${yo.x}px, ${yo.y}px)`;
561
+ yellowMouth.style.left = 40 + yellowPos.faceX + 'px';
562
+ yellowMouth.style.top = 88 + yellowPos.faceY + 'px';
563
+ yellowMouth.style.transform = 'rotate(0deg)';
564
+ }
565
+ };
566
+
567
+ // ---- Event handlers ----
568
+ const onEmailFocus = () => {
569
+ isTyping = true;
570
+ isLookingAtEachOther = true;
571
+ if (typingTimer) clearTimeout(typingTimer);
572
+ typingTimer = setTimeout(() => {
573
+ isLookingAtEachOther = false;
574
+ updateCharacters();
575
+ }, 800);
576
+ updateCharacters();
577
+ };
578
+
579
+ const onEmailBlur = () => {
580
+ isTyping = false;
581
+ isLookingAtEachOther = false;
582
+ updateCharacters();
583
+ };
584
+
585
+ const onPasswordFocus = () => {
586
+ isPasswordFocused = true;
587
+ updateCharacters();
588
+ };
589
+
590
+ const onPasswordBlur = () => {
591
+ isPasswordFocused = false;
592
+ updateCharacters();
593
+ };
594
+
595
+ const togglePassword = () => {
596
+ showPassword.value = !showPassword.value;
597
+ updateCharacters();
598
+ if (showPassword.value) schedulePeek();
599
+ };
600
+
601
+ // ---- Blinking ----
602
+ const scheduleBlinkPurple = () => {
603
+ const timer = setTimeout(() => {
604
+ isPurpleBlinking = true;
605
+ updateCharacters();
606
+ setTimeout(() => {
607
+ isPurpleBlinking = false;
608
+ updateCharacters();
609
+ scheduleBlinkPurple();
610
+ }, 150);
611
+ }, Math.random() * 4000 + 3000);
612
+ blinkTimers.add(timer);
613
+ };
614
+
615
+ const scheduleBlinkBlack = () => {
616
+ const timer = setTimeout(() => {
617
+ isBlackBlinking = true;
618
+ updateCharacters();
619
+ setTimeout(() => {
620
+ isBlackBlinking = false;
621
+ updateCharacters();
622
+ scheduleBlinkBlack();
623
+ }, 150);
624
+ }, Math.random() * 4000 + 3000);
625
+ blinkTimers.add(timer);
626
+ };
627
+
628
+ // ---- Peeking ----
629
+ const schedulePeek = () => {
630
+ if (form.password.length > 0 && showPassword.value) {
631
+ if (peekTimer) clearTimeout(peekTimer);
632
+ peekTimer = setTimeout(() => {
633
+ if (form.password.length > 0 && showPassword.value) {
634
+ isPurplePeeking = true;
635
+ updateCharacters();
636
+ setTimeout(() => {
637
+ isPurplePeeking = false;
638
+ updateCharacters();
639
+ schedulePeek();
640
+ }, 800);
641
+ }
642
+ }, Math.random() * 3000 + 2000);
643
+ }
644
+ };
645
+
646
+ // ---- Error ----
647
+ const shakeIds = [
648
+ 'purple-eyes', 'black-eyes', 'orange-eyes', 'yellow-eyes',
649
+ 'yellow-mouth', 'orange-mouth',
650
+ ];
651
+
652
+ const triggerLoginError = () => {
653
+ if (errorRecoverTimer) {
654
+ clearTimeout(errorRecoverTimer);
655
+ errorRecoverTimer = null;
656
+ }
657
+
658
+ const shakeEls = shakeIds.map(id => $(id));
659
+ shakeEls.forEach(el => el.classList.remove('shake-head'));
660
+ void document.body.offsetHeight;
661
+
662
+ isLoginError = true;
663
+ isPasswordFocused = false;
664
+ updateCharacters();
665
+
666
+ $('orange-mouth').classList.add('visible');
667
+
668
+ setTimeout(() => {
669
+ shakeEls.forEach(el => el.classList.add('shake-head'));
670
+ }, 350);
671
+
672
+ errorRecoverTimer = setTimeout(() => {
673
+ isLoginError = false;
674
+ errorRecoverTimer = null;
675
+ $('orange-mouth').classList.remove('visible');
676
+ shakeEls.forEach(el => el.classList.remove('shake-head'));
677
+ updateCharacters();
678
+ }, 2500);
679
+ };
680
+
681
+ // ---- Mouse tracking ----
682
+ const handleMouseMove = (e: MouseEvent) => {
683
+ mouseX = e.clientX;
684
+ mouseY = e.clientY;
685
+ if (!isTyping && !isLoginError) updateCharacters();
686
+ };
687
+
688
+ const blinkTimers = new Set<ReturnType<typeof setTimeout>>();
689
+
690
+ onMounted(() => {
691
+ document.addEventListener('mousemove', handleMouseMove);
692
+ scheduleBlinkPurple();
693
+ scheduleBlinkBlack();
694
+ updateCharacters();
695
+
696
+ const savedTheme = localStorage.getItem('theme');
697
+ if (savedTheme === 'dark') {
698
+ isDark.value = true;
699
+ document.documentElement.classList.add('dark');
700
+ }
701
+
702
+ const remembered = localStorage.getItem(REMEMBER_KEY);
703
+ if (remembered) {
704
+ try {
705
+ const data = JSON.parse(remembered);
706
+ form.username = data.username;
707
+ rememberMe.value = true;
708
+ } catch {
709
+ // ignore
710
+ }
711
+ }
712
+ });
713
+
714
+ onUnmounted(() => {
715
+ document.removeEventListener('mousemove', handleMouseMove);
716
+ blinkTimers.forEach(t => clearTimeout(t));
717
+ blinkTimers.clear();
718
+ if (typingTimer) clearTimeout(typingTimer);
719
+ if (errorRecoverTimer) clearTimeout(errorRecoverTimer);
720
+ if (peekTimer) clearTimeout(peekTimer);
721
+ });
722
+ </script>
723
+
724
+ <style scoped>
725
+ .login-page {
726
+ display: flex;
727
+ align-items: center;
728
+ justify-content: center;
729
+ min-height: 100vh;
730
+ background: linear-gradient(135deg, #0f0c29 0%, #302b63 50%, #24243e 100%);
731
+ position: relative;
732
+ overflow: hidden;
733
+ padding: 24px;
734
+ }
735
+
736
+ .login-page::before {
737
+ content: '';
738
+ position: absolute;
739
+ top: -50%;
740
+ left: -50%;
741
+ width: 200%;
742
+ height: 200%;
743
+ background: radial-gradient(ellipse at 20% 50%, rgba(102, 126, 234, 0.15) 0%, transparent 50%),
744
+ radial-gradient(ellipse at 80% 50%, rgba(118, 75, 162, 0.15) 0%, transparent 50%);
745
+ animation: bgShift 15s ease-in-out infinite alternate;
746
+ }
747
+
748
+ @keyframes bgShift {
749
+ 0% { transform: translate(0, 0) rotate(0deg); }
750
+ 100% { transform: translate(-2%, 2%) rotate(3deg); }
751
+ }
752
+
753
+ .login-card {
754
+ display: flex;
755
+ width: 1120px;
756
+ max-width: 100%;
757
+ height: 620px;
758
+ max-height: 90vh;
759
+ border-radius: 24px;
760
+ overflow: hidden;
761
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
762
+ position: relative;
763
+ z-index: 1;
764
+ }
765
+
766
+ /* ========= Left Panel ========= */
767
+ .left-panel {
768
+ position: relative;
769
+ display: flex;
770
+ flex-direction: column;
771
+ justify-content: space-between;
772
+ background: linear-gradient(135deg, #d4d0dc 0%, #c8c4d0 50%, #bbb7c5 100%);
773
+ padding: 40px 48px;
774
+ overflow: hidden;
775
+ width: 580px;
776
+ flex-shrink: 0;
777
+ }
778
+
779
+ .left-panel .logo {
780
+ display: flex;
781
+ align-items: center;
782
+ gap: 10px;
783
+ font-size: 16px;
784
+ font-weight: 600;
785
+ color: #fff;
786
+ z-index: 10;
787
+ position: relative;
788
+ }
789
+
790
+
791
+
792
+ .sparkle-icon {
793
+ color: #1a1a2e;
794
+ }
795
+
796
+ html.dark .sparkle-icon {
797
+ color: #e8e8f0;
798
+ }
799
+
800
+ .characters-wrapper {
801
+ position: relative;
802
+ z-index: 10;
803
+ display: flex;
804
+ align-items: flex-end;
805
+ justify-content: center;
806
+ height: 420px;
807
+ }
808
+
809
+ .characters-scene {
810
+ position: relative;
811
+ width: 480px;
812
+ height: 360px;
813
+ background: linear-gradient(135deg, #d4d0dc 0%, #c8c4d0 50%, #bbb7c5 100%);
814
+ }
815
+
816
+ .character {
817
+ position: absolute;
818
+ bottom: 0;
819
+ transition: all 0.7s ease-in-out;
820
+ transform-origin: bottom center;
821
+ }
822
+
823
+ .char-purple {
824
+ left: 88px;
825
+ width: 170px;
826
+ height: 370px;
827
+ background: #6c3ff5;
828
+ border-radius: 10px 10px 0 0;
829
+ z-index: 1;
830
+ }
831
+
832
+ .char-black {
833
+ left: 248px;
834
+ width: 115px;
835
+ height: 290px;
836
+ background: #2d2d2d;
837
+ border-radius: 8px 8px 0 0;
838
+ z-index: 2;
839
+ }
840
+
841
+ .char-orange {
842
+ left: 28px;
843
+ width: 230px;
844
+ height: 191px;
845
+ bottom: -1px;
846
+ background: #ff9b6b;
847
+ border-radius: 115px 115px 0 0;
848
+ z-index: 3;
849
+ }
850
+
851
+ .char-yellow {
852
+ left: 318px;
853
+ width: 135px;
854
+ height: 216px;
855
+ bottom: -1px;
856
+ background: #e8d754;
857
+ border-radius: 68px 68px 0 0;
858
+ z-index: 4;
859
+ }
860
+
861
+ .eyes {
862
+ position: absolute;
863
+ display: flex;
864
+ transition: all 0.7s ease-in-out;
865
+ }
866
+
867
+ .eyeball {
868
+ border-radius: 50%;
869
+ background: white;
870
+ display: flex;
871
+ align-items: center;
872
+ justify-content: center;
873
+ transition: height 0.15s ease;
874
+ overflow: hidden;
875
+ }
876
+
877
+ .pupil {
878
+ border-radius: 50%;
879
+ background: #2d2d2d;
880
+ transition: transform 0.1s ease-out;
881
+ }
882
+
883
+ .bare-pupil {
884
+ width: 12px;
885
+ height: 12px;
886
+ border-radius: 50%;
887
+ background: #2d2d2d;
888
+ transition: transform 0.7s ease-in-out;
889
+ }
890
+
891
+ .yellow-mouth {
892
+ position: absolute;
893
+ width: 50px;
894
+ height: 4px;
895
+ background: #2d2d2d;
896
+ border-radius: 2px;
897
+ transition: all 0.7s ease-in-out;
898
+ }
899
+
900
+ .orange-mouth {
901
+ position: absolute;
902
+ width: 28px;
903
+ height: 14px;
904
+ border: 3px solid #2d2d2d;
905
+ border-top: none;
906
+ border-radius: 0 0 14px 14px;
907
+ opacity: 0;
908
+ transition: all 0.7s ease-in-out;
909
+ }
910
+
911
+ .orange-mouth.visible {
912
+ opacity: 1;
913
+ }
914
+
915
+ @keyframes shakeHead {
916
+ 0%, 100% { translate: 0 0; }
917
+ 10% { translate: -9px 0; }
918
+ 20% { translate: 7px 0; }
919
+ 30% { translate: -6px 0; }
920
+ 40% { translate: 5px 0; }
921
+ 50% { translate: -4px 0; }
922
+ 60% { translate: 3px 0; }
923
+ 70% { translate: -2px 0; }
924
+ 80% { translate: 1px 0; }
925
+ 90% { translate: -0.5px 0; }
926
+ }
927
+
928
+ .eyes.shake-head,
929
+ .yellow-mouth.shake-head,
930
+ .orange-mouth.shake-head {
931
+ animation: shakeHead 0.8s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
932
+ }
933
+
934
+ .left-panel::after {
935
+ content: '';
936
+ position: absolute;
937
+ top: 20%;
938
+ right: 15%;
939
+ width: 260px;
940
+ height: 260px;
941
+ background: rgba(180, 170, 200, 0.25);
942
+ border-radius: 50%;
943
+ filter: blur(80px);
944
+ }
945
+
946
+ .left-panel::before {
947
+ content: '';
948
+ position: absolute;
949
+ bottom: 15%;
950
+ left: 10%;
951
+ width: 350px;
952
+ height: 350px;
953
+ background: rgba(200, 195, 210, 0.2);
954
+ border-radius: 50%;
955
+ filter: blur(100px);
956
+ }
957
+
958
+ .footer-links {
959
+ display: flex;
960
+ gap: 28px;
961
+ font-size: 13px;
962
+ color: rgba(80, 70, 90, 0.7);
963
+ z-index: 10;
964
+ position: relative;
965
+ }
966
+
967
+ .footer-links a {
968
+ color: inherit;
969
+ text-decoration: none;
970
+ transition: color 0.2s;
971
+ }
972
+
973
+ .footer-links a:hover {
974
+ color: #333;
975
+ }
976
+
977
+ /* ========= Right Panel ========= */
978
+ .right-panel {
979
+ flex: 1;
980
+ display: flex;
981
+ align-items: center;
982
+ justify-content: center;
983
+ background: #fff;
984
+ padding: 40px;
985
+ transition: background 0.3s;
986
+ position: relative;
987
+ }
988
+
989
+ .form-container {
990
+ width: 100%;
991
+ max-width: 400px;
992
+ }
993
+
994
+ .sparkle-icon {
995
+ display: flex;
996
+ justify-content: center;
997
+ margin-bottom: 24px;
998
+ }
999
+
1000
+ .sparkle-icon svg {
1001
+ width: 32px;
1002
+ height: 32px;
1003
+ }
1004
+
1005
+ .form-header {
1006
+ text-align: center;
1007
+ margin-bottom: 36px;
1008
+ }
1009
+
1010
+ .form-header h1 {
1011
+ font-size: 28px;
1012
+ font-weight: 700;
1013
+ color: #1a1a2e;
1014
+ letter-spacing: -0.5px;
1015
+ margin: 0 0 6px;
1016
+ }
1017
+
1018
+ .form-header p {
1019
+ font-size: 14px;
1020
+ color: #909399;
1021
+ margin: 0;
1022
+ }
1023
+
1024
+ .login-form :deep(.el-form-item) {
1025
+ margin-bottom: 22px;
1026
+ }
1027
+
1028
+ .login-form :deep(.el-input__wrapper) {
1029
+ padding: 4px 12px;
1030
+ border-radius: 8px;
1031
+ box-shadow: 0 0 0 1px #dcdfe6 inset;
1032
+ transition: box-shadow 0.25s;
1033
+ }
1034
+
1035
+ .login-form :deep(.el-input__wrapper.is-focus) {
1036
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.4) inset;
1037
+ }
1038
+
1039
+ .login-form :deep(.el-input__inner) {
1040
+ height: 38px;
1041
+ }
1042
+
1043
+ .login-form :deep(.el-input__inner:-webkit-autofill) {
1044
+ box-shadow: 0 0 0 1000px #fff inset !important;
1045
+ -webkit-text-fill-color: #606266 !important;
1046
+ caret-color: #606266 !important;
1047
+ }
1048
+
1049
+ html.dark .login-form :deep(.el-input__inner:-webkit-autofill) {
1050
+ box-shadow: 0 0 0 1000px #1a1a2e inset !important;
1051
+ -webkit-text-fill-color: #e8e8f0 !important;
1052
+ caret-color: #e8e8f0 !important;
1053
+ }
1054
+
1055
+ .toggle-password {
1056
+ display: inline-flex;
1057
+ align-items: center;
1058
+ justify-content: center;
1059
+ cursor: pointer;
1060
+ width: 24px;
1061
+ height: 24px;
1062
+ color: #909399;
1063
+ transition: color 0.2s;
1064
+ }
1065
+
1066
+ .toggle-password:hover {
1067
+ color: #667eea;
1068
+ }
1069
+
1070
+ .captcha-row {
1071
+ display: flex;
1072
+ gap: 12px;
1073
+ align-items: center;
1074
+ width: 100%;
1075
+ }
1076
+
1077
+ .captcha-box {
1078
+ width: 120px;
1079
+ height: 40px;
1080
+ cursor: pointer;
1081
+ border-radius: 8px;
1082
+ overflow: hidden;
1083
+ flex-shrink: 0;
1084
+ border: 1px solid #e8e8e8;
1085
+ transition: opacity 0.2s;
1086
+ }
1087
+
1088
+ .captcha-box:hover {
1089
+ opacity: 0.75;
1090
+ }
1091
+
1092
+ .login-btn {
1093
+ width: 100%;
1094
+ height: 48px;
1095
+ font-size: 16px;
1096
+ font-weight: 600;
1097
+ border-radius: 10px;
1098
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1099
+ border: none;
1100
+ transition: all 0.3s;
1101
+ margin-top: 4px;
1102
+ }
1103
+
1104
+ .login-btn:hover {
1105
+ transform: translateY(-2px);
1106
+ box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
1107
+ }
1108
+
1109
+ .form-actions {
1110
+ display: flex;
1111
+ flex-direction: column;
1112
+ align-items: center;
1113
+ gap: 8px;
1114
+ margin-top: 24px;
1115
+ }
1116
+
1117
+ .demo-hint {
1118
+ font-size: 12px;
1119
+ color: #909399;
1120
+ }
1121
+
1122
+ .reset-btn {
1123
+ font-size: 12px;
1124
+ }
1125
+
1126
+ /* ========= Toolbar ========= */
1127
+ .toolbar {
1128
+ position: absolute;
1129
+ top: 16px;
1130
+ right: 20px;
1131
+ display: flex;
1132
+ align-items: center;
1133
+ gap: 4px;
1134
+ z-index: 20;
1135
+ }
1136
+
1137
+ .toolbar-btn {
1138
+ padding: 6px 8px;
1139
+ border-radius: 8px;
1140
+ color: #606266;
1141
+ font-size: 16px;
1142
+ transition: all 0.2s;
1143
+ }
1144
+
1145
+ .toolbar-btn:hover {
1146
+ color: #409eff;
1147
+ background: rgba(64, 158, 255, 0.06);
1148
+ }
1149
+
1150
+ .shake {
1151
+ animation: shake 0.5s ease-in-out;
1152
+ }
1153
+
1154
+ @keyframes shake {
1155
+ 0%, 100% { transform: translateX(0); }
1156
+ 10%, 30%, 50%, 70%, 90% { transform: translateX(-6px); }
1157
+ 20%, 40%, 60%, 80% { transform: translateX(6px); }
1158
+ }
1159
+
1160
+ /* ========= Dark Mode ========= */
1161
+ html.dark .right-panel {
1162
+ background: #1a1a2e;
1163
+ }
1164
+
1165
+ html.dark .form-header h1 {
1166
+ color: #e8e8f0;
1167
+ }
1168
+
1169
+ html.dark .form-header p {
1170
+ color: #8b949e;
1171
+ }
1172
+
1173
+ html.dark .toggle-password {
1174
+ color: #8b949e;
1175
+ }
1176
+
1177
+ html.dark .captcha-box {
1178
+ border-color: #3d3d5c;
1179
+ }
1180
+
1181
+ html.dark .login-form :deep(.el-input__wrapper) {
1182
+ box-shadow: 0 0 0 1px #3d3d5c inset;
1183
+ background: rgba(255, 255, 255, 0.04);
1184
+ }
1185
+
1186
+ html.dark .login-form :deep(.el-input__wrapper.is-focus) {
1187
+ box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.5) inset;
1188
+ }
1189
+
1190
+ html.dark .login-form :deep(.el-input__inner) {
1191
+ color: #e8e8f0;
1192
+ }
1193
+
1194
+ html.dark .demo-hint {
1195
+ color: #666;
1196
+ }
1197
+
1198
+ html.dark .left-panel {
1199
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0c29 100%);
1200
+ }
1201
+
1202
+ html.dark .left-panel::after {
1203
+ background: rgba(60, 50, 100, 0.25);
1204
+ }
1205
+
1206
+ html.dark .left-panel::before {
1207
+ background: rgba(40, 35, 80, 0.2);
1208
+ }
1209
+
1210
+ html.dark .characters-scene {
1211
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f0c29 100%);
1212
+ }
1213
+
1214
+ html.dark .footer-links {
1215
+ color: rgba(200, 200, 220, 0.4);
1216
+ }
1217
+
1218
+ html.dark .footer-links a:hover {
1219
+ color: #aaa;
1220
+ }
1221
+
1222
+ html.dark .toolbar-btn {
1223
+ color: #8b949e;
1224
+ }
1225
+
1226
+ html.dark .toolbar-btn:hover {
1227
+ color: #58a6ff;
1228
+ background: rgba(88, 166, 255, 0.1);
1229
+ }
1230
+
1231
+ html.dark .login-form :deep(.el-checkbox__label) {
1232
+ color: #8b949e;
1233
+ }
1234
+
1235
+ /* ========= Responsive ========= */
1236
+ @media (max-width: 900px) {
1237
+ .login-card {
1238
+ flex-direction: column;
1239
+ height: auto;
1240
+ max-height: none;
1241
+ border-radius: 16px;
1242
+ }
1243
+
1244
+ .left-panel {
1245
+ display: none;
1246
+ }
1247
+
1248
+ .right-panel {
1249
+ padding: 36px 28px;
1250
+ }
1251
+ }
1252
+ </style>