diff --git a/frontend/assets/login.css b/frontend/assets/login.css index 2a62e8b..fbe5bc3 100644 --- a/frontend/assets/login.css +++ b/frontend/assets/login.css @@ -10,7 +10,8 @@ body { overflow: hidden; } -.login-container { +/* 背景包装器 */ +.login-wrapper { position: relative; width: 100vw; height: 100vh; @@ -18,21 +19,10 @@ body { align-items: center; justify-content: center; overflow: hidden; + background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%); } -/* 背景层 - 工业风格渐变 + 几何图案 */ -.login-background { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: - linear-gradient(135deg, #1e3a8a 0%, #1e40af 25%, #3b82f6 50%, #60a5fa 75%, #93c5fd 100%); - z-index: 0; -} - -.login-background::before { +.login-wrapper::before { content: ''; position: absolute; top: 0; @@ -44,53 +34,82 @@ body { 0deg, transparent, transparent 2px, - rgba(255, 255, 255, 0.03) 2px, - rgba(255, 255, 255, 0.03) 4px + rgba(255, 255, 255, 0.02) 2px, + rgba(255, 255, 255, 0.02) 4px ), repeating-linear-gradient( 90deg, transparent, transparent 2px, - rgba(255, 255, 255, 0.03) 2px, - rgba(255, 255, 255, 0.03) 4px + rgba(255, 255, 255, 0.02) 2px, + rgba(255, 255, 255, 0.02) 4px ); - background-size: 50px 50px; + background-size: 60px 60px; } -.login-background::after { +.login-wrapper::after { content: ''; position: absolute; - top: -50%; - right: -20%; - width: 80%; - height: 80%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.1) 0%, transparent 70%); + top: -30%; + left: -20%; + width: 70%; + height: 70%; + background: radial-gradient(circle, rgba(255, 255, 255, 0.25) 0%, rgba(255, 200, 255, 0.1) 40%, transparent 70%); border-radius: 50%; - animation: float 20s ease-in-out infinite; + animation: float-bg 10s ease-in-out infinite; } -@keyframes float { +/* 额外的动态光斑 */ +.login-wrapper .container::before { + content: ''; + position: absolute; + bottom: -40%; + right: -30%; + width: 60%; + height: 60%; + background: radial-gradient(circle, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.15) 50%, transparent 70%); + border-radius: 50%; + animation: float-bg2 12s ease-in-out infinite; + z-index: -2; +} + +@keyframes float-bg { 0%, 100% { transform: translate(0, 0) scale(1); + opacity: 0.8; } 50% { - transform: translate(-50px, 50px) scale(1.1); + transform: translate(80px, 60px) scale(1.2); + opacity: 1; } } -/* 登录卡片 */ -.login-card { +@keyframes float-bg2 { + 0%, 100% { + transform: translate(0, 0) scale(1); + opacity: 0.7; + } + 50% { + transform: translate(-60px, -50px) scale(1.15); + opacity: 1; + } +} + +/* 主容器 - Uiverse.io 风格 */ +.container { + display: flex; + width: 620px; + height: 520px; + max-width: 95%; + align-items: center; + justify-content: center; position: relative; - z-index: 1; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(20px); - border-radius: 24px; - box-shadow: - 0 20px 60px rgba(0, 0, 0, 0.3), - 0 0 0 1px rgba(255, 255, 255, 0.1) inset; - padding: 48px; - width: 90%; - max-width: 440px; + overflow: hidden; + background-color: rgba(255, 255, 255, 0.15); + border-radius: 20px; + box-shadow: 0px 0px 40px rgba(0, 0, 0, 0.08); + border: 1px solid rgba(255, 255, 255, 0.25); + backdrop-filter: blur(10px); animation: slideUp 0.6s ease-out; } @@ -105,176 +124,158 @@ body { } } -/* 登录头部 */ -.login-header { +.container::after { + position: absolute; + content: ""; + width: 80%; + height: 80%; + right: -40%; + background: rgb(157, 173, 203); + background: radial-gradient( + circle, + rgba(157, 173, 203, 0.6) 61%, + rgba(99, 122, 159, 0.4) 100% + ); + border-radius: 50%; + z-index: -1; +} + +/* 左侧表单区域 */ +.left { + width: 60%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + padding: 40px; +} + +/* 表单头部 */ +.form-header { text-align: center; - margin-bottom: 40px; -} - -.logo-container { - display: flex; - align-items: center; - justify-content: center; - gap: 12px; - margin-bottom: 8px; -} - -.logo-icon { - width: 64px; - height: 64px; - display: flex; - align-items: center; - justify-content: center; - animation: float-icon 3s ease-in-out infinite; -} - -.logo-icon svg { - width: 100%; - height: 100%; - filter: drop-shadow(0 4px 8px rgba(59, 130, 246, 0.3)); -} - -/* 比特币旋转硬币 */ -.logo-icon.bitcoin-coin { - perspective: 500px; - animation: none; -} - -.logo-icon.bitcoin-coin .coin-face { - position: absolute; - width: 100%; - height: 100%; - border-radius: 50%; - backface-visibility: hidden; -} - -.logo-icon.bitcoin-coin .front { - transform: translateZ(2px); - animation: rotate-bitcoin 7s infinite linear; - transform-style: preserve-3d; -} - -.logo-icon.bitcoin-coin .back { - transform: rotateY(180deg) translateZ(2px); - animation: rotate-bitcoin 7s infinite linear; - transform-style: preserve-3d; -} - -.logo-icon.bitcoin-coin::before { - content: ''; - position: absolute; - width: 100%; - height: 100%; - background: linear-gradient(90deg, #b36a00, #faa504, #b36a00); - border-radius: 50%; - animation: rotate-bitcoin 7s infinite linear; - transform-style: preserve-3d; -} - -@keyframes rotate-bitcoin { - 0% { transform: rotateY(0deg); } - 100% { transform: rotateY(360deg); } -} - -@keyframes float-icon { - 0%, 100% { - transform: translateY(0) scale(1); - } - 50% { - transform: translateY(-5px) scale(1.02); - } + margin-bottom: 30px; } .system-title { - font-size: 28px; + font-size: 22px; font-weight: 700; - color: #1e3a8a; - margin: 0; + color: #ffffff; + margin: 0 0 6px 0; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); } .system-subtitle { - font-size: 13px; - color: #64748b; + font-size: 12px; + color: rgba(255, 255, 255, 0.85); letter-spacing: 1px; text-transform: uppercase; margin: 0; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); } -/* 表单 */ -.login-form { - margin-bottom: 32px; -} - -.form-group { - margin-bottom: 24px; -} - -.form-group label { - display: block; - font-size: 14px; - font-weight: 600; - color: #334155; - margin-bottom: 8px; -} - -.input-wrapper { - position: relative; +/* 表单样式 */ +.form { display: flex; - align-items: center; -} - -.input-icon { - position: absolute; - left: 16px; - width: 20px; - height: 20px; - display: flex; - align-items: center; + flex-direction: column; justify-content: center; + width: 100%; + position: relative; +} + +.form::before { + position: absolute; + content: ""; + width: 50%; + height: 50%; + right: -10%; + top: 10%; + z-index: -1; pointer-events: none; + background: radial-gradient( + circle, + rgba(194, 13, 170, 0.3) 20%, + rgba(26, 186, 235, 0.2) 60%, + rgba(26, 186, 235, 0.1) 100% + ); + filter: blur(60px); + border-radius: 50%; +} + +/* 输入框块 */ +.input-block { + position: relative; + margin-bottom: 8px; z-index: 1; - color: #64748b; - transition: all 0.3s ease; } -.input-icon svg { - width: 100%; - height: 100%; -} - -.form-input { - width: 100%; - padding: 14px 16px 14px 48px; - font-size: 15px; - font-family: inherit; - color: #1e293b; - background: #f8fafc; - border: 2px solid #e2e8f0; - border-radius: 12px; - transition: all 0.3s ease; +.input, +button { + background: rgba(255, 255, 255, 0.95); outline: none; + border: 1px solid rgba(200, 200, 200, 0.4); + border-radius: 0.5rem; + padding: 12px 14px; + margin: 8px auto; + width: 100%; + display: block; + color: #2d3748; + font-weight: 500; + font-size: 1em; + font-family: inherit; + transition: all 0.3s ease; } -.form-input::placeholder { - color: #94a3b8; +.input-block label { + position: absolute; + left: 14px; + top: 37%; + pointer-events: none; + color: #5a6a7a; + font-size: 0.95em; + transition: all 0.4s ease; } -.form-input:focus { - background: #ffffff; - border-color: #3b82f6; - box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); +.input:focus + label, +.input:valid + label { + transform: translateY(-120%) scale(0.9); + color: #ffffff; + font-weight: 600; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); } -.input-wrapper:focus-within .input-icon { - color: #3b82f6; - transform: scale(1.1); +.input:focus { + background: rgba(255, 255, 255, 1); + border-color: #5e7eb6; + box-shadow: 0 0 0 3px rgba(94, 126, 182, 0.15); } -/* 验证码 */ -.captcha-wrapper { +.input { + box-shadow: inset 2px 2px 4px rgba(165, 163, 163, 0.15), + 2px 2px 4px rgba(218, 218, 218, 0.1); +} + +/* 验证码区域 */ +.captcha-block { display: flex; - gap: 12px; align-items: center; + gap: 12px; + flex-wrap: nowrap; +} + +.captcha-block .captcha-input { + flex: 1; + width: auto; + min-width: 0; +} + +.captcha-block label { + left: 14px; + top: 37%; +} + +.captcha-block .input:focus + label, +.captcha-block .input:valid + label { + transform: translateY(-120%) scale(0.9); } .captcha-image-wrapper { @@ -282,21 +283,25 @@ body { display: flex; align-items: center; justify-content: center; - width: 120px; + width: 110px; height: 48px; - background: #f8fafc; - border: 2px solid #e2e8f0; - border-radius: 12px; + background: rgba(255, 255, 255, 0.95); + border: 1px solid rgba(200, 200, 200, 0.4); + border-radius: 0.5rem; overflow: hidden; flex-shrink: 0; cursor: pointer; transition: all 0.3s ease; + margin-top: 0; + box-shadow: inset 2px 2px 4px rgba(165, 163, 163, 0.15), + 2px 2px 6px rgba(218, 218, 218, 0.15); } .captcha-image-wrapper:hover { - border-color: #3b82f6; - box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); - transform: scale(1.02); + border-color: #5e7eb6; + box-shadow: 0 0 0 3px rgba(94, 126, 182, 0.15), + inset 2px 2px 4px rgba(165, 163, 163, 0.1); + transform: scale(1.03); } .captcha-image-wrapper:active { @@ -304,63 +309,57 @@ body { } .captcha-image-wrapper.refreshing { - animation: rotate 0.3s ease; + animation: captcha-refresh 0.4s ease; } -@keyframes rotate { - from { - transform: rotate(0deg); - } - to { - transform: rotate(180deg); - } +@keyframes captcha-refresh { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(0.95); opacity: 0.7; } + 100% { transform: scale(1); opacity: 1; } } .captcha-image { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; display: block; pointer-events: none; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); } -/* 错误消息 */ -.error-message { - background: #fef2f2; - border: 1px solid #fecaca; - color: #dc2626; - padding: 12px 16px; - border-radius: 10px; - font-size: 14px; - margin-bottom: 20px; - animation: shake 0.4s ease; +/* 底部链接 */ +.forgot { + display: block; + margin: 5px 0 10px 0; + color: rgba(255, 255, 255, 0.9); + font-size: 0.85em; } -@keyframes shake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-10px); } - 75% { transform: translateX(10px); } +.forgot a { + color: rgba(255, 255, 255, 0.9); + text-decoration: none; + transition: color 0.3s ease; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15); +} + +.forgot a:hover { + color: #ffffff; } /* 登录按钮 */ -.login-btn { - width: 100%; - padding: 16px; - font-size: 16px; +button { + background-color: #5e7eb6; + color: white; + font-size: 1em; font-weight: 600; - font-family: inherit; - color: #ffffff; - background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); - border: none; - border-radius: 12px; + box-shadow: 2px 4px 12px rgba(94, 126, 182, 0.3); cursor: pointer; - transition: all 0.3s ease; - box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3); + border: none; position: relative; overflow: hidden; } -.login-btn::before { +button::before { content: ''; position: absolute; top: 0; @@ -371,25 +370,27 @@ body { transition: left 0.5s ease; } -.login-btn:hover { +button:hover { + background-color: #4a6da8; transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(59, 130, 246, 0.4); + box-shadow: 2px 6px 16px rgba(94, 126, 182, 0.4); } -.login-btn:hover::before { +button:hover::before { left: 100%; } -.login-btn:active { +button:active { transform: translateY(0); } -.login-btn:disabled { +button:disabled { opacity: 0.7; cursor: not-allowed; transform: none; } +/* 加载动画 */ .btn-loader { display: flex; gap: 6px; @@ -424,47 +425,86 @@ body { } } -/* 页脚 */ -.login-footer { - text-align: center; - padding-top: 24px; - border-top: 1px solid #e2e8f0; +/* 错误消息 */ +.error-message { + background: rgba(254, 242, 242, 0.95); + border: 1px solid #fecaca; + color: #dc2626; + padding: 10px 14px; + border-radius: 8px; + font-size: 13px; + margin-bottom: 10px; + animation: shake 0.4s ease; } -.login-footer p { - font-size: 13px; - color: #64748b; - margin: 0; +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-8px); } + 75% { transform: translateX(8px); } +} + +/* 右侧插图区域 */ +.right { + width: 40%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.img { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.img svg { + max-width: 100%; + max-height: 100%; + filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.1)); } /* 响应式设计 */ @media (max-width: 768px) { - .login-card { - padding: 32px 24px; - max-width: 100%; - margin: 20px; + .container { + flex-direction: column; + width: 95%; + height: auto; + max-height: 90vh; } - .system-title { - font-size: 24px; + .left { + width: 100%; + padding: 30px 25px; } - .logo-icon { - font-size: 40px; - } -} - -@media (max-width: 480px) { - .login-card { - padding: 24px 20px; + .right { + display: none; } .system-title { font-size: 20px; } - .logo-container { - flex-direction: column; - gap: 8px; + .container::after { + display: none; + } +} + +@media (max-width: 480px) { + .left { + padding: 25px 20px; + } + + .system-title { + font-size: 18px; + } + + .input, button { + padding: 10px 12px; + font-size: 0.95em; } } diff --git a/frontend/assets/styles.css b/frontend/assets/styles.css index 021458d..adebfc0 100644 --- a/frontend/assets/styles.css +++ b/frontend/assets/styles.css @@ -1167,6 +1167,8 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); border-radius: 20px; border: 1px solid var(--border); + display: flex; + flex-direction: column; } .metrics-title { display: flex; @@ -1205,7 +1207,8 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ .metrics-data { display: flex; flex-direction: column; - justify-content: flex-start; + justify-content: space-between; + flex: 1; } .metrics-value { margin: 0.75rem 0; @@ -1378,3 +1381,63 @@ input[type="date"]::-webkit-calendar-picker-indicator:hover{ animation: truck-road-anim 1.4s linear infinite; fill: #282828; } + +/* ==================== 顶部导航栏样式 ==================== */ +#app{flex-direction:column} +.topbar{display:flex;align-items:center;justify-content:space-between;height:56px;padding:0 20px;background:linear-gradient(90deg,var(--surface),var(--surface-2));border-bottom:1px solid var(--border);flex-shrink:0;z-index:100} +.topbar-left{display:flex;align-items:center;gap:24px} +.topbar-right{display:flex;align-items:center;gap:16px} +.brand{display:flex;align-items:center;gap:10px} +.brand-name{font-size:16px;font-weight:700;color:var(--text);white-space:nowrap} +.topnav{display:flex;align-items:center;gap:4px} +.topnav-item{position:relative;display:flex;align-items:center;gap:4px;padding:8px 14px;color:var(--text-2);text-decoration:none;font-size:14px;font-weight:500;border-radius:6px;cursor:pointer;transition:all 0.2s ease;white-space:nowrap} +.topnav-item:hover,.topnav-item.active{background:rgba(79,140,255,0.1);color:var(--text)} +.topnav-item.active{color:var(--primary);background:rgba(79,140,255,0.15)} +.topnav-caret{font-size:10px;transition:transform 0.2s ease} +.topnav-item.has-dropdown:hover .topnav-caret,.topnav-item.has-dropdown.open .topnav-caret{transform:rotate(180deg)} +.topnav-dropdown{position:absolute;top:100%;left:0;min-width:180px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);padding:6px 0;opacity:0;visibility:hidden;transform:translateY(8px);transition:all 0.2s ease;z-index:1000} +.topnav-item.has-dropdown:hover .topnav-dropdown,.topnav-item.has-dropdown.open .topnav-dropdown{opacity:1;visibility:visible;transform:translateY(0)} +.dropdown-item{display:block;padding:10px 16px;color:var(--text-2);text-decoration:none;font-size:13px;transition:all 0.15s ease} +.dropdown-item:hover{background:rgba(79,140,255,0.1);color:var(--text)} +.dropdown-item.active{color:var(--primary);background:rgba(79,140,255,0.08)} +.content.topbar-layout{flex:1;display:flex;flex-direction:column;overflow:hidden;height:calc(100vh - 56px)} +.content.topbar-layout .view{flex:1;overflow:auto;padding:20px} +[data-theme="light"] .topnav-dropdown{box-shadow:0 8px 24px rgba(0,0,0,0.1)} +.topbar .bb8-toggle{transform:scale(0.45);transform-origin:center} +.topbar .notification-bell{position:relative;background:none;border:none;cursor:pointer;font-size:20px;padding:8px} +.topbar .user-avatar-btn{display:flex;align-items:center;gap:8px;background:none;border:none;cursor:pointer;padding:4px 8px;border-radius:20px;transition:background 0.2s} +.topbar .user-avatar-btn:hover{background:rgba(79,140,255,0.1)} +.topbar .user-avatar-img{width:32px;height:32px;border-radius:50%;object-fit:cover} +.topbar .user-name-display{color:var(--text);font-size:14px;font-weight:500} +.topbar .user-dropdown{position:absolute;top:100%;right:0;margin-top:8px;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:8px;box-shadow:0 8px 24px rgba(0,0,0,0.3);z-index:1000} +.topbar .user-menu-container{position:relative} +.topbar-icon-btn{display:flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:8px;color:var(--text-2);text-decoration:none;transition:all 0.2s ease} +.topbar-icon-btn:hover{background:rgba(79,140,255,0.1);color:var(--text)} +.topbar-icon-btn svg{width:20px;height:20px} + +/* ==================== 产品介绍页面 ==================== */ +.product-intro-page{max-width:1000px;margin:0 auto;padding:20px} +.product-intro-header{text-align:center;margin-bottom:40px} +.product-intro-header h1{font-size:28px;font-weight:700;color:var(--text);margin-bottom:8px} +.product-intro-header .subtitle{font-size:16px;color:var(--text-2)} +.product-cards{display:flex;flex-direction:column;gap:24px} +.product-card{background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:32px;display:flex;gap:32px;align-items:center;flex-wrap:wrap} +.product-card.main-product{flex-direction:row} +.product-image-wrapper{flex:0 0 300px;display:flex;justify-content:center;align-items:center;background:linear-gradient(135deg,#f8fafc,#e2e8f0);border-radius:12px;padding:24px;min-height:300px} +[data-theme="dark"] .product-image-wrapper{background:linear-gradient(135deg,#1e293b,#334155)} +.product-image{max-width:100%;max-height:280px;object-fit:contain;border-radius:8px} +.product-info{flex:1;min-width:280px} +.product-info h2{font-size:24px;font-weight:600;color:var(--text);margin-bottom:12px} +.product-desc{font-size:15px;color:var(--text-2);line-height:1.7;margin-bottom:24px} +.product-features{display:grid;grid-template-columns:repeat(2,1fr);gap:16px} +.feature-item{display:flex;align-items:center;gap:10px;padding:12px 16px;background:var(--bg);border-radius:10px;transition:all 0.2s} +.feature-item:hover{background:rgba(79,140,255,0.1);transform:translateX(4px)} +.feature-icon{font-size:20px} +.feature-text{font-size:14px;color:var(--text);font-weight:500} +.product-specs{margin-top:32px;background:var(--surface);border:1px solid var(--border);border-radius:16px;padding:24px} +.product-specs h3{font-size:18px;font-weight:600;color:var(--text);margin-bottom:20px} +.specs-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px} +.spec-item{display:flex;justify-content:space-between;padding:12px 16px;background:var(--bg);border-radius:8px} +.spec-label{font-size:14px;color:var(--text-2)} +.spec-value{font-size:14px;color:var(--text);font-weight:500} +@media(max-width:768px){.product-card{flex-direction:column}.product-image-wrapper{flex:none;width:100%}.product-features{grid-template-columns:1fr}} diff --git a/frontend/assets/tuxi-station.png b/frontend/assets/tuxi-station.png new file mode 100644 index 0000000..82768b2 Binary files /dev/null and b/frontend/assets/tuxi-station.png differ diff --git a/frontend/index.html b/frontend/index.html index d313784..11d77b8 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -13,201 +13,84 @@
- -
-
- -
- +
+ -
- - -
+
+ +
@@ -316,6 +198,7 @@ + \ No newline at end of file diff --git a/frontend/js/app.js b/frontend/js/app.js index f017659..31dcde2 100644 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -32,27 +32,7 @@ // 根据用户角色控制菜单显示 function updateMenuVisibility(user) { - // 查找计划管理菜单组(包含标题"计划管理"的nav-group) - const navGroups = document.querySelectorAll('.nav-group'); - let planMgmtGroup = null; - - navGroups.forEach(group => { - const title = group.querySelector('.nav-group-title'); - if (title && title.textContent.trim() === '计划管理') { - planMgmtGroup = group; - } - }); - - if (planMgmtGroup) { - // 只有超级管理员可以看到计划管理菜单 - if (user && user.role === 'superadmin') { - planMgmtGroup.style.display = ''; - } else { - planMgmtGroup.style.display = 'none'; - } - } - - // 超级管理员专属菜单(系统管理) + // 超级管理员专属菜单 const superadminMenus = document.querySelectorAll('.superadmin-only'); superadminMenus.forEach(menu => { if (user && user.role === 'superadmin') { @@ -61,6 +41,37 @@ menu.style.display = 'none'; } }); + + // 计划管理菜单(顶部导航) + const planMenu = document.querySelector('[data-menu="plan"]'); + if (planMenu) { + if (user && user.role === 'superadmin') { + planMenu.style.display = ''; + } else { + planMenu.style.display = 'none'; + } + } + } + + // 高亮当前活动的导航项 + function updateActiveNav(path) { + // 移除所有活动状态 + document.querySelectorAll('.topnav-item').forEach(item => item.classList.remove('active')); + document.querySelectorAll('.dropdown-item').forEach(item => item.classList.remove('active')); + + // 查找匹配的导航项 + const route = path.replace('#', '').replace('/', ''); + const activeItem = document.querySelector(`.topnav-item[data-route="${route}"]`) || + document.querySelector(`.dropdown-item[data-route="${route}"]`); + + if (activeItem) { + activeItem.classList.add('active'); + // 如果是下拉菜单项,也高亮父菜单 + const parentDropdown = activeItem.closest('.topnav-item.has-dropdown'); + if (parentDropdown) { + parentDropdown.classList.add('active'); + } + } } // 创建水印 @@ -127,6 +138,9 @@ // 根据用户角色控制菜单显示 updateMenuVisibility(currentUser); + // 高亮当前导航项 + updateActiveNav(path); + // 初始化通知系统(超级管理员和管理员) if (currentUser && (currentUser.role === 'superadmin' || currentUser.role === 'admin') && window.NotificationSystem) { window.NotificationSystem.init(); @@ -186,11 +200,29 @@ .catch(() => {}); }); - // 菜单切换 - const toggleBtn = document.getElementById('menu-toggle'); - const sidebar = document.querySelector('.sidebar'); - toggleBtn?.addEventListener('click', () => { - sidebar.classList.toggle('open'); + // 顶部导航下拉菜单点击切换(移动端或点击常显示) + document.querySelectorAll('.topnav-item.has-dropdown').forEach(item => { + item.addEventListener('click', (e) => { + // 如果点击的是下拉菜单内的链接,不处理 + if (e.target.closest('.topnav-dropdown')) return; + + // 切换open状态 + const isOpen = item.classList.contains('open'); + // 关闭其他打开的菜单 + document.querySelectorAll('.topnav-item.has-dropdown.open').forEach(other => { + if (other !== item) other.classList.remove('open'); + }); + item.classList.toggle('open', !isOpen); + }); + }); + + // 点击页面其他地方关闭下拉菜单 + document.addEventListener('click', (e) => { + if (!e.target.closest('.topnav-item.has-dropdown')) { + document.querySelectorAll('.topnav-item.has-dropdown.open').forEach(item => { + item.classList.remove('open'); + }); + } }); // 主题切换 diff --git a/frontend/js/components/dashboard.js b/frontend/js/components/dashboard.js index e1f1a16..7f7c285 100644 --- a/frontend/js/components/dashboard.js +++ b/frontend/js/components/dashboard.js @@ -5,9 +5,68 @@ const Dashboard = (() => { '直通良品率': '', '良品率': '', '发货数量': truckIcon, - '不良数量': '' + '不良数量': '', + '良品/不良率': '', + '今日产量': '' }; + // 合并良品率/不良率卡片 + function rateCard(goodRate, badRate) { + const icon = metricsIcons['良品/不良率']; + const goodNum = parseFloat(String(goodRate).replace('%', '')) || 0; + const badNum = parseFloat(String(badRate).replace('%', '')) || 0; + + return `
+
+ + ${icon} + +

良品/不良率

+

+ + + +

+
+
+

${goodRate}/${badRate}

+
+
+
+
+
`; + } + + // 今日产量卡片(拼多多/圆通自动切换) + function todayProductionCard(todayPdd, todayYt, activePlatform) { + const isPdd = activePlatform === 'pdd'; + const platformName = isPdd ? '拼多多' : '圆通'; + const platformIcon = isPdd ? '' : ''; + const value = isPdd ? todayPdd : todayYt; + const color = isPdd ? { bg: '#e02e24', text: '#e02e24' } : { bg: '#1a6dd6', text: '#1a6dd6' }; + const fillWidth = Math.min((value / 10000) * 100, 100); + + return `
+
+ + ${platformIcon} + +

今日${platformName}

+

+ + + +

+
+
+

${value}

+
+
+
+
+
`; + } + function metricsCard(title, value, badgeClass) { const colors = { success: { bg: '#10B981', text: '#02972f' }, @@ -290,7 +349,10 @@ const Dashboard = (() => { const maxCount = Math.max(...pddCounts, ...ytCounts, 1); // 绘制参数(使用逻辑尺寸而非物理像素) - const padding = {left: 40, right: 20, top: 20, bottom: 30}; + // 根据最大值动态调整左边距,确保Y轴标签不被截断 + const maxDigits = Math.max(maxCount.toString().length, 2); + const dynamicLeftPadding = Math.max(50, 25 + maxDigits * 10); + const padding = {left: dynamicLeftPadding, right: 20, top: 20, bottom: 30}; const chartWidth = rect.width - padding.left - padding.right; const chartHeight = rect.height - padding.top - padding.bottom; @@ -340,52 +402,59 @@ const Dashboard = (() => { } }); - // 绘制折线 + // 绘制柔和曲线(智能避免标签重叠) const today = new Date().toISOString().split('T')[0]; - const drawLine = (counts, color) => { + + // 收集今日标签位置 + const todayLabels = []; + const drawLineWithLabels = (counts, color, labelKey) => { ctx.strokeStyle = color; - ctx.lineWidth = 2; + ctx.lineWidth = 2.5; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; ctx.beginPath(); - counts.forEach((count, i) => { - const x = padding.left + (chartWidth / (timePoints.length - 1)) * i; - const y = padding.top + chartHeight - (count / maxCount) * chartHeight; - if(i === 0) ctx.moveTo(x, y); - else ctx.lineTo(x, y); - }); + + const points = counts.map((count, i) => ({ + x: padding.left + (chartWidth / (timePoints.length - 1)) * i, + y: padding.top + chartHeight - (count / maxCount) * chartHeight + })); + + ctx.moveTo(points[0].x, points[0].y); + for(let i = 1; i < points.length; i++) { + const prev = points[i - 1]; + const curr = points[i]; + const next = points[i + 1] || curr; + const prevPrev = points[i - 2] || prev; + const tension = 0.3; + const cp1x = prev.x + (curr.x - prevPrev.x) * tension; + const cp1y = prev.y + (curr.y - prevPrev.y) * tension; + const cp2x = curr.x - (next.x - prev.x) * tension; + const cp2y = curr.y - (next.y - prev.y) * tension; + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, curr.x, curr.y); + } ctx.stroke(); - // 绘制数据点 + // 绘制数据点并收集今日标签 counts.forEach((count, i) => { const x = padding.left + (chartWidth / (timePoints.length - 1)) * i; const y = padding.top + chartHeight - (count / maxCount) * chartHeight; const isToday = timeRange === 'day' && timePoints[i] === today; if(isToday) { - // 今日数据点:更大的圆圈 + 外圈光晕 ctx.beginPath(); ctx.arc(x, y, 10, 0, Math.PI * 2); - ctx.fillStyle = color.replace(')', ', 0.2)').replace('rgb', 'rgba').replace('#', ''); - ctx.fillStyle = color + '33'; // 添加透明度 + ctx.fillStyle = color + '33'; ctx.fill(); - ctx.beginPath(); ctx.arc(x, y, 6, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); - - // 白色内圈 ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); - - // 显示今日数值标签 - ctx.fillStyle = color; - ctx.font = 'bold 12px sans-serif'; - ctx.textAlign = 'center'; - ctx.fillText(count.toString(), x, y - 14); + todayLabels.push({x, y, count, color, key: labelKey}); } else { - // 普通数据点 ctx.beginPath(); ctx.arc(x, y, 3, 0, Math.PI * 2); ctx.fillStyle = color; @@ -394,8 +463,42 @@ const Dashboard = (() => { }); }; - drawLine(pddCounts, '#f59e0b'); - drawLine(ytCounts, '#3b82f6'); + drawLineWithLabels(pddCounts, '#f59e0b', 'pdd'); + drawLineWithLabels(ytCounts, '#3b82f6', 'yt'); + + // 智能绘制今日标签,显示在高亮点的左边(数字为0时不显示) + const nonZeroLabels = todayLabels.filter(label => label.count > 0); + if(nonZeroLabels.length === 2) { + const [a, b] = nonZeroLabels; + const yDiff = Math.abs(a.y - b.y); + + if(yDiff < 20) { + // 两个标签太近,垂直错开显示在左边 + const upper = a.y < b.y ? a : b; + const lower = a.y < b.y ? b : a; + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillStyle = upper.color; + ctx.fillText(upper.count.toString(), upper.x - 14, upper.y - 8); + ctx.fillStyle = lower.color; + ctx.fillText(lower.count.toString(), lower.x - 14, lower.y + 8); + } else { + // 正常显示在左边 + nonZeroLabels.forEach(label => { + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillStyle = label.color; + ctx.fillText(label.count.toString(), label.x - 14, label.y + 4); + }); + } + } else { + nonZeroLabels.forEach(label => { + ctx.font = 'bold 12px sans-serif'; + ctx.textAlign = 'right'; + ctx.fillStyle = label.color; + ctx.fillText(label.count.toString(), label.x - 14, label.y + 4); + }); + } // 绘制图例 ctx.font = '12px sans-serif'; @@ -639,7 +742,7 @@ const Dashboard = (() => { try { // 请求全部数据(不限制),传递AbortController信号 const signal = window.__auditAbortController.signal; - const [pddRes, ytRes] = await Promise.all([ + const [pddRes, ytRes, dashRes] = await Promise.all([ fetch('/api/audit/pdd', { headers: { 'Content-Type': 'application/json' }, credentials: 'include', @@ -655,9 +758,60 @@ const Dashboard = (() => { }).then(r => r.ok ? r.json() : {list:[]}).catch((e)=>{ if(e.name === 'AbortError') console.log('[Dashboard] YT请求被取消'); return {list:[]}; + }), + fetch('/api/dashboard', { + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + signal + }).then(r => r.ok ? r.json() : null).catch((e)=>{ + if(e.name === 'AbortError') console.log('[Dashboard] Dashboard请求被取消'); + return null; }) ]); + // 实时更新良品/不良率卡片和今日产量卡片 + if(dashRes) { + // 更新良品/不良率 + const goodRateEl = document.getElementById('good-rate-value'); + const badRateEl = document.getElementById('bad-rate-value'); + if(goodRateEl) goodRateEl.textContent = dashRes.goodRate || '—'; + if(badRateEl) badRateEl.textContent = dashRes.badRate || '—'; + + // 更新今日产量卡片 + const productionCard = document.getElementById('today-production-card'); + const platformNameEl = document.getElementById('today-platform-name'); + const productionValueEl = document.getElementById('today-production-value'); + const productionFillEl = document.getElementById('today-production-fill'); + + if(productionCard && platformNameEl && productionValueEl && productionFillEl) { + const currentPlatform = productionCard.dataset.platform; + const newPlatform = dashRes.activePlatform || 'pdd'; + const isPdd = newPlatform === 'pdd'; + const value = isPdd ? (dashRes.todayPdd || 0) : (dashRes.todayYt || 0); + const platformName = isPdd ? '拼多多' : '圆通'; + const color = isPdd ? '#e02e24' : '#1a6dd6'; + const fillWidth = Math.min((value / 10000) * 100, 100); + + // 如果平台切换了,更新图标 + if(currentPlatform !== newPlatform) { + productionCard.dataset.platform = newPlatform; + const iconEl = productionCard.querySelector('.metrics-icon'); + if(iconEl) { + iconEl.innerHTML = isPdd + ? '' + : ''; + } + const percentEl = productionCard.querySelector('.metrics-percent'); + if(percentEl) percentEl.style.color = color; + } + + platformNameEl.textContent = '今日' + platformName; + productionValueEl.textContent = value; + productionFillEl.style.width = fillWidth + '%'; + productionFillEl.style.backgroundColor = color; + } + } + clearTimeout(timeoutId); const duration = Date.now() - startTime; if(duration > 3000){ @@ -733,9 +887,9 @@ const Dashboard = (() => {
${metricsCard('直通良品率', data.fpyRate || '—', 'success')} - ${metricsCard('良品率', data.goodRate, 'success')} + ${rateCard(data.goodRate || '—', data.badRate || '—')} ${metricsCard('发货数量', data.shipments, 'warning')} - ${metricsCard('不良数量', data.badCount ?? data.defects, 'danger')} + ${todayProductionCard(data.todayPdd || 0, data.todayYt || 0, data.activePlatform || 'pdd')}
diff --git a/frontend/js/components/product-intro.js b/frontend/js/components/product-intro.js new file mode 100644 index 0000000..c24d34b --- /dev/null +++ b/frontend/js/components/product-intro.js @@ -0,0 +1,78 @@ +// 产品介绍页面 +Router.register('/product-intro', async () => { + setTimeout(() => { + if(window.ProductIntroComponent.afterRender) { + window.ProductIntroComponent.afterRender(); + } + }, 0); + return window.ProductIntroComponent.render(); +}); + +window.ProductIntroComponent = { + render: () => { + return ` +
+
+

产品介绍

+

兔喜驿站智能找灯系统

+
+ +
+
+
+ 兔喜基站 +
+
+

智能基站

+

兔喜驿站找灯条对应快递的基站设备,用于控制货架上的LED灯条,帮助快速定位快递包裹位置。

+
+
+ 📡 + 蓝牙信号覆盖 +
+
+ 💡 + 精准灯条控制 +
+
+ + 快速响应 +
+
+ 🔧 + 易于安装 +
+
+
+
+
+ +
+

技术规格

+
+
+ 品牌 + 兔喜生活 +
+
+ 类型 + 智能基站 +
+
+ 应用场景 + 快递驿站 +
+
+ 功能 + 灯条定位控制 +
+
+
+
+ `; + }, + + afterRender: () => { + // 页面渲染后的逻辑 + } +}; diff --git a/frontend/login.html b/frontend/login.html index 3190787..89c1b96 100644 --- a/frontend/login.html +++ b/frontend/login.html @@ -11,109 +11,87 @@ -