Compare commits

...

47 Commits
v1.5.0 ... main

Author SHA1 Message Date
mac
9fff01a272 v2.2.1 — 添加健康检查 + Gitea Actions CI/CD 自动部署 2026-07-28 20:24:09 +08:00
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
6 changed files with 1190 additions and 1366 deletions

132
.gitea/workflows/deploy.yml Normal file
View File

@@ -0,0 +1,132 @@
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: prod-deploy
env:
DEPLOY_BASE: /opt/ziwei-power
REPO_URL: https://qiukai:${{ secrets.DEPLOY_TOKEN }}@git.qiukai.me/qiukai/ziwei-power.git
SERVICE_NAME: ziwei-power
steps:
- name: Clone and deploy
run: |
set -e
RELEASE_ID="${{ github.sha }}"
RELEASE_DIR="${DEPLOY_BASE}/releases/${RELEASE_ID}"
CLONE_DIR="/tmp/zw-deploy-${RELEASE_ID}"
echo "=== 1. Clone repository ==="
rm -rf "${CLONE_DIR}"
git clone --depth 1 --branch main "${REPO_URL}" "${CLONE_DIR}"
echo "=== 2. Prepare release directory ==="
rm -rf "${RELEASE_DIR}"
mkdir -p "${RELEASE_DIR}"
# Copy repo content to release dir (exclude .git, .env, venv, data)
rsync -a --exclude='.git' \
--exclude='.env' \
--exclude='.env.local' \
--exclude='.venv' \
--exclude='data/' \
--exclude='__pycache__' \
--exclude='.gitea' \
"${CLONE_DIR}/" "${RELEASE_DIR}/"
echo "=== 2.5 Ensure .env secrets ==="
mkdir -p "${DEPLOY_BASE}/shared"
SHARED_ENV="${DEPLOY_BASE}/shared/.env"
touch "${SHARED_ENV}"
ensure_env_var() {
key="$1"; val="$2"
if [ -z "$val" ]; then
echo " ! ${key} 未提供(CI secret 为空),跳过写入"
return
fi
if ! grep -q "^${key}=" "${SHARED_ENV}"; then
echo "${key}=${val}" >> "${SHARED_ENV}"
echo " + 已写入 ${key}"
else
echo " = ${key} 已存在,保留原值"
fi
}
ensure_env_var "SECRET_KEY" "${{ secrets.ZW_SECRET_KEY }}"
echo "=== 3. Link shared resources ==="
mkdir -p "${RELEASE_DIR}/data"
# .env from shared dir (not in git)
ln -sfn "${DEPLOY_BASE}/shared/.env" "${RELEASE_DIR}/.env"
# Database dir symlink to persist across releases
DB_DIR="$HOME/.workbuddy/data/ziwei-power"
mkdir -p "${DB_DIR}"
ln -sfn "${DB_DIR}" "${RELEASE_DIR}/data"
echo "=== 4. Setup Python venv ==="
cd "${RELEASE_DIR}"
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir -r requirements.txt
echo "=== 5. Setup systemd service ==="
if ! systemctl is-enabled "${SERVICE_NAME}" >/dev/null 2>&1; then
cat > "/etc/systemd/system/${SERVICE_NAME}.service" <<EOF
[Unit]
Description=ziwei-power 日课系统
After=network.target
[Service]
Type=simple
User=$USER
WorkingDirectory=${DEPLOY_BASE}/current
ExecStart=${DEPLOY_BASE}/current/.venv/bin/python ${DEPLOY_BASE}/current/app.py
Restart=always
RestartSec=5
Environment=PORT=5058
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
fi
echo "=== 6. Restart service ==="
ln -sfn "${RELEASE_DIR}" "${DEPLOY_BASE}/current"
systemctl restart "${SERVICE_NAME}"
sleep 3
echo "=== 7. Health check ==="
for i in 1 2 3 4 5; do
if curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
echo "Health check passed"
break
fi
echo "Attempt $i: waiting for service..."
sleep 2
done
# Final verify
if ! curl -fsS http://127.0.0.1:5058/api/health >/dev/null 2>&1; then
echo "ERROR: Health check failed after 5 attempts"
echo "Rolling back to previous release..."
PREV=$(ls -t "${DEPLOY_BASE}/releases" | sed -n '2p')
if [ -n "${PREV}" ]; then
ln -sfn "${DEPLOY_BASE}/releases/${PREV}" "${DEPLOY_BASE}/current"
systemctl restart "${SERVICE_NAME}"
echo "Rolled back to ${PREV}"
fi
exit 1
fi
echo "=== 8. Cleanup old releases ==="
ls -dt "${DEPLOY_BASE}"/releases/*/ | tail -n +6 | xargs -r rm -rf
echo "=== 9. Cleanup temp ==="
rm -rf "${CLONE_DIR}"
echo "=== Deploy complete: ${RELEASE_ID} ==="

221
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,7 +72,7 @@ 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
@@ -136,8 +136,7 @@ def compute_stats():
pillar = classify_auto(name) pillar = classify_auto(name)
all_study_items.append({ all_study_items.append({
'name': name.strip(), 'name': name.strip(),
'done': si.get('done', False) if isinstance(si, dict) else False, 'count': si.get('count', 0) if isinstance(si, dict) else 0,
'note': si.get('note', '') if isinstance(si, dict) else '',
'pillar': pillar 'pillar': pillar
}) })
@@ -149,7 +148,7 @@ def compute_stats():
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
@@ -187,7 +186,7 @@ def compute_stats():
pillar_breakdown[p]['study_items'].append(si) 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, morning_items=all_morning_items,
@@ -202,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 ──────────────────────────────────────────────
@@ -214,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})
@@ -225,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():
@@ -247,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():
@@ -264,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({
@@ -343,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:
@@ -355,61 +330,87 @@ 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})
# ── 健康检查 ──────────────────────────────────────────
@app.route('/api/health')
def api_health():
return jsonify({'ok': True, 'service': 'ziwei-power'})
# ── 启动 ────────────────────────────────────────────── # ── 启动 ──────────────────────────────────────────────

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

View File

@@ -18,7 +18,7 @@
--radius-sm: 8px; --radius-sm: 8px;
--shadow: 0 1px 2px rgba(0,0,0,0.04), 0 2px 8px rgba(74,108,247,0.05); --shadow: 0 1px 2px rgba(0,0,0,0.04), 0 2px 8px rgba(74,108,247,0.05);
--shadow-hover: 0 2px 6px rgba(0,0,0,0.04), 0 6px 18px rgba(74,108,247,0.08); --shadow-hover: 0 2px 6px rgba(0,0,0,0.04), 0 6px 18px rgba(74,108,247,0.08);
--sidebar-w: 280px; --sidebar-w: 100px;
} }
* { margin:0; padding:0; box-sizing:border-box; } * { margin:0; padding:0; box-sizing:border-box; }
@@ -66,14 +66,64 @@ body {
.sidebar-user { .sidebar-user {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 12px; gap: 8px;
padding: 20px 20px 16px; padding: 24px 12px 16px;
position: relative;
cursor: pointer;
}
.sidebar-user .user-avatar {
transition: transform 0.15s;
}
.sidebar-user:hover .user-avatar { transform: scale(1.05); }
.user-dropdown {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-hover);
z-index: 100;
min-width: 120px;
padding: 4px;
}
.user-dropdown-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
font-size: 13px;
color: var(--text-dim);
text-decoration: none;
border-radius: 6px;
transition: all 0.15s;
}
.user-dropdown-item:hover {
background: var(--danger-light);
color: var(--danger);
}
.sidebar-user .user-name {
font-size: 12px;
text-align: center;
}
.sidebar-user .btn-logout-icon {
position: absolute;
top: 8px;
right: 8px;
width: 22px;
height: 22px;
} }
.user-avatar { .user-avatar {
width: 38px; width: 44px;
height: 38px; height: 44px;
border-radius: 50%; border-radius: 50%;
background: var(--primary); background: var(--primary);
color: #FFF; color: #FFF;
@@ -131,6 +181,10 @@ body {
gap: 8px; gap: 8px;
padding: 0 20px 20px; padding: 0 20px 20px;
} }
.daily-sidebar .sidebar-stats {
padding: 0;
margin-bottom: 16px;
}
.stat-item { .stat-item {
background: var(--bg); background: var(--bg);
@@ -155,6 +209,45 @@ body {
font-weight: 400; font-weight: 400;
} }
/* ═══════════════════════════════════════════
Sidebar Dark Theme左侧导航栏专属
═══════════════════════════════════════════ */
.sidebar {
background: linear-gradient(180deg, #1E293B 0%, #0F172A 100%);
border-right: 1px solid #334155;
}
.sidebar .user-name {
color: #94A3B8;
}
.sidebar .user-avatar {
background: #4F46E5;
color: #FFF;
}
.sidebar .btn-logout-icon {
color: #64748B;
}
.sidebar .btn-logout-icon:hover {
background: rgba(248,113,113,0.15);
color: #FCA5A5;
}
.sidebar .nav-item {
color: #94A3B8;
}
.sidebar .nav-item:hover {
background: rgba(99,102,241,0.08);
color: #E2E8F0;
}
.sidebar .nav-item .icon-md {
color: #6366F1;
}
.sidebar .nav-item:hover .icon-md { color: #818CF8; }
.sidebar .nav-item.active {
background: rgba(99,102,241,0.15);
color: #FFF;
border: 1px solid rgba(99,102,241,0.3);
}
.sidebar .nav-item.active .icon-md { color: #818CF8; }
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
Calendar Widget Calendar Widget
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
@@ -162,6 +255,97 @@ body {
.calendar-widget { .calendar-widget {
padding: 0 16px 12px; padding: 0 16px 12px;
} }
.calendar-widget.inline {
margin: 0 0 16px;
padding: 12px 0;
border: 1.5px solid var(--border);
border-radius: var(--radius);
background: var(--card);
}
.calendar-widget.inline .cal-header {
padding: 0 14px 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.cal-nav-group { display: flex; gap: 4px; }
.cal-nav {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--card);
cursor: pointer;
color: var(--text-dim);
}
.cal-nav:hover { background: var(--bg); color: var(--text); }
/* 月份翻页行 */
.cal-month-header {
padding: 4px 14px 8px;
}
.cal-month-header .cal-month-label {
font-size: 14px;
font-weight: 600;
color: var(--text);
}
/* 周列表行 */
.cal-week-rows {
display: flex;
flex-direction: column;
gap: 6px;
padding: 8px 14px 10px;
}
.cal-week-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 0.5px solid var(--border);
border-radius: 8px;
background: var(--card);
cursor: pointer;
transition: all 0.15s;
}
.cal-week-row:hover { border-color: var(--primary); }
.cal-week-row.today { border-color: var(--primary); }
.cal-week-row.selected { background: var(--primary); color: #FFF; border-color: transparent; }
.cal-week-row.selected .cal-wk-label,
.cal-week-row.selected .cal-wk-date,
.cal-week-row.selected .cal-wk-badge { color: #FFF; }
.cal-week-row.selected .cal-wk-status { background: rgba(255,255,255,0.2); color: #FFF; }
.cal-wk-status {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
}
.cal-wk-status.pass { background: var(--success-light); color: var(--success); }
.cal-wk-status.fail { background: var(--danger-light); color: var(--danger); }
.cal-wk-status.empty { background: var(--bg); color: var(--text-muted); }
.cal-wk-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
}
.cal-wk-label { font-size: 12px; font-weight: 600; color: var(--text); }
.cal-wk-date { font-size: 10px; color: var(--text-muted); }
.cal-wk-badge { font-size: 10px; color: var(--text-muted); }
/* 旧样式隐藏 */
.cal-weeks { display: none; }
.cal-week-cell { display: none; }
.cal-header { .cal-header {
display: flex; display: flex;
@@ -207,59 +391,86 @@ body {
padding: 4px 0; padding: 4px 0;
} }
.cal-grid { .cal-week-list {
display: grid; display: grid;
grid-template-columns: repeat(7, 1fr); grid-template-columns: repeat(4, 1fr);
gap: 3px; gap: 6px 4px;
margin-bottom: 8px;
} }
.cal-cell { .cal-week-cell {
aspect-ratio: 1; aspect-ratio: 1.4;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 12px; font-size: 10px;
font-weight: 500; font-weight: 500;
border-radius: 6px; border-radius: 6px;
color: var(--text-dim); color: var(--text-dim);
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
border: 1.5px solid var(--border);
background: var(--card);
padding: 2px 0;
position: relative; position: relative;
border: 1.5px solid transparent;
} }
.cal-cell:hover { .cal-week-cell:hover {
background: var(--primary-light); background: var(--primary-light);
color: var(--primary); color: var(--primary);
border-color: var(--primary); border-color: var(--primary);
} }
.cal-cell.other-month { .cal-week-cell .wk-num { font-weight: 600; line-height: 1; }
color: var(--border); .cal-week-cell .wk-dot {
cursor: default; width: 5px;
} height: 5px;
border-radius: 50%;
.cal-cell.other-month:hover {
background: transparent; background: transparent;
border-color: transparent; margin-top: 3px;
color: var(--border);
} }
.cal-cell.today { .cal-week-cell.pass {
font-weight: 700; background: var(--success-light);
color: var(--primary); border-color: var(--success);
color: var(--success);
}
.cal-week-cell.pass .wk-dot { background: var(--success); }
.cal-week-cell.fail {
background: var(--danger-light);
border-color: var(--danger);
color: var(--danger);
}
.cal-week-cell.fail .wk-dot { background: var(--danger); }
.cal-week-cell.today {
border-color: var(--primary); border-color: var(--primary);
background: var(--primary-light); font-weight: 700;
box-shadow: 0 0 0 2px var(--primary-light);
} }
.cal-cell.selected { .cal-week-cell.selected {
font-weight: 600; background: var(--primary);
color: var(--primary-dark); color: #FFF;
border-color: var(--primary-dark); border-color: var(--primary);
background: #DDE3FD; }
.cal-week-cell.selected .wk-dot { background: #FFF; }
/* 月份标识 */
.cal-week-cell.month-start::before {
content: attr(data-month);
position: absolute;
top: -10px;
left: 0;
font-size: 9px;
color: var(--text-muted);
font-weight: 400;
} }
.cal-cell.selected.today { /* 保留旧 cal-cell 样式(由月历代码生成,但现已不使用) */
.cal-grid { display: none; }
font-weight: 700; font-weight: 700;
color: #FFF; color: #FFF;
border-color: var(--primary-dark); border-color: var(--primary-dark);
@@ -332,34 +543,45 @@ body {
.nav-item { .nav-item {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 4px;
padding: 10px 14px; padding: 10px 4px;
border-radius: var(--radius-sm); border-radius: var(--radius);
font-size: 13px; font-size: 11px;
font-weight: 500; font-weight: 500;
color: var(--text-dim); color: var(--text-dim);
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
transition: all 0.15s; transition: all 0.15s;
margin-bottom: 4px;
text-align: center;
} }
.nav-label { line-height: 1.2; }
.nav-item:hover { .nav-item:hover {
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
} }
.nav-item .icon-md {
flex-shrink: 0;
width: 22px;
height: 22px;
color: var(--text-muted);
transition: color 0.15s;
}
.nav-item:hover .icon-md { color: var(--primary); }
.nav-item.active { .nav-item.active {
background: var(--primary-light); background: var(--primary-light);
color: var(--primary); color: var(--primary);
font-weight: 600; font-weight: 600;
} }
.nav-item.active .icon-md { color: var(--primary); }
.nav-item .icon-sm { .icon-md { width: 20px; height: 20px; flex-shrink: 0; }
flex-shrink: 0;
width: 16px;
height: 16px;
}
/* ── Sidebar Footer ── */ /* ── Sidebar Footer ── */
@@ -452,33 +674,78 @@ body {
} }
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
Daily Grid — 三栏 Daily Panel — 左右两栏布局
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
.daily-grid { .daily-layout {
display: grid; display: flex;
grid-template-columns: 1fr 1fr; gap: 24px;
align-items: stretch;
}
/* 左侧栏 */
.daily-sidebar {
width: 200px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.daily-sidebar .sidebar-stats {
padding: 0;
margin-bottom: 16px;
}
.daily-sidebar .stat-item {
background: var(--card);
border: 1px solid var(--border);
}
.daily-sidebar .calendar-widget.inline {
margin: 0;
padding: 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--card);
}
/* 右侧主区 */
.daily-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px; gap: 16px;
margin-bottom: 20px;
} }
/* 立志 — 左上 */ .daily-tabs {
.card-morning { display: flex;
grid-column: 1; gap: 0;
grid-row: 1; border-bottom: 1.5px solid var(--border);
} }
.daily-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 18px;
border: none;
background: none;
font-size: 13px;
font-weight: 500;
color: var(--text-dim);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1.5px;
transition: all 0.15s;
font-family: inherit;
}
.daily-tab:hover { color: var(--text); }
.daily-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
/* 改过 — 左下 */ .daily-grid { display: block; }
.card-evening { .card.daily-card { display: none; }
grid-column: 1; .card.daily-card.active { display: flex; }
grid-row: 2;
}
/* 勤学 — 右列跨2行 */
.card-study {
grid-column: 2;
grid-row: 1 / 3;
}
/* ── Cards ── */ /* ── Cards ── */
@@ -565,9 +832,7 @@ body {
} }
/* 编辑模式下才显示的元素 */ /* 编辑模式下才显示的元素 */
.edit-only { display: none; } .edit-only { display: flex; }
.card.editing .edit-only { display: flex; }
.card.editing .btn-del { display: inline-flex; }
/* 默认隐藏删除按钮 */ /* 默认隐藏删除按钮 */
.btn-del { display: none; } .btn-del { display: none; }
@@ -763,7 +1028,7 @@ body {
gap: 5px; gap: 5px;
width: 100%; width: 100%;
padding: 9px; padding: 9px;
margin-top: auto; margin-top: 12px;
border: 1.5px dashed var(--primary); border: 1.5px dashed var(--primary);
background: var(--primary-light); background: var(--primary-light);
color: var(--primary); color: var(--primary);
@@ -805,172 +1070,6 @@ body {
.btn-save:active { transform: translateY(0); } .btn-save:active { transform: translateY(0); }
/* ═══════════════════════════════════════════
Weekly Panel
═══════════════════════════════════════════ */
.week-nav {
display: flex;
align-items: center;
justify-content: center;
gap: 20px;
padding: 8px 0 20px;
}
.btn-nav {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border: 1.5px solid var(--border);
background: var(--card);
border-radius: var(--radius-sm);
font-size: 13px;
font-weight: 500;
cursor: pointer;
color: var(--text);
transition: all 0.2s;
font-family: inherit;
}
.btn-nav:hover {
border-color: var(--primary);
color: var(--primary);
background: var(--primary-light);
}
#week-label {
font-size: 14px;
font-weight: 600;
min-width: 200px;
text-align: center;
color: var(--text);
}
.weekly-overview {
display: flex;
align-items: center;
gap: 40px;
padding: 20px 0;
justify-content: center;
}
.score-circle {
position: relative;
width: 140px;
height: 140px;
flex-shrink: 0;
}
.score-svg {
width: 140px;
height: 140px;
}
.score-svg circle {
transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1);
}
.score-inner {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: baseline;
justify-content: center;
}
.score-num {
font-size: 40px;
font-weight: 800;
color: var(--primary);
line-height: 1;
}
.score-unit {
font-size: 15px;
color: var(--text-muted);
margin-left: 2px;
font-weight: 500;
}
.score-meta {
display: flex;
flex-direction: column;
gap: 8px;
}
.score-text {
font-size: 15px;
font-weight: 600;
color: var(--text);
}
.score-stats {
font-size: 13px;
color: var(--text-muted);
line-height: 1.6;
}
.week-days-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 8px;
margin-top: 24px;
}
.day-cell {
background: var(--card);
border-radius: var(--radius-sm);
padding: 12px 8px;
text-align: center;
box-shadow: var(--shadow);
transition: box-shadow 0.2s;
border-top: 3px solid transparent;
}
.day-cell:hover { box-shadow: var(--shadow-hover); }
.day-cell.pass { border-top-color: var(--success); }
.day-cell.fail { border-top-color: var(--warning); }
.day-cell.empty { border-top-color: var(--border); opacity: 0.5; }
.day-cell .day-label {
font-size: 11px;
color: var(--text-muted);
margin-bottom: 4px;
font-weight: 500;
}
.day-cell .day-date {
font-size: 12px;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
}
.day-cell .day-score {
font-size: 18px;
font-weight: 700;
}
.day-cell.pass .day-score { color: var(--success); }
.day-cell.fail .day-score { color: var(--warning); }
.day-cell.empty .day-score { color: var(--text-muted); }
.day-cell .day-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
margin-top: 4px;
}
.day-cell.pass .day-badge { background: var(--success-light); color: var(--success); }
.day-cell.fail .day-badge { background: var(--warning-light); color: #D97706; }
.day-cell.empty .day-badge { background: var(--bg); color: var(--text-muted); }
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
History Panel History Panel
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
@@ -1195,7 +1294,7 @@ body {
.sync-event + .sync-event { border-top: 1px solid var(--bg); } .sync-event + .sync-event { border-top: 1px solid var(--bg); }
.sync-event-icon { flex-shrink: 0; } .sync-event-icon { flex-shrink: 0; }
.sync-event-text { flex: 1; } .sync-event-text { flex: 1; }
.sync-event-time { font-size: 11px; color: var(--text-muted); margin-left: 6px; } .sync-event-meta { font-size: 11px; color: var(--text-muted); }
.sync-modal-footer { .sync-modal-footer {
padding: 12px 20px; padding: 12px 20px;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
@@ -1203,190 +1302,6 @@ body {
justify-content: flex-end; justify-content: flex-end;
flex-shrink: 0; flex-shrink: 0;
} }
.sync-modal-footer .btn-wish-save {
width: auto;
padding: 8px 24px;
}
/* ═══════════════════════════════════════════
Wishes — 四象限
═══════════════════════════════════════════ */
#panel-wishes.editing .edit-only { display: inline-flex; }
#panel-wishes.editing .wish-del { display: inline-flex; }
.wish-form {
background: var(--bg);
border-radius: var(--radius-sm);
padding: 10px 12px;
margin-bottom: 12px;
max-width: 480px;
}
.wish-form input[type="text"],
.wish-form input[type="date"],
.wish-form select {
width: 100%;
padding: 8px 10px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
font-size: 13px;
font-family: inherit;
color: var(--text);
margin-bottom: 8px;
box-sizing: border-box;
background: var(--card);
}
.wish-form input:focus, .wish-form select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(74,108,247,0.08);
}
.wish-form-row { display: flex; gap: 8px; }
.wish-form-row select, .wish-form-row input { flex: 1; }
.wish-form-actions { display: flex; gap: 8px; }
.btn-wish-save {
flex: 1; padding: 8px; border: none; background: var(--primary); color: #FFF;
border-radius: var(--radius-sm); font-size: 13px; font-weight: 500;
cursor: pointer; font-family: inherit;
}
.btn-wish-save:hover { background: var(--primary-dark); }
.btn-wish-cancel {
flex: 1; padding: 8px; border: 1.5px solid var(--border); background: var(--card);
color: var(--text-dim); border-radius: var(--radius-sm); font-size: 13px;
cursor: pointer; font-family: inherit;
}
/* 四象限网格 */
.quad-grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 12px;
min-height: 400px;
}
.quad-cell {
background: var(--card);
border: 1.5px solid var(--border);
border-radius: var(--radius);
padding: 12px 14px;
display: flex;
flex-direction: column;
min-height: 180px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.quad-cell.drag-over {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(74,108,247,0.10);
background: var(--primary-light);
}
.quad-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
flex-shrink: 0;
}
.quad-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
min-height: 40px;
}
.wish-item {
display: flex;
align-items: center;
gap: 8px;
padding: 7px 10px;
border-radius: var(--radius-sm);
background: var(--bg);
transition: opacity 0.15s, box-shadow 0.15s;
}
.wish-item:hover { box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
.wish-item.dragging { opacity: 0.3; }
.wish-item.drag-over {
box-shadow: inset 0 0 0 2px var(--primary);
background: var(--card);
}
.wish-drag-handle {
color: var(--text-muted); cursor: grab; flex-shrink: 0;
display: flex; align-items: center;
}
.wish-drag-handle:active { cursor: grabbing; }
.wish-check {
width: 15px; height: 15px; accent-color: var(--success);
flex-shrink: 0; cursor: pointer;
}
.wish-name {
flex: 1; font-size: 13px; color: var(--text); min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.wish-item.done .wish-name { text-decoration: line-through; color: var(--text-muted); }
.wish-deadline {
font-size: 11px; color: var(--text-dim); white-space: nowrap; flex-shrink: 0;
}
.wish-del { display: none; }
.wish-del .icon-xs { width: 13px; height: 13px; }
/* 内联编辑 */
.wish-item.editing {
background: var(--card);
padding: 8px 10px;
flex-wrap: wrap;
}
.wish-edit-form {
width: 100%;
display: flex;
flex-direction: column;
gap: 6px;
}
.wish-edit-form input[type="text"],
.wish-edit-form input[type="date"],
.wish-edit-form select {
width: 100%;
padding: 6px 8px;
border: 1.5px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12px;
font-family: inherit;
color: var(--text);
background: var(--card);
box-sizing: border-box;
}
.wish-edit-form input:focus,
.wish-edit-form select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(74,108,247,0.08);
}
.wish-edit-name { font-weight: 500; }
.wish-edit-row { display: flex; gap: 6px; }
.wish-edit-row select,
.wish-edit-row input { flex: 1; }
.wish-edit-actions { display: flex; gap: 6px; }
.wish-edit-actions .btn-wish-save,
.wish-edit-actions .btn-wish-cancel {
flex: 1; padding: 6px; font-size: 12px;
}
.wishes-empty {
font-size: 13px; color: var(--text-muted); text-align: center; padding: 32px 0;
}
@media (max-width: 900px) {
.quad-grid { grid-template-columns: 1fr; grid-template-rows: auto; }
}
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
SVG icons helpers SVG icons helpers
@@ -1401,13 +1316,9 @@ body {
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
@media (max-width: 900px) { @media (max-width: 900px) {
.sidebar { width: 240px; min-width: 240px; }
:root { --sidebar-w: 240px; } :root { --sidebar-w: 240px; }
.main-content { padding: 20px 24px; } .main-content { padding: 20px 24px; }
.daily-grid { grid-template-columns: 1fr; } .daily-grid { grid-template-columns: 1fr; }
.card-morning, .card-evening, .card-study { grid-column: 1; grid-row: auto; }
.weekly-overview { flex-direction: column; gap: 16px; text-align: center; }
.week-days-grid { grid-template-columns: repeat(4, 1fr); }
.history-grid { grid-template-columns: 1fr; } .history-grid { grid-template-columns: 1fr; }
} }
@@ -1507,6 +1418,7 @@ body {
margin-left: 4px; margin-left: 4px;
white-space: nowrap; white-space: nowrap;
} }
.bp-item-note.evening { color: var(--danger); }
.bp-list-empty { .bp-list-empty {
font-size: 13px; font-size: 13px;
color: var(--text-muted); color: var(--text-muted);
@@ -1514,6 +1426,46 @@ body {
padding: 32px 0; padding: 32px 0;
} }
@media (max-width: 900px) { /* 勤学打卡卡片 - 单行样式 */
.quad-grid { grid-template-columns: 1fr; grid-template-rows: auto; } #study-cards { display: flex; flex-direction: column; gap: 8px; }
} .study-card { position:relative; display:flex; align-items:center; gap:12px; padding:10px 14px; border:1px solid var(--border); border-radius:var(--radius-sm); background:var(--card); transition:all 0.15s; }
.study-card:hover { border-color:var(--primary); box-shadow:var(--shadow); }
.study-card.checked { border-color:var(--success); background:var(--success-light); }
.study-card-info { display:flex; align-items:center; gap:8px; flex:1; }
.study-card-name { font-size:13px; color:var(--text); font-weight:500; }
.study-card-count { font-size:12px; color:var(--text-muted); font-weight:600; background:var(--bg); border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.study-card.checked .study-card-count { background:var(--success); color:#FFF; }
.study-card-btn { padding:5px 12px; border:1px solid var(--primary); border-radius:var(--radius-sm); background:transparent; color:var(--primary); font-size:12px; font-weight:500; cursor:pointer; transition:all 0.15s; font-family:inherit; white-space:nowrap; }
.study-card-btn:hover { background:var(--primary); color:#FFF; }
.study-card-btn.done { background:var(--success-light); color:var(--success); border-color:var(--success); cursor:default; }
.study-card-del { display:none; }
#panel-daily.editing-study .study-card-del { display:flex; align-items:center; justify-content:center; position:absolute; top:50%; transform:translateY(-50%); right:54px; border:none; background:var(--danger-light); color:var(--danger); cursor:pointer; font-size:14px; width:22px; height:22px; border-radius:50%; line-height:1; }
#panel-daily.editing-study .study-card-del:hover { background:var(--danger); color:#FFF; }
.edit-only-study { display: none; }
#panel-daily.editing-study .edit-only-study { display: flex; }
/* 编辑模式下显示删除按钮 */
.btn-del { display: none; }
.card.editing .btn-del { display: inline-flex; }
/* 本周日程 */
.morning-calendar { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
.morning-calendar-head { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--text-muted); margin-bottom: 8px; }
.cal-task-item { display: flex; align-items: flex-start; gap: 8px; padding: 6px 0; font-size: 13px; color: var(--text-dim); line-height: 1.5; border-bottom: 1px solid var(--bg); }
.cal-task-item:last-child { border-bottom: none; }
.cal-task-dot { flex-shrink: 0; font-size: 14px; margin-top: 1px; }
/* 拖拽排序 */
.drag-handle { display: none; cursor: grab; color: var(--text-muted); font-size: 14px; user-select: none; padding: 0 4px; line-height: 1; letter-spacing: -2px; }
.card.editing .drag-handle { display: block; }
.item-row.dragging, .evening-row.dragging { opacity: 0.5; }
.item-row.drag-over, .evening-row.drag-over { border-color: var(--primary) !important; background: var(--primary-light); }
.evening-drag { position: absolute; left: 6px; top: 8px; }
/* 任务备注 */
.btn-note-toggle { display: inline-flex; background: none; border: none; cursor: pointer; font-size: 14px; padding: 2px 4px; border-radius: 4px; transition: all 0.15s; flex-shrink: 0; }
.btn-note-toggle:hover { background: var(--bg); }
.btn-note-toggle.has-note { color: var(--primary); }
.item-note { display: none; width: 100%; padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px; font-size: 12px; font-family: inherit; color: var(--text); background: var(--bg); resize: vertical; min-height: 28px; box-sizing: border-box; margin-top: 6px; }
.item-note:focus { outline: none; border-color: var(--primary); }
.item-row { flex-wrap: wrap; }

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,94 +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>
<a class="nav-item" data-panel="blueprint" onclick="switchPanel('blueprint')">
<svg class="icon-sm"><use href="#icon-star"/></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>
<div class="panel-header-right">
<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>
<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>
@@ -163,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>
<!-- ── 历史记录面板 ── --> <!-- ── 历史记录面板 ── -->
@@ -223,60 +218,10 @@
<div id="history-grid" class="history-grid"></div> <div id="history-grid" class="history-grid"></div>
</section> </section>
<!-- ── 心愿清单面板 ── -->
<section class="panel" id="panel-wishes">
<div class="panel-header">
<h2>心愿清单</h2>
<button class="btn-edit-toggle" id="wishes-edit-toggle" onclick="toggleWishesEdit(this)" title="编辑">
<svg class="icon-sm"><use href="#icon-pencil"/></svg>
</button>
</div>
<!-- 新增表单 -->
<div class="wish-form" id="wish-form" style="display:none">
<input type="text" id="wish-name" placeholder="心愿名称…" maxlength="50">
<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 class="panel" id="panel-blueprint"> <section class="panel" id="panel-blueprint">
<div class="panel-header"> <div class="panel-header">
<h2>蓝图</h2> <h2>人生蓝图</h2>
</div> </div>
<!-- Tab 筛选 --> <!-- Tab 筛选 -->
<div class="bp-tabs"> <div class="bp-tabs">