- 新增「简记memo」一体化小程序产品原型设计文档 - 新增简记memo完整版UI视觉设计规范和界面细节 - 添加IDEA项目配置文件.gitignore - 创建404页面HTML文件,包含响应式布局和错误提示 - 添加关于页面HTML文件,展示品牌介绍和团队信息 - 实现AES加解密工具函数,支持请求体加密 - 添加用户协议页面基础框架
2151 lines
98 KiB
HTML
2151 lines
98 KiB
HTML
<!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) {
|
||
// 将字符串转换为 int:expense=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> |