docs(website): 添加简记memo产品原型和UI设计规范文档
- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 122 KiB |
|
After Width: | Height: | Size: 145 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 137 KiB |
|
After Width: | Height: | Size: 120 KiB |
@@ -0,0 +1,301 @@
|
||||
/* ===================================
|
||||
简记memo - 表单公共样式
|
||||
=================================== */
|
||||
|
||||
/* 表单容器 */
|
||||
.form-container {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 4px 24px rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
表单标签页
|
||||
=================================== */
|
||||
.form-tabs {
|
||||
display: flex;
|
||||
background: #f1f5f9;
|
||||
border-radius: 14px;
|
||||
padding: 5px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-tab {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-tab:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.form-tab.active {
|
||||
background: #fff;
|
||||
color: #6366f1;
|
||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.12);
|
||||
}
|
||||
|
||||
/* 表单组 */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
-webkit-autofill: none;
|
||||
autocomplete: off;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 文本域 */
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
resize: vertical;
|
||||
min-height: 80px;
|
||||
font-family: inherit;
|
||||
-webkit-autofill: none;
|
||||
autocomplete: off;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.form-textarea::placeholder {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 选择框 */
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
-webkit-appearance: none;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
}
|
||||
|
||||
.form-select:focus {
|
||||
outline: none;
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
/* 表单行 */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
/* 主要按钮 */
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 15px;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* 发送验证码按钮 */
|
||||
.btn-code {
|
||||
padding: 14px 16px;
|
||||
border: 2px solid #6366f1;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
color: #6366f1;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-code:hover {
|
||||
background: rgba(99, 102, 241, 0.04);
|
||||
}
|
||||
|
||||
.btn-code:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 次要链接 */
|
||||
.forgot-link {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: #64748b;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.forgot-link:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* 返回链接 */
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #64748b;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error-message {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
color: #ef4444;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 16px;
|
||||
display: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.error-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 成功提示 */
|
||||
.success-message {
|
||||
background: rgba(16, 185, 129, 0.06);
|
||||
color: #10b981;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 16px;
|
||||
display: none;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.success-message.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 底部提示 */
|
||||
.footer-tip {
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
color: #94a3b8;
|
||||
margin-top: 24px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.footer-tip a {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footer-tip a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn-code {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/* FullCalendar Base Styles */
|
||||
.fc {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
.fc-direction-ltr .fc-daygrid-event.fc-event-start {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.fc-direction-ltr .fc-daygrid-event.fc-event-end {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-top {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-number {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.fc-theme-standard td,
|
||||
.fc-theme-standard th {
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.fc-theme-standard thead {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.fc-col-header-cell-cushion {
|
||||
padding: 8px 4px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fc-daygrid-day {
|
||||
background: #fff;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.fc-daygrid-day:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.fc-daygrid-day-frame {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.fc-toolbar-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.fc-button {
|
||||
background: transparent !important;
|
||||
border: 1px solid #e5e7eb !important;
|
||||
color: #666 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 8px 16px !important;
|
||||
margin: 0 4px !important;
|
||||
font-weight: 500 !important;
|
||||
transition: all 0.2s !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-button:hover {
|
||||
background: #667eea !important;
|
||||
border-color: #667eea !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.fc-button-active {
|
||||
background: #667eea !important;
|
||||
border-color: #667eea !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.fc-button-primary:not(:disabled).fc-button-active,
|
||||
.fc-button-primary:not(:disabled):active {
|
||||
background: #667eea !important;
|
||||
border-color: #667eea !important;
|
||||
}
|
||||
|
||||
.fc-today-button {
|
||||
background: #667eea !important;
|
||||
border-color: #667eea !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.fc-day-today {
|
||||
background: rgba(102, 126, 234, 0.1) !important;
|
||||
}
|
||||
|
||||
.fc-day-today .fc-daygrid-day-frame {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.fc-day-today .fc-daygrid-day-number {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.fc-event {
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
font-size: 12px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.fc-event-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fc-more-link {
|
||||
font-size: 12px;
|
||||
color: #667eea;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fc-more-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.fc-highlight {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.fc-popover {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.fc-popover-header {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.fc-popover-body {
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/* ===================================
|
||||
简记memo - 全局样式变量
|
||||
=================================== */
|
||||
|
||||
:root {
|
||||
--primary-color: #4F9EDE;
|
||||
--primary-light: #67B8E3;
|
||||
--primary-dark: #3A8BC9;
|
||||
--secondary-color: #6366f1;
|
||||
--accent-color: #34d399;
|
||||
--warning-color: #fbbf24;
|
||||
--danger-color: #ef4444;
|
||||
--success-color: #10b981;
|
||||
|
||||
--text-primary: #1e293b;
|
||||
--text-secondary: #64748b;
|
||||
--text-muted: #94a3b8;
|
||||
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc;
|
||||
--bg-tertiary: #f1f5f9;
|
||||
|
||||
--border-color: #e2e8f0;
|
||||
--border-light: #f1f5f9;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08);
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 8px;
|
||||
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
|
||||
--gradient-primary: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
|
||||
--gradient-secondary: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
基础样式重置
|
||||
=================================== */
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-dark);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
input, textarea, select {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 12px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
input:focus, textarea:focus, select:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 3px rgba(79, 158, 222, 0.08);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
按钮样式
|
||||
=================================== */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--gradient-primary);
|
||||
color: #fff;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-0.5px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--primary-color);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
卡片样式
|
||||
=================================== */
|
||||
|
||||
.card {
|
||||
background: var(--bg-primary);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--border-color);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--border-light);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
表单样式
|
||||
=================================== */
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
布局样式
|
||||
=================================== */
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.gap-4 {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.gap-6 {
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
文字样式
|
||||
=================================== */
|
||||
|
||||
.text-sm {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.text-md {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.text-lg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-xl {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
状态提示
|
||||
=================================== */
|
||||
|
||||
.alert {
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
color: var(--success-color);
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: var(--danger-color);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
color: #d97706;
|
||||
border: 1px solid rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
响应式
|
||||
=================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
/* ===================================
|
||||
简记memo - 登录页样式
|
||||
=================================== */
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
min-height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
/* 左侧品牌区 */
|
||||
.login-left {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-logo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 3px;
|
||||
}
|
||||
|
||||
.login-slogan {
|
||||
font-size: 1rem;
|
||||
opacity: 0.9;
|
||||
text-align: center;
|
||||
line-height: 1.8;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.login-features {
|
||||
margin-top: 48px;
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.feature-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.feature-text {
|
||||
font-size: 0.875rem;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 右侧表单区 */
|
||||
.login-right {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f8fafc;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
@media (max-width: 768px) {
|
||||
.login-page {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.login-left {
|
||||
padding: 40px 24px;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
.login-features {
|
||||
margin-top: 24px;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.login-right {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面底部版权信息 */
|
||||
.page-footer {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
font-size: 0.75rem;
|
||||
color: #94a3b8;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.page-footer a {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.page-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
margin: 0 8px;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
/* 协议勾选框样式 */
|
||||
.checkbox-group {
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
cursor: pointer;
|
||||
margin-top: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link-text {
|
||||
color: #4F9EDE;
|
||||
text-decoration: none;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.link-text:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>404 - 页面走丢了</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||
overflow: hidden;
|
||||
}
|
||||
.container {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
.error-code {
|
||||
font-size: 120px;
|
||||
font-weight: 800;
|
||||
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 50%, #8b5cf6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.error-code::after {
|
||||
content: '404';
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
background: linear-gradient(135deg, rgba(79, 158, 222, 0.2) 0%, rgba(139, 92, 246, 0.2) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
z-index: -1;
|
||||
}
|
||||
.memo-icon {
|
||||
font-size: 80px;
|
||||
margin-bottom: 24px;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
color: #64748b;
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.btn {
|
||||
padding: 12px 28px;
|
||||
border-radius: 8px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(79, 158, 222, 0.3);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #fff;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
.fun-fact {
|
||||
margin-top: 40px;
|
||||
padding: 16px 24px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
color: #64748b;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.fun-fact strong {
|
||||
color: #4F9EDE;
|
||||
}
|
||||
/* 装饰元素 */
|
||||
.decoration {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
opacity: 0.1;
|
||||
}
|
||||
.decoration-1 {
|
||||
top: 10%;
|
||||
left: 10%;
|
||||
font-size: 60px;
|
||||
animation: spin 20s linear infinite;
|
||||
}
|
||||
.decoration-2 {
|
||||
top: 20%;
|
||||
right: 15%;
|
||||
font-size: 40px;
|
||||
animation: spin 15s linear infinite reverse;
|
||||
}
|
||||
.decoration-3 {
|
||||
bottom: 20%;
|
||||
left: 15%;
|
||||
font-size: 50px;
|
||||
animation: spin 25s linear infinite;
|
||||
}
|
||||
.decoration-4 {
|
||||
bottom: 15%;
|
||||
right: 10%;
|
||||
font-size: 35px;
|
||||
animation: spin 18s linear infinite reverse;
|
||||
}
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.error-code {
|
||||
font-size: 80px;
|
||||
}
|
||||
.memo-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
.title {
|
||||
font-size: 22px;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
}
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 装饰元素 -->
|
||||
<div class="decoration decoration-1">📝</div>
|
||||
<div class="decoration decoration-2">✨</div>
|
||||
<div class="decoration decoration-3">🎯</div>
|
||||
<div class="decoration decoration-4">💡</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="memo-icon">📝</div>
|
||||
<div class="error-code">404</div>
|
||||
<h1 class="title">哎呀,页面走丢了!</h1>
|
||||
<p class="subtitle">
|
||||
这个页面可能被风吹走了,或者被外星人带走了...<br>
|
||||
别担心,我们可以带你回到安全的地方!
|
||||
</p>
|
||||
<div class="actions">
|
||||
<a href="/" class="btn btn-primary">
|
||||
<svg style="width: 18px; height: 18px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
返回首页
|
||||
</a>
|
||||
<a href="/web/calendar" class="btn btn-secondary">
|
||||
<svg style="width: 18px; height: 18px; 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>
|
||||
</div>
|
||||
<div class="fun-fact">
|
||||
💡 <strong>冷知识:</strong>404 错误代码源于 1990 年的 CERN,当时办公室在 4 楼 404 房间!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 简单的鼠标跟随效果
|
||||
document.addEventListener('mousemove', function(e) {
|
||||
const decorations = document.querySelectorAll('.decoration');
|
||||
const x = e.clientX / window.innerWidth;
|
||||
const y = e.clientY / window.innerHeight;
|
||||
|
||||
decorations.forEach((dec, index) => {
|
||||
const speed = (index + 1) * 10;
|
||||
const xOffset = (x - 0.5) * speed;
|
||||
const yOffset = (y - 0.5) * speed;
|
||||
dec.style.transform = `translate(${xOffset}px, ${yOffset}px)`;
|
||||
});
|
||||
});
|
||||
|
||||
// 键盘快捷键
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'h' || e.key === 'H') {
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>关于我们 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="commonHeader"></div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main style="padding: 84px 24px 40px; min-height: calc(100vh - 140px);">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<!-- 品牌介绍 -->
|
||||
<section style="text-align: center; margin-bottom: 48px;">
|
||||
<img id="brandLogo" src="/static/images/logo.png" alt="logo" style="width: 80px; height: 80px; border-radius: 20px; margin: 0 auto 20px; display: block;" onerror="this.style.display='none'; document.getElementById('fallbackLogo').style.display='flex';">
|
||||
<div id="fallbackLogo" style="width: 80px; height: 80px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 20px; display: none; align-items: center; justify-content: center; margin: 0 auto 20px;">
|
||||
<span style="font-size: 32px;">📝</span>
|
||||
</div>
|
||||
<h1 style="font-size: 28px; font-weight: 700; color: #1e293b; margin-bottom: 12px;" id="brandName">简记memo</h1>
|
||||
<p style="font-size: 15px; color: #64748b; max-width: 500px; margin: 0 auto;" id="brandDesc">集待办任务、账单记录、心情日记于一体的个人事务管理工具</p>
|
||||
</section>
|
||||
|
||||
<!-- 关于我们 -->
|
||||
<section style="background: #fff; border-radius: 16px; padding: 24px; margin-bottom: 20px;">
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 20px; display: flex; align-items: center;">
|
||||
<span style="width: 4px; height: 18px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 2px; margin-right: 10px;"></span>
|
||||
关于我们
|
||||
</h2>
|
||||
<div style="line-height: 1.8;">
|
||||
<p style="font-size: 14px; color: #475569; margin-bottom: 16px;">
|
||||
简记memo是一款致力于帮助用户更好地管理个人事务的工具。我们相信,良好的记录习惯能够让生活更加有序,让每一天都变得更有意义。
|
||||
</p>
|
||||
<p style="font-size: 14px; color: #475569; margin-bottom: 16px;">
|
||||
我们的愿景是打造一个简洁、优雅、实用的记录平台,让用户能够轻松记录待办任务、心情变化和收支情况,同时通过成长体系激励用户养成良好的习惯。
|
||||
</p>
|
||||
<p style="font-size: 14px; color: #475569;">
|
||||
团队成员来自不同领域,我们怀着对产品的热爱和对用户体验的追求,不断优化和完善产品,希望能够为用户带来更好的使用体验。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 联系我们 -->
|
||||
<section style="background: #fff; border-radius: 16px; padding: 24px; margin-bottom: 20px;">
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 20px; display: flex; align-items: center;">
|
||||
<span style="width: 4px; height: 18px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 2px; margin-right: 10px;"></span>
|
||||
联系我们
|
||||
</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(79, 158, 222, 0.1); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg style="width: 20px; height: 20px; fill: none; stroke: #4F9EDE;" viewBox="0 0 24 24">
|
||||
<path d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 14px; color: #64748b;">邮箱</div>
|
||||
<div style="font-size: 15px; color: #1e293b;" id="contactEmail">support@jianji-memo.com</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(34, 197, 94, 0.1); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg style="width: 20px; height: 20px; fill: none; stroke: #22c55e;" viewBox="0 0 24 24">
|
||||
<path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 01.213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 00.167-.054l1.903-1.114a.864.864 0 01.717-.098 10.16 10.16 0 002.837.403c4.801 0 8.692-3.287 8.692-7.342 0-4.054-3.891-7.34-8.692-7.34z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 14px; color: #64748b;">微信公众号</div>
|
||||
<div style="font-size: 15px; color: #1e293b;" id="contactWechat">简记memo</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 团队介绍 -->
|
||||
<section style="background: #fff; border-radius: 16px; padding: 24px;">
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 20px; display: flex; align-items: center;">
|
||||
<span style="width: 4px; height: 18px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 2px; margin-right: 10px;"></span>
|
||||
团队成员
|
||||
</h2>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 20px;">
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px;">
|
||||
<span style="font-size: 24px;">👨💻</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">产品经理</div>
|
||||
<div style="font-size: 12px; color: #64748b;">负责产品规划和设计</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px;">
|
||||
<span style="font-size: 24px;">👩💻</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">前端开发</div>
|
||||
<div style="font-size: 12px; color: #64748b;">负责界面开发</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px;">
|
||||
<span style="font-size: 24px;">👨🔧</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">后端开发</div>
|
||||
<div style="font-size: 12px; color: #64748b;">负责服务端开发</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 64px; height: 64px; background: linear-gradient(135deg, #ec4899 0%, #db2777 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px;">
|
||||
<span style="font-size: 24px;">🎨</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">UI设计师</div>
|
||||
<div style="font-size: 12px; color: #64748b;">负责界面设计</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="commonFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/common-components.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/about');
|
||||
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
|
||||
initCommonComponents();
|
||||
|
||||
if (typeof SITE_CONFIG !== 'undefined') {
|
||||
document.getElementById('brandName').textContent = SITE_CONFIG.name || '简记memo';
|
||||
document.getElementById('brandDesc').textContent = SITE_CONFIG.description || '集待办任务、账单记录、心情日记于一体的个人事务管理工具';
|
||||
document.getElementById('contactEmail').textContent = SITE_CONFIG.email || 'support@jianji-memo.com';
|
||||
if (SITE_CONFIG.wechat) {
|
||||
document.getElementById('contactWechat').textContent = SITE_CONFIG.wechat.name || '简记memo';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户协议 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f1f5f9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="commonHeader"></div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main style="padding: 84px 24px 40px; min-height: calc(100vh - 140px);">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="background: #fff; border-radius: 16px; padding: 40px;">
|
||||
<h1 style="font-size: 28px; font-weight: 700; color: #1e293b; text-align: center; margin-bottom: 12px;">简记memo用户协议</h1>
|
||||
<p style="font-size: 14px; color: #64748b; text-align: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px dashed rgba(79, 158, 222, 0.2);">生效日期:2025年04月24日</p>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">1. 协议主体与范围</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">1.1 本协议是您(以下简称"用户")与简记memo服务(以下简称"本服务")个人运营者(以下简称"运营者")之间关于使用本服务所订立的契约,运营者为个人独立开发者,无所属企业/公司主体。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">1.2 您通过网站访问、使用本服务的行为,即视为您已阅读、理解并同意本协议全部条款及《隐私政策》;若您不同意本协议或《隐私政策》,应立即停止使用本服务。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">1.3 本协议适用于您使用本服务的所有行为,包括但不限于账号注册登录、数据录入、存储、编辑等核心功能。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">2. 服务内容与范围</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">2.1 本服务为用户提供免费的备忘录、待办事项、账单记录服务,核心功能包括文字内容录入、服务器存储、编辑、删除、数据同步等,所有功能仅基于用户身份识别提供。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">2.2 运营者有权根据技术发展、用户需求、法律法规调整等因素,不定期优化、调整或暂停部分/全部服务,重大调整将通过服务内公告提前7日告知用户。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">2.3 本服务为免费服务,运营者不承诺提供商业级别的服务稳定性,仅保障基础功能的正常可用。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">3. 用户权利与义务</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">3.1 权利:您有权按照本协议约定使用本服务核心功能;有权查看、编辑、删除自身存储在服务器的所有数据;有权向运营者反馈功能问题或优化建议;在遵守本协议的前提下,运营者保障您正常使用本服务的基础权益。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">3.2 义务:您保证使用本服务的行为符合《中华人民共和国网络安全法》《互联网信息服务管理办法》等法律法规及公序良俗;不得利用本服务存储、传播违法、违规、侵权、色情、暴力等内容;应对自身录入的内容及使用行为承担全部法律责任;不得对本服务进行反向工程、篡改、破解、爬虫抓取等损害服务正常运行的操作。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">3.3 您理解并同意,用户身份信息仅用于身份识别,运营者不会通过该信息获取您的其他账号信息。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">4. 服务的暂停与终止</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">4.1 运营者有权在以下情形暂停/终止服务:(1)您违反本协议约定且未在通知期限内纠正;(2)法律法规、相关平台规则要求暂停/终止;(3)不可抗力(如服务器故障、网络中断);(4)运营者自主停止运营(提前30日在服务内公告)。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">4.2 服务暂停期间,运营者将尽力恢复服务,但不承诺恢复时限;服务终止后,您存储在服务器的所有数据将保留7日,逾期将永久删除,运营者不承担数据找回、备份或赔偿责任。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">4.3 您可主动申请终止服务(联系运营者删除账号及所有数据),申请生效后,运营者将在15个工作日内完成数据清理。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">5. 免责声明</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">5.1 本服务仅提供基础数据存储与管理功能,运营者不对您存储内容的完整性、准确性、安全性做绝对担保;因网络故障、服务器维护、平台规则调整、您的设备问题等非运营方可控因素导致的数据丢失、功能异常,运营者不承担赔偿责任。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">5.2 本服务为个人开发的免费服务,运营者无义务提供7×24小时技术支持,仅根据实际情况在合理期限内解答用户咨询(通过预留邮箱/公众号)。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">5.3 因您违反法律法规或本协议约定,导致第三方索赔、行政机关处罚的,由您自行承担全部责任,运营者不承担连带责任。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">6. 协议的变更</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">6.1 运营者可根据法律法规调整、业务优化需要修改本协议,修改后的协议将在服务内公示,公示满7日后自动生效。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">6.2 若您不同意修改后的协议,应立即停止使用本服务并可申请删除账号数据;继续使用本服务的,视为您同意修改后的协议。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">7. 法律适用与争议解决</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">7.1 本协议适用中华人民共和国法律(不含港澳台地区法律)。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">7.2 因本协议产生的争议,双方应友好协商解决;协商不成的,任何一方可向运营者所在地有管辖权的人民法院提起诉讼。</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">8. 联系与其他</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;" id="agreementContact">8.1 若您对本协议有任何疑问,可通过以下方式联系运营者:联系邮箱:memo@miaoall.cn;微信公众号:孙三苗(sunsanmiao)。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">8.2 本协议的条款如被有权机关认定为无效或不可执行,不影响其余条款的效力。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">8.3 本协议与《隐私政策》共同构成您与运营者之间的完整约定,两者冲突时,以《隐私政策》中关于个人信息保护的内容为准。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="commonFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/common-components.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/agreement');
|
||||
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
|
||||
initCommonComponents();
|
||||
|
||||
if (typeof SITE_CONFIG !== 'undefined') {
|
||||
const email = SITE_CONFIG.email || 'memo@miaoall.cn';
|
||||
const wechatName = SITE_CONFIG.wechat?.name || '孙三苗';
|
||||
const wechatAccount = SITE_CONFIG.wechat?.account || 'sunsanmiao';
|
||||
document.getElementById('agreementContact').textContent = `8.1 若您对本协议有任何疑问,可通过以下方式联系运营者:联系邮箱:${email};微信公众号:${wechatName}(${wechatAccount})。`;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,429 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>成长规则 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
}
|
||||
|
||||
@keyframes glow {
|
||||
0%, 100% { box-shadow: 0 0 5px currentColor; }
|
||||
50% { box-shadow: 0 0 20px currentColor; }
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
.animate-fadeInUp { animation: fadeInUp 0.6s ease forwards; }
|
||||
.animate-pulse { animation: pulse 2s ease-in-out infinite; }
|
||||
.animate-glow { animation: glow 2s ease-in-out infinite; }
|
||||
.animate-slideIn { animation: slideIn 0.4s ease forwards; }
|
||||
|
||||
.delay-1 { animation-delay: 0.1s; }
|
||||
.delay-2 { animation-delay: 0.2s; }
|
||||
.delay-3 { animation-delay: 0.3s; }
|
||||
.delay-4 { animation-delay: 0.4s; }
|
||||
.delay-5 { animation-delay: 0.5s; }
|
||||
|
||||
.card-hover:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 32px rgba(0,0,0,0.08);
|
||||
}
|
||||
|
||||
.rarity-primary { color: #9CA3AF; }
|
||||
.rarity-secondary { color: #10B981; }
|
||||
.rarity-advanced { color: #3B82F6; }
|
||||
.rarity-rare { color: #8B5CF6; }
|
||||
.rarity-epic { color: #F59E0B; }
|
||||
.rarity-legendary { color: #EF4444; }
|
||||
|
||||
.bg-rarity-primary { background: rgba(156, 163, 175, 0.1); }
|
||||
.bg-rarity-secondary { background: rgba(16, 185, 129, 0.1); }
|
||||
.bg-rarity-advanced { background: rgba(59, 130, 246, 0.1); }
|
||||
.bg-rarity-rare { background: rgba(139, 92, 246, 0.1); }
|
||||
.bg-rarity-epic { background: rgba(245, 158, 11, 0.1); }
|
||||
.bg-rarity-legendary { background: rgba(239, 68, 68, 0.1); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="commonHeader"></div>
|
||||
|
||||
<main style="padding: 84px 24px 40px; min-height: calc(100vh - 140px);">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="text-align: center; margin-bottom: 48px;" class="animate-fadeInUp">
|
||||
<div style="width: 80px; height: 80px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; box-shadow: 0 8px 24px rgba(79, 158, 222, 0.3);">
|
||||
<span style="font-size: 40px;">📊</span>
|
||||
</div>
|
||||
<h1 style="font-size: 32px; font-weight: 700; color: #1e293b; margin-bottom: 12px;">成长规则</h1>
|
||||
<p style="font-size: 16px; color: #64748b; max-width: 500px; margin: 0 auto;">了解如何获取经验,提升等级,解锁成就与特权</p>
|
||||
</div>
|
||||
|
||||
<section id="levelSystem" class="animate-fadeInUp delay-1" style="background: #fff; border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(79, 158, 222, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">📈</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">等级体系</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">从Lv.1到Lv.10,等级越高,称号越拉风</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.6; margin-bottom: 24px;">
|
||||
成长等级从Lv.1到Lv.10,通过累计经验值提升等级。<strong style="color: #4F9EDE;">所有核心功能完全免费开放</strong>,不受等级限制!等级特权只是额外的荣誉展示和增值服务。
|
||||
</p>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px;">
|
||||
<div v-for="(level, index) in levels" :key="level.level"
|
||||
class="card-hover"
|
||||
style="padding: 16px; background: #f8fafc; border-radius: 8px; transition: all 0.3s; cursor: pointer; position: relative; overflow: hidden;"
|
||||
:style="{ animationDelay: `${0.1 * index}s` }">
|
||||
<div style="position: absolute; top: 0; right: 0; width: 60px; height: 60px; opacity: 0.1;">{{ level.icon }}</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||||
<div style="width: 32px; height: 32px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; color: #fff;"
|
||||
:style="{ background: `linear-gradient(135deg, ${level.color} 0%, ${adjustColor(level.color, -20)} 100%)` }">
|
||||
Lv.{{ level.level }}
|
||||
</div>
|
||||
<span style="font-size: 14px; font-weight: 600; color: #1e293b;">{{ level.title }}</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-bottom: 8px;">需要 {{ level.exp.toLocaleString() }} EXP</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
<span v-for="privilege in level.privileges" :key="privilege"
|
||||
style="padding: 3px 8px; background: rgba(79, 158, 222, 0.08); border-radius: 4px; font-size: 11px; color: #4F9EDE;">
|
||||
{{ privilege }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="expRules" class="animate-fadeInUp delay-2" style="background: #fff; border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(245, 158, 11, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">⭐</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">经验获取</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">完成任务、记账、记录心情都能获得经验</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 20px;">
|
||||
<div v-for="(rule, index) in expRules" :key="rule.name"
|
||||
class="card-hover"
|
||||
style="padding: 20px; background: #f8fafc; border-radius: 8px; transition: all 0.3s; animation: fadeInUp 0.5s ease forwards;"
|
||||
:style="{ animationDelay: `${0.15 * index}s` }">
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 12px;">
|
||||
<div style="width: 44px; height: 44px; background: rgba(79, 158, 222, 0.1); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<span style="font-size: 22px;">{{ rule.icon }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 15px; font-weight: 600; color: #1e293b;">{{ rule.name }}</div>
|
||||
<div style="font-size: 12px; color: #64748b;">{{ rule.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 10px 12px; background: rgba(79, 158, 222, 0.05); border-radius: 6px; margin-bottom: 8px;">
|
||||
<div style="font-size: 14px; font-weight: 600; color: #4F9EDE;">{{ rule.exp }}</div>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: #64748b; line-height: 1.5;">{{ rule.detail }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(245, 158, 11, 0.05) 100%); border-radius: 8px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<svg style="width: 20px; height: 20px; fill: #f59e0b;" viewBox="0 0 24 24">
|
||||
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||
</svg>
|
||||
<span style="font-size: 14px; font-weight: 500; color: #d97706;">每日经验上限:120 EXP</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="streakBonus" class="animate-fadeInUp delay-3" style="background: #fff; border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(239, 68, 68, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">🔥</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">连续打卡加成</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">连续记录天数越多,加成越高</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 20px;">
|
||||
<div v-for="(rule, index) in streakBonusRules" :key="rule.range"
|
||||
style="padding: 16px; background: #f8fafc; border-radius: 8px; text-align: center; animation: fadeInUp 0.5s ease forwards;"
|
||||
:style="{ animationDelay: `${0.1 * index}s` }">
|
||||
<div style="font-size: 16px; font-weight: 700; margin-bottom: 4px;" :style="{ color: rule.color }">
|
||||
{{ rule.bonus }} EXP/天
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #64748b;">{{ rule.range }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; background: #f0fdf4; border-radius: 8px; border-left: 4px solid #22c55e;">
|
||||
<p style="font-size: 13px; color: #166534; line-height: 1.5;">
|
||||
<strong>💡 温馨提示:</strong>断签后连续天数不会完全清零,而是根据断签前的连续天数保留一部分:1-7天清零,8-30天保留3天,31-90天保留7天,91天以上保留15天。
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="protection" class="animate-fadeInUp delay-4" style="background: #fff; border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(59, 130, 246, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">🛡️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">新用户保护期</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">帮助新用户快速成长</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 16px; align-items: center;">
|
||||
<div style="flex: 1; min-width: 200px; padding: 20px; background: linear-gradient(135deg, rgba(59, 130, 246, 0.1) 0%, rgba(59, 130, 246, 0.05) 100%); border-radius: 8px;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: #3B82F6; margin-bottom: 4px;">50%</div>
|
||||
<div style="font-size: 13px; color: #475569;">保护期内额外经验加成</div>
|
||||
</div>
|
||||
<div style="flex: 2; min-width: 280px;">
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.6;">
|
||||
新用户前7天享受保护期加成!保护期内每次获取经验都会额外获得50%的经验加成,帮助你快速成长。
|
||||
</p>
|
||||
<p style="font-size: 13px; color: #64748b; margin-top: 8px;">
|
||||
保护期结束后,连续天数中断时,只会减少部分天数,不会完全重置。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="achievements" class="animate-fadeInUp delay-5" style="background: #fff; border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(139, 92, 246, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">🎖️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">成就系统</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">完成特定目标可解锁成就,获得额外经验奖励</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap;">
|
||||
<button v-for="cat in achievementCategories" :key="cat.key"
|
||||
@click="activeCategory = cat.key"
|
||||
style="padding: 8px 16px; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; border: none;"
|
||||
:style="activeCategory === cat.key
|
||||
? 'background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); color: #fff;'
|
||||
: 'background: #f1f5f9; color: #64748b;'">
|
||||
<span>{{ cat.icon }}</span> {{ cat.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 8px; margin-bottom: 20px; flex-wrap: wrap;">
|
||||
<div v-for="(rarity, key) in rarityConfigs" :key="key"
|
||||
style="display: flex; align-items: center; gap: 6px;">
|
||||
<div style="width: 12px; height: 12px; border-radius: 4px;" :style="{ background: rarity.color }"></div>
|
||||
<span style="font-size: 12px; color: #64748b;">{{ rarity.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px;">
|
||||
<div v-for="(achievement, index) in filteredAchievements" :key="achievement.id"
|
||||
class="card-hover"
|
||||
style="padding: 16px; border-radius: 8px; transition: all 0.3s; animation: fadeInUp 0.5s ease forwards;"
|
||||
:class="`bg-rarity-${achievement.rarity}`"
|
||||
:style="{ animationDelay: `${0.05 * index}s` }">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
||||
<div style="width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 18px; background: rgba(255,255,255,0.8);">
|
||||
{{ achievement.icon || '🏆' }}
|
||||
</div>
|
||||
<div :class="`rarity-${achievement.rarity}`" style="font-size: 11px; font-weight: 500;">
|
||||
{{ achievement.rarityName || achievement.rarity }}
|
||||
</div>
|
||||
</div>
|
||||
<h4 style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 4px;">{{ achievement.name }}</h4>
|
||||
<p style="font-size: 12px; color: #64748b; margin-bottom: 8px; line-height: 1.4;">{{ achievement.description || achievement.desc || '' }}</p>
|
||||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||||
<span style="font-size: 13px; font-weight: 600; color: #4F9EDE;">+{{ achievement.expReward || achievement.exp || 0 }} EXP</span>
|
||||
<span v-if="achievement.target" style="font-size: 11px; color: #94a3b8;">目标: {{ achievement.target }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="tips" class="animate-fadeInUp delay-1" style="background: linear-gradient(135deg, rgba(79, 158, 222, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); border-radius: 8px; padding: 28px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(255,255,255,0.8); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">💡</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">快速升级秘籍</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">掌握这些技巧,升级更快</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
<div v-for="(tip, index) in growthTips" :key="index"
|
||||
style="display: flex; align-items: flex-start; gap: 10px; padding: 14px; background: rgba(255,255,255,0.6); border-radius: 8px; animation: fadeInUp 0.5s ease forwards;"
|
||||
:style="{ animationDelay: `${0.1 * index}s` }">
|
||||
<div style="width: 24px; height: 24px; background: rgba(79, 158, 222, 0.1); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: #4F9EDE; flex-shrink: 0;">
|
||||
{{ index + 1 }}
|
||||
</div>
|
||||
<p style="font-size: 13px; color: #475569; line-height: 1.5;">{{ tip }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="faq" class="animate-fadeInUp delay-2" style="background: #fff; border-radius: 8px; padding: 28px;">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 20px;">
|
||||
<div style="width: 40px; height: 40px; background: rgba(100, 116, 139, 0.1); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 12px;">
|
||||
<span style="font-size: 20px;">❓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 style="font-size: 18px; font-weight: 600; color: #1e293b;">常见问题</h2>
|
||||
<p style="font-size: 13px; color: #64748b;">解答你的疑惑</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="faq-list">
|
||||
<div v-for="(item, index) in faqList" :key="index"
|
||||
class="faq-item"
|
||||
style="border-bottom: 1px solid #f1f5f9; padding: 16px 0; animation: fadeInUp 0.5s ease forwards;"
|
||||
:style="{ animationDelay: `${0.1 * index}s` }">
|
||||
<div style="display: flex; align-items: center; gap: 10px; cursor: pointer;" @click="toggleFaq(index)">
|
||||
<div style="width: 24px; height: 24px; background: rgba(79, 158, 222, 0.1); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: #4F9EDE; flex-shrink: 0;">
|
||||
Q
|
||||
</div>
|
||||
<span style="font-size: 14px; font-weight: 500; color: #1e293b;">{{ item.Q }}</span>
|
||||
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8; margin-left: auto; transition: transform 0.2s;"
|
||||
:style="{ transform: faqOpen === index ? 'rotate(180deg)' : 'rotate(0)' }" viewBox="0 0 24 24">
|
||||
<path d="M6 9l6 6 6-6"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div v-show="faqOpen === index"
|
||||
style="margin-top: 12px; padding-left: 34px; animation: slideIn 0.3s ease;">
|
||||
<p style="font-size: 14px; color: #64748b; line-height: 1.6;">{{ item.A }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="commonFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/toast.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/common-components.js"></script>
|
||||
<script>
|
||||
var app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
levels: [],
|
||||
expRules: [],
|
||||
streakBonusRules: [],
|
||||
achievementCategories: [],
|
||||
achievements: [],
|
||||
rarityConfigs: {},
|
||||
growthTips: [],
|
||||
faqList: [],
|
||||
activeCategory: "all",
|
||||
faqOpen: -1,
|
||||
loading: true
|
||||
},
|
||||
computed: {
|
||||
filteredAchievements: function() {
|
||||
if (this.activeCategory === "all") {
|
||||
return this.achievements;
|
||||
}
|
||||
return this.achievements.filter(item => item.category === this.activeCategory);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
adjustColor: function(hex, amount) {
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.min(255, Math.max(0, (num >> 16) + amount));
|
||||
const g = Math.min(255, Math.max(0, ((num >> 8) & 0x00FF) + amount));
|
||||
const b = Math.min(255, Math.max(0, (num & 0x0000FF) + amount));
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
},
|
||||
toggleFaq: function(index) {
|
||||
this.faqOpen = this.faqOpen === index ? -1 : index;
|
||||
},
|
||||
loadRulesFromAPI: function() {
|
||||
fetch('/api/web/growth/rules')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const sections = data.data.sections;
|
||||
sections.forEach(section => {
|
||||
switch (section.type) {
|
||||
case 'level_system':
|
||||
this.levels = section.levels || [];
|
||||
break;
|
||||
case 'exp_rules':
|
||||
this.expRules = section.rules || [];
|
||||
break;
|
||||
case 'streak_bonus':
|
||||
this.streakBonusRules = section.rules || [];
|
||||
break;
|
||||
case 'achievement_system':
|
||||
this.achievementCategories = [{ key: "all", name: "全部", icon: "📦" }].concat(section.categories || []);
|
||||
this.achievements = section.achievements || [];
|
||||
// 构建 rarityConfigs - 使用固定的稀有度配置
|
||||
this.rarityConfigs = {
|
||||
"初级": { name: "初级", color: "#9CA3AF" },
|
||||
"中级": { name: "中级", color: "#22C55E" },
|
||||
"高级": { name: "高级", color: "#3B82F6" },
|
||||
"稀有": { name: "稀有", color: "#8B5CF6" },
|
||||
"史诗": { name: "史诗", color: "#F59E0B" },
|
||||
"传说": { name: "传说", color: "#EF4444" }
|
||||
};
|
||||
break;
|
||||
case 'tips':
|
||||
this.growthTips = section.tips || [];
|
||||
break;
|
||||
case 'faq':
|
||||
this.faqList = section.questions || [];
|
||||
break;
|
||||
}
|
||||
});
|
||||
this.loading = false;
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('加载规则失败:', err);
|
||||
this.loading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
// 使用公共头部底部
|
||||
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/growth-rules');
|
||||
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
|
||||
initCommonComponents();
|
||||
// 从接口加载数据
|
||||
this.loadRulesFromAPI();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,257 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>成长中心 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
||||
.nav-link.active { background: rgba(79, 158, 222, 0.1); color: #4F9EDE; }
|
||||
.achievement-card { transition: all 0.2s; }
|
||||
.achievement-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
|
||||
.level-item { transition: all 0.15s; }
|
||||
.level-item:hover { background: #f1f5f9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="webHeader"></div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<div style="margin-top: 56px; min-height: calc(100vh - 56px - 40px); padding: 24px;">
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<!-- 左右布局 -->
|
||||
<div style="display: grid; grid-template-columns: 360px 1fr; gap: 24px;">
|
||||
<!-- 左侧:等级信息 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<!-- 等级卡片 -->
|
||||
<div style="background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 12px; padding: 32px 24px; color: #fff; text-align: center;">
|
||||
<div style="width: 100px; height: 100px; background: rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; position: relative;">
|
||||
<div style="font-size: 36px; font-weight: 700;" id="userLevel">Lv.1</div>
|
||||
<div style="position: absolute; bottom: -6px; right: -6px; width: 32px; height: 32px; background: #fbbf24; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px;">⭐</div>
|
||||
</div>
|
||||
<h2 style="font-size: 20px; font-weight: 600; margin-bottom: 4px;">成长等级</h2>
|
||||
<p style="font-size: 13px; opacity: 0.9; margin-bottom: 20px;" id="expInfo">0 / 100 经验</p>
|
||||
|
||||
<div style="background: rgba(255,255,255,0.2); border-radius: 4px; height: 8px; overflow: hidden;">
|
||||
<div id="expBar" style="height: 100%; background: #fff; border-radius: 4px; transition: width 0.5s; width: 0%;"></div>
|
||||
</div>
|
||||
<p style="font-size: 12px; opacity: 0.85; margin-top: 8px;" id="expRemain">还需 100 经验升级</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 20px;">
|
||||
<h3 style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 16px;">数据统计</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||||
<div style="text-align: center; padding: 16px; background: #f8fafc; border-radius: 8px;">
|
||||
<div style="font-size: 24px; font-weight: 700; color: #4F9EDE;" id="statTotalExp">0</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">累计经验</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 16px; background: #f8fafc; border-radius: 8px;">
|
||||
<div style="font-size: 24px; font-weight: 700; color: #f59e0b;" id="statAchievements">0</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">获得成就</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 16px; background: #f8fafc; border-radius: 8px;">
|
||||
<div style="font-size: 24px; font-weight: 700; color: #22c55e;" id="statDays">0</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">连续记录</div>
|
||||
</div>
|
||||
<div style="text-align: center; padding: 16px; background: #f8fafc; border-radius: 8px;">
|
||||
<div style="font-size: 24px; font-weight: 700; color: #8b5cf6;" id="statTasks">0</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-top: 4px;">完成任务</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 等级列表 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 20px; flex: 1;">
|
||||
<h3 style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 16px;">等级列表</h3>
|
||||
<div id="levelList" style="display: flex; flex-direction: column; gap: 8px; max-height: 400px; overflow-y: auto;">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:成就展示 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 24px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
|
||||
<h3 style="font-size: 18px; font-weight: 600; color: #1e293b;">我的成就</h3>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<select id="achievementFilter" onchange="filterAchievements()" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; color: #475569; background: #fff; cursor: pointer;">
|
||||
<option value="all">全部</option>
|
||||
<option value="unlocked">已获取</option>
|
||||
<option value="locked">未获取</option>
|
||||
</select>
|
||||
<span style="font-size: 13px; color: #4F9EDE;" id="achievementProgress">0 / 8</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="achievementsGrid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px;">
|
||||
<!-- 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="webFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/toast.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/web-components.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化公共头部底部
|
||||
initWebPage('growth');
|
||||
// 加载成长信息
|
||||
loadGrowthInfo();
|
||||
});
|
||||
|
||||
function loadGrowthInfo() {
|
||||
const token = localStorage.getItem('user_token');
|
||||
if (!token) return;
|
||||
|
||||
fetch('/api/web/growth/info', {
|
||||
headers: { 'token': token }
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const g = data.data;
|
||||
|
||||
// 等级信息(使用后端返回数据)
|
||||
document.getElementById('userLevel').textContent = 'Lv.' + g.level;
|
||||
document.getElementById('expInfo').textContent = g.levelProgress + '%';
|
||||
document.getElementById('expRemain').textContent = '还需 ' + g.nextLevelExp + ' 经验升级';
|
||||
document.getElementById('expBar').style.width = Math.min(g.levelProgress, 100) + '%';
|
||||
|
||||
// 统计数据
|
||||
document.getElementById('statTotalExp').textContent = g.experience || 0;
|
||||
document.getElementById('statAchievements').textContent = g.achievements?.unlocked || 0;
|
||||
document.getElementById('statDays').textContent = g.streakDays || 0;
|
||||
document.getElementById('statTasks').textContent = g.todayProgress?.tasksCompleted || 0;
|
||||
|
||||
// 成就进度
|
||||
const totalAch = g.achievements?.total || 0;
|
||||
const unlockedAch = g.achievements?.unlocked || 0;
|
||||
document.getElementById('achievementProgress').textContent = unlockedAch + ' / ' + totalAch;
|
||||
|
||||
// 渲染成就列表
|
||||
renderAchievements(g.achievements?.list || [], totalAch);
|
||||
|
||||
// 渲染等级列表(使用后端数据)
|
||||
renderLevelListFromAPI(g.level);
|
||||
}
|
||||
})
|
||||
.catch(err => console.error('加载成长信息失败:', err));
|
||||
}
|
||||
|
||||
// 渲染成就列表
|
||||
// 存储成就数据用于过滤
|
||||
let allAchievementsData = [];
|
||||
let unlockedAchievementIds = [];
|
||||
|
||||
function renderAchievements(unlockedList, total) {
|
||||
const container = document.getElementById('achievementsGrid');
|
||||
const token = localStorage.getItem('user_token');
|
||||
|
||||
// 先获取所有成就定义
|
||||
fetch('/api/web/growth/rules', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const sections = data.data.sections;
|
||||
const achSection = sections.find(s => s.type === 'achievement_system');
|
||||
if (achSection && achSection.achievements) {
|
||||
allAchievementsData = achSection.achievements;
|
||||
unlockedAchievementIds = unlockedList.map(a => a.id || a.achievement_id);
|
||||
|
||||
renderAchievementList('all');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function filterAchievements() {
|
||||
const filter = document.getElementById('achievementFilter').value;
|
||||
renderAchievementList(filter);
|
||||
}
|
||||
|
||||
function renderAchievementList(filter) {
|
||||
const container = document.getElementById('achievementsGrid');
|
||||
let html = '';
|
||||
|
||||
let filteredAchievements = allAchievementsData;
|
||||
if (filter === 'unlocked') {
|
||||
filteredAchievements = allAchievementsData.filter(ach => unlockedAchievementIds.includes(ach.id));
|
||||
} else if (filter === 'locked') {
|
||||
filteredAchievements = allAchievementsData.filter(ach => !unlockedAchievementIds.includes(ach.id));
|
||||
}
|
||||
|
||||
filteredAchievements.forEach(ach => {
|
||||
const isUnlocked = unlockedAchievementIds.includes(ach.id);
|
||||
const color = ach.rarityColor || '#9CA3AF';
|
||||
const icon = ach.icon || '🏆';
|
||||
const name = ach.name || '未知成就';
|
||||
const desc = ach.description || ach.desc || '';
|
||||
const rarityName = ach.rarityName || '';
|
||||
|
||||
html += `
|
||||
<div class="achievement-card" style="text-align: center; padding: 20px; border-radius: 12px; background: ${isUnlocked ? 'linear-gradient(135deg, rgba(79,158,222,0.08) 0%, rgba(79,158,222,0.02) 100%)' : '#f8fafc'}; ${isUnlocked ? '' : 'opacity: 0.6;'}">
|
||||
<div style="width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; background: ${isUnlocked ? color : '#e2e8f0'}20;">
|
||||
<span style="font-size: 24px;">${isUnlocked ? icon : '🔒'}</span>
|
||||
</div>
|
||||
<h4 style="font-size: 14px; font-weight: 600; color: ${isUnlocked ? color : '#1e293b'}; margin-bottom: 4px;">${name}</h4>
|
||||
${desc ? '<p style="font-size: 12px; color: #64748b; margin-bottom: 4px;">' + desc + '</p>' : ''}
|
||||
${rarityName ? '<span style="display: inline-block; padding: 2px 8px; background: ' + color + '15; color: ' + color + '; font-size: 11px; border-radius: 4px;">' + rarityName + '</span>' : ''}
|
||||
${isUnlocked ? '<span style="display: inline-block; margin-top: 6px; padding: 2px 8px; background: #22c55e15; color: #22c55e; font-size: 11px; border-radius: 4px;">✓ 已解锁</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
if (filteredAchievements.length === 0) {
|
||||
html = '<div style="text-align: center; padding: 40px; color: #94a3b8; grid-column: 1/-1;"><span style="font-size: 48px; display: block; margin-bottom: 12px;">🏆</span><p>' + (filter === 'unlocked' ? '暂无已解锁成就' : filter === 'locked' ? '所有成就已解锁!' : '暂无成就数据') + '</p></div>';
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderLevelListFromAPI(currentLevel) {
|
||||
const token = localStorage.getItem('user_token');
|
||||
fetch('/api/web/growth/rules', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const sections = data.data.sections;
|
||||
const levelSection = sections.find(s => s.type === 'level_system');
|
||||
if (levelSection && levelSection.levels) {
|
||||
const container = document.getElementById('levelList');
|
||||
let html = '';
|
||||
levelSection.levels.forEach(item => {
|
||||
const isCurrent = item.level === currentLevel;
|
||||
html += `
|
||||
<div style="display: flex; align-items: center; gap: 12px; padding: 12px; border-radius: 8px; ${isCurrent ? 'background: linear-gradient(135deg, rgba(79,158,222,0.1) 0%, rgba(79,158,222,0.05) 100%); border: 1px solid rgba(79,158,222,0.2);' : 'background: #f8fafc;'}">
|
||||
<div style="width: 36px; height: 36px; border-radius: 6px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 700; background: ${isCurrent ? 'linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%)' : '#e2e8f0'}; color: ${isCurrent ? '#fff' : '#64748b'};">Lv.${item.level}</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 13px; font-weight: 600; color: ${isCurrent ? '#4F9EDE' : '#1e293b'};">${item.icon} ${item.title}</div>
|
||||
<div style="font-size: 12px; color: #64748b;">${item.exp} 经验</div>
|
||||
</div>
|
||||
${isCurrent ? '<span style="font-size: 11px; color: #4F9EDE; background: rgba(79,158,222,0.1); padding: 2px 8px; border-radius: 4px;">当前</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,437 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>简记memo - 让记录更简单</title>
|
||||
<meta name="description" content="集待办任务、账单记录、心情日记于一体的个人事务管理工具">
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
||||
.nav-link.active { background: rgba(79, 158, 222, 0.1); color: #4F9EDE; }
|
||||
|
||||
/* 头部滚动效果增强 */
|
||||
.header-scrolled {
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.08) !important;
|
||||
}
|
||||
|
||||
/* Hero Section 增强 */
|
||||
.hero-section {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hero-section::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -10%;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
background: radial-gradient(circle, rgba(79, 158, 222, 0.08) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-section::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -30%;
|
||||
left: -5%;
|
||||
width: 400px;
|
||||
height: 400px;
|
||||
background: radial-gradient(circle, rgba(139, 92, 246, 0.06) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgba(79, 158, 222, 0.1);
|
||||
border-radius: 20px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid rgba(79, 158, 222, 0.15);
|
||||
animation: fadeInUp 0.6s ease forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.hero-title-anim {
|
||||
animation: fadeInUp 0.6s ease 0.1s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hero-desc-anim {
|
||||
animation: fadeInUp 0.6s ease 0.2s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hero-btn-anim {
|
||||
animation: fadeInUp 0.6s ease 0.3s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.hero-card-anim {
|
||||
animation: fadeInUp 0.6s ease 0.4s forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero-section {
|
||||
padding: 32px 16px 20px !important;
|
||||
min-height: calc(100vh - 56px) !important;
|
||||
padding-top: calc(56px + 32px) !important;
|
||||
}
|
||||
|
||||
.hero-section > div {
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
.hero-section > div > div {
|
||||
grid-template-columns: 1fr !important;
|
||||
gap: 24px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-section h1 {
|
||||
font-size: 32px !important;
|
||||
line-height: 1.3 !important;
|
||||
margin-bottom: 12px !important;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 15px !important;
|
||||
line-height: 1.6 !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
.hero-section .feature-card {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.hero-section > div > div > div:last-child {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.hero-section > div > div > div:last-child > div:first-child {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.hero-section > div > div > div:last-child > div:first-child > div {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
min-height: 180px;
|
||||
}
|
||||
|
||||
.hero-section .btn-group {
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
.hero-section .btn-group a {
|
||||
padding: 14px 24px !important;
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
#features {
|
||||
padding: 40px 16px !important;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 16px !important;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
.feature-card h3 {
|
||||
font-size: 15px !important;
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
#about {
|
||||
padding: 40px 16px !important;
|
||||
}
|
||||
|
||||
.about-content {
|
||||
flex-direction: column !important;
|
||||
gap: 24px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-content > div {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#footer {
|
||||
padding: 30px 16px !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-links {
|
||||
flex-direction: column !important;
|
||||
gap: 12px !important;
|
||||
margin-bottom: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.hero-section h1 {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
.hero-section p {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.features-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.hero-section .btn-group {
|
||||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.hero-section .btn-group a {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="commonHeader"></div>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section" style="min-height: calc(100vh - 64px); display: flex; align-items: center; padding: 80px 24px 40px; background: linear-gradient(135deg, #e0f2fe 0%, #f0f9ff 50%, #faf5ff 100%);">
|
||||
<div style="max-width: 1000px; margin: 0 auto; width: 100%;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 40px; align-items: center; position: relative; z-index: 1;">
|
||||
<div>
|
||||
<div class="hero-badge">
|
||||
<span style="width: 8px; height: 8px; background: #4F9EDE; border-radius: 50%; display: inline-block;"></span>
|
||||
<span style="font-size: 13px; color: #4F9EDE; font-weight: 500;">全新版本上线</span>
|
||||
</div>
|
||||
<h1 class="hero-title-anim" style="font-size: 42px; font-weight: 700; color: #1e293b; line-height: 1.2; margin-bottom: 16px;" id="siteName">简记memo</h1>
|
||||
<p class="hero-desc-anim" style="font-size: 16px; color: #64748b; line-height: 1.7; margin-bottom: 28px;" id="siteDesc">集待办任务、账单记录、心情日记于一体的个人事务管理工具。简洁优雅的界面设计,让记录成为一种享受。</p>
|
||||
<div class="btn-group hero-btn-anim" style="display: flex; gap: 12px; flex-wrap: wrap;">
|
||||
<a href="/login" style="display: inline-flex; align-items: center; justify-content: center; padding: 12px 28px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 8px; color: #fff; font-size: 14px; font-weight: 600; text-decoration: none; box-shadow: 0 4px 12px rgba(79, 158, 222, 0.3); transition: all 0.25s ease;">
|
||||
立即体验
|
||||
</a>
|
||||
<a href="#features" style="display: inline-flex; align-items: center; justify-content: center; padding: 12px 28px; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; color: #475569; font-size: 14px; font-weight: 500; text-decoration: none; transition: all 0.25s ease;">
|
||||
了解更多
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-card-anim" style="position: relative;">
|
||||
<div style="width: 380px; height: 380px; background: linear-gradient(135deg, rgba(79, 158, 222, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%); border-radius: 16px; display: flex; align-items: center; justify-content: center;">
|
||||
<div style="width: 320px; height: 320px; background: #fff; border-radius: 12px; box-shadow: 0 8px 24px rgba(0,0,0,0.06); padding: 20px;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
|
||||
<span style="font-size: 15px; font-weight: 600; color: #1e293b;">今日待办</span>
|
||||
<span style="font-size: 11px; color: #94a3b8;" id="currentDate">5月13日</span>
|
||||
</div>
|
||||
<div style="space-y: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 6px;">
|
||||
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #4F9EDE; display: flex; align-items: center; justify-content: center;">
|
||||
<span style="color: #4F9EDE; font-size: 10px;">✓</span>
|
||||
</div>
|
||||
<span style="font-size: 13px; color: #1e293b; text-decoration: line-through; opacity: 0.6;">完成项目文档</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #fef3c7; border-radius: 6px;">
|
||||
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #f59e0b; display: flex; align-items: center; justify-content: center;">
|
||||
</div>
|
||||
<span style="font-size: 13px; color: #1e293b;">参加会议</span>
|
||||
<span style="margin-left: auto; padding: 3px 6px; background: rgba(245, 158, 11, 0.2); border-radius: 4px; font-size: 10px; color: #d97706;">重要</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; gap: 10px; padding: 10px; background: #f8fafc; border-radius: 6px;">
|
||||
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid #cbd5e1; display: flex; align-items: center; justify-content: center;">
|
||||
</div>
|
||||
<span style="font-size: 13px; color: #64748b;">整理文件</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" style="padding: 60px 24px; background: #fff;">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="text-align: center; margin-bottom: 48px;">
|
||||
<span style="font-size: 13px; color: #4F9EDE; font-weight: 500;">功能特性</span>
|
||||
<h2 style="font-size: 28px; font-weight: 700; color: #1e293b; margin-top: 8px; margin-bottom: 12px;" id="featureTitle">为什么选择简记memo</h2>
|
||||
<p style="font-size: 15px; color: #64748b; max-width: 500px; margin: 0 auto;">简洁优雅的界面设计,强大实用的功能,帮助您更好地管理生活</p>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 16px;">
|
||||
<div class="feature-card" style="background: #f8fafc; border-radius: 8px; padding: 24px; transition: all 0.2s;">
|
||||
<div style="width: 48px; height: 48px; background: linear-gradient(135deg, rgba(79, 158, 222, 0.1) 0%, rgba(79, 158, 222, 0.05) 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;">
|
||||
<span style="font-size: 24px;">📝</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">待办管理</h3>
|
||||
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">轻松创建和管理待办任务,设置优先级和截止日期,让您的日程井井有条。</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" style="background: #fef3c7; border-radius: 8px; padding: 24px; transition: all 0.2s;">
|
||||
<div style="width: 48px; height: 48px; background: rgba(245, 158, 11, 0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;">
|
||||
<span style="font-size: 24px;">😊</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">心情日记</h3>
|
||||
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">记录每一天的心情变化,用文字和表情记录生活的点点滴滴。</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" style="background: #dcfce7; border-radius: 8px; padding: 24px; transition: all 0.2s;">
|
||||
<div style="width: 48px; height: 48px; background: rgba(34, 197, 94, 0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;">
|
||||
<span style="font-size: 24px;">💰</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">账单记录</h3>
|
||||
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">便捷记录收支情况,分类统计一目了然,让理财变得简单。</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card" style="background: #e0e7ff; border-radius: 8px; padding: 24px; transition: all 0.2s;">
|
||||
<div style="width: 48px; height: 48px; background: rgba(99, 102, 241, 0.15); border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px;">
|
||||
<span style="font-size: 24px;">🏆</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">成长体系</h3>
|
||||
<p style="font-size: 13px; color: #64748b; line-height: 1.5;">完成任务获得经验值,提升等级解锁成就,让记录更有成就感。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- How It Works -->
|
||||
<section style="padding: 60px 24px; background: #f8fafc;">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="text-align: center; margin-bottom: 48px;">
|
||||
<span style="font-size: 13px; color: #4F9EDE; font-weight: 500;">使用流程</span>
|
||||
<h2 style="font-size: 28px; font-weight: 700; color: #1e293b; margin-top: 8px;">简单三步开始记录</h2>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 32px;">
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 72px; height: 72px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
|
||||
<span style="font-size: 32px;">1️⃣</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">注册账号</h3>
|
||||
<p style="font-size: 13px; color: #64748b;">使用邮箱快速注册,开启您的记录之旅</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 72px; height: 72px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
|
||||
<span style="font-size: 32px;">2️⃣</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">创建任务</h3>
|
||||
<p style="font-size: 13px; color: #64748b;">添加待办事项、记录心情或账单</p>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="width: 72px; height: 72px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
|
||||
<span style="font-size: 32px;">3️⃣</span>
|
||||
</div>
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 8px;">查看统计</h3>
|
||||
<p style="font-size: 13px; color: #64748b;">查看完成进度、等级成长和成就</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA Section -->
|
||||
<section style="padding: 60px 24px; background: #fff;">
|
||||
<div style="max-width: 600px; margin: 0 auto; text-align: center;">
|
||||
<h2 style="font-size: 26px; font-weight: 700; color: #1e293b; margin-bottom: 12px;">开始您的记录之旅</h2>
|
||||
<p style="font-size: 15px; color: #64748b; margin-bottom: 28px;" id="ctaDesc">加入简记memo,让每一天都值得记录</p>
|
||||
<a href="/login" style="display: inline-flex; align-items: center; justify-content: center; padding: 14px 40px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 8px; color: #fff; font-size: 15px; font-weight: 600; text-decoration: none; box-shadow: 0 4px 12px rgba(79, 158, 222, 0.3);">
|
||||
立即体验
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="commonFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/common-components.js"></script>
|
||||
<script src="/static/js/toast.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/');
|
||||
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
|
||||
initCommonComponents();
|
||||
|
||||
// 设置动态内容
|
||||
if (typeof SITE_CONFIG !== 'undefined') {
|
||||
document.getElementById('siteName').textContent = SITE_CONFIG.name || '简记memo';
|
||||
document.getElementById('featureTitle').textContent = '为什么选择' + (SITE_CONFIG.name || '简记memo');
|
||||
document.getElementById('ctaDesc').textContent = '加入' + (SITE_CONFIG.name || '简记memo') + ',让每一天都值得记录';
|
||||
}
|
||||
if (typeof APP_INFO !== 'undefined') {
|
||||
document.getElementById('siteDesc').textContent = APP_INFO.description + '。简洁优雅的界面设计,让记录成为一种享受。';
|
||||
}
|
||||
|
||||
// 设置当前日期
|
||||
const today = new Date();
|
||||
document.getElementById('currentDate').textContent = today.getMonth() + 1 + '月' + today.getDate() + '日';
|
||||
|
||||
const navLinks = document.querySelectorAll('a.nav-link');
|
||||
navLinks.forEach(link => {
|
||||
link.addEventListener('click', function(e) {
|
||||
if (this.getAttribute('href') === '#features') {
|
||||
e.preventDefault();
|
||||
document.querySelector('#features').scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 头部滚动效果
|
||||
const header = document.querySelector('#commonHeader header');
|
||||
if (header) {
|
||||
let ticking = false;
|
||||
window.addEventListener('scroll', function() {
|
||||
if (!ticking) {
|
||||
window.requestAnimationFrame(function() {
|
||||
if (window.scrollY > 10) {
|
||||
header.classList.add('header-scrolled');
|
||||
} else {
|
||||
header.classList.remove('header-scrolled');
|
||||
}
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>隐私政策 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f1f5f9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="commonHeader"></div>
|
||||
|
||||
<!-- 页面内容 -->
|
||||
<main style="padding: 84px 24px 40px; min-height: calc(100vh - 140px);">
|
||||
<div style="max-width: 1000px; margin: 0 auto;">
|
||||
<div style="background: #fff; border-radius: 16px; padding: 40px;">
|
||||
<h1 style="font-size: 28px; font-weight: 700; color: #1e293b; text-align: center; margin-bottom: 12px;">简记memo隐私政策</h1>
|
||||
<p style="font-size: 14px; color: #64748b; text-align: center; margin-bottom: 32px; padding-bottom: 20px; border-bottom: 1px dashed rgba(79, 158, 222, 0.2);">生效日期:2025年04月24日</p>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">1. 总则</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">1.1 为保护用户隐私,规范本服务的个人信息处理行为,本服务运营者(个人开发者,以下简称"运营者")依据《中华人民共和国个人信息保护法》《中华人民共和国网络安全法》等法律法规,制定本隐私政策。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">1.2 本隐私政策适用于您使用本服务过程中涉及的个人信息处理活动,您使用本服务即视为同意本政策;若您不同意本政策,请勿使用本服务。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">1.3 本服务由个人独立开发及运营,无所属企业/公司主体,所有信息处理行为均由运营者个人负责。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">2. 个人信息的收集</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">2.1 登录相关信息:您注册账号时提供的邮箱地址、昵称等信息,用于识别您的用户身份。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">2.2 业务数据:您在本服务内录入的备忘录、待办事项、账单内容、编辑记录、时间戳等数据,将存储于运营者的服务器中,仅用于为您提供数据同步、查看、编辑等核心功能。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">2.3 设备与日志信息:为保障服务正常运行,可能被动获取基础设备信息(如设备型号、系统版本)、操作日志(如页面访问记录、功能使用记录),该类信息仅用于故障排查、功能优化。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">2.4 运营者不会主动收集您的姓名、身份证号、手机号、地理位置等其他个人敏感信息。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">3. 个人信息的使用</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">3.1 用户身份信息仅用于识别您的用户身份,保障您能正常访问、管理自己存储在服务器的业务数据。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">3.2 您的业务数据仅用于为您本人提供查看、编辑、删除、同步等核心功能,运营者不会利用该数据进行商业推广、数据分析、第三方共享等其他用途。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">3.3 设备与日志信息仅用于定位和解决服务运行故障、优化产品体验,不会用于识别个人身份或向第三方披露。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">4. 个人信息的存储与保护</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">4.1 存储方式:用户身份信息及您的业务数据存储于运营者的服务器中;设备与日志信息仅在故障排查期间临时存储,排查完成后即时删除。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">4.2 存储期限:用户身份信息与业务数据将持续存储,直至您主动申请删除账号;设备与日志信息存储期限不超过7日。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">4.3 安全措施:运营者通过服务器访问权限控制、数据加密传输等技术手段保障数据安全,但因网络攻击、服务器故障等非运营方可控因素导致的数据泄露,运营者不承担责任。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">4.4 您可通过服务内的删除功能清理自身业务数据,如需彻底删除账号及所有数据,可联系运营者协助处理。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">5. 个人信息的共享、转让与公开披露</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">5.1 运营者不会将您的身份信息、业务数据、设备信息等任何信息共享、转让给第三方,除非获得您的明确授权,或法律法规、司法机关/行政机关的强制性要求。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">5.2 未经您同意,运营者不会公开披露您的任何信息;因法律法规要求披露时,将在允许范围内尽可能提前通知您。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">6. 您的权利</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">6.1 您有权随时查看、编辑、删除自己存储在本服务内的业务数据;</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">6.2 您有权联系运营者申请删除账号及所有关联数据;</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">6.3 您有权向运营者查询自身个人信息的处理情况,运营者将在合理期限内予以答复;</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">6.4 若您发现本服务存在违规处理您信息的行为,有权向运营者反馈或向相关平台投诉。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">7. 未成年人保护</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">7.1 本服务面向所有年龄段用户,若未成年人使用本服务,应在监护人指导下进行,监护人应承担相应监护责任。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">7.2 运营者不会主动收集未成年人的任何额外个人信息,若监护人发现未成年人的信息被不当处理,可联系运营者协助处理。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">8. 隐私政策的变更</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">8.1 运营者可根据法律法规调整或业务优化,修改本隐私政策,修改后的政策将在服务内公示,公示满7日后生效。</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">8.2 若您不同意修改后的政策,应停止使用本服务并可申请删除账号数据;继续使用的,视为同意修改后的政策。</p>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 28px;">
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">9. 联系我们</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">9.1 若您对本隐私政策有任何疑问、意见或投诉,可通过以下方式联系运营者:</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; padding-left: 2em; margin-bottom: 6px;" id="privacyEmail">联系邮箱:memo@miaoall.cn</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; padding-left: 2em;" id="privacyWechat">微信公众号:孙三苗(sunsanmiao)</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">9.2 运营者将在15个工作日内回复您的咨询或处理请求。</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div style="font-size: 16px; font-weight: 600; color: #4F9EDE; background: rgba(79, 158, 222, 0.1); border-left: 4px solid #4F9EDE; padding: 10px 14px; border-radius: 0 8px 8px 0; margin-bottom: 14px;">10. 其他</div>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">10.1 本隐私政策的解释权归运营者个人所有;</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em; margin-bottom: 10px;">10.2 若本政策与法律法规冲突,以法律法规为准;</p>
|
||||
<p style="font-size: 14px; color: #475569; line-height: 1.8; text-indent: 2em;">10.3 您使用本服务的行为,同时受相关平台的隐私政策及运营规范约束。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="commonFooter"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/common-components.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('commonHeader').innerHTML = renderCommonHeader('/privacy');
|
||||
document.getElementById('commonFooter').innerHTML = renderCommonFooter();
|
||||
initCommonComponents();
|
||||
|
||||
if (typeof SITE_CONFIG !== 'undefined') {
|
||||
document.getElementById('privacyEmail').textContent = '联系邮箱:' + (SITE_CONFIG.email || 'memo@miaoall.cn');
|
||||
if (SITE_CONFIG.wechat) {
|
||||
document.getElementById('privacyWechat').textContent = '微信公众号:' + (SITE_CONFIG.wechat.name || '孙三苗') + '(' + (SITE_CONFIG.wechat.account || 'sunsanmiao') + ')';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,509 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>个人中心 - 简记memo</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.15.14/lib/theme-chalk/index.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f8fafc; }
|
||||
.nav-link.active { background: rgba(79, 158, 222, 0.1); color: #4F9EDE; }
|
||||
.menu-card { transition: all 0.2s; cursor: pointer; }
|
||||
.menu-card:hover { transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,0.08); }
|
||||
.stat-card { transition: all 0.2s; }
|
||||
.stat-card:hover { transform: translateY(-2px); }
|
||||
.avatar-option { cursor: pointer; transition: all 0.2s; border: 2px solid transparent; }
|
||||
.avatar-option:hover { transform: scale(1.05); }
|
||||
.avatar-option.selected { border-color: #4F9EDE; box-shadow: 0 0 0 3px rgba(79, 158, 222, 0.2); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 公共头部 -->
|
||||
<div id="webHeader"></div>
|
||||
|
||||
<div style="margin-top: 56px; min-height: calc(100vh - 56px - 40px); padding: 24px;">
|
||||
<div style="max-width: 1200px; margin: 0 auto;">
|
||||
<!-- 左右布局 -->
|
||||
<div style="display: grid; grid-template-columns: 380px 1fr; gap: 24px;">
|
||||
<!-- 左侧:用户信息 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<!-- 用户信息卡片 -->
|
||||
<div style="background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 12px; padding: 32px 24px; color: #fff; text-align: center;">
|
||||
<div id="userAvatarContainer" style="width: 100px; height: 100px; border-radius: 50%; background: rgba(255,255,255,0.2); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; font-size: 40px; font-weight: 600; cursor: pointer; position: relative; overflow: hidden;" onclick="showAvatarModal()">
|
||||
U
|
||||
<div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>
|
||||
</div>
|
||||
<h2 style="font-size: 22px; font-weight: 700; margin-bottom: 6px;" id="userNickname">用户</h2>
|
||||
<p style="font-size: 14px; opacity: 0.9; margin-bottom: 8px;" id="userEmail">未绑定邮箱</p>
|
||||
<p style="font-size: 13px; opacity: 0.8;" id="userSignature">还没有个性签名</p>
|
||||
<button onclick="showEditModal()" style="margin-top: 20px; padding: 10px 24px; background: rgba(255,255,255,0.2); border: 1px solid rgba(255,255,255,0.3); border-radius: 8px; font-size: 13px; color: #fff; cursor: pointer;">
|
||||
编辑资料
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 20px;">
|
||||
<h3 style="font-size: 14px; font-weight: 600; color: #1e293b; margin-bottom: 16px;">快捷入口</h3>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;">
|
||||
<div onclick="navigateTo('/web/calendar')" class="menu-card" style="display: flex; align-items: center; gap: 12px; padding: 14px; border-radius: 10px; background: #f8fafc;">
|
||||
<div style="width: 40px; height: 40px; background: linear-gradient(135deg, rgba(79, 158, 222, 0.1) 0%, rgba(79, 158, 222, 0.05) 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg style="width: 20px; height: 20px; fill: none; stroke: #4F9EDE;" 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>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">日程管理</div>
|
||||
<div style="font-size: 12px; color: #64748b;">管理每日待办任务</div>
|
||||
</div>
|
||||
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
<div onclick="navigateTo('/web/growth')" class="menu-card" style="display: flex; align-items: center; gap: 12px; padding: 14px; border-radius: 10px; background: #f8fafc;">
|
||||
<div style="width: 40px; height: 40px; background: linear-gradient(135deg, rgba(245, 158, 11, 0.1) 0%, rgba(245, 158, 11, 0.05) 100%); border-radius: 10px; display: flex; align-items: center; justify-content: center;">
|
||||
<svg style="width: 20px; height: 20px; fill: none; stroke: #f59e0b;" 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>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-size: 14px; font-weight: 600; color: #1e293b;">成长中心</div>
|
||||
<div style="font-size: 12px; color: #64748b;">查看等级与成就</div>
|
||||
</div>
|
||||
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设置 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 8px;">
|
||||
<div onclick="navigateTo('/about')" style="display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; cursor: pointer; border-radius: 8px; transition: all 0.15s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<svg style="width: 18px; height: 18px; fill: none; stroke: #64748b;" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
|
||||
<span style="font-size: 14px; color: #1e293b;">关于我们</span>
|
||||
</div>
|
||||
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
<div onclick="navigateTo('/privacy')" style="display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; cursor: pointer; border-radius: 8px; transition: all 0.15s;" onmouseover="this.style.background='#f8fafc'" onmouseout="this.style.background='transparent'">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<svg style="width: 18px; height: 18px; fill: none; stroke: #64748b;" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
<span style="font-size: 14px; color: #1e293b;">隐私政策</span>
|
||||
</div>
|
||||
<svg style="width: 16px; height: 16px; fill: none; stroke: #94a3b8;" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:统计数据 -->
|
||||
<div style="display: flex; flex-direction: column; gap: 16px;">
|
||||
<!-- 统计卡片 -->
|
||||
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px;">
|
||||
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: #4F9EDE;" id="statTodoTotal">0</div>
|
||||
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">总待办</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: #22c55e;" id="statTodoDone">0</div>
|
||||
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">已完成</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: #f59e0b;" id="statLevel">1</div>
|
||||
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">当前等级</div>
|
||||
</div>
|
||||
<div class="stat-card" style="background: #fff; border-radius: 12px; padding: 24px; text-align: center;">
|
||||
<div style="font-size: 32px; font-weight: 700; color: #8b5cf6;" id="statExp">0</div>
|
||||
<div style="font-size: 13px; color: #64748b; margin-top: 6px;">经验值</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 成长进度 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 24px;">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b; margin-bottom: 20px;">成长进度</h3>
|
||||
<div style="display: flex; align-items: center; gap: 20px;">
|
||||
<div style="width: 80px; height: 80px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 24px; font-weight: 700;">
|
||||
Lv.<span id="growthLevel">1</span>
|
||||
</div>
|
||||
<div style="flex: 1;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px;">
|
||||
<span style="font-size: 15px; font-weight: 600; color: #1e293b;">当前等级</span>
|
||||
<span style="font-size: 13px; color: #64748b;" id="growthExpText">0 / 100 经验</span>
|
||||
</div>
|
||||
<div style="background: #e2e8f0; border-radius: 4px; height: 10px; overflow: hidden;">
|
||||
<div id="growthExpBar" style="height: 100%; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 4px; transition: width 0.5s; width: 0%;"></div>
|
||||
</div>
|
||||
<p style="font-size: 12px; color: #94a3b8; margin-top: 8px;" id="growthRemain">还需 100 经验升级</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最近成就 -->
|
||||
<div style="background: #fff; border-radius: 12px; padding: 24px; flex: 1;">
|
||||
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #1e293b;">我的成就</h3>
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<select id="profileAchFilter" onchange="filterProfileAchievements()" style="padding: 6px 12px; border: 1px solid #e2e8f0; border-radius: 6px; font-size: 13px; color: #475569; background: #fff; cursor: pointer;">
|
||||
<option value="all">全部分类</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="recentAchievements" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 16px;">
|
||||
<!-- 动态渲染 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公共底部 -->
|
||||
<div id="webFooter"></div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<div id="editModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; padding: 24px;">
|
||||
<div style="background: #fff; border-radius: 16px; padding: 28px; width: 100%; max-width: 400px;">
|
||||
<h3 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 24px;">编辑资料</h3>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-size: 13px; font-weight: 500; color: #475569; margin-bottom: 8px;">昵称</label>
|
||||
<input type="text" id="editNickname" placeholder="请输入昵称" style="width: 100%; padding: 12px 16px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 14px; outline: none;" onfocus="this.style.borderColor='#4F9EDE'" onblur="this.style.borderColor='#e2e8f0'">
|
||||
</div>
|
||||
<div style="margin-bottom: 28px;">
|
||||
<label style="display: block; font-size: 13px; font-weight: 500; color: #475569; margin-bottom: 8px;">签名</label>
|
||||
<textarea id="editSignature" placeholder="请输入个性签名" rows="3" style="width: 100%; padding: 12px 16px; border: 1px solid #e2e8f0; border-radius: 10px; font-size: 14px; resize: none; outline: none;" onfocus="this.style.borderColor='#4F9EDE'" onblur="this.style.borderColor='#e2e8f0'"></textarea>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 12px;">
|
||||
<button onclick="closeEditModal()" style="padding: 10px 20px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; background: #fff;">取消</button>
|
||||
<button onclick="saveProfile()" style="padding: 10px 20px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; font-size: 14px; color: #fff; cursor: pointer;">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 头像选择弹窗 -->
|
||||
<div id="avatarModal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; padding: 24px;">
|
||||
<div style="background: #fff; border-radius: 16px; padding: 28px; width: 100%; max-width: 480px;">
|
||||
<h3 style="font-size: 18px; font-weight: 600; color: #1e293b; margin-bottom: 24px;">选择头像</h3>
|
||||
<div id="avatarGrid" style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;">
|
||||
<!-- 动态生成头像选项 -->
|
||||
</div>
|
||||
<div style="display: flex; justify-content: flex-end; gap: 12px;">
|
||||
<button onclick="closeAvatarModal()" style="padding: 10px 20px; border: 1px solid #e2e8f0; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; background: #fff;">取消</button>
|
||||
<button onclick="saveAvatar()" style="padding: 10px 20px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; font-size: 14px; color: #fff; cursor: pointer;">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
|
||||
<script src="https://unpkg.com/element-ui@2.15.14/lib/index.js"></script>
|
||||
<script src="/static/js/config.js"></script>
|
||||
<script src="/static/js/toast.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/web-components.js"></script>
|
||||
<script>
|
||||
// 默认头像列表
|
||||
const DEFAULT_AVATARS = [
|
||||
{ url: '/static/avatar/ginger_cat.jpg', name: 'ginger_cat', label: '橘猫' },
|
||||
{ url: '/static/avatar/ragdoll.jpg', name: 'ragdoll', label: '布偶猫' },
|
||||
{ url: '/static/avatar/hamster.jpg', name: 'hamster', label: '小仓鼠' },
|
||||
{ url: '/static/avatar/dragon.jpg', name: 'dragon', label: '龙' },
|
||||
{ url: '/static/avatar/pony.jpg', name: 'pony', label: '小马' },
|
||||
{ url: '/static/avatar/lamb.jpg', name: 'lamb', label: '小羊' },
|
||||
{ url: '/static/avatar/corgi.jpg', name: 'corgi', label: '柯基' },
|
||||
{ url: '/static/avatar/piglet.jpg', name: 'piglet', label: '小猪' },
|
||||
];
|
||||
|
||||
let selectedAvatarUrl = '';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 初始化公共头部底部
|
||||
initWebPage('profile');
|
||||
// 加载页面数据
|
||||
loadUserInfo();
|
||||
loadStats();
|
||||
loadGrowthInfo();
|
||||
renderAvatarGrid();
|
||||
});
|
||||
|
||||
function navigateTo(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function loadUserInfo() {
|
||||
const token = localStorage.getItem('user_token');
|
||||
const nickname = localStorage.getItem('user_nickname') || '用户';
|
||||
const avatar = localStorage.getItem('user_avatar');
|
||||
const email = localStorage.getItem('user_email');
|
||||
|
||||
document.getElementById('userNickname').textContent = nickname;
|
||||
// 优先显示localStorage中的邮箱
|
||||
if (email) {
|
||||
document.getElementById('userEmail').textContent = email;
|
||||
}
|
||||
|
||||
const avatarContainer = document.getElementById('userAvatarContainer');
|
||||
if (avatar) {
|
||||
avatarContainer.innerHTML = `<img src="${avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;"><div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>`;
|
||||
selectedAvatarUrl = avatar;
|
||||
} else {
|
||||
avatarContainer.textContent = nickname.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
fetch('/api/web/user/info', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const user = data.data;
|
||||
document.getElementById('userNickname').textContent = user.nickname || '用户';
|
||||
// 接口返回的邮箱优先
|
||||
if (user.email) {
|
||||
document.getElementById('userEmail').textContent = user.email;
|
||||
localStorage.setItem('user_email', user.email);
|
||||
}
|
||||
if (user.signature) {
|
||||
document.getElementById('userSignature').textContent = user.signature;
|
||||
}
|
||||
if (user.avatar) {
|
||||
const container = document.getElementById('userAvatarContainer');
|
||||
container.innerHTML = `<img src="${user.avatar}" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover;"><div style="position: absolute; bottom: 0; left: 0; right: 0; height: 28px; background: rgba(0,0,0,0.3); display: flex; align-items: center; justify-content: center; font-size: 12px;">更换</div>`;
|
||||
localStorage.setItem('user_avatar', user.avatar);
|
||||
selectedAvatarUrl = user.avatar;
|
||||
}
|
||||
localStorage.setItem('user_nickname', user.nickname || '用户');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadStats() {
|
||||
const token = localStorage.getItem('user_token');
|
||||
|
||||
// 加载成长信息
|
||||
fetch('/api/web/growth/info', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
document.getElementById('statLevel').textContent = data.data.level || 1;
|
||||
document.getElementById('statExp').textContent = data.data.total_exp || 0;
|
||||
}
|
||||
});
|
||||
|
||||
// 加载待办统计
|
||||
fetch('/api/web/todo/list', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'token': token },
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const todos = data.data || [];
|
||||
const total = todos.length;
|
||||
const done = todos.filter(t => t.done === 1 || t.status === 1).length;
|
||||
document.getElementById('statTodoTotal').textContent = total;
|
||||
document.getElementById('statTodoDone').textContent = done;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadGrowthInfo() {
|
||||
const token = localStorage.getItem('user_token');
|
||||
|
||||
fetch('/api/web/growth/info', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const g = data.data;
|
||||
document.getElementById('growthLevel').textContent = g.level || 1;
|
||||
|
||||
// 直接使用后端返回的字段
|
||||
const currentExp = g.currentExp || 0;
|
||||
const expToNext = g.nextLevelExp || 100;
|
||||
const progress = g.levelProgress || 0;
|
||||
|
||||
document.getElementById('growthExpText').textContent = currentExp + ' / ' + expToNext + ' 经验';
|
||||
document.getElementById('growthRemain').textContent = '还需 ' + expToNext + ' 经验升级';
|
||||
document.getElementById('growthExpBar').style.width = Math.min(progress, 100) + '%';
|
||||
|
||||
// 更新统计数据
|
||||
document.getElementById('statLevel').textContent = g.level || 1;
|
||||
document.getElementById('statExp').textContent = g.experience || 0;
|
||||
|
||||
// 渲染成就
|
||||
renderProfileAchievements(g.achievements?.list || []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 存储成就数据用于筛选
|
||||
let profileAchievementsData = [];
|
||||
let profileAchCategories = [];
|
||||
|
||||
// 渲染个人中心成就
|
||||
function renderProfileAchievements(unlockedList) {
|
||||
const token = localStorage.getItem('user_token');
|
||||
|
||||
// 获取成就分类
|
||||
fetch('/api/web/growth/rules', { headers: { 'token': token } })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
const sections = data.data.sections;
|
||||
const achSection = sections.find(s => s.type === 'achievement_system');
|
||||
if (achSection && achSection.categories) {
|
||||
profileAchCategories = achSection.categories;
|
||||
// 更新分类下拉框 - 使用小写字段名
|
||||
const select = document.getElementById('profileAchFilter');
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="all">全部分类</option>' +
|
||||
profileAchCategories.map(c => `<option value="${c.key || c.Key}">${c.name || c.Name}</option>`).join('');
|
||||
}
|
||||
}
|
||||
|
||||
// 按稀有度排序(传说 > 史诗 > 稀有 > 高级 > 中级 > 初级)
|
||||
const rarityOrder = { '传说': 6, '史诗': 5, '稀有': 4, '高级': 3, '中级': 2, '初级': 1 };
|
||||
profileAchievementsData = (unlockedList || []).sort((a, b) => {
|
||||
return (rarityOrder[b.rarityName || b.rarity] || 0) - (rarityOrder[a.rarityName || a.rarity] || 0);
|
||||
});
|
||||
|
||||
renderProfileAchList('all');
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('加载成就分类失败:', err);
|
||||
// 即使没有分类,也尝试渲染成就
|
||||
profileAchievementsData = unlockedList || [];
|
||||
renderProfileAchList('all');
|
||||
});
|
||||
}
|
||||
|
||||
function filterProfileAchievements() {
|
||||
const filter = document.getElementById('profileAchFilter').value;
|
||||
renderProfileAchList(filter);
|
||||
}
|
||||
|
||||
function renderProfileAchList(filter) {
|
||||
const container = document.getElementById('recentAchievements');
|
||||
|
||||
let filtered = profileAchievementsData;
|
||||
if (filter !== 'all') {
|
||||
filtered = profileAchievementsData.filter(ach => ach.category === filter);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div style="text-align: center; padding: 40px; color: #94a3b8; grid-column: 1/-1;"><span style="font-size: 48px; display: block; margin-bottom: 12px;">🏆</span><p>' + (filter === 'all' ? '暂无成就,继续努力吧!' : '该分类暂无成就') + '</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
filtered.forEach(ach => {
|
||||
const color = ach.rarityColor || '#4F9EDE';
|
||||
const icon = ach.icon || '🏆';
|
||||
const name = ach.name || '未知成就';
|
||||
const desc = ach.description || ach.desc || '';
|
||||
const rarityName = ach.rarityName || ach.rarity || '';
|
||||
|
||||
html += `
|
||||
<div style="text-align: center; padding: 16px; border-radius: 12px; background: linear-gradient(135deg, rgba(79,158,222,0.08) 0%, rgba(79,158,222,0.02) 100%);">
|
||||
<div style="width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; margin: 0 auto 10px; background: ${color}20;">
|
||||
<span style="font-size: 20px;">${icon}</span>
|
||||
</div>
|
||||
<h4 style="font-size: 13px; font-weight: 600; color: ${color}; margin-bottom: 2px;">${name}</h4>
|
||||
${desc ? '<p style="font-size: 11px; color: #64748b; margin-bottom: 4px;">' + desc + '</p>' : ''}
|
||||
${rarityName ? '<span style="display: inline-block; padding: 2px 8px; background: ' + color + '15; color: ' + color + '; font-size: 10px; border-radius: 4px;">' + rarityName + '</span>' : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderAvatarGrid() {
|
||||
const container = document.getElementById('avatarGrid');
|
||||
let html = '';
|
||||
DEFAULT_AVATARS.forEach(avatar => {
|
||||
const isSelected = selectedAvatarUrl === avatar.url;
|
||||
html += `
|
||||
<div class="avatar-option ${isSelected ? 'selected' : ''}" onclick="selectAvatar('${avatar.url}', this)" style="border-radius: 50%; overflow: hidden; width: 80px; height: 80px; margin: 0 auto;">
|
||||
<img src="${avatar.url}" style="width: 100%; height: 100%; object-fit: cover;" alt="${avatar.label}">
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function selectAvatar(url, element) {
|
||||
selectedAvatarUrl = url;
|
||||
document.querySelectorAll('.avatar-option').forEach(el => el.classList.remove('selected'));
|
||||
element.classList.add('selected');
|
||||
}
|
||||
|
||||
function showAvatarModal() {
|
||||
renderAvatarGrid();
|
||||
document.getElementById('avatarModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeAvatarModal() {
|
||||
document.getElementById('avatarModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveAvatar() {
|
||||
if (!selectedAvatarUrl) {
|
||||
$toast.error('请选择头像');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('user_token');
|
||||
fetch('/api/web/user/edit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'token': token },
|
||||
body: JSON.stringify({ avatar: selectedAvatarUrl })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
$toast.success('头像更换成功');
|
||||
localStorage.setItem('user_avatar', selectedAvatarUrl);
|
||||
loadUserInfo();
|
||||
closeAvatarModal();
|
||||
} else {
|
||||
$toast.error(data.msg || '更换失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showEditModal() {
|
||||
const nickname = document.getElementById('userNickname').textContent;
|
||||
const signature = document.getElementById('userSignature').textContent;
|
||||
document.getElementById('editNickname').value = nickname;
|
||||
document.getElementById('editSignature').value = signature === '还没有个性签名' ? '' : signature;
|
||||
document.getElementById('editModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
document.getElementById('editModal').style.display = 'none';
|
||||
}
|
||||
|
||||
function saveProfile() {
|
||||
const token = localStorage.getItem('user_token');
|
||||
const nickname = document.getElementById('editNickname').value;
|
||||
const signature = document.getElementById('editSignature').value;
|
||||
|
||||
if (!nickname) {
|
||||
$toast.error('请输入昵称');
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/web/user/edit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'token': token },
|
||||
body: JSON.stringify({ nickname, signature })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 200) {
|
||||
$toast.success('保存成功');
|
||||
closeEditModal();
|
||||
document.getElementById('userNickname').textContent = nickname;
|
||||
document.getElementById('userSignature').textContent = signature || '还没有个性签名';
|
||||
localStorage.setItem('user_nickname', nickname);
|
||||
} else {
|
||||
$toast.error(data.msg || '保存失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 223 KiB |
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||