docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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()">×</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 = '';
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
|
||||
<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();
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
Vendored
+6
File diff suppressed because one or more lines are too long
@@ -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;
|
||||
|
||||
})();
|
||||
@@ -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;
|
||||
|
||||
})();
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user