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

2151 lines
98 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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; min-height: 100vh; }
.nav-link.active { background: rgba(79, 158, 222, 0.1); color: #4F9EDE; }
.active-tab { background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%) !important; color: #fff !important; }
.active-tab svg { stroke: #fff !important; }
.task-item:hover { background: #f1f5f9; }
.mood-selector span:hover { transform: scale(1.1); }
.mood-selector .selected { transform: scale(1.15); box-shadow: 0 0 12px rgba(79, 158, 222, 0.4); }
.date-dot { width: 5px; height: 5px; border-radius: 50%; }
.date-cell { position: relative; }
.date-cell:hover { border: 1px solid rgba(79, 158, 222, 0.3) !important; }
.date-cell:hover .add-btn { opacity: 1; transform: scale(1); }
.add-btn {
opacity: 0;
transform: scale(0.8);
transition: all 0.2s;
position: absolute;
bottom: 4px;
right: 4px;
width: 22px;
height: 22px;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 13px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(79, 158, 222, 0.3);
}
.add-btn:hover { background: linear-gradient(135deg, #3A8BC9 0%, #2D7BB5 100%); }
.context-menu {
position: absolute;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
padding: 4px;
z-index: 100;
min-width: 140px;
border: 1px solid #e2e8f0;
}
.context-menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
color: #475569;
transition: all 0.15s;
}
.context-menu-item:hover {
background: rgba(79, 158, 222, 0.08);
color: #4F9EDE;
}
/* 抽屉样式 */
.drawer-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.4);
z-index: 1000;
display: none;
animation: fadeIn 0.2s ease;
}
.drawer-overlay.active { display: block; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.drawer {
position: fixed;
right: 0;
top: 0;
bottom: 0;
width: 480px;
background: #fff;
box-shadow: -4px 0 20px rgba(0,0,0,0.1);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 1001;
display: flex;
flex-direction: column;
overflow-x: hidden;
}
.drawer.active { transform: translateX(0); }
.drawer-header {
padding: 20px;
border-bottom: 1px solid #f1f5f9;
display: flex;
align-items: center;
justify-content: space-between;
}
.drawer-title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
}
.drawer-close {
width: 32px;
height: 32px;
border-radius: 50%;
background: #f1f5f9;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.15s;
}
.drawer-close:hover { background: #e2e8f0; }
.drawer-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.drawer-footer {
padding: 16px 20px;
border-top: 1px solid #f1f5f9;
display: flex;
gap: 12px;
}
.btn-primary {
flex: 1;
background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%);
border: none;
color: #fff;
padding: 12px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(79, 158, 222, 0.3);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
flex: 1;
padding: 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
color: #475569;
cursor: pointer;
transition: all 0.2s;
background: #fff;
}
.btn-secondary:hover {
background: #f8fafc;
}
.form-group {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: #475569;
margin-bottom: 8px;
}
.form-label span.required { color: #ef4444; margin-left: 2px; }
.form-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
transition: all 0.15s;
background: #fff;
}
.form-input:focus {
outline: none;
border-color: #4F9EDE;
box-shadow: 0 0 0 2px rgba(79, 158, 222, 0.1);
}
.form-textarea {
width: 100%;
padding: 10px 12px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
resize: none;
transition: all 0.15s;
min-height: 80px;
}
.form-textarea:focus {
outline: none;
border-color: #4F9EDE;
box-shadow: 0 0 0 2px rgba(79, 158, 222, 0.1);
}
.category-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
}
.category-btn {
padding: 10px 6px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 12px;
color: #475569;
cursor: pointer;
transition: all 0.15s;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.category-btn:hover {
border-color: #4F9EDE;
background: rgba(79, 158, 222, 0.05);
}
.category-btn.selected {
border-color: #4F9EDE;
background: rgba(79, 158, 222, 0.1);
}
.priority-row {
display: flex;
gap: 10px;
}
.priority-btn {
flex: 1;
padding: 10px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.priority-btn:hover { border-color: #4F9EDE; }
.priority-btn.selected {
border-color: #4F9EDE;
background: rgba(79, 158, 222, 0.1);
}
.priority-btn.high { color: #dc2626; }
.priority-btn.medium { color: #d97706; }
.priority-btn.low { color: #64748b; }
.time-input-group {
display: flex;
gap: 10px;
}
.time-input {
flex: 1;
}
.quick-amount {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.quick-amount-btn {
padding: 8px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 20px;
font-size: 13px;
color: #475569;
cursor: pointer;
transition: all 0.15s;
}
.quick-amount-btn:hover {
border-color: #4F9EDE;
background: rgba(79, 158, 222, 0.05);
}
@keyframes bounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
@keyframes popIn {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.mood-item {
padding: 12px 6px;
background: #fff;
border: 2px solid transparent;
border-radius: 50%;
text-align: center;
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
aspect-ratio: 1;
justify-content: center;
}
.mood-item:hover {
background: #f1f5f9;
transform: scale(1.05);
}
.mood-item.selected {
background: #fff;
border-color: #22c55e;
animation: popIn 0.3s ease;
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.2);
}
.mood-emoji {
font-size: 28px;
transition: transform 0.2s;
}
.mood-item:hover .mood-emoji {
transform: scale(1.15);
}
.mood-item.selected .mood-emoji {
transform: scale(1.15);
}
.mood-label {
font-size: 11px;
color: #64748b;
}
.phrase-tag {
padding: 6px 12px;
background: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 20px;
font-size: 12px;
color: #475569;
cursor: pointer;
transition: all 0.2s;
}
.phrase-tag:hover {
background: rgba(79, 158, 222, 0.08);
border-color: #4F9EDE;
}
.phrase-tag.selected {
background: rgba(79, 158, 222, 0.15);
border-color: #4F9EDE;
color: #4F9EDE;
}
.mood-preview-enter {
animation: fadeInUp 0.4s ease;
}
/* 响应式适配 */
@media (max-width: 1024px) {
main > div {
grid-template-columns: 1fr !important;
}
.calendar-grid {
grid-template-columns: repeat(7, 1fr) !important;
gap: 4px !important;
}
.date-cell {
min-height: 70px !important;
padding: 4px !important;
}
.date-cell .date-num {
font-size: 12px !important;
}
}
@media (max-width: 768px) {
main {
padding: 12px !important;
margin-top: 56px !important;
}
.drawer {
width: 100% !important;
max-width: 100% !important;
}
.drawer-header {
padding: 16px !important;
}
.drawer-content {
padding: 16px !important;
}
.drawer-footer {
padding: 12px 16px !important;
}
.category-grid {
grid-template-columns: repeat(4, 1fr) !important;
gap: 8px !important;
}
.mood-selector {
grid-template-columns: repeat(5, 1fr) !important;
gap: 8px !important;
}
.mood-item {
padding: 8px 4px !important;
}
.mood-emoji {
font-size: 24px !important;
}
.mood-label {
font-size: 11px !important;
}
/* 日历适配 */
.calendar-grid {
gap: 2px !important;
}
.date-cell {
min-height: 60px !important;
padding: 2px !important;
}
.weekday-header {
padding: 8px 4px !important;
font-size: 12px !important;
}
/* 今日统计卡片 */
.stats-grid {
grid-template-columns: repeat(2, 1fr) !important;
gap: 12px !important;
}
/* 标签页 */
#tabTodo, #tabMood, #tabBill {
padding: 12px 8px !important;
font-size: 12px !important;
}
#tabTodo svg, #tabMood svg, #tabBill svg {
width: 14px !important;
height: 14px !important;
}
}
@media (max-width: 480px) {
main {
padding: 8px !important;
}
.category-grid {
grid-template-columns: repeat(3, 1fr) !important;
gap: 6px !important;
}
.mood-selector {
grid-template-columns: repeat(5, 1fr) !important;
gap: 6px !important;
}
.mood-item {
padding: 6px 2px !important;
border-radius: 12px !important;
}
.mood-emoji {
font-size: 22px !important;
}
.mood-label {
font-size: 10px !important;
}
.calendar-grid {
gap: 1px !important;
}
.date-cell {
min-height: 50px !important;
padding: 2px !important;
}
.date-cell .date-num {
font-size: 11px !important;
}
.weekday-header {
padding: 6px 2px !important;
font-size: 11px !important;
}
/* 抽屉优化 */
.drawer-title {
font-size: 15px !important;
}
.form-group {
margin-bottom: 12px !important;
}
.form-label {
font-size: 12px !important;
margin-bottom: 6px !important;
}
.form-input, .form-textarea {
padding: 10px !important;
font-size: 14px !important;
}
.btn-primary, .btn-secondary {
padding: 12px !important;
font-size: 14px !important;
}
/* 快捷金额按钮 */
.quick-amount {
gap: 6px !important;
}
.quick-amount-btn {
padding: 6px 10px !important;
font-size: 12px !important;
}
/* 短语标签 */
.phrase-tag {
padding: 5px 10px !important;
font-size: 11px !important;
}
}
@media (max-width: 360px) {
.mood-selector {
grid-template-columns: repeat(4, 1fr) !important;
}
.category-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.date-cell {
min-height: 45px !important;
}
}
</style>
</head>
<body>
<div id="app">
<!-- 公共头部 -->
<div id="webHeader"></div>
<!-- 页面内容 -->
<main style="margin-top: 56px; min-height: calc(100vh - 56px - 40px); padding: 20px;">
<div style="max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: 450px 1fr; gap: 20px;">
<!-- 左侧日历面板 -->
<div style="background: #fff; border-radius: 8px; padding: 24px; height: fit-content; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
<!-- 日历头部 -->
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
<div>
<h1 style="font-size: 20px; font-weight: 600; color: #1e293b;">日程日历</h1>
<p style="font-size: 13px; color: #64748b; margin-top: 2px;" id="currentMonth">2024年5月</p>
</div>
<div style="display: flex; gap: 6px;">
<button id="prevMonth" style="width: 34px; height: 34px; background: #f8fafc; border: none; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; hover:background:#f1f5f9;">
<svg style="width: 14px; height: 14px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
<path d="M15 19l-7-7 7-7"/>
</svg>
</button>
<button id="todayBtn" style="width: 34px; height: 34px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; font-size: 13px; font-weight: 600; color: #fff; cursor: pointer;">
</button>
<button id="nextMonth" style="width: 34px; height: 34px; background: #f8fafc; border: none; border-radius: 8px; font-size: 14px; color: #475569; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.15s; hover:background:#f1f5f9;">
<svg style="width: 14px; height: 14px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
<path d="M9 5l7 7-7 7"/>
</svg>
</button>
</div>
</div>
<!-- 星期标题(周一开始) -->
<div style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px; margin-bottom: 8px;">
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
<div style="text-align: center; font-size: 12px; font-weight: 500; color: #94a3b8; padding: 6px;"></div>
</div>
<!-- 日期网格 -->
<div id="calendarGrid" style="display: grid; grid-template-columns: repeat(7, 1fr); gap: 4px;"></div>
<!-- 图例 -->
<div style="margin-top: 20px; padding-top: 16px; border-top: 1px solid #f1f5f9;">
<h4 style="font-size: 12px; font-weight: 500; color: #64748b; margin-bottom: 10px;">图例</h4>
<div style="display: flex; gap: 16px;">
<div style="display: flex; align-items: center; gap: 4px;">
<div class="date-dot" style="background: #4F9EDE;"></div>
<span style="font-size: 11px; color: #64748b;">待办</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<div class="date-dot" style="background: #f59e0b;"></div>
<span style="font-size: 11px; color: #64748b;">心情</span>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<div class="date-dot" style="background: #22c55e;"></div>
<span style="font-size: 11px; color: #64748b;">账单</span>
</div>
</div>
</div>
</div>
<!-- 右侧内容面板 -->
<div style="display: flex; flex-direction: column; gap: 20px;">
<!-- 今日概览 -->
<div style="background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border-radius: 8px; padding: 24px; color: #fff; box-shadow: 0 2px 12px rgba(79, 158, 222, 0.15);">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div>
<p style="font-size: 13px; opacity: 0.9; margin-bottom: 4px;" id="todayText">今日</p>
<h2 style="font-size: 26px; font-weight: 600;" id="todayDate">5月14日 周四</h2>
</div>
<div style="text-align: center;">
<div id="todayMood" style="font-size: 36px;">😊</div>
<div style="font-size: 11px; opacity: 0.9; margin-top: 2px;" id="todayMoodLabel">心情</div>
</div>
</div>
<div style="display: flex; gap: 32px; margin-top: 18px;">
<div>
<div style="font-size: 24px; font-weight: 600;" id="todoCount">0</div>
<div style="font-size: 12px; opacity: 0.9;">待办任务</div>
</div>
<div>
<div style="font-size: 24px; font-weight: 600;" id="billCount">0</div>
<div style="font-size: 12px; opacity: 0.9;">今日账单</div>
</div>
<div>
<div style="font-size: 24px; font-weight: 600;" id="todayExp">0</div>
<div style="font-size: 12px; opacity: 0.9;">获得经验</div>
</div>
</div>
<!-- 今日经验进度 -->
<div style="margin-top: 16px; padding-top: 14px; border-top: 1px solid rgba(255,255,255,0.2);">
<div style="display: flex; justify-content: space-between; font-size: 12px; opacity: 0.9; margin-bottom: 6px;">
<span>今日进度</span>
<span id="todayExpProgress">0 / 120 EXP</span>
</div>
<div style="background: rgba(255,255,255,0.25); border-radius: 4px; height: 8px; overflow: hidden;">
<div id="todayExpBar" style="height: 100%; background: #fff; border-radius: 4px; transition: width 0.3s; width: 0%;"></div>
</div>
</div>
</div>
<!-- 内容标签页 -->
<div style="background: #fff; border-radius: 8px; overflow: hidden; flex: 1; box-shadow: 0 2px 8px rgba(0,0,0,0.04);">
<!-- 标签切换 -->
<div style="display: flex; border-bottom: 1px solid #f1f5f9;">
<button id="tabTodo" class="active-tab" onclick="switchTab('todo')" style="flex: 1; padding: 14px; border: none; background: transparent; font-size: 13px; font-weight: 500; color: #475569; cursor: pointer; transition: all 0.15s;">
<span style="display: flex; align-items: center; justify-content: center; gap: 6px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
待办任务
</span>
</button>
<button id="tabMood" onclick="switchTab('mood')" style="flex: 1; padding: 14px; border: none; background: transparent; font-size: 13px; font-weight: 500; color: #475569; cursor: pointer; transition: all 0.15s;">
<span style="display: flex; align-items: center; justify-content: center; gap: 6px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"/>
<path d="M8 14s1.5-2 4-2 4 2 4 2"/>
<circle cx="9" cy="9" r="1.5"/>
<circle cx="15" cy="9" r="1.5"/>
</svg>
心情日记
</span>
</button>
<button id="tabBill" onclick="switchTab('bill')" style="flex: 1; padding: 14px; border: none; background: transparent; font-size: 13px; font-weight: 500; color: #475569; cursor: pointer; transition: all 0.15s;">
<span style="display: flex; align-items: center; justify-content: center; gap: 6px;">
<svg style="width: 16px; height: 16px; fill: none; stroke: currentColor;" viewBox="0 0 24 24">
<path d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
账单记录
</span>
</button>
</div>
<!-- 待办任务列表 -->
<div id="contentTodo" style="padding: 20px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="font-size: 16px; color: #1e293b; margin: 0;">待办任务</h3>
<button onclick="openTodoDrawer()" style="padding: 8px 16px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 20px; color: #fff; font-size: 12px; cursor: pointer;">
+ 添加任务
</button>
</div>
<div id="todoList"></div>
</div>
<!-- 心情日记列表 -->
<div id="contentMood" style="padding: 20px; display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="font-size: 16px; color: #1e293b; margin: 0;">心情日记</h3>
<button id="moodAddBtn" onclick="openMoodDrawer()" style="padding: 8px 16px; background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); border: none; border-radius: 20px; color: #fff; font-size: 12px; cursor: pointer;">
+ 记录心情
</button>
</div>
<div id="moodList"></div>
</div>
<!-- 账单记录列表 -->
<div id="contentBill" style="padding: 20px; display: none;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h3 style="font-size: 16px; color: #1e293b; margin: 0;">账单记录</h3>
<button onclick="openBillDrawer()" style="padding: 8px 16px; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); border: none; border-radius: 20px; color: #fff; font-size: 12px; cursor: pointer;">
+ 添加账单
</button>
</div>
<div id="billList"></div>
</div>
</div>
</div>
</div>
</main>
<!-- 公共底部 -->
<div id="webFooter"></div>
</div>
<!-- 右键菜单 -->
<div id="contextMenu" class="context-menu" style="display: none;">
<div class="context-menu-item" onclick="handleContextAction('todo')">
<svg style="width: 14px; height: 14px; fill: none; stroke: #4F9EDE;" viewBox="0 0 24 24">
<path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
</svg>
<span>添加待办</span>
</div>
<div class="context-menu-item" onclick="handleContextAction('mood')">
<svg style="width: 14px; height: 14px; fill: none; stroke: #f59e0b;" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10"/>
<path d="M8 14s1.5-2 4-2 4 2 4 2"/>
<circle cx="9" cy="9" r="1.5"/>
<circle cx="15" cy="9" r="1.5"/>
</svg>
<span>记录心情</span>
</div>
<div class="context-menu-item" onclick="handleContextAction('bill')">
<svg style="width: 14px; height: 14px; fill: none; stroke: #22c55e;" viewBox="0 0 24 24">
<path d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>添加账单</span>
</div>
</div>
<!-- 待办抽屉 -->
<div id="todoDrawerOverlay" class="drawer-overlay" onclick="closeTodoDrawer()"></div>
<div id="todoDrawer" class="drawer">
<div class="drawer-header">
<span class="drawer-title">新增待办</span>
<button class="drawer-close" onclick="closeTodoDrawer()">
<svg style="width: 16px; height: 16px; fill: none; stroke: #64748b;" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="drawer-content">
<div class="form-group">
<label class="form-label">待办标题<span class="required">*</span></label>
<input type="text" id="todoTitle" placeholder="请输入待办标题" class="form-input">
</div>
<div class="form-group">
<label class="form-label">日期<span class="required">*</span></label>
<input type="date" id="todoDate" class="form-input" value="">
</div>
<div class="form-group">
<label class="form-label">时间段</label>
<div class="time-input-group">
<input type="time" id="todoStartTime" class="form-input time-input" value="11:00">
<span style="display: flex; align-items: center; color: #94a3b8; font-size: 14px;">-</span>
<input type="time" id="todoEndTime" class="form-input time-input" value="12:00">
</div>
</div>
<div class="form-group">
<label class="form-label">分类</label>
<div class="category-grid">
<button class="category-btn" onclick="setTodoCategory('work')" id="catWork">工作</button>
<button class="category-btn" onclick="setTodoCategory('life')" id="catLife">生活</button>
<button class="category-btn" onclick="setTodoCategory('study')" id="catStudy">学习</button>
<button class="category-btn" onclick="setTodoCategory('custom')" id="catCustom">自定义</button>
</div>
</div>
<div class="form-group">
<label class="form-label">优先级</label>
<div class="priority-row">
<button class="priority-btn high" onclick="setTodoPriority('high')" id="todoHigh">🔴 高</button>
<button class="priority-btn medium" onclick="setTodoPriority('medium')" id="todoMedium" style="border-color: #d97706; background: rgba(245, 158, 11, 0.1);">🟡 中</button>
<button class="priority-btn low" onclick="setTodoPriority('low')" id="todoLow">🟢 低</button>
</div>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<textarea id="todoRemark" placeholder="选填" class="form-textarea"></textarea>
</div>
</div>
<div class="drawer-footer">
<button class="btn-secondary" onclick="closeTodoDrawer()">取消</button>
<button class="btn-primary" onclick="saveTodo()">确认</button>
</div>
</div>
<!-- 心情抽屉 -->
<div id="moodDrawerOverlay" class="drawer-overlay" onclick="closeMoodDrawer()"></div>
<div id="moodDrawer" class="drawer" style="width: 500px;">
<div class="drawer-header">
<span class="drawer-title">记录心情</span>
<button class="drawer-close" onclick="closeMoodDrawer()">
<svg style="width: 16px; height: 16px; fill: none; stroke: #64748b;" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="drawer-content" style="background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); padding: 16px;">
<div class="mood-selector" style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px;">
</div>
<div id="moodSelectedTag" style="display: none; text-align: center; margin: 20px 0; padding: 12px; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); border-radius: 20px; color: #fff; font-weight: 600; font-size: 16px;">
✨ 是开心呀 ✨
</div>
<div class="form-group" style="margin-bottom: 16px;">
<textarea id="moodContent" placeholder="今天想对自己说点什么..." class="form-textarea" style="min-height: 120px; font-size: 14px; padding: 14px; background: #fff; border-radius: 12px; border: none; resize: none;"></textarea>
<div style="text-align: right; margin-top: 4px;">
<span style="font-size: 11px; color: #94a3b8;" id="moodContentCount">0/200</span>
</div>
</div>
<div id="moodPhrases" style="display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px;">
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: flex-end;">
<input type="date" id="moodDate" class="form-input" value="" style="width: auto; min-width: 160px; border-radius: 8px;">
</div>
</div>
<div class="drawer-footer">
<button class="btn-secondary" onclick="closeMoodDrawer()">取消</button>
<button class="btn-primary" onclick="saveMood()" style="background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);">发送心情</button>
</div>
</div>
<!-- 账单抽屉 -->
<div id="billDrawerOverlay" class="drawer-overlay" onclick="closeBillDrawer()"></div>
<div id="billDrawer" class="drawer">
<div class="drawer-header">
<span class="drawer-title">记一笔</span>
<button class="drawer-close" onclick="closeBillDrawer()">
<svg style="width: 16px; height: 16px; fill: none; stroke: #64748b;" viewBox="0 0 24 24">
<path d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div class="drawer-content">
<div class="form-group">
<div style="display: flex; gap: 8px;">
<button id="billTypeExpense" onclick="setBillType('expense')" style="flex: 1; padding: 12px; background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%); border: none; border-radius: 8px; color: #fff; font-size: 14px; font-weight: 500; cursor: pointer;">💰 支出</button>
<button id="billTypeIncome" onclick="setBillType('income')" style="flex: 1; padding: 12px; background: #f1f5f9; border: 1px solid #e2e8f0; border-radius: 8px; color: #475569; font-size: 14px; font-weight: 500; cursor: pointer;">💵 收入</button>
</div>
</div>
<div class="form-group">
<label class="form-label">金额<span class="required">*</span></label>
<div style="font-size: 28px; font-weight: 600; color: #ef4444; margin-bottom: 10px;">
¥ <span id="billAmountDisplay">0.00</span>
</div>
<input type="number" id="billAmount" placeholder="请输入金额" class="form-input" step="0.01" oninput="updateBillAmount()">
<div class="quick-amount">
<button class="quick-amount-btn" onclick="addQuickAmount(10)">¥10</button>
<button class="quick-amount-btn" onclick="addQuickAmount(20)">¥20</button>
<button class="quick-amount-btn" onclick="addQuickAmount(50)">¥50</button>
<button class="quick-amount-btn" onclick="addQuickAmount(100)">¥100</button>
<button class="quick-amount-btn" onclick="addQuickAmount(200)">¥200</button>
<button class="quick-amount-btn" onclick="addQuickAmount(500)">¥500</button>
</div>
</div>
<div class="form-group">
<label class="form-label">分类<span class="required">*</span></label>
<div id="billCategory" class="category-grid">
</div>
</div>
<div class="form-group">
<label class="form-label">支付渠道</label>
<div id="billChannel" class="category-grid">
</div>
</div>
<div class="form-group">
<label class="form-label">备注</label>
<input type="text" id="billRemark" placeholder="添加备注(选填)" class="form-input">
<div style="display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;">
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#午餐')">#午餐</span>
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#打车')">#打车</span>
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#地铁')">#地铁</span>
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#工资')">#工资</span>
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#购物')">#购物</span>
<span style="padding: 4px 10px; background: #f1f5f9; border-radius: 12px; font-size: 12px; color: #64748b; cursor: pointer;" onclick="addRemarkTag('#娱乐')">#娱乐</span>
</div>
</div>
<div class="form-group">
<label class="form-label">日期<span class="required">*</span></label>
<input type="date" id="billDate" class="form-input" value="">
</div>
</div>
<div class="drawer-footer">
<button class="btn-secondary" onclick="closeBillDrawer()">取消</button>
<button class="btn-primary" onclick="saveBill()">确认账单</button>
</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/constants.js"></script>
<script src="/static/js/toast.js"></script>
<script src="/static/js/request-guard.js"></script>
<script src="/static/js/auth.js"></script>
<script src="/static/js/web-components.js"></script>
<script>
let currentDate = new Date();
let selectedDate = null;
let selectedTodoCategory = 'work';
let selectedTodoPriority = 2; // 1-高, 2-中, 3-低
let selectedMood = '';
let billType = 1; // 1-支出, 2-收入
let billCategory = '';
let billChannel = '';
let constants = {};
let calendarData = {};
document.addEventListener('DOMContentLoaded', function() {
// 检查登录状态
checkLoginAndInit();
});
// 检查登录状态并初始化
function checkLoginAndInit() {
const token = localStorage.getItem('user_token');
if (!token || Auth.isTokenExpired()) {
// 未登录或登录过期,清除所有数据并跳转
localStorage.clear();
window.location.href = '/login';
return;
}
// 已登录,初始化页面
initPage();
}
// 初始化页面
function initPage() {
// 初始化公共头部底部
initWebPage('calendar');
// 加载页面数据
loadConstants();
initCalendar();
updateTodayDisplay();
loadTodayData();
document.addEventListener('click', function(e) {
if (!e.target.closest('.date-cell') && !e.target.closest('.add-btn') && !e.target.closest('.context-menu')) {
document.getElementById('contextMenu').style.display = 'none';
}
});
const now = new Date();
const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
document.getElementById('todoDate').value = todayStr;
document.getElementById('moodDate').value = todayStr;
document.getElementById('billDate').value = todayStr;
document.getElementById('moodContent').addEventListener('input', updateMoodContentCount);
}
// 退出登录
async function loadConstants() {
const token = localStorage.getItem('user_token');
if (!token) return;
try {
const res = await fetch('/api/web/constants/all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': token
}
});
const data = await res.json();
if (data.code === 200) {
constants = data.data;
renderMoodSelector();
renderBillCategories();
renderBillChannels();
}
} catch (err) {
console.error('加载常量失败:', err);
// 使用本地常量作为后备
constants = {
moodList: MOOD_LIST,
billCategories: DEFAULT_CATE_LIST,
payChannels: PAY_CHANNELS
};
renderMoodSelector();
renderBillCategories();
renderBillChannels();
}
}
function initCalendar() {
// 初始加载时先渲染空日历,然后加载数据
renderCalendar(currentDate);
loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
document.getElementById('prevMonth').addEventListener('click', function() {
currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1);
loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
});
document.getElementById('nextMonth').addEventListener('click', function() {
currentDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1);
loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
});
document.getElementById('todayBtn').addEventListener('click', function() {
currentDate = new Date();
selectedDate = currentDate;
loadCalendarData(currentDate.getFullYear(), currentDate.getMonth() + 1);
updateTodayDisplay();
loadTodayData();
});
}
async function loadCalendarData(year, month) {
const token = localStorage.getItem('user_token');
if (!token) {
renderCalendar(new Date(year, month - 1, 1));
return;
}
try {
const res = await fetch('/api/web/calendar/data', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ year, month })
});
const data = await res.json();
if (data.code === 200) {
calendarData = data.data || {};
} else {
calendarData = {};
}
} catch (err) {
calendarData = {};
}
renderCalendar(new Date(year, month - 1, 1));
}
function renderCalendar(date) {
const year = date.getFullYear();
const month = date.getMonth();
document.getElementById('currentMonth').textContent = year + '年' + (month + 1) + '月';
// 获取当月第一天是周几(0=周日, 1=周一...)
// 转换为周一开始:周日(0)变成6,其他减1
let firstDay = new Date(year, month, 1).getDay();
firstDay = firstDay === 0 ? 6 : firstDay - 1;
const daysInMonth = new Date(year, month + 1, 0).getDate();
const today = new Date();
let html = '';
for (let i = 0; i < firstDay; i++) {
html += '<div style="height: 52px;"></div>';
}
for (let day = 1; day <= daysInMonth; day++) {
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const isToday = today.getDate() === day && today.getMonth() === month && today.getFullYear() === year;
const isSelected = selectedDate && selectedDate.getDate() === day && selectedDate.getMonth() === month && selectedDate.getFullYear() === year;
const dayData = calendarData[dateStr] || {};
let dayStyle = 'height: 52px; padding: 6px; text-align: center; border-radius: 6px; cursor: pointer; transition: all 0.15s; position: relative; border: 1px solid transparent;';
if (isSelected) {
dayStyle += ' background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); color: #fff;';
} else if (isToday) {
dayStyle += ' background: rgba(79, 158, 222, 0.06); border: 1px solid #4F9EDE;';
} else if (dayData.todo_undone) {
// 有待办未完成 - 淡红色背景
dayStyle += ' background: rgba(239, 68, 68, 0.08);';
} else if (dayData.todo_done) {
// 待办全部完成 - 淡绿色背景
dayStyle += ' background: rgba(34, 197, 94, 0.08);';
}
let dotsHtml = '';
if (dayData.todo || dayData.mood || dayData.bill) {
dotsHtml = '<div style="display: flex; justify-content: center; gap: 2px; margin-top: 3px;">';
if (dayData.todo) dotsHtml += '<div class="date-dot" style="background: #4F9EDE;"></div>';
if (dayData.mood) dotsHtml += '<div class="date-dot" style="background: #f59e0b;"></div>';
if (dayData.bill) dotsHtml += '<div class="date-dot" style="background: #22c55e;"></div>';
dotsHtml += '</div>';
}
html += `
<div class="date-cell" style="${dayStyle}" data-date="${dateStr}" onclick="selectDate(${year}, ${month}, ${day})">
<div style="font-size: 15px; font-weight: 500;">${day}</div>
${dotsHtml}
<div class="add-btn" onclick="showContextMenu(event)" data-date="${dateStr}">+</div>
</div>
`;
}
document.getElementById('calendarGrid').innerHTML = html;
}
function selectDate(year, month, day) {
// month 是 0-11 的月份索引,不需要再减1
selectedDate = new Date(year, month, day);
renderCalendar(new Date(year, month, 1));
updateTodayDisplay();
loadTodayData();
// 使用本地时间格式化,避免时区问题
const dateStr = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
document.getElementById('todoDate').value = dateStr;
document.getElementById('moodDate').value = dateStr;
document.getElementById('billDate').value = dateStr;
}
function showContextMenu(e) {
e.stopPropagation();
const dateStr = e.target.getAttribute('data-date');
const rect = e.target.getBoundingClientRect();
const menu = document.getElementById('contextMenu');
menu.style.display = 'block';
menu.style.left = rect.right + 6 + 'px';
menu.style.top = rect.top - 16 + 'px';
menu.setAttribute('data-date', dateStr);
}
function handleContextAction(action) {
const dateStr = document.getElementById('contextMenu').getAttribute('data-date');
document.getElementById('contextMenu').style.display = 'none';
selectedDate = new Date(dateStr);
document.getElementById('todoDate').value = dateStr;
document.getElementById('moodDate').value = dateStr;
document.getElementById('billDate').value = dateStr;
updateTodayDisplay();
if (action === 'todo') openTodoDrawer();
else if (action === 'mood') openMoodDrawer();
else if (action === 'bill') openBillDrawer();
}
function updateTodayDisplay() {
const displayDate = selectedDate || new Date();
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
const month = displayDate.getMonth() + 1;
const day = displayDate.getDate();
const weekday = weekdays[displayDate.getDay()];
const today = new Date();
const isToday = today.getDate() === day && today.getMonth() === displayDate.getMonth() && today.getFullYear() === displayDate.getFullYear();
document.getElementById('todayText').textContent = isToday ? '今日' : '选择日期';
document.getElementById('todayDate').textContent = month + '月' + day + '日 ' + weekday;
}
function loadTodayData() {
const token = localStorage.getItem('user_token');
if (!token) return;
// 使用本地时间格式化,避免时区问题
let dateStr;
if (selectedDate) {
dateStr = `${selectedDate.getFullYear()}-${String(selectedDate.getMonth() + 1).padStart(2, '0')}-${String(selectedDate.getDate()).padStart(2, '0')}`;
} else {
const now = new Date();
dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
}
fetch('/api/web/todo/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ date: dateStr })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
renderTodoList(data.data);
document.getElementById('todoCount').textContent = data.data?.length || 0;
}
});
fetch('/api/web/mood/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ date: dateStr })
})
.then(res => res.json())
.then(data => {
const moodAddBtn = document.getElementById('moodAddBtn');
if (data.code === 200 && data.data) {
// 使用正确的字段名 emoji(数据库字段)
const emoji = data.data.emoji || data.data.mood || '😊';
document.getElementById('todayMood').textContent = emoji;
document.getElementById('todayMoodLabel').textContent = getMoodLabel(emoji);
renderMoodList([data.data]);
// 已有心情记录,隐藏右上角新增按钮
if (moodAddBtn) moodAddBtn.style.display = 'none';
} else {
document.getElementById('todayMood').textContent = '😊';
document.getElementById('todayMoodLabel').textContent = '心情';
renderMoodList([]);
// 没有心情记录,显示右上角新增按钮
if (moodAddBtn) moodAddBtn.style.display = 'inline-block';
}
});
fetch('/api/web/bill/list', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ date: dateStr })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
renderBillList(data.data);
document.getElementById('billCount').textContent = data.data?.length || 0;
}
});
// 加载今日成长进度
loadTodayProgress();
}
// 加载今日成长进度
function loadTodayProgress() {
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 progress = data.data.todayProgress || {};
const todayExp = progress.totalExp || 0;
const maxExp = 120; // 每日经验上限
document.getElementById('todayExp').textContent = todayExp;
document.getElementById('todayExpProgress').textContent = todayExp + ' / ' + maxExp + ' EXP';
document.getElementById('todayExpBar').style.width = Math.min((todayExp / maxExp) * 100, 100) + '%';
}
});
}
function getMoodLabel(emoji) {
if (!emoji) return '-';
const found = getMoodByEmoji(emoji);
return found ? found.label : '-';
}
function renderTodoList(todos) {
if (!todos || todos.length === 0) {
document.getElementById('todoList').innerHTML = `
<div style="text-align: center; padding: 40px; color: #94a3b8;">
<span style="font-size: 48px; display: block; margin-bottom: 12px;">📋</span>
<p style="font-size: 14px; margin-bottom: 12px;">暂无待办任务</p>
<button onclick="openTodoDrawer()" style="padding: 10px 24px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; color: #fff; font-size: 13px; cursor: pointer;">
+ 添加任务
</button>
</div>
`;
return;
}
let html = '';
todos.forEach(todo => {
// 优先级:1-高, 2-中, 3-低
const priorityMap = { 1: { color: '#ef4444', text: '高' }, 2: { color: '#d97706', text: '中' }, 3: { color: '#64748b', text: '低' } };
const priorityInfo = priorityMap[todo.priority] || priorityMap[3];
const isDone = todo.done === 1 || todo.status === 1;
const categoryLabel = getCategoryLabel(todo.category, 'todo');
// 显示时间
let timeDisplay = '';
if (todo.start_time || todo.end_time) {
timeDisplay = todo.end_time ? `${todo.start_time}-${todo.end_time}` : todo.start_time;
}
html += `
<div style="display: flex; align-items: center; gap: 12px; padding: 14px; background: #fff; border-radius: 8px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
<div style="width: 20px; height: 20px; border-radius: 50%; border: 2px solid ${isDone ? '#22c55e' : priorityInfo.color}; background: ${isDone ? '#22c55e' : 'transparent'}; display: flex; align-items: center; justify-content: center;">
${isDone ? '<span style="color: #fff; font-size: 12px;">✓</span>' : ''}
</div>
<div style="flex: 1;">
<div style="font-size: 14px; color: ${isDone ? '#94a3b8' : '#1e293b'}; ${isDone ? 'text-decoration: line-through;' : ''}">${todo.title || todo.content || '未命名任务'}</div>
<div style="display: flex; gap: 10px; margin-top: 3px;">
${categoryLabel ? `<span style="font-size: 11px; color: #64748b;">${categoryLabel}</span>` : ''}
${timeDisplay ? `<span style="font-size: 11px; color: #94a3b8;">${timeDisplay}</span>` : ''}
</div>
</div>
<div style="display: flex; align-items: center; gap: 6px;">
<span style="padding: 3px 8px; background: rgba(79, 158, 222, 0.08); border-radius: 4px; font-size: 10px; color: ${priorityInfo.color};">${priorityInfo.text}</span>
<button onclick="toggleTodo(${todo.id}, ${isDone ? 1 : 0})" style="padding: 4px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; border: 1px solid ${isDone ? '#f59e0b' : '#22c55e'}; background: ${isDone ? '#fef3c7' : '#dcfce7'}; color: ${isDone ? '#d97706' : '#16a34a'};">
${isDone ? '取消完成' : '完成'}
</button>
<svg style="width: 14px; height: 14px; fill: none; stroke: #94a3b8; cursor: pointer;" viewBox="0 0 24 24" onclick="editTodo(${todo.id}, '${encodeURIComponent(todo.title || '')}', '${todo.category || ''}', ${todo.priority || 2}, '${todo.start_time || ''}', '${todo.end_time || ''}', '${encodeURIComponent(todo.remark || '')}'); event.stopPropagation();">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<svg style="width: 14px; height: 14px; fill: none; stroke: #ef4444; cursor: pointer;" viewBox="0 0 24 24" onclick="deleteTodo(${todo.id})">
<path d="M3 6h18M19 6v14c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2V6m3 0V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v2"/>
</svg>
</div>
</div>
`;
});
document.getElementById('todoList').innerHTML = html;
}
function getCategoryLabel(value, type) {
const found = getCategoryByValue(value, type);
return found ? found.label : '';
}
function renderMoodList(moods) {
if (!moods || moods.length === 0) {
document.getElementById('moodList').innerHTML = `
<div style="text-align: center; padding: 40px; color: #94a3b8;">
<span style="font-size: 48px; display: block; margin-bottom: 12px;">😊</span>
<p style="font-size: 14px; margin-bottom: 12px;">暂无心情记录</p>
<button onclick="openMoodDrawer()" style="padding: 10px 24px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; color: #fff; font-size: 13px; cursor: pointer;">
+ 记录心情
</button>
</div>
`;
return;
}
let html = '';
moods.forEach(mood => {
const emoji = mood.emoji || mood.mood || '😊';
const content = mood.content || mood.note || '';
const displayDate = mood.created_at || mood.date || '';
html += `
<div style="padding: 16px; background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%); border-radius: 6px;">
<div style="display: flex; align-items: flex-start; gap: 12px;">
<span style="font-size: 32px;">${emoji}</span>
<div style="flex: 1;">
${content ? `<p style="font-size: 14px; color: #1e293b; line-height: 1.5;">${content}</p>` : ''}
<p style="font-size: 11px; color: #92400e; margin-top: 8px;">${displayDate}</p>
</div>
<div style="display: flex; gap: 8px;">
<svg style="width: 14px; height: 14px; fill: none; stroke: #92400e; cursor: pointer;" viewBox="0 0 24 24" onclick="editMood(${mood.id}, '${emoji}', '${encodeURIComponent(content)}')">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<svg style="width: 14px; height: 14px; fill: none; stroke: #92400e; cursor: pointer;" viewBox="0 0 24 24" onclick="deleteMood(${mood.id})">
<path d="M3 6h18M19 6v14c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2V6m3 0V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v2"/>
</svg>
</div>
</div>
</div>
`;
});
document.getElementById('moodList').innerHTML = html;
}
function renderBillList(bills) {
if (!bills || bills.length === 0) {
document.getElementById('billList').innerHTML = `
<div style="text-align: center; padding: 40px; color: #94a3b8;">
<span style="font-size: 48px; display: block; margin-bottom: 12px;">💰</span>
<p style="font-size: 14px; margin-bottom: 12px;">暂无账单记录</p>
<button onclick="openBillDrawer()" style="padding: 10px 24px; background: linear-gradient(135deg, #4F9EDE 0%, #3A8BC9 100%); border: none; border-radius: 8px; color: #fff; font-size: 13px; cursor: pointer;">
+ 添加账单
</button>
</div>
`;
return;
}
let html = '';
let totalExpense = 0;
let totalIncome = 0;
bills.forEach(bill => {
// type: 1=支出, 2=收入
const isExpense = bill.type === 1;
const money = parseFloat(bill.money) || 0;
if (isExpense) totalExpense += money;
else totalIncome += money;
const catInfo = getBillCategoryInfo(bill.cate);
const displayMoney = money.toFixed(2);
html += `
<div style="display: flex; align-items: center; gap: 12px; padding: 14px; background: #fff; border-radius: 8px; margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05);">
<div style="width: 40px; height: 40px; background: ${isExpense ? '#fee2e2' : '#dcfce7'}; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 20px;">
${catInfo?.icon || '📦'}
</div>
<div style="flex: 1;">
<div style="display: flex; align-items: center; gap: 6px;">
<span style="font-size: 14px; font-weight: 500; color: #1e293b;">${catInfo?.label || bill.cate || '未知分类'}</span>
<span style="font-size: 10px; color: #94a3b8; padding: 2px 5px; background: #e2e8f0; border-radius: 3px;">${isExpense ? '支出' : '收入'}</span>
</div>
${bill.note ? `<div style="font-size: 11px; color: #64748b; margin-top: 2px;">${bill.note}</div>` : ''}
</div>
<div style="text-align: right;">
<div style="font-size: 15px; font-weight: 600; color: ${isExpense ? '#ef4444' : '#22c55e'};">${isExpense ? '-' : '+'}${displayMoney}</div>
<div style="font-size: 11px; color: #94a3b8;">${bill.date || bill.created_at || ''}</div>
</div>
<div style="display: flex; gap: 8px;">
<svg style="width: 14px; height: 14px; fill: none; stroke: #94a3b8; cursor: pointer;" viewBox="0 0 24 24" onclick="editBill(${bill.id}, ${bill.type}, ${money}, '${bill.cate || ''}', '${bill.channel || ''}', '${encodeURIComponent(bill.note || '')}', '${bill.date || ''}')">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
<svg style="width: 14px; height: 14px; fill: none; stroke: #ef4444; cursor: pointer;" viewBox="0 0 24 24" onclick="deleteBill(${bill.id})">
<path d="M3 6h18M19 6v14c0 1.1-.9 2-2 2H7c-1.1 0-2-.9-2-2V6m3 0V4c0-1.1.9-2 2-2h4c1.1 0 2 .9 2 2v2"/>
</svg>
</div>
</div>
`;
});
html += `
<div style="margin-top: 12px; padding: 16px; border-top: 1px solid #e2e8f0; display: flex; justify-content: space-between; background: #fff; border-radius: 8px;">
<div>
<span style="font-size: 13px; color: #64748b;">支出</span>
<span style="font-size: 18px; font-weight: 600; color: #ef4444; margin-left: 12px;">-${totalExpense.toFixed(2)}</span>
</div>
<div>
<span style="font-size: 13px; color: #64748b;">收入</span>
<span style="font-size: 18px; font-weight: 600; color: #22c55e; margin-left: 12px;">+${totalIncome.toFixed(2)}</span>
</div>
<div>
<span style="font-size: 13px; color: #64748b;">结余</span>
<span style="font-size: 18px; font-weight: 600; color: ${totalIncome - totalExpense >= 0 ? '#22c55e' : '#ef4444'}; margin-left: 12px;">${totalIncome - totalExpense >= 0 ? '+' : ''}${(totalIncome - totalExpense).toFixed(2)}</span>
</div>
</div>
`;
document.getElementById('billList').innerHTML = html;
}
function getBillCategoryInfo(category) {
const found = getCategoryByValue(category, 'bill');
return found || { label: category, icon: '📦' };
}
function switchTab(tab) {
document.getElementById('tabTodo').classList.remove('active-tab');
document.getElementById('tabMood').classList.remove('active-tab');
document.getElementById('tabBill').classList.remove('active-tab');
document.getElementById('contentTodo').style.display = 'none';
document.getElementById('contentMood').style.display = 'none';
document.getElementById('contentBill').style.display = 'none';
document.getElementById('tab' + tab.charAt(0).toUpperCase() + tab.slice(1)).classList.add('active-tab');
document.getElementById('content' + tab.charAt(0).toUpperCase() + tab.slice(1)).style.display = 'block';
}
function renderMoodSelector() {
const moodList = constants.moodList || MOOD_LIST;
let html = '';
moodList.forEach((mood, index) => {
const phrasesData = encodeURIComponent(JSON.stringify(mood.phrases));
html += `
<div class="mood-item" data-emoji="${mood.emoji}" data-label="${mood.label}" data-phrases="${phrasesData}" style="animation: fadeInUp 0.3s ease; animation-delay: ${index * 0.03}s; animation-fill-mode: backwards;">
<span class="mood-emoji">${mood.emoji}</span>
<span class="mood-label">${mood.label}</span>
</div>
`;
});
const container = document.querySelector('.mood-selector');
container.innerHTML = html;
// 使用事件委托,避免onclick属性中的引号冲突
container.querySelectorAll('.mood-item').forEach(item => {
item.addEventListener('click', function() {
const emoji = this.dataset.emoji;
const label = this.dataset.label;
const phrases = JSON.parse(decodeURIComponent(this.dataset.phrases));
selectMood(emoji, label, phrases, this);
});
});
}
function renderBillCategories() {
const cats = constants.billCategories || DEFAULT_CATE_LIST;
// billType: 1=支出, 2=收入
const typeFilter = billType === 2 ? 2 : 1;
const filtered = cats.filter(c => c.type === typeFilter);
let html = '';
filtered.forEach(cat => {
const isSelected = billCategory === cat.value;
html += `
<button onclick="setBillCategory('${cat.value}')" class="category-btn ${isSelected ? 'selected' : ''}">
<span style="font-size: 18px;">${cat.icon}</span>
<span style="font-size: 12px;">${cat.label}</span>
</button>
`;
});
document.getElementById('billCategory').innerHTML = html;
}
function renderBillChannels() {
const channels = constants.payChannels || PAY_CHANNELS;
// billType: 1=支出, 2=收入
const typeFilter = billType === 2 ? 2 : 1;
const filtered = channels.filter(ch => {
const types = ch.types || [1, 2];
return types.includes(typeFilter);
});
let html = '';
filtered.forEach(ch => {
const isSelected = billChannel === ch.value;
const icon = CHANNEL_ICONS[ch.value] || '💳';
html += `
<button onclick="setBillChannel('${ch.value}')" class="category-btn ${isSelected ? 'selected' : ''}">
<span style="font-size: 16px;">${icon}</span>
<span style="font-size: 12px;">${ch.label}</span>
</button>
`;
});
document.getElementById('billChannel').innerHTML = html;
}
// 待办抽屉操作
function openTodoDrawer() {
// 重置编辑状态(新增模式)
editingTodoId = null;
document.querySelector('#todoDrawer .btn-primary').textContent = '确认';
// 清空表单
document.getElementById('todoTitle').value = '';
document.getElementById('todoStartTime').value = '';
document.getElementById('todoEndTime').value = '';
document.getElementById('todoRemark').value = '';
document.getElementById('moodSelectedTag').style.display = 'none';
document.getElementById('todoDrawerOverlay').classList.add('active');
document.getElementById('todoDrawer').classList.add('active');
}
function closeTodoDrawer() {
document.getElementById('todoDrawerOverlay').classList.remove('active');
document.getElementById('todoDrawer').classList.remove('active');
}
function setTodoCategory(cat) {
selectedTodoCategory = cat;
document.querySelectorAll('.category-grid button').forEach(el => el.style.borderColor = '#e2e8f0');
document.getElementById('cat' + cat.charAt(0).toUpperCase() + cat.slice(1)).style.borderColor = '#4F9EDE';
}
function setTodoPriority(key) {
// 将 key 转换为 int 值:high=1, medium=2, low=3
const priorityMap = { 'high': 1, 'medium': 2, 'low': 3 };
selectedTodoPriority = priorityMap[key] || 2;
document.querySelectorAll('.priority-row button').forEach(el => {
el.style.borderColor = '#e2e8f0';
el.style.background = '#fff';
});
document.getElementById('todo' + key.charAt(0).toUpperCase() + key.slice(1)).style.borderColor = '#4F9EDE';
document.getElementById('todo' + key.charAt(0).toUpperCase() + key.slice(1)).style.background = 'rgba(79, 158, 222, 0.08)';
}
function saveTodo() {
// 请求锁保护,防止重复提交
if (RequestLock && !RequestLock.executeWithLock('saveTodo', null, 2000)) {
return;
}
const token = localStorage.getItem('user_token');
const title = document.getElementById('todoTitle').value;
const date = document.getElementById('todoDate').value;
const startTime = document.getElementById('todoStartTime').value;
const endTime = document.getElementById('todoEndTime').value;
if (!title) {
$toast.error('请输入待办标题');
RequestLock.unlock('saveTodo'); // 解锁以便重新提交
return;
}
if (!date) {
$toast.error('请选择日期');
RequestLock.unlock('saveTodo');
return;
}
const requestBody = {
title: title,
category: selectedTodoCategory,
priority: selectedTodoPriority,
start_time: startTime,
end_time: endTime,
remark: document.getElementById('todoRemark').value,
date: date
};
// 如果是编辑模式,添加 id
if (editingTodoId) {
requestBody.id = editingTodoId;
}
// 编辑用 edit 接口,新增用 add 接口
const apiUrl = editingTodoId ? '/api/web/todo/edit' : '/api/web/todo/add';
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify(requestBody)
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
const isEdit = !!editingTodoId;
// 重置编辑状态
editingTodoId = null;
document.querySelector('#todoDrawer .btn-primary').textContent = '确认';
closeTodoDrawer();
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
$toast.success(isEdit ? '修改成功' : '添加成功');
} else {
$toast.error(data.msg || '操作失败');
}
});
}
// 切换待办完成状态(通过按钮调用,传入 id 和当前状态)
function toggleTodo(id, currentStatus) {
const token = localStorage.getItem('user_token');
// currentStatus: 0=未完成, 1=已完成 → 切换为反状态
const newStatus = currentStatus === 1 ? 0 : 1;
fetch('/api/web/todo/done', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ id: id, status: newStatus })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
$toast.success(newStatus === 1 ? '已完成' : '已取消完成');
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
} else {
$toast.error(data.msg || '操作失败');
}
});
}
// 编辑待办
let editingTodoId = null;
function editTodo(id, title, category, priority, startTime, endTime, remark) {
// 解码内容
const decodedTitle = title ? decodeURIComponent(title) : '';
const decodedRemark = remark ? decodeURIComponent(remark) : '';
// 打开待办抽屉并设置值(注意:openTodoDrawer会重置editingTodoId,所以要在之后设置)
openTodoDrawer();
// 设置编辑状态(必须在openTodoDrawer之后,否则会被重置)
editingTodoId = id;
// 填充表单
document.getElementById('todoTitle').value = decodedTitle;
document.getElementById('todoStartTime').value = startTime || '';
document.getElementById('todoEndTime').value = endTime || '';
document.getElementById('todoRemark').value = decodedRemark;
// 设置分类
selectedTodoCategory = category || 'work';
document.querySelectorAll('.category-btn').forEach(el => {
el.classList.remove('selected');
if (el.textContent.includes({'work': '工作', 'life': '生活', 'study': '学习', 'custom': '自定义'}[selectedTodoCategory])) {
el.classList.add('selected');
}
});
// 设置优先级
const priorityKey = { 1: 'high', 2: 'medium', 3: 'low' }[priority] || 'medium';
setTodoPriority(priorityKey);
// 修改提交按钮文字
document.querySelector('#todoDrawer .btn-primary').textContent = '修改任务';
}
function deleteTodo(id) {
$toast.confirm('确定删除该任务?').then(() => {
const token = localStorage.getItem('user_token');
fetch('/api/web/todo/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
$toast.success('删除成功');
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
} else {
$toast.error(data.msg || '删除失败');
}
});
});
}
// 心情抽屉操作
function openMoodDrawer() {
// 重置编辑状态(新增模式)
editingMoodId = null;
document.querySelector('#moodDrawer .btn-primary').textContent = '发送心情';
// 清空表单
document.getElementById('moodContent').value = '';
document.getElementById('moodSelectedTag').style.display = 'none';
selectedMood = '';
document.querySelectorAll('.mood-item').forEach(el => el.classList.remove('selected'));
document.getElementById('moodDrawerOverlay').classList.add('active');
document.getElementById('moodDrawer').classList.add('active');
// 确保表情选择器已渲染
renderMoodSelector();
updateMoodContentCount();
document.getElementById('moodSelectedTag').style.display = 'none';
selectedMood = '';
document.querySelectorAll('.mood-item').forEach(el => el.classList.remove('selected'));
document.getElementById('moodPhrases').innerHTML = '';
document.getElementById('moodContent').value = '';
}
function closeMoodDrawer() {
document.getElementById('moodDrawerOverlay').classList.remove('active');
document.getElementById('moodDrawer').classList.remove('active');
}
function selectMood(mood, label, phrases, element) {
selectedMood = mood;
document.querySelectorAll('.mood-item').forEach(el => el.classList.remove('selected'));
if (element) element.classList.add('selected');
const tagEl = document.getElementById('moodSelectedTag');
tagEl.style.display = 'block';
tagEl.innerHTML = `✨ 是${label}呀 ✨`;
let phrasesHtml = '';
phrases.forEach(phrase => {
phrasesHtml += `
<span class="phrase-tag" onclick="selectPhrase('${phrase}')">
${phrase}
</span>
`;
});
document.getElementById('moodPhrases').innerHTML = phrasesHtml;
}
function selectPhrase(phrase) {
document.querySelectorAll('.phrase-tag').forEach(el => el.classList.remove('selected'));
event.target.classList.add('selected');
document.getElementById('moodContent').value = phrase;
updateMoodContentCount();
}
function updateMoodContentCount() {
const content = document.getElementById('moodContent').value;
const count = content.length;
const countEl = document.getElementById('moodContentCount');
if (countEl) {
countEl.textContent = count + '/200';
if (count >= 200) {
countEl.style.color = '#ef4444';
} else {
countEl.style.color = '#94a3b8';
}
}
}
function saveMood() {
// 请求锁保护,防止重复提交
if (RequestLock && !RequestLock.executeWithLock('saveMood', null, 2000)) {
return;
}
if (!selectedMood) {
$toast.error('请选择心情');
RequestLock.unlock('saveMood');
return;
}
const token = localStorage.getItem('user_token');
const requestBody = {
emoji: selectedMood,
content: document.getElementById('moodContent').value,
date: document.getElementById('moodDate').value
};
// 如果是编辑模式,添加 id
if (editingMoodId) {
requestBody.id = editingMoodId;
}
fetch('/api/web/mood/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify(requestBody)
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
// 重置编辑状态
editingMoodId = null;
document.querySelector('#moodDrawer .btn-primary').textContent = '发送心情';
closeMoodDrawer();
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
} else {
$toast.error(data.msg || '保存失败');
}
});
}
// 编辑心情
let editingMoodId = null;
function editMood(id, emoji, content) {
// 解码内容
const decodedContent = content ? decodeURIComponent(content) : '';
// 打开心情抽屉并设置值(注意:openMoodDrawer会重置editingMoodId,所以要在之后设置)
openMoodDrawer();
// 设置编辑状态(必须在openMoodDrawer之后,否则会被重置)
editingMoodId = id;
// 设置选中的表情
selectedMood = emoji;
document.querySelectorAll('.mood-item').forEach(el => {
el.classList.remove('selected');
if (el.dataset.emoji === emoji) {
el.classList.add('selected');
}
});
// 显示表情标签
const moodInfo = getMoodByEmoji(emoji);
const tagEl = document.getElementById('moodSelectedTag');
if (moodInfo) {
tagEl.style.display = 'block';
tagEl.innerHTML = `✨ 是${moodInfo.label}呀 ✨`;
}
// 设置内容
document.getElementById('moodContent').value = decodedContent;
updateMoodContentCount();
// 修改提交按钮文字
document.querySelector('#moodDrawer .btn-primary').textContent = '修改心情';
}
function deleteMood(id) {
$toast.confirm('确定删除该记录?').then(() => {
const token = localStorage.getItem('user_token');
fetch('/api/web/mood/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
$toast.success('删除成功');
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
} else {
$toast.error(data.msg || '删除失败');
}
});
});
}
// 账单抽屉操作
function openBillDrawer() {
// 重置编辑状态(新增模式)
editingBillId = null;
document.querySelector('#billDrawer .btn-primary').textContent = '确认账单';
// 清空表单
document.getElementById('billAmount').value = '';
document.getElementById('billAmountDisplay').textContent = '0.00';
document.getElementById('billRemark').value = '';
billCategory = '';
billChannel = '';
// 默认选中支出
setBillType('expense');
document.getElementById('billDrawerOverlay').classList.add('active');
document.getElementById('billDrawer').classList.add('active');
}
function closeBillDrawer() {
document.getElementById('billDrawerOverlay').classList.remove('active');
document.getElementById('billDrawer').classList.remove('active');
}
function setBillType(type) {
// 将字符串转换为 intexpense=1, income=2
billType = type === 'income' ? 2 : 1;
billCategory = '';
// billType: 1=支出, 2=收入
const isExpense = billType === 1;
document.getElementById('billTypeExpense').style.background = isExpense ? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)' : '#f1f5f9';
document.getElementById('billTypeExpense').style.color = isExpense ? '#fff' : '#475569';
document.getElementById('billTypeExpense').style.border = isExpense ? 'none' : '1px solid #e2e8f0';
document.getElementById('billTypeIncome').style.background = !isExpense ? 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)' : '#f1f5f9';
document.getElementById('billTypeIncome').style.color = !isExpense ? '#fff' : '#475569';
document.getElementById('billTypeIncome').style.border = !isExpense ? 'none' : '1px solid #e2e8f0';
renderBillCategories();
}
function setBillCategory(category) {
billCategory = category;
document.querySelectorAll('#billCategory button').forEach(el => {
el.style.borderColor = '#e2e8f0';
el.style.background = '#f8fafc';
});
event.target.style.borderColor = '#4F9EDE';
event.target.style.background = 'rgba(79, 158, 222, 0.1)';
}
function setBillChannel(channel) {
billChannel = channel;
document.querySelectorAll('#billChannel button').forEach(el => {
el.style.borderColor = '#e2e8f0';
el.style.background = '#f8fafc';
});
event.target.style.borderColor = '#4F9EDE';
event.target.style.background = 'rgba(79, 158, 222, 0.1)';
}
function updateBillAmount() {
const amount = document.getElementById('billAmount').value;
document.getElementById('billAmountDisplay').textContent = parseFloat(amount) ? parseFloat(amount).toFixed(2) : '0.00';
}
function addQuickAmount(amount) {
const current = parseFloat(document.getElementById('billAmount').value) || 0;
const newValue = current + amount;
document.getElementById('billAmount').value = newValue;
updateBillAmount();
}
function addRemarkTag(tag) {
const remark = document.getElementById('billRemark');
remark.value = remark.value ? remark.value + ' ' + tag : tag;
}
function saveBill() {
// 请求锁保护,防止重复提交
if (RequestLock && !RequestLock.executeWithLock('saveBill', null, 2000)) {
return;
}
if (!billCategory) {
$toast.error('请选择分类');
RequestLock.unlock('saveBill');
return;
}
const amount = document.getElementById('billAmount').value;
if (!amount || parseFloat(amount) <= 0) {
$toast.error('请输入有效金额');
RequestLock.unlock('saveBill');
return;
}
const token = localStorage.getItem('user_token');
const requestBody = {
type: billType,
cate: billCategory,
money: parseFloat(amount),
channel: billChannel,
note: document.getElementById('billRemark').value,
date: document.getElementById('billDate').value
};
// 如果是编辑模式,添加 id
if (editingBillId) {
requestBody.id = editingBillId;
}
// 编辑用 edit 接口,新增用 add 接口
const apiUrl = editingBillId ? '/api/web/bill/edit' : '/api/web/bill/add';
fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify(requestBody)
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
const isEdit = !!editingBillId;
// 重置编辑状态
editingBillId = null;
document.querySelector('#billDrawer .btn-primary').textContent = '确认账单';
closeBillDrawer();
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
$toast.success(isEdit ? '修改成功' : '添加成功');
} else {
$toast.error(data.msg || '操作失败');
}
});
}
// 获取账单分类信息
function getBillCategoryInfo(cate) {
const cats = constants.billCategories || DEFAULT_CATE_LIST;
return cats.find(c => c.value === cate);
}
// 编辑账单
let editingBillId = null;
function editBill(id, type, money, cate, channel, note, date) {
// 打开账单抽屉并设置值(注意:openBillDrawer会重置editingBillId,所以要在之后设置)
openBillDrawer();
// 设置编辑状态(必须在openBillDrawer之后,否则会被重置)
editingBillId = id;
// 设置金额
document.getElementById('billAmount').value = money || '';
updateBillAmount();
// 设置类型
const typeStr = type === 2 ? 'income' : 'expense';
setBillType(typeStr);
// 设置分类
billCategory = cate || '';
renderBillCategories();
// 设置渠道
billChannel = channel || '';
renderBillChannels();
// 设置备注
document.getElementById('billRemark').value = note ? decodeURIComponent(note) : '';
// 设置日期
document.getElementById('billDate').value = date || '';
// 修改提交按钮文字
document.querySelector('#billDrawer .btn-primary').textContent = '修改账单';
}
function deleteBill(id) {
$toast.confirm('确定删除该账单?').then(() => {
const token = localStorage.getItem('user_token');
fetch('/api/web/bill/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'token': token },
body: JSON.stringify({ id })
})
.then(res => res.json())
.then(data => {
if (data.code === 200) {
$toast.success('删除成功');
loadTodayData();
// 刷新日历数据
const current = currentDate || new Date();
loadCalendarData(current.getFullYear(), current.getMonth() + 1);
} else {
$toast.error(data.msg || '删除失败');
}
});
});
}
</script>
</body>
</html>