Files
simple-memo/static/js/common-components.js
T
sunct 3c3bf53ae4 docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
2026-07-31 14:12:32 +08:00

386 lines
22 KiB
JavaScript

// 公共头部组件渲染函数
function renderCommonHeader(currentPath) {
return `
<header id="commonHeader" style="position: fixed; top: 0; left: 0; right: 0; z-index: 999; background: rgba(255, 255, 255, 0.98); backdrop-filter: blur(20px); box-shadow: 0 1px 3px rgba(0,0,0,0.06);">
<div class="header-content" style="max-width: 1200px; margin: 0 auto; padding: 0 24px; display: flex; align-items: center; height: 64px;">
<a href="/" class="header-logo" style="display: flex; align-items: center; text-decoration: none; margin-right: 40px;">
<img src="${SITE_CONFIG.path}" alt="${SITE_CONFIG.name}" style="height: 36px; width: auto; border-radius: 8px; margin-right: 10px;" onerror="this.style.display='none'">
<span style="font-size: 18px; font-weight: 700; color: ${SITE_CONFIG.brand.primaryColor};">${SITE_CONFIG.name}</span>
</a>
<nav class="nav-menu" style="flex: 1; display: flex; gap: 8px;">
<a href="/" class="nav-link ${currentPath === '/' ? 'active' : ''}" data-path="/" style="padding: 8px 16px; border-radius: 8px; color: #475569; font-size: 14px; font-weight: 500; text-decoration: none; transition: all 0.2s;">首页</a>
<a href="/growth-rules" class="nav-link ${currentPath === '/growth-rules' ? 'active' : ''}" style="padding: 8px 16px; border-radius: 8px; color: #475569; font-size: 14px; font-weight: 500; text-decoration: none; transition: all 0.2s;">成长规则</a>
<a href="/about" class="nav-link ${currentPath === '/about' ? 'active' : ''}" style="padding: 8px 16px; border-radius: 8px; color: #475569; font-size: 14px; font-weight: 500; text-decoration: none; transition: all 0.2s;">关于我们</a>
</nav>
<div id="userMenuContainer" class="user-menu-container" style="display: flex; align-items: center;"></div>
<!-- 移动端菜单按钮 -->
<button id="mobileMenuBtn" class="mobile-menu-btn" style="display: none; flex-direction: column; gap: 4px; padding: 8px; background: none; border: none; cursor: pointer; border-radius: 8px; transition: background 0.2s; margin-left: auto;" aria-label="菜单">
<span style="width: 24px; height: 2px; background: #475569; border-radius: 1px; transition: all 0.2s;"></span>
<span style="width: 24px; height: 2px; background: #475569; border-radius: 1px; transition: all 0.2s;"></span>
<span style="width: 24px; height: 2px; background: #475569; border-radius: 1px; transition: all 0.2s;"></span>
</button>
</div>
<!-- 移动端下拉菜单遮罩 -->
<div id="mobileMenuOverlay" style="display: none; position: fixed; top: 64px; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 998; backdrop-filter: blur(4px);"></div>
<!-- 移动端下拉菜单 -->
<div id="mobileDropdownMenu" style="display: none; position: absolute; top: 64px; left: 0; right: 0; background: #fff; box-shadow: 0 8px 32px rgba(0,0,0,0.12); z-index: 999; max-height: calc(100vh - 64px); overflow-y: auto; animation: slideDown 0.3s ease-out;">
<div style="padding: 16px 0;">
<!-- Logo区域 -->
<div style="display: flex; align-items: center; padding: 0 20px 16px; border-bottom: 1px solid #f1f5f9;">
<img src="${SITE_CONFIG.path}" alt="${SITE_CONFIG.name}" style="height: 40px; width: auto; border-radius: 8px; margin-right: 12px;" onerror="this.style.display='none'">
<span style="font-size: 18px; font-weight: 700; color: ${SITE_CONFIG.brand.primaryColor};">${SITE_CONFIG.name}</span>
</div>
<!-- 导航链接 -->
<div style="padding: 8px 0;">
<a href="/" class="mobile-nav-item ${currentPath === '/' ? 'active' : ''}" data-path="/" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/></svg>
首页
</a>
<a href="/growth-rules" class="mobile-nav-item ${currentPath === '/growth-rules' ? 'active' : ''}" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/></svg>
成长规则
</a>
<a href="/about" class="mobile-nav-item ${currentPath === '/about' ? 'active' : ''}" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
关于我们
</a>
</div>
<!-- 用户菜单区域 -->
<div id="mobileUserMenu" style="border-top: 1px solid #f1f5f9; margin-top: 8px;"></div>
</div>
</div>
</header>
<style>
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.nav-link:hover, .nav-link.active {
background: rgba(79, 158, 222, 0.1);
color: ${SITE_CONFIG.brand.primaryColor};
}
.mobile-nav-item:hover, .mobile-nav-item.active {
background: rgba(79, 158, 222, 0.06);
color: ${SITE_CONFIG.brand.primaryColor};
}
.dropdown-item:hover {
background: rgba(79, 158, 222, 0.08);
}
.mobile-menu-btn:hover {
background: #f1f5f9;
}
.mobile-menu-btn.active span:nth-child(1) {
transform: rotate(45deg) translateY(6px);
}
.mobile-menu-btn.active span:nth-child(2) {
opacity: 0;
}
.mobile-menu-btn.active span:nth-child(3) {
transform: rotate(-45deg) translateY(-6px);
}
@media (max-width: 768px) {
.header-content {
padding: 0 16px;
height: 56px;
}
.header-logo span {
font-size: 16px;
}
.header-logo img {
height: 32px;
}
.nav-menu {
display: none !important;
}
.user-menu-container {
display: none !important;
}
#mobileMenuBtn {
display: flex !important;
}
#commonHeader {
height: 56px;
}
#mobileDropdownMenu {
top: 56px;
max-height: calc(100vh - 56px);
}
#mobileMenuOverlay {
top: 56px;
}
}
@media (min-width: 769px) {
#mobileMenuBtn, #mobileDropdownMenu, #mobileMenuOverlay {
display: none !important;
}
}
</style>
`;
}
// 用户菜单渲染函数
function renderUserMenu(isLoggedIn, userInfo) {
if (isLoggedIn && userInfo) {
const defaultAvatar = '/static/avatar/default.png';
const avatarSrc = localStorage.getItem('user_avatar') || userInfo.avatar || defaultAvatar;
const nickname = localStorage.getItem('user_nickname') || userInfo.nickname || '用户';
return `
<div class="user-menu" style="position: relative;">
<div class="user-dropdown-trigger" style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(79, 158, 222, 0.08); border-radius: 20px; color: #4F9EDE; cursor: pointer;">
<img src="${avatarSrc}" alt="头像" style="width: 28px; height: 28px; border-radius: 50%; object-fit: cover;" onerror="this.src='${defaultAvatar}';">
<span style="font-size: 13px; font-weight: 500;">${nickname}</span>
<svg style="width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2;" viewBox="0 0 24 24">
<path d="M6 9l6 6 6-6"/>
</svg>
</div>
<div class="user-dropdown-menu" style="position: absolute; top: calc(100% + 8px); right: 0; min-width: 160px; background: #fff; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); padding: 8px 0; opacity: 0; visibility: hidden; transform: translateY(-8px); transition: all 0.2s; z-index: 1000;">
<a href="/web/calendar" class="dropdown-item" style="display: flex; align-items: center; gap: 10px; padding: 10px 16px; text-decoration: none; color: #475569; font-size: 14px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
日历
</a>
<a href="/web/growth" class="dropdown-item" style="display: flex; align-items: center; gap: 10px; padding: 10px 16px; text-decoration: none; color: #475569; font-size: 14px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
成长中心
</a>
<a href="/web/profile" class="dropdown-item" style="display: flex; align-items: center; gap: 10px; padding: 10px 16px; text-decoration: none; color: #475569; font-size: 14px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24"><circle cx="12" cy="8" r="5"/><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>
个人中心
</a>
<div style="height: 1px; background: #e2e8f0; margin: 4px 0;"></div>
<a href="#" id="logoutBtn" class="dropdown-item" style="display: flex; align-items: center; gap: 10px; padding: 10px 16px; text-decoration: none; color: #ef4444; font-size: 14px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg>
退出登录
</a>
</div>
</div>
`;
} else {
return `
<a href="/login" class="login-btn" style="padding: 8px 20px; background: ${SITE_CONFIG.brand.gradient}; border-radius: 20px; color: #fff; font-size: 13px; font-weight: 500; text-decoration: none; transition: all 0.2s;">登录</a>
`;
}
}
// 公共底部渲染函数
function renderCommonFooter() {
return `
<footer id="commonFooter" style="background: #f8fafc; border-top: 1px solid #e2e8f0; padding: 40px 24px;">
<div style="max-width: 1200px; margin: 0 auto;">
<div style="display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 16px;">
<div style="display: flex; align-items: center; gap: 12px;">
<img src="${SITE_CONFIG.path}" alt="${SITE_CONFIG.name}" style="height: 32px; width: auto; border-radius: 6px;" onerror="this.style.display='none'">
<span style="font-size: 14px; font-weight: 600; color: #1e293b;">${SITE_CONFIG.name}</span>
</div>
<div style="display: flex; gap: 24px; flex-wrap: wrap;">
<a href="/agreement" style="font-size: 13px; color: #64748b; text-decoration: none;">用户协议</a>
<a href="/privacy" style="font-size: 13px; color: #64748b; text-decoration: none;">隐私政策</a>
<a href="/about" style="font-size: 13px; color: #64748b; text-decoration: none;">联系我们</a>
</div>
</div>
<div style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #e2e8f0; text-align: center;">
<span style="font-size: 12px; color: #94a3b8;">${SITE_CONFIG.copyright}</span>
<span style="margin: 0 12px; color: #e2e8f0;">|</span>
<a href="${APP_INFO.beian.icpLink}" target="_blank" style="font-size: 12px; color: #94a3b8; text-decoration: none;">${APP_INFO.beian.icp}</a>
</div>
</div>
</footer>
`;
}
// 初始化公共组件
function initCommonComponents() {
const userMenuContainer = document.getElementById('userMenuContainer');
if (!userMenuContainer) return;
const userInfo = UserStore.get();
// 兼容:如果 UserStore 没有 token 但 localStorage 有 user_token,自动同步
let isLoggedIn = userInfo && userInfo.token;
if (!isLoggedIn && localStorage.getItem('user_token')) {
const token = localStorage.getItem('user_token');
const nickname = localStorage.getItem('user_nickname') || '用户';
const avatar = localStorage.getItem('user_avatar') || '/static/avatar/default.png';
UserStore.set({ token, nickname, avatar });
isLoggedIn = true;
}
userMenuContainer.innerHTML = renderUserMenu(isLoggedIn, isLoggedIn ? UserStore.get() : null);
// 渲染移动端用户菜单
const mobileUserMenu = document.getElementById('mobileUserMenu');
if (mobileUserMenu) {
mobileUserMenu.innerHTML = renderMobileUserMenu(isLoggedIn, isLoggedIn ? UserStore.get() : null);
}
if (isLoggedIn) {
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) {
logoutBtn.addEventListener('click', function(e) {
e.preventDefault();
fetch('/api/web/logout', { method: 'POST' }).finally(() => {
Auth.logout();
window.location.href = '/';
});
});
}
const mobileLogoutBtn = document.getElementById('mobileLogoutBtn');
if (mobileLogoutBtn) {
mobileLogoutBtn.addEventListener('click', function(e) {
e.preventDefault();
fetch('/api/web/logout', { method: 'POST' }).finally(() => {
Auth.logout();
window.location.href = '/';
});
});
}
const trigger = document.querySelector('.user-dropdown-trigger');
const menu = document.querySelector('.user-dropdown-menu');
if (trigger && menu) {
trigger.addEventListener('click', function() {
menu.style.opacity = menu.style.opacity === '1' ? '0' : '1';
menu.style.visibility = menu.style.visibility === 'visible' ? 'hidden' : 'visible';
menu.style.transform = menu.style.transform === 'translateY(0)' ? 'translateY(-8px)' : 'translateY(0)';
});
document.addEventListener('click', function(e) {
if (!trigger.contains(e.target) && !menu.contains(e.target)) {
menu.style.opacity = '0';
menu.style.visibility = 'hidden';
menu.style.transform = 'translateY(-8px)';
}
});
}
}
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
link.addEventListener('mouseenter', function() {
this.style.background = '#f1f5f9';
this.style.color = SITE_CONFIG.brand.primaryColor;
});
link.addEventListener('mouseleave', function() {
if (!this.classList.contains('active')) {
this.style.background = 'transparent';
this.style.color = '#475569';
}
});
});
// 移动端菜单交互
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileDropdownMenu = document.getElementById('mobileDropdownMenu');
const mobileMenuOverlay = document.getElementById('mobileMenuOverlay');
function closeMobileMenu() {
mobileDropdownMenu.style.display = 'none';
mobileMenuOverlay.style.display = 'none';
mobileMenuBtn.classList.remove('active');
document.body.style.overflow = '';
}
function openMobileMenu() {
mobileDropdownMenu.style.display = 'block';
mobileMenuOverlay.style.display = 'block';
mobileMenuBtn.classList.add('active');
document.body.style.overflow = 'hidden';
}
if (mobileMenuBtn && mobileDropdownMenu && mobileMenuOverlay) {
mobileMenuBtn.addEventListener('click', function(e) {
e.stopPropagation();
const isOpen = mobileDropdownMenu.style.display === 'block';
if (isOpen) {
closeMobileMenu();
} else {
openMobileMenu();
}
});
// 点击遮罩层关闭菜单
mobileMenuOverlay.addEventListener('click', closeMobileMenu);
// 点击菜单链接时关闭菜单
const mobileNavItems = document.querySelectorAll('.mobile-nav-item');
mobileNavItems.forEach(link => {
link.addEventListener('click', closeMobileMenu);
});
// 点击移动端用户菜单链接时关闭菜单
const mobileUserLinks = mobileUserMenu.querySelectorAll('a');
mobileUserLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu);
});
}
}
// 移动端用户菜单渲染函数
function renderMobileUserMenu(isLoggedIn, userInfo) {
if (isLoggedIn && userInfo) {
const nickname = localStorage.getItem('user_nickname') || userInfo.nickname || '用户';
return `
<div style="padding: 8px 0;">
<a href="/web/calendar" class="mobile-nav-item" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
日历
</a>
<a href="/web/profile" class="mobile-nav-item" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><circle cx="12" cy="8" r="5"/><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/></svg>
个人中心
</a>
<a href="/web/growth" class="mobile-nav-item" style="display: flex; align-items: center; padding: 14px 20px; color: #1e293b; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2Z"/></svg>
成长中心
</a>
<a href="#" id="mobileLogoutBtn" class="mobile-nav-item" style="display: flex; align-items: center; padding: 14px 20px; color: #ef4444; font-size: 15px; font-weight: 500; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><path d="M16 17l5-5-5-5"/><path d="M21 12H9"/></svg>
退出登录
</a>
</div>
`;
} else {
return `
<div style="padding: 8px 0;">
<a href="/login" class="mobile-nav-item" style="display: flex; align-items: center; padding: 14px 20px; color: ${SITE_CONFIG.brand.primaryColor}; font-size: 15px; font-weight: 600; text-decoration: none; transition: all 0.2s;">
<svg style="width: 20px; height: 20px; fill: none; stroke: currentColor; margin-right: 14px;" viewBox="0 0 24 24"><path d="M16 11V7a4 4 0 0 0-8 0v4M5 9h14l1 12H4L5 9"/></svg>
登录 / 注册
</a>
</div>
`;
}
}
function navigateTo(url) {
window.location.href = url;
}