docs(website): 添加简记memo产品原型和UI设计规范文档

- 新增「简记memo」一体化小程序产品原型设计文档
- 新增简记memo完整版UI视觉设计规范和界面细节
- 添加IDEA项目配置文件.gitignore
- 创建404页面HTML文件,包含响应式布局和错误提示
- 添加关于页面HTML文件,展示品牌介绍和团队信息
- 实现AES加解密工具函数,支持请求体加密
- 添加用户协议页面基础框架
This commit is contained in:
sunct
2026-07-31 14:12:32 +08:00
commit 3c3bf53ae4
115 changed files with 21304 additions and 0 deletions
+172
View File
@@ -0,0 +1,172 @@
// static/js/auth.js
// 统一认证管理:Token过期检查、错误处理、自动登出
(function() {
'use strict';
// ==================== 防重复标记 ====================
var _isHandlingExpired = false; // 正在处理过期(防重复)
var _hasNotified = false; // 本次会话是否已提示过
var Auth = {
DEFAULT_EXPIRE_MS: 7200 * 1000, // 2小时
isTokenExpired: function() {
var token = localStorage.getItem('user_token');
if (!token) return true;
var expiresAt = localStorage.getItem('user_token_expires');
if (!expiresAt) {
var tokenAge = Date.now() - (parseInt(localStorage.getItem('user_token_created')) || Date.now());
return tokenAge > this.DEFAULT_EXPIRE_MS;
}
return Date.now() > parseInt(expiresAt);
},
isLoggedIn: function() {
return !this.isTokenExpired() && !!localStorage.getItem('user_token');
},
/**
* 清除登录状态(仅清数据,不跳转)
*/
logout: function() {
// 清除所有用户相关的 localStorage key
localStorage.removeItem('user_token');
localStorage.removeItem('user_token_expires');
localStorage.removeItem('user_token_created');
localStorage.removeItem('user_nickname');
localStorage.removeItem('user_avatar');
// user_info 由 UserStore.clear() 处理
if (window.UserStore) {
UserStore.clear();
}
// 立即更新头部 DOM,把用户菜单替换为登录按钮
this._updateHeaderUI();
},
/**
* 更新头部用户菜单 UI
*/
_updateHeaderUI: function() {
// PC端
var pcContainer = document.getElementById('userMenuContainer');
if (pcContainer && window.renderUserMenu) {
pcContainer.innerHTML = renderUserMenu(false, null);
}
// 移动端
var mobileContainer = document.getElementById('mobileUserMenu');
if (mobileContainer && window.renderMobileUserMenu) {
mobileContainer.innerHTML = renderMobileUserMenu(false, null);
}
},
/**
* 处理登录过期(防重复调用)
*/
handleTokenExpired: function(reason) {
// 防重复:如果已经在处理中,直接返回
if (_isHandlingExpired) return;
_isHandlingExpired = true;
console.warn('Token已过期:', reason);
// 1. 先清除数据
this.logout();
// 2. 只提示一次
if (!_hasNotified) {
_hasNotified = true;
if (window.$toast) {
$toast.warning('登录已过期,请重新登录');
}
}
// 3. 立即跳转(不再用 setTimeout,避免期间其他请求重复触发)
// 如果当前已经在登录页,不跳转
if (window.location.pathname !== '/login') {
window.location.href = '/login';
}
},
/**
* 初始化:页面加载时检查 token
*/
init: function() {
if (this.isTokenExpired()) {
var token = localStorage.getItem('user_token');
if (token) {
// 有 token 但过期了 → 处理
this.handleTokenExpired('本地检测到token已过期');
} else {
// 没有 token → 仅更新 UI(不提示、不跳转)
this._updateHeaderUI();
}
} else {
// token 有效,记录创建时间
if (!localStorage.getItem('user_token_created')) {
localStorage.setItem('user_token_created', String(Date.now()));
}
}
},
getAuthHeaders: function() {
return { 'token': localStorage.getItem('user_token') };
}
};
window.Auth = Auth;
// ==================== 全局 Fetch 拦截 ====================
var originalFetch = window.fetch;
window.fetch = function(url, options) {
options = options || {};
options.headers = options.headers || {};
var isInternalAPI = typeof url === 'string' && url.indexOf('/api/') !== -1 && url.indexOf('://') === -1;
// 自动注入 token
if (isInternalAPI) {
var token = localStorage.getItem('user_token');
if (token && !options.headers['token']) {
options.headers['token'] = token;
}
}
return originalFetch(url, options)
.then(function(response) {
// 非 API 或正在处理过期 → 直接返回
if (!isInternalAPI || _isHandlingExpired) {
return response;
}
// 克隆响应,尝试解析 JSON 检查认证错误
var clone = response.clone();
clone.json().then(function(data) {
if (data.code === 1000 || data.code === 401) {
Auth.handleTokenExpired(data.msg || '登录已过期');
}
}).catch(function() {
// JSON 解析失败(如非 JSON 响应),忽略
});
return response;
});
};
// ==================== 初始化 ====================
// 确保 DOM 加载完成后再初始化(避免 _updateHeaderUI 找不到元素)
function doInit() {
Auth.init();
}
if (document.readyState === 'complete' || document.readyState === 'interactive') {
// DOM 已就绪,但需要等下一帧确保其他脚本也执行完
setTimeout(doInit, 0);
} else {
document.addEventListener('DOMContentLoaded', doInit);
}
})();
+386
View File
@@ -0,0 +1,386 @@
// 公共头部组件渲染函数
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;
}
+139
View File
@@ -0,0 +1,139 @@
document.addEventListener('DOMContentLoaded', async function() {
const currentPath = window.location.pathname;
try {
const headerResponse = await fetch('/static/html/_header.html');
const headerHtml = await headerResponse.text();
document.body.insertAdjacentHTML('afterbegin', headerHtml);
const navLinks = document.querySelectorAll('.nav-link');
navLinks.forEach(link => {
const href = link.getAttribute('href');
const page = link.getAttribute('data-page');
let isActive = false;
if (page === 'home' && (currentPath === '/' || currentPath === '/index.html')) {
isActive = true;
} else if (page === 'growth' && (currentPath === '/growth' || currentPath === '/growth.html')) {
isActive = true;
} else if (page === 'rules' && (currentPath === '/growth-rules' || currentPath === '/growth-rules.html')) {
isActive = true;
} else if (page === 'about' && (currentPath === '/about' || currentPath === '/about.html')) {
isActive = true;
}
if (isActive) {
link.classList.add('active');
}
});
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mainNav = document.getElementById('mainNav');
if (mobileMenuBtn && mainNav) {
mobileMenuBtn.addEventListener('click', () => {
mobileMenuBtn.classList.toggle('active');
mainNav.classList.toggle('active');
});
mainNav.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
mobileMenuBtn.classList.remove('active');
mainNav.classList.remove('active');
});
});
}
} catch (error) {
console.error('Failed to load header:', error);
}
try {
const footerResponse = await fetch('/static/html/_footer.html');
const footerHtml = await footerResponse.text();
document.body.insertAdjacentHTML('beforeend', footerHtml);
} catch (error) {
console.error('Failed to load footer:', error);
}
// Initialize Mini Program links
initMiniProgramLinks();
});
// Mini Program Links Handler
function initMiniProgramLinks() {
const miniproLinks = document.querySelectorAll('.minipro-link');
miniproLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const href = this.getAttribute('href');
let title = this.getAttribute('data-title') || '小程序功能';
let desc = this.getAttribute('data-desc') || '该功能需要在微信小程序中使用';
if (href.includes('todo')) {
title = '待办任务';
desc = '高效管理日常任务,完成任务即可获得经验值';
} else if (href.includes('bill')) {
title = '账单记录';
desc = '简单快捷的记账体验,清晰了解资金流向';
} else if (href.includes('mood')) {
title = '心情日记';
desc = '记录每天的心情点滴,留下珍贵的回忆';
} else if (href.includes('growth')) {
title = '成长之路';
desc = '丰富的成就系统和等级体系,让记录更有趣';
}
showMiniProgramModal(title, desc);
});
});
}
function showMiniProgramModal(title, desc) {
let overlay = document.getElementById('minipro-modal-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'minipro-modal-overlay';
overlay.className = 'minipro-modal-overlay';
overlay.innerHTML = `
<div class="minipro-modal">
<div class="minipro-modal-header">
<h3 class="minipro-modal-title">提示</h3>
<button class="minipro-modal-close" onclick="closeMiniProgramModal()">&times;</button>
</div>
<div class="minipro-modal-body">
<div class="minipro-modal-icon">📱</div>
<h3 id="minipro-modal-title">${title}</h3>
<p id="minipro-modal-desc">${desc}</p>
<p style="font-size: 0.875rem; color: var(--text-secondary);">
请打开微信,搜索"简记memo"小程序体验完整功能
</p>
</div>
<div class="minipro-modal-footer">
<button class="btn btn-secondary" onclick="closeMiniProgramModal()">知道了</button>
</div>
</div>
`;
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
closeMiniProgramModal();
}
});
} else {
document.getElementById('minipro-modal-title').textContent = title;
document.getElementById('minipro-modal-desc').textContent = desc;
}
overlay.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeMiniProgramModal() {
const overlay = document.getElementById('minipro-modal-overlay');
if (overlay) {
overlay.classList.remove('active');
document.body.style.overflow = '';
}
}
+133
View File
@@ -0,0 +1,133 @@
/* ===================================
简记memo - 项目公共配置
=================================== */
const SITE_CONFIG = {
path: '/static/images/logo.png',
name: '简记memo',
subtitle: '让记录更简单',
slogan: '记录,是对抗遗忘的方式。\n生活的美好藏在点滴细节里,\n用简记memo,写下时光的足迹。',
version: 'v1.0.0',
copyright: `© ${new Date().getFullYear()} 简记memo. All rights reserved.`,
email: 'memo@miaoall.cn',
wechat: {
name: '孙三苗',
account: 'sunsanmiao'
},
defaultAvatar: '/static/avatar/default.png',
brand: {
primaryColor: '#4F9EDE',
secondaryColor: '#3A8BC9',
accentColor: '#67B8E3',
gradient: 'linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%)',
lightGradient: 'linear-gradient(135deg, #67B8E3 0%, #4F9EDE 100%)'
}
};
const APP_INFO = {
name: '简记memo',
slogan: '简洁高效的个人事务管理工具',
description: '集待办任务、账单记录、心情日记于一体的个人事务管理工具',
features: [
{ icon: '📅', text: '日历管理' },
{ icon: '✅', text: '待办事项' },
{ icon: '💰', text: '账单记录' },
{ icon: '😊', text: '心情日记' }
],
beian: {
icp: '京ICP备2026013204号',
icpLink: 'https://beian.miit.gov.cn/',
police: '京公网安备11011402055644号',
policeLink: 'http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=京公网安备11011402055644号'
}
};
function renderSiteFooter(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = `
<div class="site-footer">
<div class="footer-brand">
<img src="${SITE_CONFIG.path}" alt="${SITE_CONFIG.name}" class="footer-logo" onerror="this.style.display='none'">
<span class="footer-name">${SITE_CONFIG.name}</span>
</div>
<div class="footer-info">
<p class="footer-slogan">${SITE_CONFIG.slogan.replace(/\n/g, '<br>')}</p>
<p class="footer-copyright">${SITE_CONFIG.copyright}</p>
<p class="footer-beian">
<a href="${APP_INFO.beian.icpLink}" target="_blank">${APP_INFO.beian.icp}</a>
&nbsp;|&nbsp;
<a href="${APP_INFO.beian.policeLink}" target="_blank">${APP_INFO.beian.police}</a>
</p>
</div>
</div>
`;
}
function renderSimpleFooter(containerId) {
const container = document.getElementById(containerId);
if (!container) return;
container.innerHTML = `
<div class="simple-footer">
<span>${SITE_CONFIG.copyright}</span>
<span class="footer-divider">|</span>
<a href="${APP_INFO.beian.icpLink}" target="_blank">${APP_INFO.beian.icp}</a>
<span class="footer-divider">|</span>
<a href="${APP_INFO.beian.policeLink}" target="_blank">${APP_INFO.beian.police}</a>
</div>
`;
}
function getAppName() {
return SITE_CONFIG.name;
}
function getLogoPath() {
return SITE_CONFIG.path;
}
function getBrandGradient() {
return SITE_CONFIG.brand.gradient;
}
const UserStore = {
KEY: 'user_info',
set(data) {
localStorage.setItem(this.KEY, JSON.stringify(data));
},
get() {
const data = localStorage.getItem(this.KEY);
return data ? JSON.parse(data) : null;
},
clear() {
localStorage.removeItem(this.KEY);
},
setToken(token) {
const user = this.get() || {};
user.token = token;
this.set(user);
},
getToken() {
const user = this.get();
return user ? user.token : null;
},
setUserInfo(info) {
const user = this.get() || {};
const mergedUser = { ...user, ...info };
this.set(mergedUser);
},
getUserInfo() {
return this.get();
}
};
+121
View File
@@ -0,0 +1,121 @@
const DEFAULT_CATE_LIST = [
{"value": "food", "label": "餐饮", "icon": "🍚", "color": "#ff6b6b", "type": 1},
{"value": "transport", "label": "交通", "icon": "🚇", "color": "#4ecdc4", "type": 1},
{"value": "shopping", "label": "购物", "icon": "🛍️", "color": "#ffe66d", "type": 1},
{"value": "entertainment", "label": "娱乐", "icon": "🎮", "color": "#a855f7", "type": 1},
{"value": "home", "label": "居家", "icon": "🏠", "color": "#84b3d9", "type": 1},
{"value": "medical", "label": "医疗", "icon": "🏥", "color": "#ef4444", "type": 1},
{"value": "education", "label": "教育", "icon": "📚", "color": "#22c55e", "type": 1},
{"value": "social", "label": "社交", "icon": "👥", "color": "#f97316", "type": 1},
{"value": "digital", "label": "数码", "icon": "📱", "color": "#3b82f6", "type": 1},
{"value": "clothing", "label": "服饰", "icon": "👔", "color": "#ec4899", "type": 1},
{"value": "beauty", "label": "美容", "icon": "💄", "color": "#d946ef", "type": 1},
{"value": "sports", "label": "运动", "icon": "⚽", "color": "#10b981", "type": 1},
{"value": "pet", "label": "宠物", "icon": "🐶", "color": "#fbbf24", "type": 1},
{"value": "baby", "label": "育儿", "icon": "🍼", "color": "#fb7185", "type": 1},
{"value": "travel", "label": "旅游", "icon": "✈️", "color": "#06b6d4", "type": 1},
{"value": "other_expense", "label": "其他", "icon": "📦", "color": "#9ca3af", "type": 1},
{"value": "salary", "label": "工资", "icon": "💼", "color": "#22c55e", "type": 2},
{"value": "bonus", "label": "奖金", "icon": "🎁", "color": "#fbbf24", "type": 2},
{"value": "side_hustle", "label": "副业", "icon": "💻", "color": "#3b82f6", "type": 2},
{"value": "investment", "label": "理财", "icon": "📈", "color": "#a855f7", "type": 2},
{"value": "transfer", "label": "转账", "icon": "💳", "color": "#84b3d9", "type": 2},
{"value": "red_packet", "label": "红包", "icon": "🧧", "color": "#ef4444", "type": 2},
{"value": "other_income", "label": "其他", "icon": "🎯", "color": "#6b7280", "type": 2},
];
const TODO_BASE_CATE = [
{"value": "work", "label": "工作", "color": "#84b3d9"},
{"value": "life", "label": "生活", "color": "#a0d0e0"},
{"value": "study", "label": "学习", "color": "#b8d8e6"},
];
const TODO_PRIORITY_LIST = [
{"label": "高", "value": 1, "icon": "🔴", key: "high"},
{"label": "中", "value": 2, "icon": "🟡", key: "medium"},
{"label": "低", "value": 3, "icon": "🟢", key: "low"},
];
const MOOD_LIST = [
{"emoji": "😊", "label": "开心", "phrases": ["今天心情真好", "阳光灿烂的一天", "嘴角不自觉上扬", "满心欢喜"]},
{"emoji": "😌", "label": "平静", "phrases": ["岁月静好", "内心一片安宁", "享受这份宁静", "平和自在"]},
{"emoji": "😔", "label": "低落", "phrases": ["有点小忧伤", "心情沉沉的", "需要一点温暖", "静待花开"]},
{"emoji": "😤", "label": "生气", "phrases": ["有点烦躁呢", "深呼吸冷静一下", "需要冷静片刻", "平复一下心情"]},
{"emoji": "😴", "label": "疲惫", "phrases": ["有点累了", "好好休息一下", "给自己放个假", "累并快乐着"]},
{"emoji": "😰", "label": "焦虑", "phrases": ["有点担心", "相信一切会好", "慢慢来不着急", "放宽心"]},
{"emoji": "😄", "label": "兴奋", "phrases": ["超级开心!", "激动人心的时刻", "迫不及待了", "满心期待"]},
{"emoji": "🤔", "label": "思考", "phrases": ["正在思考中", "让我想想", "深思熟虑", "思绪万千"]},
{"emoji": "😢", "label": "难过", "phrases": ["有点难过", "需要一个拥抱", "一切都会过去", "明天会更好"]},
{"emoji": "😎", "label": "酷", "phrases": ["今天超酷的", "自信满满", "做自己就好", "洒脱自在"]},
{"emoji": "🥰", "label": "幸福", "phrases": ["被幸福包围", "甜蜜的感觉", "幸福感爆棚", "满心欢喜"]},
{"emoji": "😠", "label": "愤怒", "phrases": ["真的生气了", "需要冷静一下", "平复一下情绪", "深呼吸"]},
{"emoji": "🥳", "label": "庆祝", "phrases": ["值得庆祝!", "太棒了!", "举杯庆祝", "喜笑颜开"]},
{"emoji": "😱", "label": "惊讶", "phrases": ["哇!好惊喜", "太意外了", "不可思议", "令人震惊"]},
{"emoji": "😇", "label": "感恩", "phrases": ["心怀感恩", "感谢生活", "感恩遇见", "心存感激"]},
{"emoji": "💪", "label": "加油", "phrases": ["加油加油!", "相信自己", "勇往直前", "全力以赴"]},
{"emoji": "😘", "label": "害羞", "phrases": ["有点害羞", "不好意思啦", "脸红心跳", "羞涩一笑"]},
{"emoji": "🤩", "label": "崇拜", "phrases": ["超级崇拜!", "偶像光芒", "闪闪发光", "心生敬意"]},
{"emoji": "😪", "label": "困倦", "phrases": ["好困呀", "眼皮打架了", "需要小憩", "睡个好觉"]},
{"emoji": "😋", "label": "满足", "phrases": ["心满意足", "幸福感满满", "知足常乐", "十分满足"]},
];
const PAY_CHANNELS = [
{"value": "wechat", "label": "微信", "types": [1, 2]},
{"value": "alipay", "label": "支付宝", "types": [1, 2]},
{"value": "unionpay", "label": "云闪付", "types": [1, 2]},
{"value": "bank", "label": "银行卡", "types": [1, 2]},
{"value": "cash", "label": "现金", "types": [1, 2]},
{"value": "online_banking", "label": "网银", "types": [1, 2]},
{"value": "pos", "label": "POS机", "types": [1, 2]},
{"value": "other", "label": "其他", "types": [1, 2]},
{"value": "salary", "label": "工资", "types": [2]},
{"value": "red_packet", "label": "红包", "types": [2]},
{"value": "transfer_in", "label": "转账", "types": [2]},
{"value": "corporate", "label": "对公转账", "types": [2]},
{"value": "credit", "label": "信用卡", "types": [1]},
{"value": "huawei", "label": "花呗", "types": [1]},
{"value": "white", "label": "白条", "types": [1]},
{"value": "jd_pay", "label": "京东支付", "types": [1]},
{"value": "meituan_pay", "label": "美团支付", "types": [1]},
{"value": "gift_card", "label": "储值卡", "types": [1]},
];
const CHANNEL_ICONS = {
'wechat': '💬',
'alipay': '🔷',
'cash': '💵',
'bank': '🏦',
'credit': '💳',
'salary': '💼',
'red_packet': '🧧',
'transfer': '🔄',
'unionpay': '⚡',
'online_banking': '🌐',
'pos': '🖥️',
'other': '📦',
'huawei': '🌸',
'white': '🤍',
'jd_pay': '🔴',
'meituan_pay': '🔘',
'gift_card': '🎫',
'corporate': '🏢',
'transfer_in': '📥'
};
function getCategoryByValue(value, type) {
if (type === 'bill') {
return DEFAULT_CATE_LIST.find(c => c.value === value);
}
if (type === 'todo') {
return TODO_BASE_CATE.find(c => c.value === value);
}
return null;
}
function getMoodByEmoji(emoji) {
return MOOD_LIST.find(m => m.emoji === emoji);
}
function getChannelByValue(value) {
return PAY_CHANNELS.find(ch => ch.value === value);
}
+6
View File
File diff suppressed because one or more lines are too long
+163
View File
@@ -0,0 +1,163 @@
// static/js/request-guard.js
// 前端请求安全工具:防抖、节流、重复提交防护
(function() {
'use strict';
// ==================== 防抖函数 ====================
/**
* 防抖函数 - 在事件触发n秒后再执行,n秒内再次触发则重新计时
* @param {Function} func - 要执行的函数
* @param {number} wait - 等待时间(毫秒)
* @param {boolean} immediate - 是否立即执行
*/
window.debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this;
var args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
};
// ==================== 节流函数 ====================
/**
* 节流函数 - 在规定时间内只执行一次
* @param {Function} func - 要执行的函数
* @param {number} wait - 时间间隔(毫秒)
*/
window.throttle = function(func, wait) {
var timeout;
var previous = 0;
return function() {
var context = this;
var args = arguments;
var now = Date.now();
var remaining = wait - (now - previous);
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(function() {
previous = Date.now();
timeout = null;
func.apply(context, args);
}, remaining);
}
};
};
// ==================== 请求锁(防止重复提交) ====================
var RequestLock = {
locks: {},
/**
* 锁定指定key的请求
* @param {string} key - 锁的标识
* @param {number} duration - 锁定时长(毫秒),默认3秒
* @returns {boolean} - 是否成功锁定
*/
lock: function(key, duration) {
duration = duration || 3000;
if (this.locks[key]) {
return false; // 已存在锁
}
this.locks[key] = true;
setTimeout(function() {
delete RequestLock.locks[key];
}, duration);
return true;
},
/**
* 检查是否存在锁
* @param {string} key - 锁的标识
* @returns {boolean}
*/
isLocked: function(key) {
return !!this.locks[key];
},
/**
* 解锁
* @param {string} key - 锁的标识
*/
unlock: function(key) {
delete this.locks[key];
},
/**
* 带锁执行请求
* @param {string} key - 锁的标识
* @param {Function} requestFn - 请求函数(可选,传null则只做锁检查)
* @param {number} duration - 锁定时长
* @returns {boolean} - 是否执行了请求
*/
executeWithLock: function(key, requestFn, duration) {
if (this.isLocked(key)) {
if (typeof $toast !== 'undefined') {
$toast.warning('操作太频繁,请稍后再试');
}
return false;
}
this.lock(key, duration);
if (typeof requestFn === 'function') {
requestFn();
}
return true;
}
};
window.RequestLock = RequestLock;
// ==================== 请求ID生成器(防重复提交) ====================
var RequestID = {
cache: {},
/**
* 生成请求ID
* @param {string} userId - 用户ID
* @param {string} action - 操作类型
* @returns {string}
*/
generate: function(userId, action) {
return userId + '_' + action + '_' + Date.now();
},
/**
* 记录请求ID(用于检测重复)
* @param {string} id - 请求ID
* @param {number} ttl - 有效期(毫秒)
* @returns {boolean} - 是否是新请求
*/
record: function(id, ttl) {
ttl = ttl || 5000; // 默认5秒内不允许重复
if (this.cache[id]) {
return false;
}
this.cache[id] = true;
var self = this;
setTimeout(function() {
delete self.cache[id];
}, ttl);
return true;
},
/**
* 检查请求是否重复
* @param {string} id - 请求ID
* @returns {boolean}
*/
isDuplicate: function(id) {
return !!this.cache[id];
}
};
window.RequestID = RequestID;
})();
+241
View File
@@ -0,0 +1,241 @@
// static/js/toast.js
// 美观的Toast消息提示组件(替代原生alert)
(function() {
'use strict';
// Toast样式
const style = document.createElement('style');
style.textContent = `
.toast-container {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 10000;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
pointer-events: none;
}
.toast {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
color: #fff;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: toastIn 0.3s ease;
pointer-events: auto;
max-width: 400px;
text-align: center;
line-height: 1.5;
}
.toast.success {
background: linear-gradient(135deg, #52c41a, #389e0d);
}
.toast.error {
background: linear-gradient(135deg, #ff4d4f, #cf1322);
}
.toast.warning {
background: linear-gradient(135deg, #faad14, #d48806);
}
.toast.info {
background: linear-gradient(135deg, #1890ff, #096dd9);
}
.toast.toast-out {
animation: toastOut 0.3s ease forwards;
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes toastOut {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
/* 确认对话框样式 */
.toast-confirm-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 10001;
display: flex;
align-items: center;
justify-content: center;
animation: fadeIn 0.2s ease;
}
.toast-confirm-overlay.fade-out {
animation: fadeOut 0.2s ease forwards;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.toast-confirm-box {
background: #fff;
border-radius: 12px;
padding: 24px;
min-width: 280px;
max-width: 320px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
animation: scaleIn 0.2s ease;
}
.toast-confirm-box.scale-out {
animation: scaleOut 0.2s ease forwards;
}
@keyframes scaleIn {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
@keyframes scaleOut {
from { transform: scale(1); opacity: 1; }
to { transform: scale(0.9); opacity: 0; }
}
.toast-confirm-message {
font-size: 15px;
color: #1e293b;
text-align: center;
margin-bottom: 20px;
line-height: 1.5;
}
.toast-confirm-buttons {
display: flex;
gap: 12px;
}
.toast-confirm-btn {
flex: 1;
padding: 10px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.toast-confirm-btn.cancel {
background: #f1f5f9;
color: #64748b;
}
.toast-confirm-btn.cancel:hover {
background: #e2e8f0;
}
.toast-confirm-btn.ok {
background: linear-gradient(135deg, #ff4d4f, #cf1322);
color: #fff;
}
.toast-confirm-btn.ok:hover {
opacity: 0.9;
}
`;
document.head.appendChild(style);
// Toast容器
let container = null;
function getContainer() {
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
return container;
}
// 显示Toast
function showToast(message, type, duration) {
type = type || 'info';
duration = duration || 3000;
const toast = document.createElement('div');
toast.className = 'toast ' + type;
toast.textContent = message;
const c = getContainer();
c.appendChild(toast);
// 自动移除
setTimeout(function() {
toast.classList.add('toast-out');
setTimeout(function() {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, duration);
return toast;
}
// 确认对话框
function showConfirm(message) {
return new Promise(function(resolve) {
const overlay = document.createElement('div');
overlay.className = 'toast-confirm-overlay';
overlay.innerHTML = `
<div class="toast-confirm-box">
<div class="toast-confirm-message">${message}</div>
<div class="toast-confirm-buttons">
<button class="toast-confirm-btn cancel">取消</button>
<button class="toast-confirm-btn ok">确定</button>
</div>
</div>
`;
document.body.appendChild(overlay);
const close = function(result) {
overlay.classList.add('fade-out');
const box = overlay.querySelector('.toast-confirm-box');
if (box) box.classList.add('scale-out');
setTimeout(function() {
if (overlay.parentNode) {
overlay.parentNode.removeChild(overlay);
}
resolve(result);
}, 200);
};
overlay.querySelector('.cancel').addEventListener('click', function() {
close(false);
});
overlay.querySelector('.ok').addEventListener('click', function() {
close(true);
});
overlay.addEventListener('click', function(e) {
if (e.target === overlay) {
close(false);
}
});
});
}
// 便捷方法
window.$toast = {
success: function(msg, duration) { return showToast(msg, 'success', duration); },
error: function(msg, duration) { return showToast(msg, 'error', duration); },
warning: function(msg, duration) { return showToast(msg, 'warning', duration); },
info: function(msg, duration) { return showToast(msg, 'info', duration); },
show: function(msg, type, duration) { return showToast(msg, type, duration); },
confirm: function(msg) { return showConfirm(msg); }
};
// 兼容Vue.prototype.$message风格
window.$message = window.$toast;
})();
+115
View File
@@ -0,0 +1,115 @@
// static/js/web-components.js
// Web工作台公共组件(头部、底部)
// 渲染工作台头部
function renderWebHeader(activeNav) {
const logoPath = (typeof SITE_CONFIG !== 'undefined') ? SITE_CONFIG.path : '/static/images/logo.png';
const siteName = (typeof SITE_CONFIG !== 'undefined') ? SITE_CONFIG.name : '简记';
const navItems = [
{ path: '/web/calendar', label: '日程', id: 'calendar' },
{ path: '/web/growth', label: '成长中心', id: 'growth' },
{ path: '/web/profile', label: '个人中心', id: 'profile' }
];
let navHtml = '';
navItems.forEach(item => {
const isActive = item.id === activeNav;
navHtml += `<a href="${item.path}" class="nav-link${isActive ? ' active' : ''}" style="padding: 6px 14px; border-radius: 6px; font-size: 13px; color: #475569; text-decoration: none;${isActive ? ' background: rgba(79, 158, 222, 0.1); color: #4F9EDE;' : ''}">${item.label}</a>`;
});
const nickname = localStorage.getItem('user_nickname') || '用户';
const avatar = localStorage.getItem('user_avatar');
const avatarHtml = avatar
? `<img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;">`
: nickname.charAt(0).toUpperCase();
return `
<header style="position: fixed; top: 0; left: 0; right: 0; height: 56px; background: #fff; border-bottom: 1px solid #e2e8f0; z-index: 100; display: flex; align-items: center; justify-content: space-between; padding: 0 24px;">
<div style="display: flex; align-items: center; gap: 20px;">
<a href="/" style="display: flex; align-items: center; gap: 8px; text-decoration: none;">
<img src="${logoPath}" alt="${siteName}" style="height: 28px; width: auto;" onerror="this.style.display='none'; this.nextElementSibling.style.display='inline';">
<span style="font-size: 18px; font-weight: 600; color: #1e293b; display: none;">${siteName}</span>
</a>
<nav style="display: flex; gap: 4px;">
${navHtml}
</nav>
</div>
<div style="display: flex; align-items: center; gap: 16px;">
<a href="/web/profile" style="display: flex; align-items: center; gap: 8px; text-decoration: none; padding: 6px 10px; border-radius: 6px; transition: background 0.15s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
<div style="width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); display: flex; align-items: center; justify-content: center; color: #fff; font-size: 14px; font-weight: 500;">${avatarHtml}</div>
<span style="font-size: 13px; color: #475569;">${nickname}</span>
</a>
<button onclick="handleWebLogout()" style="padding: 6px 12px; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 12px; color: #64748b; cursor: pointer; display: flex; align-items: center; gap: 4px;">
<svg style="width: 14px; height: 14px; 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"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>
退出
</button>
</div>
</header>
`;
}
// 渲染工作台底部
function renderWebFooter() {
return `
<footer style="height: 40px; background: #fff; border-top: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: center; gap: 24px; font-size: 12px; color: #94a3b8;">
<span>© 2024 简记Memo</span>
<a href="/about" style="color: #64748b; text-decoration: none;">关于</a>
<a href="/privacy" style="color: #64748b; text-decoration: none;">隐私</a>
</footer>
`;
}
// 退出登录
function handleWebLogout() {
if (typeof $toast !== 'undefined' && $toast.confirm) {
$toast.confirm('确定退出登录?').then(function(confirmed) {
if (confirmed) {
doLogout();
}
});
} else {
if (confirm('确定退出登录?')) {
doLogout();
}
}
}
function doLogout() {
localStorage.clear();
window.location.href = '/login';
}
// 检查登录状态
function checkWebLogin() {
const token = localStorage.getItem('user_token');
if (!token) {
window.location.href = '/login';
return false;
}
// 检查token是否过期
const expiresAt = localStorage.getItem('user_token_expires');
if (expiresAt && Date.now() > parseInt(expiresAt)) {
localStorage.clear();
window.location.href = '/login';
return false;
}
return true;
}
// 初始化Web页面
function initWebPage(activeNav) {
if (!checkWebLogin()) return;
// 插入头部
const headerContainer = document.getElementById('webHeader');
if (headerContainer) {
headerContainer.innerHTML = renderWebHeader(activeNav);
}
// 插入底部
const footerContainer = document.getElementById('webFooter');
if (footerContainer) {
footerContainer.innerHTML = renderWebFooter();
}
}