Compare commits

...

56 Commits

Author SHA1 Message Date
mac
979e7fd097 v2.2.0 — 删除评分+心愿模块(HTML/JS/CSS/Python) 2026-07-28 19:33:17 +08:00
mac
a2f009f986 v2.1.30 — 修复备注保存丢失:row引用改用全局变量 2026-07-28 16:42:14 +08:00
mac
30904723d2 v2.1.29 — 备注弹窗放大到720px/300px 2026-07-28 16:40:20 +08:00
mac
07fa69478f v2.1.28 — 备注按钮常规模式也显示 2026-07-28 16:39:06 +08:00
mac
377d1ba2cf v2.1.27 — 备注改为弹窗编辑 2026-07-28 16:35:26 +08:00
mac
f674bb4c66 v2.1.26 — 本周重点每条任务支持备注 2026-07-28 16:19:59 +08:00
mac
e5d416f89f v2.1.25 — 修复拖拽排序drop事件this上下文丢失 2026-07-28 15:07:43 +08:00
mac
e09883a33f v2.1.24 — 本周重点+责善支持拖拽排序 2026-07-28 14:07:49 +08:00
mac
55dc83ce83 v2.1.23 — 本周重点拆分手动任务+日历日程两张卡片 2026-07-28 13:53:30 +08:00
mac
ae817fe8f9 v2.1.22 — 修复编辑模式下删除按钮不显示 2026-07-28 13:48:09 +08:00
mac
9e296e59e6 v2.1.21 — 恢复本周重点+责善改过的编辑按钮 2026-07-28 13:46:47 +08:00
mac
3f1becd374 v2.1.20 — 同步日程自动填充加入日期【7月28日 09:00】格式 2026-07-28 13:44:48 +08:00
mac
b5d7720e52 v2.1.19 — 日历同步弹窗显示每个事件的日期 2026-07-28 13:40:45 +08:00
mac
caec0196bb v2.1.18 — 增加一条按钮与上方内容加12px间距 2026-07-27 09:25:24 +08:00
mac
d1404765d2 v2.1.17 — 勤学打卡改回单行横排样式 2026-07-27 09:19:33 +08:00
mac
e2c97ca1fb v2.1.16 — 勤学打卡优化样式:默认隐藏×,编辑模式才显示 2026-07-27 09:17:24 +08:00
mac
8188364813 v2.1.15 — 勤学打卡支持增加和删除习惯 2026-07-27 09:14:31 +08:00
mac
b50d8e17e2 v2.1.14 — 勤学打卡改为3×3网格卡片布局 2026-07-27 09:13:21 +08:00
mac
c70aeff2ed v2.1.13 — 本周重点+责善改过去掉编辑模式,默认显示增加一条 2026-07-27 09:07:45 +08:00
mac
f63ab8b8ea v2.1.12 — 勤学打卡每天最多一次,已打卡按钮禁用 2026-07-27 08:56:47 +08:00
mac
b48e95c800 v2.1.11 — 本周重点去3条限制 + 勤学改为卡片打卡按钮 2026-07-27 08:53:48 +08:00
mac
bf1be1aa9f v2.1.10 — 移除周列表强制4周填充逻辑,避免跨月虚周 2026-07-27 08:47:49 +08:00
mac
083935f2da v2.1.9 — 早间立志改为本周重点 2026-07-27 08:43:07 +08:00
mac
e6ae7f2eb4 v2.1.8 — 周列表移除圆形状态icon,只保留文字标签 2026-07-27 08:35:19 +08:00
mac
24561e1f59 v2.1.7 — 修复周列表点击事件变量名错误 2026-07-24 17:43:23 +08:00
mac
9fad18733e v2.1.6 — 日历改为周列表式:每行显示周号+日期范围+状态 2026-07-24 17:42:15 +08:00
mac
5e97f6d6d8 v2.1.5 — 退出改为点击头像弹出下拉菜单 2026-07-24 17:39:40 +08:00
mac
99bc6d3c6c v2.1.4 — 侧边栏深色主题:深蓝渐变背景+蓝色图标+浅灰文字 2026-07-24 17:38:42 +08:00
mac
5d3192dfaf v2.1.3 — 周标签从W27改为第27周 2026-07-24 17:37:25 +08:00
mac
9c9511e817 v2.1.2 — 周选择改为2列一行grid布局 2026-07-24 17:36:50 +08:00
mac
b5dbb13956 v2.1.1 — 月份改成左右翻页选择器 2026-07-24 17:35:56 +08:00
mac
5ef340cb08 v2.1.0 — 日历三级选择:年→月→周,4个月格+4周格 2026-07-24 17:34:52 +08:00
mac
073b570681 fix: 更新fmtWeek支持和calMonth渲染逻辑 2026-07-24 17:34:15 +08:00
mac
ce0e374e86 v2.0.9 — 打卡页设计重构:左侧200px统计+日历,右侧Tab+内容 2026-07-24 17:31:33 +08:00
mac
f7bbc505b9 v2.0.8 — 打卡页左右两栏:左侧统计+日历,右侧Tab+内容 2026-07-24 17:27:15 +08:00
mac
351e1f18ae v2.0.7 — 日历周选择器移入打卡面板内 2026-07-24 17:25:57 +08:00
mac
0e75326b5f v2.0.6 — 侧边栏改为窄列式(OPC风格):图标在上+短文字在下 2026-07-24 17:18:44 +08:00
mac
cd90c2d799 v2.0.5 — 侧边栏菜单美化:大图标+蓝色激活态 2026-07-24 17:11:46 +08:00
mac
c2cbbdb12a v2.0.4 — 修复Tab切换:提高daily-card优先级覆盖card的display:flex 2026-07-24 17:10:37 +08:00
mac
8223f72544 v2.0.3 — 周历改4周一行,月起始加月份标签 2026-07-24 16:53:06 +08:00
mac
36b58a8b73 v2.0.2 — 每周打卡三卡片改为Tab切换 2026-07-24 16:34:20 +08:00
mac
5b73fc0eae v2.0.1 — 修复 database.py 缺少聚合函数 2026-07-24 16:23:07 +08:00
mac
5b637e7fed v2.0.0 — 日打卡彻底改为周打卡 2026-07-24 16:20:40 +08:00
mac
350c9fb877 v1.5.3 — 日历同步按日期自动填充
- 后端 api_calendar_sync_all 查询后自动去重保存到各日期 checkin
- 不再只填充当前选中的日期
- 弹窗显示已填充天数
2026-06-05 12:19:22 +08:00
mac
19ae81ae6e v1.5.2 — 责善显示改进方案
- 责善列表项追加 → 反思内容
- 红色标注区分改进方案
2026-06-04 17:46:44 +08:00
mac
535af35911 v1.5.1 — 蓝图重命名为人生蓝图,图标改为太阳 2026-06-04 16:49:27 +08:00
mac
fc338f426a v1.5.0 — 蓝图 VI 统一
- 移除头部 mission 文字和六板块卡片
- Tab 改用 SVG 图标(bolt/放大镜/书本/列表)
- 板块用彩色圆点标识(六色 VI)
- 整体风格对齐天机阁设计系统
2026-06-04 16:43:08 +08:00
mac
5300ee426d v1.4.6 — 备注左对齐 2026-06-04 16:39:58 +08:00
mac
8ac6f1af6b v1.4.5 — 蓝图去统计数+勤学显示备注
- 六板块卡片移除统计数字,仅保留 emoji+名称
- 勤学项在列表中显示备注 note
2026-06-04 16:38:54 +08:00
mac
8c81fae334 v1.4.4 — 蓝图改为 Tab 筛选+明细列表
- 顶栏六板块卡片只显示统计数
- 新增立志/责善/勤学/全部四个 Tab
- 明细列表显示每条记录的板块+类型+内容
- 类型标签颜色区分(立志黄/责善红/勤学绿)
2026-06-04 16:19:50 +08:00
mac
67505414d9 v1.4.3 — 修复蓝图无数据: apiGetStats 取错字段 2026-06-04 16:01:09 +08:00
mac
03745e19c3 v1.4.2 — 自动归类取代手动选择板块
- 后端关键词自动归类立志/责善/勤学到六板块
- 移除DOM板块选择器,简化UI
- 蓝图面板展示各板块立志·责善·勤学统计+详情列表
- 旧数据自动兼容、新数据无需手动选分类
2026-06-04 15:58:56 +08:00
mac
be9f10e968 v1.4.1 — 修复板块选择器不触发自动保存
- blur/change 事件增加 select 支持
- 板块选择后立即自动保存,不再阻塞日历切换
2026-06-04 15:50:41 +08:00
mac
6e1b793b40 v1.4.0 — 蓝图六板块体系
- 立志项新增板块选择器(医疗服务/医药营销/医疗支付/AI智能/公司治理/个人修养)
- 数据格式从纯字符串升级为 {text, pillar},向后兼容旧数据
- 新增蓝图面板:展示使命 + 六板块卡片 + 各板块立志统计
- stats API 新增 morning_items 返回全部立志数据
2026-06-04 15:43:53 +08:00
mac
e90e8c30ff v1.3.6 — 日历同步按钮样式与日期输入框统一 2026-06-04 15:18:46 +08:00
mac
046f144896 v1.3.5 — 日期+日历同步右对齐
- panel-header-right 包裹日期和按钮,靠右排列不分散
2026-06-04 15:14:52 +08:00
5 changed files with 1346 additions and 1344 deletions

301
app.py
View File

@@ -6,7 +6,7 @@ from datetime import timedelta
from functools import wraps from functools import wraps
from flask import Flask, render_template, request, jsonify, session, redirect, url_for from flask import Flask, render_template, request, jsonify, session, redirect, url_for
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from database import init_db, get_checkin, save_checkin, delete_checkin, get_all_checkins, get_wishes, save_wish, update_wish, delete_wish, reorder_wishes from database import init_db, get_checkin, save_checkin, delete_checkin, get_all_checkins, get_weeks_list, get_daily_backup
app = Flask(__name__) app = Flask(__name__)
# 固定密钥确保 gunicorn 多 worker 下 session 可互通 # 固定密钥确保 gunicorn 多 worker 下 session 可互通
@@ -72,11 +72,35 @@ def logout():
def compute_stats(): def compute_stats():
"""计算统计数据,供 API 和模板共用""" """计算统计数据,供 API 和模板共用"""
rows = get_all_checkins() rows = get_all_checkins()
total_days = len(rows) total_weeks = len(rows)
total_morning = 0 total_morning = 0
total_evening = 0 total_evening = 0
total_study = 0 total_study = 0
calendar = {} calendar = {}
all_morning_items = []
all_evening_items = []
all_study_items = []
# 关键词 → 板块 自动归类规则
PILLAR_KEYWORDS = {
'医药营销': ['科普','科研','学术会议','学术','会议','患者管理','临床推广','临床','药企','处方','KOL','代表','拜访','荣昌','恒瑞','信达','鲁银','OPC','项目','标杆','客户'],
'医疗服务': ['诊疗','治疗','医生','医院','科室','手术','门诊','患者','','护理','诊断','影像','检验'],
'医疗支付': ['医保','商保','理赔','支付','保险','费用','报销','商业保险','保单'],
'AI 智能': ['AI','数字人','智能','算法','自动化','平台','系统','网站','开发','代码','程序','模型'],
'公司治理': ['团队','组织','管理','招聘','HR','制度','流程','财务','考核','KPI','规划','人才','架构','预算'],
'个人修养': ['早起','睡觉','俯卧撑','运动','冥想','阅读','复盘','写作','习惯','修身','立志','责善','改过','勤学'],
}
def classify_auto(text):
"""根据文本内容自动归类"""
if not text or not text.strip():
return ''
text_lower = text.strip()
for pillar, keywords in PILLAR_KEYWORDS.items():
for kw in keywords:
if kw in text_lower:
return pillar
return '' # 未匹配
for row in rows: for row in rows:
d = row['date'] d = row['date']
@@ -85,12 +109,46 @@ def compute_stats():
evening = data.get('evening', []) evening = data.get('evening', [])
study = data.get('study', []) study = data.get('study', [])
morning_count = sum(1 for x in morning if isinstance(x, str) and x.strip()) # 收集并自动归类
for mi in morning:
text = mi if isinstance(mi, str) else mi.get('text', '')
if isinstance(text, str) and text.strip():
auto_pillar = mi.get('pillar', '') if isinstance(mi, dict) else ''
if not auto_pillar:
auto_pillar = classify_auto(text)
all_morning_items.append({'text': text.strip(), 'pillar': auto_pillar})
for ei in evening:
if isinstance(ei, dict):
m = (ei.get('mistake') or '').strip()
imp = (ei.get('improvement') or '').strip()
if m or imp:
pillar = ei.get('pillar', '') or classify_auto(m + ' ' + imp)
all_evening_items.append({'mistake': m, 'improvement': imp, 'pillar': pillar})
elif isinstance(ei, str) and ei.strip():
all_evening_items.append({'mistake': ei.strip(), 'improvement': '', 'pillar': classify_auto(ei)})
for si in study:
name = si.get('name', '') if isinstance(si, dict) else str(si)
if name.strip():
pillar = si.get('pillar', '') if isinstance(si, dict) else ''
if not pillar:
pillar = classify_auto(name)
all_study_items.append({
'name': name.strip(),
'count': si.get('count', 0) if isinstance(si, dict) else 0,
'pillar': pillar
})
morning_count = sum(1 for x in morning if (
isinstance(x, str) and x.strip() or
isinstance(x, dict) and (x.get('text', '') or '').strip()
))
evening_count = sum(1 for x in evening if ( evening_count = sum(1 for x in evening if (
isinstance(x, str) and x.strip() or isinstance(x, str) and x.strip() or
isinstance(x, dict) and (x.get('mistake', '') or '').strip() isinstance(x, dict) and (x.get('mistake', '') or '').strip()
)) ))
study_count = sum(1 for x in study if x.get('done')) study_count = sum(1 for x in study if x.get('count', 0) > 0)
total_morning += morning_count total_morning += morning_count
total_evening += evening_count total_evening += evening_count
@@ -103,10 +161,38 @@ def compute_stats():
# day_score = 60 + extra * 5 (不在此处使用,仅用于日历状态) # day_score = 60 + extra * 5 (不在此处使用,仅用于日历状态)
calendar[d] = 'pass' if all_three else 'fail' calendar[d] = 'pass' if all_three else 'fail'
# 构建板块统计
pillars = ['医疗服务','医药营销','医疗支付','AI 智能','公司治理','个人修养']
pillar_breakdown = {}
for p in pillars:
pillar_breakdown[p] = {
'morning': 0, 'evening': 0, 'study': 0,
'morning_items': [], 'evening_items': [], 'study_items': []
}
for mi in all_morning_items:
p = mi['pillar']
if p in pillar_breakdown:
pillar_breakdown[p]['morning'] += 1
pillar_breakdown[p]['morning_items'].append(mi)
for ei in all_evening_items:
p = ei['pillar']
if p in pillar_breakdown:
pillar_breakdown[p]['evening'] += 1
pillar_breakdown[p]['evening_items'].append(ei)
for si in all_study_items:
p = si['pillar']
if p in pillar_breakdown:
pillar_breakdown[p]['study'] += 1
pillar_breakdown[p]['study_items'].append(si)
return dict( return dict(
total_days=total_days, total_morning=total_morning, total_weeks=total_weeks, total_morning=total_morning,
total_evening=total_evening, total_study=total_study, total_evening=total_evening, total_study=total_study,
calendar=calendar calendar=calendar,
morning_items=all_morning_items,
evening_items=all_evening_items,
study_items=all_study_items,
pillar_breakdown=pillar_breakdown
) )
@@ -115,11 +201,9 @@ def compute_stats():
def index(): def index():
import json import json
stats = compute_stats() stats = compute_stats()
wishes = [dict(w) for w in get_wishes()]
return render_template('index.html', return render_template('index.html',
username=session.get('display_name', session.get('username', '')), username=session.get('display_name', session.get('username', '')),
initial_stats=json.dumps(stats, ensure_ascii=False), initial_stats=json.dumps(stats, ensure_ascii=False))
initial_wishes=json.dumps(wishes, ensure_ascii=False))
# ── API ────────────────────────────────────────────── # ── API ──────────────────────────────────────────────
@@ -127,10 +211,10 @@ def index():
@app.route('/api/checkin', methods=['GET']) @app.route('/api/checkin', methods=['GET'])
@login_required @login_required
def api_get_checkin(): def api_get_checkin():
date = request.args.get('date', '') week = request.args.get('week', '')
if not date: if not week:
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400 return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
row = get_checkin(date) row = get_checkin(week)
return jsonify({'ok': True, 'data': row}) return jsonify({'ok': True, 'data': row})
@@ -138,21 +222,29 @@ def api_get_checkin():
@login_required @login_required
def api_save_checkin(): def api_save_checkin():
body = request.get_json(force=True) body = request.get_json(force=True)
date = body.get('date', '') week = body.get('week', '')
if not date: if not week:
return jsonify({'ok': False, 'error': '缺少 date 字段'}), 400 return jsonify({'ok': False, 'error': '缺少 week 字段'}), 400
data = body.get('data', {}) data = body.get('data', {})
save_checkin(date, data) save_checkin(week, data)
return jsonify({'ok': True}) return jsonify({'ok': True})
@app.route('/api/checkin/<date>', methods=['DELETE']) @app.route('/api/checkin/<week>', methods=['DELETE'])
@login_required @login_required
def api_delete_checkin(date): def api_delete_checkin(week):
delete_checkin(date) delete_checkin(week)
return jsonify({'ok': True}) return jsonify({'ok': True})
@app.route('/api/checkin/weeks', methods=['GET'])
@login_required
def api_weeks():
"""返回所有已打卡周号"""
weeks = get_weeks_list()
return jsonify({'ok': True, 'data': weeks})
@app.route('/api/history', methods=['GET']) @app.route('/api/history', methods=['GET'])
@login_required @login_required
def api_history(): def api_history():
@@ -160,6 +252,17 @@ def api_history():
return jsonify({'ok': True, 'data': rows}) return jsonify({'ok': True, 'data': rows})
@app.route('/api/history/daily', methods=['GET'])
@login_required
def api_history_daily():
"""查看某周的原始日记录"""
week = request.args.get('week', '')
if not week:
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
rows = get_daily_backup(week)
return jsonify({'ok': True, 'data': rows})
@app.route('/api/stats', methods=['GET']) @app.route('/api/stats', methods=['GET'])
@login_required @login_required
def api_stats(): def api_stats():
@@ -177,69 +280,29 @@ def api_user():
}) })
# ── 心愿清单 API ──────────────────────────────
@app.route('/api/wishes', methods=['GET'])
@login_required
def api_get_wishes():
wishes = [dict(w) for w in get_wishes()]
return jsonify({'ok': True, 'data': wishes})
@app.route('/api/wishes', methods=['POST'])
@login_required
def api_create_wish():
body = request.get_json(force=True)
name = body.get('name', '').strip()
if not name:
return jsonify({'ok': False, 'error': '名称不能为空'}), 400
quadrant = body.get('quadrant', '重要不紧急')
deadline = body.get('deadline', '')
wid = save_wish(name, quadrant, deadline)
return jsonify({'ok': True, 'id': wid})
@app.route('/api/wishes/<int:wish_id>', methods=['PUT'])
@login_required
def api_update_wish(wish_id):
body = request.get_json(force=True)
update_wish(wish_id, **body)
return jsonify({'ok': True})
@app.route('/api/wishes/<int:wish_id>', methods=['DELETE'])
@login_required
def api_delete_wish(wish_id):
delete_wish(wish_id)
return jsonify({'ok': True})
@app.route('/api/wishes/reorder', methods=['PUT'])
@login_required
def api_reorder_wishes():
body = request.get_json(force=True)
order = body.get('order', [])
if not isinstance(order, list):
return jsonify({'ok': False, 'error': 'order 必须是列表'}), 400
reorder_wishes(order)
return jsonify({'ok': True})
# ── 日历同步 API ──────────────────────────── # ── 日历同步 API ────────────────────────────
CALENDAR_CACHE = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'ziwei-power', 'calendar_cache.json') CALENDAR_CACHE = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'ziwei-power', 'calendar_cache.json')
DING_MCP_URL = 'https://mcp-gw.dingtalk.com/server/95959163f85d8b58a167f65cd8bd3d22690b16c75ebacd0fa095016396a10e0d?key=0993e4d4e44c50ea25a0db840cc5815e' DING_MCP_URL = 'https://mcp-gw.dingtalk.com/server/95959163f85d8b58a167f65cd8bd3d22690b16c75ebacd0fa095016396a10e0d?key=0993e4d4e44c50ea25a0db840cc5815e'
def _fetch_dingtalk_events(date_str): def _fetch_dingtalk_events_week(week_str):
"""调用钉钉 MCP 实时查询指定日期的日程""" """调用钉钉 MCP 查询指定周(周一~周日)的全部日程"""
import json as _json, urllib.request as _ur import json as _json, urllib.request as _ur
from datetime import datetime, timezone, timedelta from datetime import datetime, timezone, timedelta
tz = timezone(timedelta(hours=8)) tz = timezone(timedelta(hours=8))
try: try:
d = datetime.strptime(date_str, '%Y-%m-%d').replace(tzinfo=tz) year_part, week_part = week_str.split('-W')
except ValueError: year = int(year_part)
week = int(week_part)
except (ValueError, IndexError):
return [] return []
jan1 = datetime(year, 1, 1, tzinfo=tz)
monday = jan1 + timedelta(days=(week - 1) * 7 - jan1.weekday())
results = []
for i in range(7):
d = monday + timedelta(days=i)
ds = d.strftime('%Y-%m-%d')
start_ts = int(d.replace(hour=0, minute=0, second=0).timestamp() * 1000) start_ts = int(d.replace(hour=0, minute=0, second=0).timestamp() * 1000)
end_ts = int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000) end_ts = int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000)
body = _json.dumps({ body = _json.dumps({
@@ -256,8 +319,7 @@ def _fetch_dingtalk_events(date_str):
data = _json.loads(resp.read()) data = _json.loads(resp.read())
events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', []) events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', [])
except Exception: except Exception:
return None # 网络错误返回 None调用方回退缓存 continue
results = []
for e in events: for e in events:
summary = (e.get('summary') or '').strip() summary = (e.get('summary') or '').strip()
if not summary: if not summary:
@@ -268,61 +330,80 @@ def _fetch_dingtalk_events(date_str):
if start and end: if start and end:
time_str = start[11:16] + '-' + end[11:16] time_str = start[11:16] + '-' + end[11:16]
results.append({ results.append({
'date': date_str, 'summary': summary, 'date': ds, 'summary': summary,
'time': time_str, 'location': e.get('location') or '' 'time': time_str, 'location': e.get('location') or ''
}) })
# 写入缓存
try:
all_cached = []
try:
with open(CALENDAR_CACHE, 'r') as f:
all_cached = _json.load(f)
except (FileNotFoundError, _json.JSONDecodeError):
pass
# 替换同一天的旧缓存
all_cached = [c for c in all_cached if c.get('date') != date_str] + results
with open(CALENDAR_CACHE, 'w') as f:
_json.dump(all_cached, f, ensure_ascii=False, indent=2)
except Exception:
pass
return results return results
@app.route('/api/calendar-sync', methods=['GET']) @app.route('/api/calendar-sync', methods=['GET'])
@login_required @login_required
def api_calendar_sync(): def api_calendar_sync():
import json as _json week = request.args.get('week', '')
date = request.args.get('date', '') if not week:
if not date: return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400 events = _fetch_dingtalk_events_week(week)
# 优先实时查询钉钉 MCP
events = _fetch_dingtalk_events(date)
if events is None:
# MCP 不可用,回退缓存
try:
with open(CALENDAR_CACHE, 'r') as f:
all_cached = _json.load(f)
events = [e for e in all_cached if e.get('date') == date]
except (FileNotFoundError, _json.JSONDecodeError):
events = []
return jsonify({'ok': True, 'data': events}) return jsonify({'ok': True, 'data': events})
@app.route('/api/calendar-sync-all', methods=['GET']) @app.route('/api/calendar-sync-all', methods=['GET'])
@login_required @login_required
def api_calendar_sync_all(): def api_calendar_sync_all():
"""批量查询过去15天~未来15天的钉钉日程按日期分组返回""" """批量查询过去4周~未来4周的钉钉日程并自动填充到对应周的 checkin"""
import json as _json import json as _json
from datetime import datetime, timedelta from datetime import datetime, timedelta
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
results = [] results = []
for delta in range(-15, 16): saved_weeks = 0
d = today + timedelta(days=delta) for delta in range(-4, 5):
ds = d.strftime('%Y-%m-%d') # 计算目标周
events = _fetch_dingtalk_events(ds) target = today + timedelta(weeks=delta)
iso_year, iso_week, _ = target.isocalendar()
week_key = f"{iso_year}-W{iso_week:02d}"
events = _fetch_dingtalk_events_week(week_key)
if events: if events:
results.append({'date': ds, 'events': events}) results.append({'date': week_key, 'events': events})
return jsonify({'ok': True, 'data': results}) # 自动去重填充
existing = get_checkin(week_key)
data = existing['data'] if existing else {'morning': [], 'evening': [], 'study': []}
morning = data.get('morning', [])
existing_texts = set()
for mi in morning:
t = mi if isinstance(mi, str) else mi.get('text', '')
if t.strip():
existing_texts.add(t.strip())
added = False
for evt in events:
summary = (evt.get('summary') or '').strip()
if not summary:
continue
date_str = evt.get('date', '')
if date_str:
try:
parts = date_str.split('-')
date_label = f"{int(parts[1])}{int(parts[2])}"
except:
date_label = date_str
else:
date_label = ''
time_str = evt.get('time', '')
prefix = f"{date_label} {time_str}" if date_label and time_str else \
(f"{date_label}" if date_label else \
(f"{time_str}" if time_str else ''))
text = f"{prefix}{summary}" if prefix else summary
loc = evt.get('location', '')
if loc:
text += f" @{loc}"
text = text.strip()
if text not in existing_texts:
morning.append(text)
existing_texts.add(text)
added = True
if added:
data['morning'] = morning
save_checkin(week_key, data)
saved_weeks += 1
return jsonify({'ok': True, 'data': results, 'saved_days': saved_weeks})
# ── 启动 ────────────────────────────────────────────── # ── 启动 ──────────────────────────────────────────────

View File

@@ -11,7 +11,7 @@ os.makedirs(DB_DIR, exist_ok=True)
DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db') DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db')
# 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑 # 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑
CURRENT_SCHEMA_VERSION = 3 CURRENT_SCHEMA_VERSION = 2
def get_conn(): def get_conn():
@@ -56,23 +56,27 @@ def init_db():
''') ''')
if current < 2: if current < 2:
# v2: 心愿清单 # v2: 日打卡 → 周打卡
# 备份旧表 → 聚合 → 新建周表
existing = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins'").fetchone()
if existing:
# 检查是否已有备份表(幂等)
backup_exists = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins_daily_backup'").fetchone()
if not backup_exists:
conn.execute("ALTER TABLE checkins RENAME TO checkins_daily_backup")
# 重新创建 checkins 表结构不变date 语义变为周号)
conn.execute(''' conn.execute('''
CREATE TABLE IF NOT EXISTS wishes ( CREATE TABLE IF NOT EXISTS checkins (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, date TEXT UNIQUE NOT NULL,
priority TEXT NOT NULL DEFAULT '', data TEXT NOT NULL,
deadline TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0, updated_at TEXT NOT NULL
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
) )
''') ''')
# 聚合日数据到周
if current < 3: if not backup_exists:
# v3: 优先级改为四象限 _aggregate_daily_to_weekly(conn)
conn.execute("ALTER TABLE wishes ADD COLUMN quadrant TEXT NOT NULL DEFAULT '重要不紧急'")
conn.execute("UPDATE wishes SET quadrant = CASE priority WHEN '' THEN '重要紧急' WHEN '' THEN '重要不紧急' WHEN '' THEN '不紧急不重要' ELSE '重要不紧急' END WHERE quadrant = '重要不紧急'")
# ── 将来加字段/改表在此扩展 ── # ── 将来加字段/改表在此扩展 ──
# if current < 2: # if current < 2:
@@ -147,57 +151,88 @@ def get_all_checkins():
return results return results
# ── 心愿清单 CRUD ────────────────────────────────── def get_weeks_list():
"""返回所有已有打卡记录的周号列表"""
def get_wishes():
"""获取所有心愿,按 sort_order 排序"""
conn = get_conn() conn = get_conn()
rows = conn.execute('SELECT * FROM wishes ORDER BY sort_order').fetchall() rows = conn.execute('SELECT DISTINCT date FROM checkins ORDER BY date').fetchall()
conn.close() conn.close()
return [dict(row) for row in rows] return [r['date'] for r in rows]
def save_wish(name, quadrant, deadline): def get_daily_backup(week_str):
"""新增一条心愿""" """获取某周的原始日记录(从备份表)"""
now = datetime.now().isoformat() from datetime import datetime as _dt, timedelta
if '-W' not in week_str:
return []
year_part, week_part = week_str.split('-W')
try:
year = int(year_part)
week = int(week_part)
except (ValueError, IndexError):
return []
jan1 = _dt(year, 1, 1)
delta = timedelta(days=(week - 1) * 7 - jan1.weekday())
monday = jan1 + delta
dates = [monday.strftime('%Y-%m-%d')]
for i in range(1, 7):
dates.append((monday + timedelta(days=i)).strftime('%Y-%m-%d'))
conn = get_conn() conn = get_conn()
max_order = conn.execute('SELECT COALESCE(MAX(sort_order), -1) + 1 AS n FROM wishes').fetchone()['n'] placeholders = ','.join('?' for _ in dates)
exists = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins_daily_backup'").fetchone()
if not exists:
conn.close()
return []
rows = conn.execute(
f"SELECT date, data FROM checkins_daily_backup WHERE date IN ({placeholders}) ORDER BY date",
dates
).fetchall()
conn.close()
results = []
for row in rows:
results.append({'date': row['date'], 'data': json.loads(row['data'])})
return results
def _aggregate_daily_to_weekly(conn):
"""将 checkins_daily_backup 的日数据聚合为周数据,写入 checkins"""
from datetime import datetime as _dt
rows = conn.execute('SELECT date, data FROM checkins_daily_backup ORDER BY date').fetchall()
weekly = {}
for row in rows:
try:
d = _dt.strptime(row['date'], '%Y-%m-%d')
except ValueError:
continue
iso_year, iso_week, _ = d.isocalendar()
week_key = f"{iso_year}-W{iso_week:02d}"
if week_key not in weekly:
weekly[week_key] = {'morning': [], 'evening': [], 'study': []}
data = json.loads(row['data'])
for mi in data.get('morning', []):
text = mi if isinstance(mi, str) else mi.get('text', '')
if text:
existing_texts = {x if isinstance(x, str) else x.get('text', '') for x in weekly[week_key]['morning']}
if text not in existing_texts:
weekly[week_key]['morning'].append(mi)
for ei in data.get('evening', []):
mst = ei.get('mistake', '') if isinstance(ei, dict) else ''
if mst:
existing = {x.get('mistake', '') if isinstance(x, dict) else x for x in weekly[week_key]['evening']}
if mst not in existing:
weekly[week_key]['evening'].append(ei if isinstance(ei, dict) else {'mistake': ei, 'improvement': ''})
existing_names = {}
for si in weekly[week_key]['study']:
nm = si.get('name', '') if isinstance(si, dict) else str(si)
if nm:
existing_names[nm] = si
for si in data.get('study', []):
nm = si.get('name', '') if isinstance(si, dict) else str(si)
if nm and nm not in existing_names:
weekly[week_key]['study'].append(si)
existing_names[nm] = si
now = _dt.now().isoformat()
for week_key in sorted(weekly.keys()):
conn.execute( conn.execute(
'INSERT INTO wishes (name, quadrant, deadline, done, sort_order, created_at) VALUES (?, ?, ?, 0, ?, ?)', 'INSERT INTO checkins (date, data, created_at, updated_at) VALUES (?, ?, ?, ?)',
(name, quadrant, deadline, max_order, now) (week_key, json.dumps(weekly[week_key], ensure_ascii=False), now, now)
) )
conn.commit()
wish_id = conn.execute('SELECT last_insert_rowid()').fetchone()[0]
conn.close()
return wish_id
def update_wish(wish_id, **kwargs):
"""更新心愿字段"""
allowed = ['name', 'quadrant', 'deadline', 'done']
updates = {k: v for k, v in kwargs.items() if k in allowed}
if not updates:
return
conn = get_conn()
sets = ', '.join(f'{k} = ?' for k in updates)
vals = list(updates.values()) + [wish_id]
conn.execute(f'UPDATE wishes SET {sets} WHERE id = ?', vals)
conn.commit()
conn.close()
def delete_wish(wish_id):
"""删除心愿"""
conn = get_conn()
conn.execute('DELETE FROM wishes WHERE id = ?', (wish_id,))
conn.commit()
conn.close()
def reorder_wishes(order_list):
"""批量更新排序order_list = [id1, id2, ...]"""
conn = get_conn()
for idx, wid in enumerate(order_list):
conn.execute('UPDATE wishes SET sort_order = ? WHERE id = ?', (idx, wid))
conn.commit()
conn.close()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@
<title>紫微 · 磁场管理</title> <title>紫微 · 磁场管理</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script>window.__INITIAL_STATS__ = {{ initial_stats | safe }};</script> <script>window.__INITIAL_STATS__ = {{ initial_stats | safe }};</script>
<script>window.__INITIAL_WISHES__ = {{ initial_wishes | safe }};</script>
</head> </head>
<body> <body>
{% include "icons.html" %} {% include "icons.html" %}
@@ -22,7 +21,7 @@
</div> </div>
<div class="sync-modal-loading" id="sync-loading"> <div class="sync-modal-loading" id="sync-loading">
<div class="sync-spinner"></div> <div class="sync-spinner"></div>
<p>正在从钉钉同步过去15天~未来15天日程…</p> <p>正在从钉钉同步过去4周~未来4周日程…</p>
</div> </div>
<div class="sync-modal-results" id="sync-results" style="display:none"> <div class="sync-modal-results" id="sync-results" style="display:none">
<div class="sync-summary" id="sync-summary"></div> <div class="sync-summary" id="sync-summary"></div>
@@ -34,28 +33,77 @@
</div> </div>
</div> </div>
<!-- 备注编辑弹窗 -->
<div class="sync-modal-overlay" id="note-modal" style="display:none">
<div class="sync-modal" style="max-width:720px">
<div class="sync-modal-header">
<h3 id="note-modal-title">备注</h3>
<button class="sync-modal-close" onclick="closeNoteModal()">&times;</button>
</div>
<div style="padding:16px">
<textarea id="note-modal-textarea" style="width:100%; min-height:300px; padding:12px; border:1px solid var(--border); border-radius:8px; font-size:14px; font-family:inherit; line-height:1.6; resize:vertical; box-sizing:border-box" placeholder="写点备注…"></textarea>
</div>
<div class="sync-modal-footer">
<button class="btn-wish-cancel" onclick="closeNoteModal()">取消</button>
<button class="btn-wish-save" onclick="saveNoteModal()">保存</button>
</div>
</div>
</div>
<div class="app-layout"> <div class="app-layout">
<!-- ═══ 左侧边栏 ═══ --> <!-- ═══ 左侧边栏 ═══ -->
<aside class="sidebar"> <aside class="sidebar">
<!-- 用户信息 --> <!-- 用户信息 -->
<div class="sidebar-user"> <div class="sidebar-user" id="sidebar-user-menu" onclick="toggleUserMenu(event)">
<div class="user-avatar">{{ username[0] }}</div> <div class="user-avatar">{{ username[0] }}</div>
<div class="user-info">
<span class="user-name">{{ username }}</span> <span class="user-name">{{ username }}</span>
<span class="user-tag">道阁 · 紫微</span> <div class="user-dropdown" id="user-dropdown" style="display:none">
</div> <a class="user-dropdown-item" href="/logout" onclick="return confirm('确定退出登录?')">
<a href="/logout" class="btn-logout-icon" title="退出登录" onclick="return confirm('确定退出登录?')"> <svg class="icon-sm"><use href="#icon-logout"/></svg> 退出登录
<svg class="icon-sm"><use href="#icon-logout"/></svg>
</a> </a>
</div> </div>
</div>
<!-- 统计信息 --> <!-- 功能入口 -->
<nav class="sidebar-nav">
<a class="nav-item active" data-panel="daily" onclick="switchPanel('daily')">
<svg class="icon-md"><use href="#icon-calendar"/></svg>
<span class="nav-label">打卡</span>
</a>
<a class="nav-item" data-panel="history" onclick="switchPanel('history')">
<svg class="icon-md"><use href="#icon-list-bullet"/></svg>
<span class="nav-label">记录</span>
</a>
<a class="nav-item" data-panel="blueprint" onclick="switchPanel('blueprint')">
<svg class="icon-md"><use href="#icon-sun"/></svg>
<span class="nav-label">蓝图</span>
</a>
</nav>
</aside>
<!-- ═══ 右侧主内容区 ═══ -->
<main class="main-content">
<!-- ── 每周打卡面板 ── -->
<section class="panel active" id="panel-daily">
<div class="panel-header">
<h2>每周打卡</h2>
<div class="panel-header-right">
<input type="week" id="check-date" class="check-date-input" onchange="loadCheckin()">
<button class="btn-cal-sync" onclick="syncCalendar()" title="同步钉钉日历">
<svg class="icon-sm"><use href="#icon-calendar"/></svg> 日历同步
</button>
</div>
</div>
<div class="daily-layout">
<!-- 左侧:统计 + 日历 -->
<div class="daily-sidebar">
<div class="sidebar-stats" id="sidebar-stats"> <div class="sidebar-stats" id="sidebar-stats">
<div class="stat-item"> <div class="stat-item">
<span class="stat-num" id="stat-days">--</span> <span class="stat-num" id="stat-days">--</span>
<span class="stat-label">打卡</span> <span class="stat-label">打卡</span>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="stat-num" id="stat-morning">--</span> <span class="stat-num" id="stat-morning">--</span>
@@ -66,88 +114,66 @@
<span class="stat-label">勤学</span> <span class="stat-label">勤学</span>
</div> </div>
</div> </div>
<div class="calendar-widget inline">
<!-- 日历 -->
<div class="calendar-widget">
<div class="cal-header"> <div class="cal-header">
<button class="cal-nav" onclick="changeCalYear(-1)">
<svg class="icon-sm"><use href="#icon-chevron-left"/></svg>
</button>
<span class="cal-month-label" id="cal-year-label">2026年</span>
<button class="cal-nav" onclick="changeCalYear(1)">
<svg class="icon-sm"><use href="#icon-chevron-right"/></svg>
</button>
</div>
<div class="cal-header cal-month-header">
<button class="cal-nav" onclick="changeCalMonth(-1)"> <button class="cal-nav" onclick="changeCalMonth(-1)">
<svg class="icon-sm"><use href="#icon-chevron-left"/></svg> <svg class="icon-sm"><use href="#icon-chevron-left"/></svg>
</button> </button>
<span class="cal-month-label" id="cal-month-label">2026年 6</span> <span class="cal-month-label" id="cal-month-label">7</span>
<button class="cal-nav" onclick="changeCalMonth(1)"> <button class="cal-nav" onclick="changeCalMonth(1)">
<svg class="icon-sm"><use href="#icon-chevron-right"/></svg> <svg class="icon-sm"><use href="#icon-chevron-right"/></svg>
</button> </button>
</div> </div>
<div class="cal-weekdays"> <div class="cal-week-rows" id="cal-weeks"></div>
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="cal-grid" id="cal-grid"></div>
<div class="cal-legend">
<span class="legend-dot pass"></span> 达标
<span class="legend-dot fail"></span> 未达标
<span class="legend-dot empty"></span> 未打卡
</div> </div>
</div> </div>
<!-- 右侧Tab + 内容 -->
<!-- 功能入口 --> <div class="daily-main">
<nav class="sidebar-nav"> <div class="daily-tabs">
<a class="nav-item active" data-panel="daily" onclick="switchPanel('daily')"> <button class="daily-tab active" data-tab="morning" onclick="switchDailyTab(this)">
<svg class="icon-sm"><use href="#icon-calendar"/></svg> <svg class="icon-sm"><use href="#icon-sun"/></svg> 本周重点
每日打卡
</a>
<a class="nav-item" data-panel="weekly" onclick="switchPanel('weekly')">
<svg class="icon-sm"><use href="#icon-chart-bar"/></svg>
每周评分
</a>
<a class="nav-item" data-panel="wishes" onclick="switchPanel('wishes')">
<svg class="icon-sm"><use href="#icon-star"/></svg>
心愿清单
</a>
<a class="nav-item" data-panel="history" onclick="switchPanel('history')">
<svg class="icon-sm"><use href="#icon-list-bullet"/></svg>
历史记录
</a>
</nav>
<!-- 底部新建按钮 -->
<div class="sidebar-footer">
<button class="btn-new-day" onclick="goToday()">
<svg class="icon-sm"><use href="#icon-sun"/></svg>
回到今天
</button> </button>
</div> <button class="daily-tab" data-tab="evening" onclick="switchDailyTab(this)">
</aside> <svg class="icon-sm"><use href="#icon-magnifying-glass"/></svg> 责善改过
</button>
<!-- ═══ 右侧主内容区 ═══ --> <button class="daily-tab" data-tab="study" onclick="switchDailyTab(this)">
<main class="main-content"> <svg class="icon-sm"><use href="#icon-book-open"/></svg> 勤学打卡
<!-- ── 每日打卡面板 ── -->
<section class="panel active" id="panel-daily">
<div class="panel-header">
<h2>每日打卡</h2>
<input type="date" id="check-date" class="check-date-input" onchange="loadCheckin()">
<button class="btn-cal-sync" onclick="syncCalendar()" title="同步钉钉日历">
<svg class="icon-sm"><use href="#icon-calendar"/></svg> 日历同步
</button> </button>
</div> </div>
<div class="daily-grid"> <div class="daily-grid">
<div class="card card-morning"> <div class="card card-morning daily-card active" data-card="morning">
<div class="card-head"> <div class="card-head">
<div class="card-head-left"> <div class="card-head-left">
<svg class="icon-h2"><use href="#icon-sun"/></svg> <svg class="icon-h2"><use href="#icon-sun"/></svg>
<h3>早间立志</h3> <h3>本周重点</h3>
</div> </div>
<button class="btn-edit-toggle" onclick="toggleEditMode(this)" title="编辑"> <button class="btn-edit-toggle" onclick="toggleEditMode(this)" title="编辑">
<svg class="icon-sm"><use href="#icon-pencil"/></svg> <svg class="icon-sm"><use href="#icon-pencil"/></svg>
</button> </button>
</div> </div>
<p class="card-desc">今天最重要的 1~3 件</p> <p class="card-desc">本周最重要的事</p>
<div id="morning-list"></div> <div id="morning-list"></div>
<button class="btn-add edit-only" onclick="addMorning()"> <button class="btn-add edit-only" onclick="addMorning()">
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条 <svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
</button> </button>
<div class="morning-calendar" id="morning-calendar" style="display:none">
<div class="morning-calendar-head">
<svg class="icon-sm"><use href="#icon-calendar"/></svg>
<span>本周日程</span>
</div> </div>
<div class="card card-evening"> <div id="morning-calendar-list"></div>
</div>
</div>
<div class="card card-evening daily-card" data-card="evening">
<div class="card-head"> <div class="card-head">
<div class="card-head-left"> <div class="card-head-left">
<svg class="icon-h2"><use href="#icon-magnifying-glass"/></svg> <svg class="icon-h2"><use href="#icon-magnifying-glass"/></svg>
@@ -157,56 +183,31 @@
<svg class="icon-sm"><use href="#icon-pencil"/></svg> <svg class="icon-sm"><use href="#icon-pencil"/></svg>
</button> </button>
</div> </div>
<p class="card-desc">今天犯的错 &amp; 改进方案最多5条</p> <p class="card-desc">本周反思改正最多5条</p>
<div id="evening-list"></div> <div id="evening-list"></div>
<button class="btn-add edit-only" onclick="addEvening()"> <button class="btn-add edit-only" onclick="addEvening()">
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条 <svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
</button> </button>
</div> </div>
<div class="card card-study"> <div class="card card-study daily-card" data-card="study">
<div class="card-head"> <div class="card-head">
<div class="card-head-left">
<svg class="icon-h2"><use href="#icon-book-open"/></svg> <svg class="icon-h2"><use href="#icon-book-open"/></svg>
<h3>勤学打卡</h3> <h3>勤学打卡</h3>
</div> </div>
<p class="card-desc">今日修行清单</p> <button class="btn-edit-toggle" id="study-edit-toggle" onclick="toggleStudyEdit()" title="编辑">
<div id="study-list"></div> <svg class="icon-sm"><use href="#icon-pencil"/></svg>
</div>
</div>
</section>
<!-- ── 每周评分面板 ── -->
<section class="panel" id="panel-weekly">
<div class="panel-header">
<h2>每周评分</h2>
</div>
<div class="week-nav">
<button class="btn-nav" onclick="changeWeek(-1)">
<svg class="icon-nav"><use href="#icon-chevron-left"/></svg> 上周
</button>
<span id="week-label">--</span>
<button class="btn-nav" onclick="changeWeek(1)">
下周 <svg class="icon-nav"><use href="#icon-chevron-right"/></svg>
</button> </button>
</div> </div>
<div class="weekly-overview"> <p class="card-desc">本周修行清单</p>
<div class="score-circle"> <div id="study-cards"></div>
<svg viewBox="0 0 120 120" class="score-svg"> <button class="btn-add edit-only-study" onclick="addStudyItem()" style="margin-top:10px">
<circle cx="60" cy="60" r="50" fill="none" stroke="#E5E7EB" stroke-width="8"/> <svg class="icon-sm"><use href="#icon-plus"/></svg> 增加习惯
<circle cx="60" cy="60" r="50" fill="none" stroke="#4A6CF7" stroke-width="8" </button>
stroke-dasharray="314" stroke-dashoffset="314" stroke-linecap="round"
class="score-ring" transform="rotate(-90,60,60)"/>
</svg>
<div class="score-inner">
<span class="score-num" id="weekly-score">--</span>
<span class="score-unit"></span>
</div> </div>
</div> </div>
<div class="score-meta"> </div><!-- /daily-main -->
<span class="score-text" id="score-text">选择一周查看评分</span> </div><!-- /daily-layout -->
<div class="score-stats" id="score-stats"></div>
</div>
</div>
<div id="week-days-grid" class="week-days-grid"></div>
</section> </section>
<!-- ── 历史记录面板 ── --> <!-- ── 历史记录面板 ── -->
@@ -217,54 +218,29 @@
<div id="history-grid" class="history-grid"></div> <div id="history-grid" class="history-grid"></div>
</section> </section>
<!-- ── 心愿清单面板 ── --> <!-- ── 蓝图面板 ── -->
<section class="panel" id="panel-wishes"> <section class="panel" id="panel-blueprint">
<div class="panel-header"> <div class="panel-header">
<h2>心愿清单</h2> <h2>人生蓝图</h2>
<button class="btn-edit-toggle" id="wishes-edit-toggle" onclick="toggleWishesEdit(this)" title="编辑"> </div>
<svg class="icon-sm"><use href="#icon-pencil"/></svg> <!-- Tab 筛选 -->
<div class="bp-tabs">
<button class="bp-tab active" data-filter="all" onclick="filterBlueprint(this)">
<svg class="icon-sm"><use href="#icon-list-bullet"/></svg> 全部
</button>
<button class="bp-tab" data-filter="morning" onclick="filterBlueprint(this)">
<svg class="icon-sm"><use href="#icon-bolt"/></svg> 立志
</button>
<button class="bp-tab" data-filter="evening" onclick="filterBlueprint(this)">
<svg class="icon-sm"><use href="#icon-magnifying-glass"/></svg> 责善
</button>
<button class="bp-tab" data-filter="study" onclick="filterBlueprint(this)">
<svg class="icon-sm"><use href="#icon-book-open"/></svg> 勤学
</button> </button>
</div> </div>
<!-- 新增表单 --> <!-- 明细列表 -->
<div class="wish-form" id="wish-form" style="display:none"> <div class="bp-list" id="bp-list"></div>
<input type="text" id="wish-name" placeholder="心愿名称…" maxlength="50"> <div class="bp-list-empty" id="bp-list-empty" style="display:none">暂无记录</div>
<div class="wish-form-row">
<select id="wish-quadrant">
<option value="重要紧急">重要紧急</option>
<option value="重要不紧急" selected>重要不紧急</option>
<option value="紧急不重要">紧急不重要</option>
<option value="不紧急不重要">不紧急不重要</option>
</select>
<input type="date" id="wish-deadline">
</div>
<div class="wish-form-actions">
<button class="btn-wish-save" onclick="addWish()">添加</button>
<button class="btn-wish-cancel" onclick="hideWishForm()">取消</button>
</div>
</div>
<button class="btn-add edit-only" id="wishes-add-btn" onclick="showWishForm()" style="margin-bottom:16px">
<svg class="icon-sm"><use href="#icon-plus"/></svg> 新增心愿
</button>
<!-- 四象限网格 -->
<div class="quad-grid">
<div class="quad-cell" data-quadrant="重要紧急" id="quad-重要紧急">
<div class="quad-title">重要 & 紧急</div>
<div class="quad-list" id="quad-list-重要紧急"></div>
</div>
<div class="quad-cell" data-quadrant="重要不紧急" id="quad-重要不紧急">
<div class="quad-title">重要 & 不紧急</div>
<div class="quad-list" id="quad-list-重要不紧急"></div>
</div>
<div class="quad-cell" data-quadrant="紧急不重要" id="quad-紧急不重要">
<div class="quad-title">紧急 & 不重要</div>
<div class="quad-list" id="quad-list-紧急不重要"></div>
</div>
<div class="quad-cell" data-quadrant="不紧急不重要" id="quad-不紧急不重要">
<div class="quad-title">不紧急 & 不重要</div>
<div class="quad-list" id="quad-list-不紧急不重要"></div>
</div>
</div>
<div class="wishes-empty" id="wishes-empty" style="display:none">暂无心愿,点击上方按钮添加</div>
</section> </section>
</main> </main>