时长{task.duration}s
diff --git a/web/src/components/LoginModal.module.css b/web/src/components/LoginModal.module.css
new file mode 100644
index 0000000..6f34eca
--- /dev/null
+++ b/web/src/components/LoginModal.module.css
@@ -0,0 +1,168 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 50;
+ background: rgba(0, 0, 0, 0.5);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ animation: overlayIn 0.3s ease-out;
+}
+
+@keyframes overlayIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+
+.panel {
+ position: relative;
+ width: 100%;
+ max-width: 400px;
+ margin: 0 20px;
+ background: rgba(255, 255, 255, 0.06);
+ backdrop-filter: blur(24px) saturate(180%);
+ -webkit-backdrop-filter: blur(24px) saturate(180%);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+ padding: 36px 32px 32px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.05) inset;
+ animation: panelIn 0.3s ease-out;
+}
+
+@keyframes panelIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.closeBtn {
+ position: absolute;
+ top: 14px;
+ right: 14px;
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.4);
+ cursor: pointer;
+ border-radius: 6px;
+ transition: all 0.2s;
+}
+
+.closeBtn:hover {
+ color: rgba(255, 255, 255, 0.8);
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ margin-bottom: 28px;
+}
+
+.headerLogo {
+ width: 28px;
+ height: 28px;
+}
+
+.headerTitle {
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 18px;
+ font-weight: 400;
+ color: #f1f0ff;
+ letter-spacing: 0.05em;
+}
+
+.form {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+}
+
+.field {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.label {
+ font-size: 13px;
+ color: #8b8ea8;
+ font-weight: 500;
+}
+
+.input {
+ height: 44px;
+ padding: 0 14px;
+ background: rgba(255, 255, 255, 0.04);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ color: #f1f0ff;
+ font-size: 14px;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.input::placeholder {
+ color: #4c4f6b;
+}
+
+.input:focus {
+ border-color: rgba(126, 220, 200, 0.5);
+}
+
+.error {
+ color: #ff4d4f;
+ font-size: 13px;
+ text-align: center;
+ padding: 8px;
+ background: rgba(255, 77, 79, 0.08);
+ border-radius: 8px;
+}
+
+.submitBtn {
+ height: 44px;
+ width: 55%;
+ align-self: center;
+ margin-top: 18px;
+ background: rgba(120, 220, 200, 0.08);
+ border: 1px solid rgba(120, 220, 200, 0.3);
+ color: #7edcc8;
+ border-radius: 10px;
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 15px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+}
+
+.submitBtn:hover {
+ background: rgba(120, 220, 200, 0.18);
+ box-shadow: 0 0 24px rgba(120, 220, 200, 0.12);
+}
+
+.submitBtn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.hint {
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.25);
+ text-align: center;
+ margin: 0;
+}
diff --git a/web/src/components/LoginModal.tsx b/web/src/components/LoginModal.tsx
new file mode 100644
index 0000000..e3708e2
--- /dev/null
+++ b/web/src/components/LoginModal.tsx
@@ -0,0 +1,89 @@
+import { useState, useCallback } from 'react';
+import { useAuthStore } from '../store/auth';
+import logoImg from '../assets/logo_32.png';
+import styles from './LoginModal.module.css';
+
+interface Props {
+ isOpen: boolean;
+ onClose: () => void;
+ onSuccess: () => void;
+}
+
+export function LoginModal({ isOpen, onClose, onSuccess }: Props) {
+ const login = useAuthStore((s) => s.login);
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const [loading, setLoading] = useState(false);
+
+ const handleSubmit = useCallback(async (e: React.FormEvent) => {
+ e.preventDefault();
+ setError('');
+
+ if (!username.trim()) { setError('请输入用户名或邮箱'); return; }
+ if (password.length < 6) { setError('密码至少6位'); return; }
+
+ setLoading(true);
+ try {
+ await login(username, password);
+ onSuccess();
+ } catch (err: any) {
+ const msg = err.response?.data?.message || err.response?.data?.error || '登录失败,请重试';
+ setError(msg);
+ } finally {
+ setLoading(false);
+ }
+ }, [username, password, login, onSuccess]);
+
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}>
+
+
+
+

+
Air Drama
+
+
+
+
+
+ );
+}
diff --git a/web/src/components/ProtectedRoute.tsx b/web/src/components/ProtectedRoute.tsx
index a42562e..79cfde6 100644
--- a/web/src/components/ProtectedRoute.tsx
+++ b/web/src/components/ProtectedRoute.tsx
@@ -34,11 +34,11 @@ export function ProtectedRoute({ children, requireAdmin, requireTeamAdmin, requi
}
if (requireAdmin && user?.role !== 'super_admin') {
- return
;
+ return
;
}
if (requireTeamAdmin && user?.role !== 'team_admin') {
- return
;
+ return
;
}
// requireTeamMember: must have a team (team_admin or member)
diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx
index 324b148..f3c25ae 100644
--- a/web/src/components/Sidebar.tsx
+++ b/web/src/components/Sidebar.tsx
@@ -1,5 +1,6 @@
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/auth';
+import logoImg from '../assets/logo_32.png';
import styles from './Sidebar.module.css';
export function Sidebar() {
@@ -18,12 +19,8 @@ export function Sidebar() {
return (