1
This commit is contained in:
+41
-41
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -28,8 +28,8 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/assets/index-tvhdMiao.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D0-PljDg.css">
|
||||
<script type="module" crossorigin src="/assets/index-4U-_hqyL.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-eLRg4pQk.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -241,43 +241,11 @@
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.login-left-section {
|
||||
padding: 20px 12px 12px;
|
||||
padding: 24px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.login-right-section {
|
||||
padding: 12px 12px 24px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background-attachment: scroll;
|
||||
background-position: center top;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 100% !important;
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
|
||||
.login-card .ant-card-body {
|
||||
padding: 20px 16px !important;
|
||||
}
|
||||
|
||||
.shiny-text-container {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.shiny-text {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.login-brand-name {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,6 @@ const AppLayout: React.FC = () => {
|
||||
const [rechargeOptions, setRechargeOptions] = useState<any[]>([]);
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
|
||||
const [unreadCount, setUnreadCount] = useState(0);
|
||||
const [mobileExpandedParent, setMobileExpandedParent] = useState<MenuConfig | null>(null);
|
||||
const [siteName, setSiteName] = useState(() => {
|
||||
const cached = localStorage.getItem('siteInfo');
|
||||
const name = cached ? JSON.parse(cached).siteName || '' : '';
|
||||
@@ -284,24 +283,6 @@ const AppLayout: React.FC = () => {
|
||||
const selectedKey = location.pathname.startsWith('/records') ? '/records' : location.pathname;
|
||||
const sidebarW = SIDEBAR_W;
|
||||
|
||||
// Compute menu hierarchy (used by both desktop sidebar and mobile nav)
|
||||
const childMap: Record<string, MenuConfig[]> = {};
|
||||
menuItems.forEach(m => {
|
||||
const pid = m.parent_id ?? m.parentId;
|
||||
if (pid) {
|
||||
if (!childMap[pid]) childMap[pid] = [];
|
||||
childMap[pid].push(m);
|
||||
}
|
||||
});
|
||||
const topLevelItems = menuItems.filter(m => !(m.parent_id ?? m.parentId));
|
||||
topLevelItems.sort((a, b) => {
|
||||
const orderA = typeof a.sortOrder === 'number' ? a.sortOrder : Infinity;
|
||||
const orderB = typeof b.sortOrder === 'number' ? b.sortOrder : Infinity;
|
||||
return orderA - orderB;
|
||||
});
|
||||
// For mobile nav: show top-level items that either have a path or have children
|
||||
const topLevelForNav = topLevelItems.filter(m => m.path || (childMap[m.id] && childMap[m.id].length > 0));
|
||||
|
||||
const userMenuItems = [
|
||||
{ key: 'profile', icon: <UserOutlined />, label: `账号: ${user?.username}`, disabled: true },
|
||||
{ key: 'credits', icon: <WalletOutlined style={{ color: '#c9a96e' }} />, label: `积分: ${user?.credits ?? 0}`, disabled: true },
|
||||
@@ -480,7 +461,23 @@ const AppLayout: React.FC = () => {
|
||||
{/* Menu */}
|
||||
<div style={{ flex: 1, padding: '8px 8px', overflow: 'auto' }}>
|
||||
{(() => {
|
||||
// childMap, topLevelItems are computed at component level above
|
||||
const childMap: Record<string, MenuConfig[]> = {};
|
||||
menuItems.forEach(m => {
|
||||
const pid = m.parent_id ?? m.parentId;
|
||||
if (pid) {
|
||||
if (!childMap[pid]) childMap[pid] = [];
|
||||
childMap[pid].push(m);
|
||||
}
|
||||
});
|
||||
|
||||
const topLevelItems = menuItems.filter(m => !(m.parent_id ?? m.parentId));
|
||||
|
||||
topLevelItems.sort((a, b) => {
|
||||
const orderA = typeof a.sortOrder === 'number' ? a.sortOrder : Infinity;
|
||||
const orderB = typeof b.sortOrder === 'number' ? b.sortOrder : Infinity;
|
||||
return orderA - orderB;
|
||||
});
|
||||
|
||||
const items: React.ReactNode[] = [];
|
||||
|
||||
const renderMenuItem = (item: MenuConfig, depth: number = 0) => {
|
||||
@@ -713,77 +710,27 @@ const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Bottom Nav - Two Level */}
|
||||
<div className="mobile-bottom-nav-wrapper">
|
||||
{/* Sub-menu panel (level-2) */}
|
||||
{mobileExpandedParent && (
|
||||
<div className="mobile-submenu-panel">
|
||||
<div className="mobile-submenu-header" onClick={() => setMobileExpandedParent(null)}>
|
||||
<LeftOutlined style={{ fontSize: 16, color: '#6366f1' }} />
|
||||
<span className="mobile-submenu-title">{mobileExpandedParent.label}</span>
|
||||
{/* Mobile Bottom Nav */}
|
||||
<div className="mobile-bottom-nav">
|
||||
{menuItems.filter((item) => (item.menu_type ?? item.menuType) !== 'group').map((item) => {
|
||||
if (!item.path) return null;
|
||||
const isActive = item.path === selectedKey;
|
||||
return (
|
||||
<div key={item.id}
|
||||
className={`nav-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => navigate(item.path)}>
|
||||
<span className="nav-icon">{iconMap[item.icon] || <HomeOutlined />}</span>
|
||||
<span>{item.label}</span>
|
||||
</div>
|
||||
<div className="mobile-submenu-list">
|
||||
{(childMap[mobileExpandedParent.id] || []).sort((a, b) => {
|
||||
const orderA = typeof a.sortOrder === 'number' ? a.sortOrder : Infinity;
|
||||
const orderB = typeof b.sortOrder === 'number' ? b.sortOrder : Infinity;
|
||||
return orderA - orderB;
|
||||
}).map(child => (
|
||||
<div
|
||||
key={child.id}
|
||||
className={`mobile-submenu-item ${child.path === selectedKey ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (child.path) {
|
||||
navigate(child.path);
|
||||
setMobileExpandedParent(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="mobile-submenu-icon">{iconMap[child.icon] || <RightOutlined />}</span>
|
||||
<span>{child.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom nav (level-1) */}
|
||||
<div className="mobile-bottom-nav">
|
||||
{topLevelForNav.map((item) => {
|
||||
const hasChildren = childMap[item.id] && childMap[item.id].length > 0;
|
||||
const isActive = mobileExpandedParent?.id === item.id
|
||||
|| (!mobileExpandedParent && item.path === selectedKey)
|
||||
|| (!mobileExpandedParent && hasChildren && (childMap[item.id] || []).some(c => c.path === selectedKey));
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`nav-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => {
|
||||
if (hasChildren) {
|
||||
setMobileExpandedParent(prev => prev?.id === item.id ? null : item);
|
||||
} else if (item.path) {
|
||||
navigate(item.path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="nav-icon">{iconMap[item.icon] || <HomeOutlined />}</span>
|
||||
<span>{item.label}</span>
|
||||
{hasChildren && (
|
||||
<span className="nav-expand-dot" style={{
|
||||
width: 4, height: 4, borderRadius: '50%',
|
||||
background: isActive ? '#6366f1' : '#cbd5e1',
|
||||
}} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="nav-item" onClick={handleMobileRecharge}>
|
||||
<span className="nav-icon"><PlusCircleOutlined /></span>
|
||||
<span>充值</span>
|
||||
</div>
|
||||
<div className="nav-item" onClick={handleLogout}>
|
||||
<span className="nav-icon"><LogoutOutlined /></span>
|
||||
<span>退出</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="nav-item" onClick={handleMobileRecharge}>
|
||||
<span className="nav-icon"><PlusCircleOutlined /></span>
|
||||
<span>充值</span>
|
||||
</div>
|
||||
<div className="nav-item" onClick={handleLogout}>
|
||||
<span className="nav-icon"><LogoutOutlined /></span>
|
||||
<span>退出</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -213,12 +213,9 @@ html, body {
|
||||
}
|
||||
|
||||
/* ── Mobile Bottom Nav ─────────────────── */
|
||||
.mobile-bottom-nav-wrapper {
|
||||
display: none;
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
|
||||
}
|
||||
.mobile-bottom-nav {
|
||||
display: none;
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 200;
|
||||
height: 60px;
|
||||
background: rgba(255,255,255,0.95);
|
||||
backdrop-filter: blur(24px);
|
||||
@@ -231,7 +228,6 @@ html, body {
|
||||
cursor: pointer; transition: all 0.2s;
|
||||
color: #8b8fa3; font-size: 9px; font-weight: 400;
|
||||
letter-spacing: 0;
|
||||
position: relative;
|
||||
}
|
||||
.mobile-bottom-nav .nav-item.active {
|
||||
color: #08080c;
|
||||
@@ -242,87 +238,12 @@ html, body {
|
||||
.mobile-bottom-nav .nav-item.active .nav-icon {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.mobile-bottom-nav .nav-item .nav-expand-dot {
|
||||
position: absolute; top: 6px; right: calc(50% - 18px);
|
||||
}
|
||||
|
||||
/* ── Mobile Submenu Panel ───────────────── */
|
||||
.mobile-submenu-panel {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 0; right: 0;
|
||||
background: rgba(255,255,255,0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
box-shadow: 0 -8px 32px rgba(0,0,0,0.1);
|
||||
border: 1px solid rgba(0,0,0,0.05);
|
||||
border-bottom: none;
|
||||
max-height: 50vh;
|
||||
overflow: hidden;
|
||||
animation: submenuSlideUp 0.25s ease-out;
|
||||
}
|
||||
@keyframes submenuSlideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.mobile-submenu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.mobile-submenu-header:hover {
|
||||
background: rgba(99,102,241,0.04);
|
||||
}
|
||||
.mobile-submenu-title {
|
||||
flex: 1;
|
||||
}
|
||||
.mobile-submenu-list {
|
||||
padding: 8px 12px 16px;
|
||||
overflow-y: auto;
|
||||
max-height: calc(50vh - 48px);
|
||||
}
|
||||
.mobile-submenu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #475569;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.mobile-submenu-item:hover {
|
||||
background: rgba(99,102,241,0.06);
|
||||
}
|
||||
.mobile-submenu-item.active {
|
||||
background: linear-gradient(135deg, rgba(99,102,241,0.08) 0%, rgba(139,92,246,0.06) 100%);
|
||||
color: #4f46e5;
|
||||
font-weight: 500;
|
||||
}
|
||||
.mobile-submenu-icon {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.mobile-submenu-item.active .mobile-submenu-icon {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* ── Responsive Breakpoints ────────────── */
|
||||
@media (max-width: 768px) {
|
||||
.desktop-sidebar { display: none !important; }
|
||||
.desktop-content { margin-left: 0 !important; padding: 16px !important; padding-bottom: 80px !important; overflow-x: hidden !important; }
|
||||
.desktop-sidebar-toggle-zone { display: none !important; }
|
||||
.mobile-bottom-nav-wrapper { display: block !important; }
|
||||
.mobile-bottom-nav { display: flex !important; }
|
||||
|
||||
/* Prevent horizontal overflow globally */
|
||||
@@ -446,8 +367,6 @@ html, body {
|
||||
.mobile-bottom-nav { height: 56px !important; }
|
||||
.mobile-bottom-nav .nav-item .nav-icon { font-size: 18px !important; }
|
||||
.mobile-bottom-nav .nav-item { font-size: 9px !important; gap: 2px !important; }
|
||||
.mobile-submenu-panel { bottom: 56px !important; }
|
||||
.mobile-submenu-item { padding: 10px 14px !important; font-size: 13px !important; }
|
||||
|
||||
/* InitialReplication page - Small Mobile */
|
||||
.replication-container {
|
||||
|
||||
@@ -313,7 +313,7 @@ const LoginPage: React.FC = () => {
|
||||
<div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 16 }}>
|
||||
{siteLogo ? (
|
||||
<img src={siteLogo} alt="logo" className="login-logo" style={{ width: 48, height: 48, borderRadius: 12, objectFit: 'contain' }} />
|
||||
<img src={siteLogo} alt="logo" style={{ width: 48, height: 48, borderRadius: 12, objectFit: 'contain' }} />
|
||||
) : (
|
||||
<div style={{
|
||||
width: 48, height: 48, borderRadius: 12,
|
||||
@@ -324,7 +324,7 @@ const LoginPage: React.FC = () => {
|
||||
<ThunderboltOutlined style={{ fontSize: 24, color: '#fff' }} />
|
||||
</div>
|
||||
)}
|
||||
<span className="login-brand-name" style={{ color: '#1e293b', fontSize: 24, fontWeight: 800, letterSpacing: -0.5 }}>
|
||||
<span style={{ color: '#1e293b', fontSize: 24, fontWeight: 800, letterSpacing: -0.5 }}>
|
||||
{siteName}
|
||||
</span>
|
||||
</div>
|
||||
@@ -371,7 +371,7 @@ const LoginPage: React.FC = () => {
|
||||
|
||||
{/* Right side - login/register form */}
|
||||
<div className="login-right-section">
|
||||
<Card className="login-card" style={{
|
||||
<Card style={{
|
||||
width: '100%', maxWidth: 400, borderRadius: 16,
|
||||
boxShadow: '0 12px 40px rgba(0,0,0,0.08)',
|
||||
border: '1px solid #e2e8f0', background: '#fff',
|
||||
|
||||
Reference in New Issue
Block a user