diff --git a/admin/package-lock.json b/admin/package-lock.json index 080bb31..e4107a0 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -11,6 +11,7 @@ "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "clsx": "^2.1.1", + "framer-motion": "^12.42.2", "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", @@ -1327,6 +1328,33 @@ } } }, + "node_modules/framer-motion": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.42.2.tgz", + "integrity": "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2588,6 +2616,21 @@ ], "license": "MIT" }, + "node_modules/motion-dom": { + "version": "12.42.2", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.42.2.tgz", + "integrity": "sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.39.0" + } + }, + "node_modules/motion-utils": { + "version": "12.39.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.39.0.tgz", + "integrity": "sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3072,8 +3115,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "optional": true + "license": "0BSD" }, "node_modules/typescript": { "version": "6.0.3", diff --git a/admin/package.json b/admin/package.json index 2cd5ae7..6768a7c 100644 --- a/admin/package.json +++ b/admin/package.json @@ -13,6 +13,7 @@ "@tailwindcss/typography": "^0.5.20", "@tailwindcss/vite": "^4.3.3", "clsx": "^2.1.1", + "framer-motion": "^12.42.2", "lucide-react": "^1.24.0", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/admin/src/components/login-backdrop.tsx b/admin/src/components/login-backdrop.tsx deleted file mode 100644 index 959946c..0000000 --- a/admin/src/components/login-backdrop.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import { useEffect, useRef } from 'react' - -/** 登录页背景:青蓝粒子网络(呼应 logo 的节点网络),纯 canvas 无依赖 */ -export function LoginBackdrop() { - const ref = useRef(null) - - useEffect(() => { - const canvas = ref.current - if (!canvas) return - const ctx = canvas.getContext('2d') - if (!ctx) return - - const reduce = window.matchMedia( - '(prefers-reduced-motion: reduce)', - ).matches - const dpr = Math.min(window.devicePixelRatio || 1, 2) - let w = 0 - let h = 0 - let raf = 0 - const dots: { x: number; y: number; vx: number; vy: number }[] = [] - - const resize = () => { - w = canvas.clientWidth - h = canvas.clientHeight - canvas.width = w * dpr - canvas.height = h * dpr - ctx.setTransform(dpr, 0, 0, dpr, 0, 0) - } - - const init = () => { - dots.length = 0 - const n = Math.min(64, Math.floor((w * h) / 16000)) - for (let i = 0; i < n; i++) { - dots.push({ - x: Math.random() * w, - y: Math.random() * h, - vx: (Math.random() - 0.5) * 0.28, - vy: (Math.random() - 0.5) * 0.28, - }) - } - } - - const frame = () => { - ctx.clearRect(0, 0, w, h) - for (const p of dots) { - p.x += p.vx - p.y += p.vy - if (p.x < 0 || p.x > w) p.vx *= -1 - if (p.y < 0 || p.y > h) p.vy *= -1 - } - for (let i = 0; i < dots.length; i++) { - for (let j = i + 1; j < dots.length; j++) { - const a = dots[i] - const b = dots[j] - const d = Math.hypot(a.x - b.x, a.y - b.y) - if (d < 130) { - ctx.strokeStyle = `rgba(56,189,248,${(1 - d / 130) * 0.16})` - ctx.lineWidth = 1 - ctx.beginPath() - ctx.moveTo(a.x, a.y) - ctx.lineTo(b.x, b.y) - ctx.stroke() - } - } - } - for (const p of dots) { - ctx.fillStyle = 'rgba(56,189,248,0.55)' - ctx.beginPath() - ctx.arc(p.x, p.y, 1.4, 0, Math.PI * 2) - ctx.fill() - } - if (!reduce) raf = requestAnimationFrame(frame) - } - - const onResize = () => { - resize() - init() - } - - resize() - init() - frame() - window.addEventListener('resize', onResize) - return () => { - cancelAnimationFrame(raf) - window.removeEventListener('resize', onResize) - } - }, []) - - return ( -