Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d3192dfaf | ||
|
|
9c9511e817 | ||
|
|
b5dbb13956 | ||
|
|
5ef340cb08 | ||
|
|
073b570681 | ||
|
|
ce0e374e86 | ||
|
|
f7bbc505b9 | ||
|
|
351e1f18ae | ||
|
|
0e75326b5f | ||
|
|
cd90c2d799 | ||
|
|
c2cbbdb12a | ||
|
|
8223f72544 | ||
|
|
36b58a8b73 | ||
|
|
5b73fc0eae | ||
|
|
5b637e7fed | ||
|
|
350c9fb877 | ||
|
|
19ae81ae6e | ||
|
|
535af35911 | ||
|
|
fc338f426a | ||
|
|
5300ee426d | ||
|
|
8ac6f1af6b | ||
|
|
8c81fae334 | ||
|
|
67505414d9 | ||
|
|
03745e19c3 | ||
|
|
be9f10e968 | ||
|
|
6e1b793b40 | ||
|
|
e90e8c30ff | ||
|
|
046f144896 |
285
app.py
285
app.py
@@ -6,7 +6,7 @@ from datetime import timedelta
|
||||
from functools import wraps
|
||||
from flask import Flask, render_template, request, jsonify, session, redirect, url_for
|
||||
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, get_wishes, save_wish, update_wish, delete_wish, reorder_wishes
|
||||
|
||||
app = Flask(__name__)
|
||||
# 固定密钥确保 gunicorn 多 worker 下 session 可互通
|
||||
@@ -72,11 +72,35 @@ def logout():
|
||||
def compute_stats():
|
||||
"""计算统计数据,供 API 和模板共用"""
|
||||
rows = get_all_checkins()
|
||||
total_days = len(rows)
|
||||
total_weeks = len(rows)
|
||||
total_morning = 0
|
||||
total_evening = 0
|
||||
total_study = 0
|
||||
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:
|
||||
d = row['date']
|
||||
@@ -85,7 +109,42 @@ def compute_stats():
|
||||
evening = data.get('evening', [])
|
||||
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(),
|
||||
'done': si.get('done', False) if isinstance(si, dict) else False,
|
||||
'note': si.get('note', '') if isinstance(si, dict) else '',
|
||||
'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 (
|
||||
isinstance(x, str) and x.strip() or
|
||||
isinstance(x, dict) and (x.get('mistake', '') or '').strip()
|
||||
@@ -103,10 +162,38 @@ def compute_stats():
|
||||
# day_score = 60 + extra * 5 (不在此处使用,仅用于日历状态)
|
||||
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(
|
||||
total_days=total_days, total_morning=total_morning,
|
||||
total_weeks=total_weeks, total_morning=total_morning,
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@@ -127,10 +214,10 @@ def index():
|
||||
@app.route('/api/checkin', methods=['GET'])
|
||||
@login_required
|
||||
def api_get_checkin():
|
||||
date = request.args.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400
|
||||
row = get_checkin(date)
|
||||
week = request.args.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
|
||||
row = get_checkin(week)
|
||||
return jsonify({'ok': True, 'data': row})
|
||||
|
||||
|
||||
@@ -138,21 +225,29 @@ def api_get_checkin():
|
||||
@login_required
|
||||
def api_save_checkin():
|
||||
body = request.get_json(force=True)
|
||||
date = body.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 字段'}), 400
|
||||
week = body.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 字段'}), 400
|
||||
data = body.get('data', {})
|
||||
save_checkin(date, data)
|
||||
save_checkin(week, data)
|
||||
return jsonify({'ok': True})
|
||||
|
||||
|
||||
@app.route('/api/checkin/<date>', methods=['DELETE'])
|
||||
@app.route('/api/checkin/<week>', methods=['DELETE'])
|
||||
@login_required
|
||||
def api_delete_checkin(date):
|
||||
delete_checkin(date)
|
||||
def api_delete_checkin(week):
|
||||
delete_checkin(week)
|
||||
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'])
|
||||
@login_required
|
||||
def api_history():
|
||||
@@ -160,6 +255,17 @@ def api_history():
|
||||
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'])
|
||||
@login_required
|
||||
def api_stats():
|
||||
@@ -231,98 +337,111 @@ CALENDAR_CACHE = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'zi
|
||||
DING_MCP_URL = 'https://mcp-gw.dingtalk.com/server/95959163f85d8b58a167f65cd8bd3d22690b16c75ebacd0fa095016396a10e0d?key=0993e4d4e44c50ea25a0db840cc5815e'
|
||||
|
||||
|
||||
def _fetch_dingtalk_events(date_str):
|
||||
"""调用钉钉 MCP 实时查询指定日期的日程"""
|
||||
def _fetch_dingtalk_events_week(week_str):
|
||||
"""调用钉钉 MCP 查询指定周(周一~周日)的全部日程"""
|
||||
import json as _json, urllib.request as _ur
|
||||
from datetime import datetime, timezone, timedelta
|
||||
tz = timezone(timedelta(hours=8))
|
||||
try:
|
||||
d = datetime.strptime(date_str, '%Y-%m-%d').replace(tzinfo=tz)
|
||||
except ValueError:
|
||||
year_part, week_part = week_str.split('-W')
|
||||
year = int(year_part)
|
||||
week = int(week_part)
|
||||
except (ValueError, IndexError):
|
||||
return []
|
||||
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)
|
||||
body = _json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'tools/call', 'id': 1,
|
||||
'params': {'name': 'list_calendar_events', 'arguments': {
|
||||
'calendarId': 'primary', 'startTime': start_ts, 'endTime': end_ts, 'limit': 20
|
||||
}}
|
||||
}).encode('utf-8')
|
||||
req = _ur.Request(DING_MCP_URL, data=body, headers={
|
||||
'Content-Type': 'application/json', 'Accept': 'application/json'
|
||||
})
|
||||
try:
|
||||
with _ur.urlopen(req, timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', [])
|
||||
except Exception:
|
||||
return None # 网络错误返回 None,调用方回退缓存
|
||||
jan1 = datetime(year, 1, 1, tzinfo=tz)
|
||||
monday = jan1 + timedelta(days=(week - 1) * 7 - jan1.weekday())
|
||||
results = []
|
||||
for e in events:
|
||||
summary = (e.get('summary') or '').strip()
|
||||
if not summary:
|
||||
continue
|
||||
start = e.get('start', {}).get('dateTime', '')
|
||||
end = e.get('end', {}).get('dateTime', '')
|
||||
time_str = ''
|
||||
if start and end:
|
||||
time_str = start[11:16] + '-' + end[11:16]
|
||||
results.append({
|
||||
'date': date_str, 'summary': summary,
|
||||
'time': time_str, 'location': e.get('location') or ''
|
||||
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)
|
||||
end_ts = int(d.replace(hour=23, minute=59, second=59).timestamp() * 1000)
|
||||
body = _json.dumps({
|
||||
'jsonrpc': '2.0', 'method': 'tools/call', 'id': 1,
|
||||
'params': {'name': 'list_calendar_events', 'arguments': {
|
||||
'calendarId': 'primary', 'startTime': start_ts, 'endTime': end_ts, 'limit': 20
|
||||
}}
|
||||
}).encode('utf-8')
|
||||
req = _ur.Request(DING_MCP_URL, data=body, headers={
|
||||
'Content-Type': 'application/json', 'Accept': 'application/json'
|
||||
})
|
||||
# 写入缓存
|
||||
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
|
||||
with _ur.urlopen(req, timeout=10) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
events = data.get('result', {}).get('structuredContent', {}).get('result', {}).get('events', [])
|
||||
except Exception:
|
||||
continue
|
||||
for e in events:
|
||||
summary = (e.get('summary') or '').strip()
|
||||
if not summary:
|
||||
continue
|
||||
start = e.get('start', {}).get('dateTime', '')
|
||||
end = e.get('end', {}).get('dateTime', '')
|
||||
time_str = ''
|
||||
if start and end:
|
||||
time_str = start[11:16] + '-' + end[11:16]
|
||||
results.append({
|
||||
'date': ds, 'summary': summary,
|
||||
'time': time_str, 'location': e.get('location') or ''
|
||||
})
|
||||
return results
|
||||
|
||||
|
||||
@app.route('/api/calendar-sync', methods=['GET'])
|
||||
@login_required
|
||||
def api_calendar_sync():
|
||||
import json as _json
|
||||
date = request.args.get('date', '')
|
||||
if not date:
|
||||
return jsonify({'ok': False, 'error': '缺少 date 参数'}), 400
|
||||
# 优先实时查询钉钉 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 = []
|
||||
week = request.args.get('week', '')
|
||||
if not week:
|
||||
return jsonify({'ok': False, 'error': '缺少 week 参数'}), 400
|
||||
events = _fetch_dingtalk_events_week(week)
|
||||
return jsonify({'ok': True, 'data': events})
|
||||
|
||||
|
||||
@app.route('/api/calendar-sync-all', methods=['GET'])
|
||||
@login_required
|
||||
def api_calendar_sync_all():
|
||||
"""批量查询过去15天~未来15天的钉钉日程,按日期分组返回"""
|
||||
"""批量查询过去4周~未来4周的钉钉日程,并自动填充到对应周的 checkin"""
|
||||
import json as _json
|
||||
from datetime import datetime, timedelta
|
||||
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
results = []
|
||||
for delta in range(-15, 16):
|
||||
d = today + timedelta(days=delta)
|
||||
ds = d.strftime('%Y-%m-%d')
|
||||
events = _fetch_dingtalk_events(ds)
|
||||
saved_weeks = 0
|
||||
for delta in range(-4, 5):
|
||||
# 计算目标周
|
||||
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:
|
||||
results.append({'date': ds, 'events': events})
|
||||
return jsonify({'ok': True, 'data': results})
|
||||
results.append({'date': week_key, 'events': events})
|
||||
# 自动去重填充
|
||||
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
|
||||
text = f"【{evt['time']}】{summary}" if evt.get('time') 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})
|
||||
|
||||
|
||||
# ── 启动 ──────────────────────────────────────────────
|
||||
|
||||
112
database.py
112
database.py
@@ -11,7 +11,7 @@ os.makedirs(DB_DIR, exist_ok=True)
|
||||
DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db')
|
||||
|
||||
# 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑
|
||||
CURRENT_SCHEMA_VERSION = 3
|
||||
CURRENT_SCHEMA_VERSION = 4
|
||||
|
||||
|
||||
def get_conn():
|
||||
@@ -74,6 +74,29 @@ def init_db():
|
||||
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 < 4:
|
||||
# v4: 日打卡 → 周打卡
|
||||
# 备份旧表 → 聚合 → 新建周表
|
||||
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('''
|
||||
CREATE TABLE IF NOT EXISTS checkins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT UNIQUE NOT NULL,
|
||||
data TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''')
|
||||
# 聚合日数据到周
|
||||
if not backup_exists:
|
||||
_aggregate_daily_to_weekly(conn)
|
||||
|
||||
# ── 将来加字段/改表在此扩展 ──
|
||||
# if current < 2:
|
||||
# conn.execute('ALTER TABLE checkins ADD COLUMN tags TEXT DEFAULT ""')
|
||||
@@ -201,3 +224,90 @@ def reorder_wishes(order_list):
|
||||
conn.execute('UPDATE wishes SET sort_order = ? WHERE id = ?', (idx, wid))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_weeks_list():
|
||||
"""返回所有已有打卡记录的周号列表"""
|
||||
conn = get_conn()
|
||||
rows = conn.execute('SELECT DISTINCT date FROM checkins ORDER BY date').fetchall()
|
||||
conn.close()
|
||||
return [r['date'] for r in rows]
|
||||
|
||||
|
||||
def get_daily_backup(week_str):
|
||||
"""获取某周的原始日记录(从备份表)"""
|
||||
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()
|
||||
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(
|
||||
'INSERT INTO checkins (date, data, created_at, updated_at) VALUES (?, ?, ?, ?)',
|
||||
(week_key, json.dumps(weekly[week_key], ensure_ascii=False), now, now)
|
||||
)
|
||||
|
||||
484
static/app.js
484
static/app.js
@@ -5,13 +5,14 @@
|
||||
/* ================================================================
|
||||
State
|
||||
================================================================ */
|
||||
var calendarStatus = {}; // { "2026-06-01": "pass", ... }
|
||||
var calYear, calMonth; // 日历显示的月份
|
||||
var activePanel = 'daily'; // 当前面板
|
||||
var weekOffset = 0; // 每周评分偏移
|
||||
var lastSavedData = null; // 上次保存快照
|
||||
var lastSavedDate = null; // 上次保存日期
|
||||
var selectedDate = null; // 日历中选中日期
|
||||
var calendarStatus = {}; // { "2026-W30": "pass", ... }
|
||||
var calYear;
|
||||
var calMonth = new Date().getMonth(); // 月份索引 0-11
|
||||
var activePanel = 'daily';
|
||||
var weekOffset = 0;
|
||||
var lastSavedData = null;
|
||||
var lastSavedWeek = null;
|
||||
var selectedWeek = null;
|
||||
|
||||
/* 勤学预设项目 */
|
||||
var PRESET_STUDY_ITEMS = [
|
||||
@@ -25,6 +26,16 @@
|
||||
'知识库能力提升'
|
||||
];
|
||||
|
||||
/* 蓝图六板块 */
|
||||
var PILLARS = [
|
||||
{key:'medical_service', name:'医疗服务', emoji:'🥼'},
|
||||
{key:'pharma_mkt', name:'医药营销', emoji:'💊'},
|
||||
{key:'payment', name:'医疗支付', emoji:'🛡️'},
|
||||
{key:'ai', name:'AI 智能', emoji:'🤖'},
|
||||
{key:'governance', name:'公司治理', emoji:'🏛️'},
|
||||
{key:'self_cultivation', name:'个人修养', emoji:'🎯'}
|
||||
];
|
||||
|
||||
/* ================================================================
|
||||
Toast
|
||||
================================================================ */
|
||||
@@ -41,18 +52,18 @@
|
||||
/* ================================================================
|
||||
API
|
||||
================================================================ */
|
||||
function apiGetCheckin(date, cb) {
|
||||
fetch('/api/checkin?date=' + encodeURIComponent(date))
|
||||
function apiGetCheckin(week, cb) {
|
||||
fetch('/api/checkin?week=' + encodeURIComponent(week))
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||||
.catch(function(e){ cb(e.message); });
|
||||
}
|
||||
|
||||
function apiSaveCheckin(date, data, cb) {
|
||||
function apiSaveCheckin(week, data, cb) {
|
||||
fetch('/api/checkin', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({date: date, data: data})
|
||||
body: JSON.stringify({week: week, data: data})
|
||||
})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||||
@@ -76,7 +87,7 @@
|
||||
function apiGetStats(cb) {
|
||||
fetch('/api/stats')
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(res){ if (res.ok) cb(null, res.data); else cb(res.error); })
|
||||
.then(function(res){ if (res.ok) cb(null, res); else cb(res.error); })
|
||||
.catch(function(e){ cb(e.message); });
|
||||
}
|
||||
|
||||
@@ -106,6 +117,7 @@
|
||||
if (name === 'weekly') loadWeekly();
|
||||
if (name === 'history') loadHistory();
|
||||
if (name === 'wishes') loadWishes();
|
||||
if (name === 'blueprint') loadBlueprint();
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
@@ -120,91 +132,86 @@
|
||||
return y + '-' + m + '-' + dd;
|
||||
}
|
||||
|
||||
/** 判断两个日期是否同一天 */
|
||||
function isSameDay(a, b) {
|
||||
return a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate();
|
||||
/** 获取当前周的 YYYY-WW 格式 */
|
||||
function fmtWeek(d) {
|
||||
var jan1 = new Date(d.getFullYear(), 0, 1);
|
||||
var dayOfYear = Math.floor((d - jan1) / 86400000);
|
||||
var week = Math.ceil((dayOfYear + jan1.getDay() + 1) / 7);
|
||||
return d.getFullYear() + '-W' + ('0' + week).slice(-2);
|
||||
}
|
||||
|
||||
/** 切换日历月份 */
|
||||
window.changeCalMonth = function(dir) {
|
||||
calMonth += dir;
|
||||
if (calMonth > 12) { calMonth = 1; calYear++; }
|
||||
if (calMonth < 1) { calMonth = 12; calYear--; }
|
||||
/** 切换日历年 */
|
||||
window.changeCalYear = function(dir) {
|
||||
calYear += dir;
|
||||
renderCalendar();
|
||||
};
|
||||
|
||||
/** 渲染日历 */
|
||||
/** 切换月份 */
|
||||
window.changeCalMonth = function(dir) {
|
||||
calMonth += dir;
|
||||
if (calMonth > 11) { calMonth = 0; calYear++; }
|
||||
if (calMonth < 0) { calMonth = 11; calYear--; }
|
||||
renderCalendar();
|
||||
};
|
||||
|
||||
/** 渲染年-月-周选择器 */
|
||||
function renderCalendar() {
|
||||
var today = new Date();
|
||||
var firstDay = new Date(calYear, calMonth - 1, 1);
|
||||
var lastDay = new Date(calYear, calMonth, 0);
|
||||
var startDayOfWeek = firstDay.getDay(); // 0=Sun
|
||||
var totalDays = lastDay.getDate();
|
||||
var prevMonthLastDay = new Date(calYear, calMonth - 1, 0).getDate();
|
||||
document.getElementById('cal-year-label').textContent = calYear + '年';
|
||||
document.getElementById('cal-month-label').textContent = (calMonth + 1) + '月';
|
||||
|
||||
// 月份标签
|
||||
document.getElementById('cal-month-label').textContent =
|
||||
calYear + '年 ' + calMonth + '月';
|
||||
|
||||
var grid = document.getElementById('cal-grid');
|
||||
var html = '';
|
||||
|
||||
// 前月填充
|
||||
for (var p = startDayOfWeek - 1; p >= 0; p--) {
|
||||
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</div>';
|
||||
// 计算该月包含的周
|
||||
var firstDay = new Date(calYear, calMonth, 1);
|
||||
var lastDay = new Date(calYear, calMonth + 1, 0);
|
||||
var monday = new Date(firstDay);
|
||||
monday.setDate(monday.getDate() - monday.getDay() + (monday.getDay() === 0 ? -6 : 1));
|
||||
var weeks = [];
|
||||
while (monday <= lastDay) {
|
||||
weeks.push(fmtWeek(monday));
|
||||
monday.setDate(monday.getDate() + 7);
|
||||
}
|
||||
// 确保至少有4周显示
|
||||
while (weeks.length < 4) {
|
||||
var w = fmtWeek(monday);
|
||||
if (weeks.indexOf(w) < 0) weeks.push(w);
|
||||
monday.setDate(monday.getDate() + 7);
|
||||
}
|
||||
|
||||
// 当月
|
||||
for (var d = 1; d <= totalDays; d++) {
|
||||
var cur = new Date(calYear, calMonth - 1, d);
|
||||
var ds = fmtDate(cur);
|
||||
var classes = ['cal-cell'];
|
||||
var isOtherMonth = false;
|
||||
|
||||
if (isSameDay(cur, today)) classes.push('today');
|
||||
if (ds === selectedDate) classes.push('selected');
|
||||
var status = calendarStatus[ds];
|
||||
if (status) classes.push(status);
|
||||
|
||||
html += '<div class="' + classes.join(' ') + '" data-date="' + ds + '">' + d + '</div>';
|
||||
var todayWk = fmtWeek(new Date());
|
||||
var wHtml = '';
|
||||
for (var i2 = 0; i2 < weeks.length; i2++) {
|
||||
var w = weeks[i2];
|
||||
var cls = 'cal-week-cell';
|
||||
var status = calendarStatus[w];
|
||||
if (status) cls += ' ' + status;
|
||||
if (w === todayWk) cls += ' today';
|
||||
if (w === selectedWeek) cls += ' selected';
|
||||
wHtml += '<div class="' + cls + '" data-week="' + w + '" title="' + w + '">' +
|
||||
'第' + w.split('-W')[1].replace(/^0/, '') + '周</div>';
|
||||
}
|
||||
|
||||
// 后月填充 — 始终 42 格(6行 × 7列),保持高度一致
|
||||
var totalCells = startDayOfWeek + totalDays;
|
||||
var remaining = 42 - totalCells;
|
||||
for (var n = 1; n <= remaining; n++) {
|
||||
html += '<div class="cal-cell other-month">' + n + '</div>';
|
||||
}
|
||||
|
||||
grid.innerHTML = html;
|
||||
|
||||
// 点击事件
|
||||
grid.querySelectorAll('.cal-cell:not(.other-month)').forEach(function(cell) {
|
||||
var we = document.getElementById('cal-weeks');
|
||||
we.innerHTML = wHtml;
|
||||
we.querySelectorAll('.cal-week-cell').forEach(function(cell) {
|
||||
cell.addEventListener('click', function() {
|
||||
var date = this.dataset.date;
|
||||
if (date) navigateToDate(date);
|
||||
var wk = this.dataset.week;
|
||||
if (wk) navigateToWeek(wk);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 导航到指定日期 */
|
||||
function navigateToDate(date) {
|
||||
selectedDate = date;
|
||||
function navigateToWeek(week) {
|
||||
selectedWeek = week;
|
||||
switchPanel('daily');
|
||||
document.getElementById('check-date').value = date;
|
||||
document.getElementById('check-date').value = week;
|
||||
loadCheckin();
|
||||
renderCalendar(); // 刷新日历高亮
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
/** 回到今天 */
|
||||
window.goToday = function() {
|
||||
var today = fmtDate(new Date());
|
||||
calYear = new Date().getFullYear();
|
||||
calMonth = new Date().getMonth() + 1;
|
||||
window.goToCurrentWeek = function() {
|
||||
var now = new Date();
|
||||
calYear = now.getFullYear();
|
||||
calMonth = now.getMonth();
|
||||
renderCalendar();
|
||||
navigateToDate(today);
|
||||
navigateToWeek(fmtWeek(now));
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
@@ -213,7 +220,7 @@
|
||||
function loadStats() {
|
||||
apiGetStats(function(err, data) {
|
||||
if (!err && data) {
|
||||
document.getElementById('stat-days').textContent = data.total_days;
|
||||
document.getElementById('stat-days').textContent = data.total_weeks;
|
||||
document.getElementById('stat-morning').textContent = data.total_morning;
|
||||
document.getElementById('stat-study').textContent = data.total_study;
|
||||
calendarStatus = data.calendar || {};
|
||||
@@ -269,7 +276,10 @@
|
||||
// 早间立志
|
||||
document.getElementById('morning-list').innerHTML = '';
|
||||
if (morning.length === 0) morning = [''];
|
||||
for (var m=0; m<morning.length; m++) addMorningRow(morning[m]);
|
||||
for (var m=0; m<morning.length; m++) {
|
||||
var txt = typeof morning[m] === 'string' ? morning[m] : (morning[m].text || '');
|
||||
addMorningRow(txt);
|
||||
}
|
||||
|
||||
// 责善改过
|
||||
document.getElementById('evening-list').innerHTML = '';
|
||||
@@ -299,11 +309,12 @@
|
||||
function addMorningRow(val) {
|
||||
var container = document.getElementById('morning-list');
|
||||
var idx = container.children.length;
|
||||
val = val || '';
|
||||
var text = typeof val === 'string' ? val : (val && val.text ? val.text : '');
|
||||
text = text || '';
|
||||
var div = document.createElement('div');
|
||||
div.className = 'item-row';
|
||||
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
||||
'<input type="text" value="' + esc(val) + '" placeholder="今天最重要的一件事…">' +
|
||||
'<input type="text" value="' + esc(text) + '" placeholder="本周最重要的一件事…">' +
|
||||
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||||
container.appendChild(div);
|
||||
renumberMorning();
|
||||
@@ -395,7 +406,7 @@
|
||||
var totalDays = data.length;
|
||||
var totalEvents = 0;
|
||||
data.forEach(function(d){ totalEvents += d.events.length; });
|
||||
summary.innerHTML = '同步完成!共 <b>' + totalDays + '</b> 天有日程,合计 <b>' + totalEvents + '</b> 个会议';
|
||||
summary.innerHTML = '同步完成!共 <b>' + totalDays + '</b> 周有日程,合计 <b>' + totalEvents + '</b> 个会议' + (res.saved_days ? ',已自动填充 <b>' + res.saved_days + '</b> 周' : '');
|
||||
|
||||
// 渲染结果列表
|
||||
var html = '';
|
||||
@@ -413,8 +424,8 @@
|
||||
});
|
||||
list.innerHTML = html;
|
||||
|
||||
// 自动填充今天到选中日期的日程
|
||||
autoFillTodayEvents(data);
|
||||
// 刷新当前页
|
||||
loadCheckin();
|
||||
})
|
||||
.catch(function(){
|
||||
loading.style.display = 'none';
|
||||
@@ -424,30 +435,6 @@
|
||||
});
|
||||
};
|
||||
|
||||
function autoFillTodayEvents(data) {
|
||||
var todayDate = document.getElementById('check-date').value;
|
||||
var added = 0;
|
||||
data.forEach(function(day) {
|
||||
if (day.date !== todayDate) return;
|
||||
day.events.forEach(function(e) {
|
||||
var text = e.time ? '【' + e.time + '】' + e.summary : e.summary;
|
||||
if (e.location) text += ' @' + e.location;
|
||||
var existing = document.querySelectorAll('#morning-list input[type="text"]');
|
||||
var already = false;
|
||||
existing.forEach(function(inp){ if (inp.value.indexOf(e.summary) >= 0) already = true; });
|
||||
if (already) return;
|
||||
var count = document.querySelectorAll('#morning-list .item-row').length;
|
||||
if (count >= 3) return;
|
||||
addMorningRow(text);
|
||||
added++;
|
||||
});
|
||||
});
|
||||
if (added > 0) {
|
||||
showToast('已自动填充 ' + added + ' 条今日日程', 'info');
|
||||
triggerAutoSave();
|
||||
}
|
||||
}
|
||||
|
||||
window.closeSyncModal = function() {
|
||||
var overlay = document.getElementById('sync-modal-overlay');
|
||||
if (overlay) overlay.style.display = 'none';
|
||||
@@ -472,29 +459,37 @@
|
||||
btn.classList.toggle('active', editing);
|
||||
};
|
||||
|
||||
/* 切换每周打卡 Tab */
|
||||
window.switchDailyTab = function(btn) {
|
||||
var tab = btn.dataset.tab;
|
||||
document.querySelectorAll('.daily-tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
document.querySelectorAll('.daily-card').forEach(function(c){ c.classList.remove('active'); });
|
||||
var card = document.querySelector('.daily-card[data-card="' + tab + '"]');
|
||||
if (card) card.classList.add('active');
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
Load & Auto Save Checkin
|
||||
================================================================ */
|
||||
|
||||
window.loadCheckin = function() {
|
||||
var date = document.getElementById('check-date').value;
|
||||
if (!date) return;
|
||||
var week = document.getElementById('check-date').value;
|
||||
if (!week) return;
|
||||
|
||||
// 切换日期前先保存当前数据
|
||||
var currentData = buildData();
|
||||
var currentStr = JSON.stringify(currentData);
|
||||
if (lastSavedDate && lastSavedData !== null && currentStr !== lastSavedData) {
|
||||
// 有未保存的变更,先静默保存
|
||||
if (lastSavedWeek && lastSavedData !== null && currentStr !== lastSavedData) {
|
||||
var hasContent = currentData.morning.some(function(x){return x && x.trim();}) ||
|
||||
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
|
||||
currentData.study.some(function(x){return x.done;});
|
||||
if (hasContent) {
|
||||
apiSaveCheckin(lastSavedDate, currentData, function(){}); // 静默保存
|
||||
apiSaveCheckin(lastSavedWeek, currentData, function(){});
|
||||
}
|
||||
}
|
||||
|
||||
lastSavedDate = date;
|
||||
apiGetCheckin(date, function(err, row){
|
||||
lastSavedWeek = week;
|
||||
apiGetCheckin(week, function(err, row){
|
||||
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
||||
fillForm(row ? row.data : null);
|
||||
lastSavedData = JSON.stringify(buildData());
|
||||
@@ -503,30 +498,37 @@
|
||||
|
||||
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
||||
function triggerAutoSave() {
|
||||
var date = document.getElementById('check-date').value;
|
||||
if (!date) return;
|
||||
var week = document.getElementById('check-date').value;
|
||||
if (!week) return;
|
||||
var data = buildData();
|
||||
var dataStr = JSON.stringify(data);
|
||||
if (dataStr === lastSavedData) return;
|
||||
apiSaveCheckin(date, data, function(err){
|
||||
apiSaveCheckin(week, data, function(err){
|
||||
if (err) { showToast('保存失败', 'error'); return; }
|
||||
lastSavedData = dataStr;
|
||||
loadStats();
|
||||
});
|
||||
}
|
||||
|
||||
/** 为每日打卡面板绑定自动保存事件 */
|
||||
/** 为每周打卡面板绑定自动保存事件 */
|
||||
function bindAutoSave() {
|
||||
var panel = document.getElementById('panel-daily');
|
||||
if (!panel) return;
|
||||
|
||||
// 输入框 & 文本域:失去焦点时保存
|
||||
// 输入框 & 文本域 & 下拉选择器:失去焦点时保存
|
||||
panel.addEventListener('blur', function(e) {
|
||||
if (e.target.matches('input[type="text"], textarea, input[type="date"]')) {
|
||||
if (e.target.matches('input[type="text"], textarea, input[type="week"], select')) {
|
||||
triggerAutoSave();
|
||||
}
|
||||
}, true);
|
||||
|
||||
// 选择器:change 事件也保存(及时响应)
|
||||
panel.addEventListener('change', function(e) {
|
||||
if (e.target.matches('select.morning-pillar')) {
|
||||
triggerAutoSave();
|
||||
}
|
||||
});
|
||||
|
||||
// 复选框:change 事件
|
||||
panel.addEventListener('change', function(e) {
|
||||
if (e.target.matches('input[type="checkbox"]')) {
|
||||
@@ -564,97 +566,70 @@
|
||||
function loadWeekly() {
|
||||
var today = new Date();
|
||||
today.setDate(today.getDate() + weekOffset * 7);
|
||||
var monday = getMonday(today);
|
||||
var sunday = new Date(monday);
|
||||
sunday.setDate(monday.getDate() + 6);
|
||||
var wk = fmtWeek(today);
|
||||
document.getElementById('week-label').textContent = wk;
|
||||
|
||||
document.getElementById('week-label').textContent =
|
||||
fmtDate(monday) + ' ~ ' + fmtDate(sunday);
|
||||
|
||||
apiGetHistory(function(err, rows){
|
||||
apiGetCheckin(wk, function(err, row){
|
||||
if (err) { showToast('加载失败', 'error'); return; }
|
||||
var map = {};
|
||||
for (var i=0; i<rows.length; i++) map[rows[i].date] = rows[i].data;
|
||||
|
||||
var totalDays = 0, totalScore = 0, dayCells = [];
|
||||
|
||||
for (var j=0; j<7; j++) {
|
||||
var cur = new Date(monday);
|
||||
cur.setDate(monday.getDate() + j);
|
||||
var ds = fmtDate(cur);
|
||||
var dd = map[ds];
|
||||
|
||||
if (dd) {
|
||||
totalDays++;
|
||||
var morning = dd.morning || [];
|
||||
var evening = dd.evening || [];
|
||||
var study = dd.study || [];
|
||||
|
||||
var morningCount = morning.filter(function(x){ return x && typeof x === 'string' && x.trim(); }).length;
|
||||
var eveningCount = evening.filter(function(x){
|
||||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||
return mst && typeof mst === 'string' && mst.trim();
|
||||
}).length;
|
||||
var studyCount = study.filter(function(x){ return x.done; }).length;
|
||||
|
||||
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
|
||||
var dayScore, status;
|
||||
if (allThree) {
|
||||
dayScore = 60 + (morningCount - 1 + eveningCount - 1 + studyCount - 1) * 5;
|
||||
status = 'pass';
|
||||
} else {
|
||||
dayScore = (morningCount + eveningCount + studyCount) * 5;
|
||||
status = 'fail';
|
||||
}
|
||||
totalScore += dayScore;
|
||||
|
||||
dayCells.push({
|
||||
ds: ds, dayScore: dayScore, status: status,
|
||||
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
|
||||
});
|
||||
} else {
|
||||
dayCells.push({
|
||||
ds: ds, dayScore: 0, status: 'empty',
|
||||
weekday: '周' + WEEKDAYS_CN[cur.getDay()]
|
||||
});
|
||||
}
|
||||
var dd = row ? row.data : null;
|
||||
if (!dd) {
|
||||
document.getElementById('weekly-score').textContent = '--';
|
||||
document.getElementById('score-text').textContent = '本周暂无记录';
|
||||
document.getElementById('score-stats').textContent = '';
|
||||
document.getElementById('week-days-grid').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
var score = totalScore;
|
||||
var morning = dd.morning || [];
|
||||
var evening = dd.evening || [];
|
||||
var study = dd.study || [];
|
||||
|
||||
var morningCount = morning.filter(function(x){ return x && typeof x === 'string' && x.trim(); }).length;
|
||||
if (!morningCount) {
|
||||
morningCount = morning.filter(function(x){
|
||||
return x && typeof x === 'object' && (x.text || '').trim();
|
||||
}).length;
|
||||
}
|
||||
var eveningCount = evening.filter(function(x){
|
||||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||
return mst && typeof mst === 'string' && mst.trim();
|
||||
}).length;
|
||||
var studyCount = study.filter(function(x){ return x.done; }).length;
|
||||
|
||||
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
|
||||
var score, status;
|
||||
if (allThree) {
|
||||
score = 60 + (morningCount - 1) * 5 + (eveningCount - 1) * 3 + studyCount * 3;
|
||||
status = 'pass';
|
||||
} else {
|
||||
score = 0;
|
||||
status = 'fail';
|
||||
}
|
||||
|
||||
var grade = score >= 90 ? '优秀' : score >= 75 ? '良好' : score >= 60 ? '达标' : '未达标';
|
||||
|
||||
document.getElementById('weekly-score').textContent = score;
|
||||
|
||||
var ring = document.querySelector('.score-ring');
|
||||
if (ring) {
|
||||
var R = 50, C = 2 * Math.PI * R;
|
||||
ring.setAttribute('stroke-dasharray', C);
|
||||
ring.setAttribute('stroke-dashoffset', C - (C * totalDays / 7));
|
||||
var ratio = Math.min(score / 100, 1);
|
||||
ring.setAttribute('stroke-dashoffset', C - (C * ratio));
|
||||
}
|
||||
|
||||
var txt = '';
|
||||
if (totalDays >= 7) txt = '全勤!本周每天都有记录';
|
||||
else if (totalDays >= 5) txt = '良好,保持了大部分记录';
|
||||
else if (totalDays >= 3) txt = '一般,需要更规律打卡';
|
||||
else txt = '本周记录偏少,加油!';
|
||||
var txt = grade + ' · ' + score + ' 分';
|
||||
document.getElementById('score-text').textContent = txt;
|
||||
|
||||
var completedDays = dayCells.filter(function(c){ return c.status === 'pass'; }).length;
|
||||
var totalFilled = dayCells.filter(function(c){ return c.status !== 'empty'; }).length;
|
||||
document.getElementById('score-stats').textContent =
|
||||
'本周填卡 ' + totalFilled + '/7 天 · 达标 ' + completedDays + ' 天';
|
||||
'立志 ' + morningCount + ' · 责善 ' + eveningCount + ' · 勤学 ' + studyCount + '/8';
|
||||
|
||||
var gridHtml = '';
|
||||
for (var g=0; g<dayCells.length; g++) {
|
||||
var cell = dayCells[g];
|
||||
var dateParts = cell.ds.split('-');
|
||||
var shortDate = dateParts[1] + '/' + dateParts[2];
|
||||
var badgeText = cell.status === 'pass' ? '达标' : (cell.status === 'fail' ? '未达标' : '未打卡');
|
||||
gridHtml += '<div class="day-cell ' + cell.status + '">' +
|
||||
'<div class="day-label">' + cell.weekday + '</div>' +
|
||||
'<div class="day-date">' + shortDate + '</div>' +
|
||||
'<div class="day-score">' + cell.dayScore + '<span style="font-size:11px;font-weight:400">分</span></div>' +
|
||||
'<div class="day-badge">' + badgeText + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
// 渲染周统计网格(简化版)
|
||||
var gridHtml = '<div class="day-cell ' + status + '">' +
|
||||
'<div class="day-label">' + wk + '</div>' +
|
||||
'<div class="day-date">' + grade + '</div>' +
|
||||
'<div class="day-score">' + score + '<span style="font-size:11px;font-weight:400">分</span></div>' +
|
||||
'<div class="day-badge">' + (status === 'pass' ? '达标' : '未达标') + '</div>' +
|
||||
'</div>';
|
||||
document.getElementById('week-days-grid').innerHTML = gridHtml;
|
||||
});
|
||||
}
|
||||
@@ -677,7 +652,9 @@
|
||||
var html = '';
|
||||
for (var i=0; i<rows.length; i++) {
|
||||
var r = rows[i];
|
||||
var morning = (r.data.morning || []).filter(function(x){ return x && typeof x === 'string' && x.trim(); });
|
||||
var morning = (r.data.morning || []).filter(function(x){
|
||||
return (typeof x === 'string' && x.trim()) || (typeof x === 'object' && (x.text||'').trim());
|
||||
});
|
||||
var evening = (r.data.evening || []).filter(function(x){
|
||||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||
return mst && typeof mst === 'string' && mst.trim();
|
||||
@@ -685,9 +662,9 @@
|
||||
var study = (r.data.study || []).filter(function(x){ return x.done; });
|
||||
|
||||
var tags = [];
|
||||
if (morning.length) tags.push('<span><svg class="icon-xs" style="color:var(--primary)"><use href="#icon-sun"/></svg> 立志 ' + morning.length + '</span>');
|
||||
if (evening.length) tags.push('<span><svg class="icon-xs" style="color:var(--danger)"><use href="#icon-magnifying-glass"/></svg> 改过 ' + evening.length + '</span>');
|
||||
if (study.length) tags.push('<span><svg class="icon-xs" style="color:#059669"><use href="#icon-book-open"/></svg> 勤学 ' + study.length + '</span>');
|
||||
if (morning.length) tags.push('<span><svg class="icon-xs" style="color:var(--primary)"><use href="#icon-sun"/></svg> ' + morning.length + '</span>');
|
||||
if (evening.length) tags.push('<span><svg class="icon-xs" style="color:var(--danger)"><use href="#icon-magnifying-glass"/></svg> ' + evening.length + '</span>');
|
||||
if (study.length) tags.push('<span><svg class="icon-xs" style="color:#059669"><use href="#icon-book-open"/></svg> ' + study.length + '/8</span>');
|
||||
|
||||
html += '<div class="history-card">' +
|
||||
'<div class="info">' +
|
||||
@@ -701,13 +678,13 @@
|
||||
});
|
||||
}
|
||||
|
||||
window.deleteHistory = function(date) {
|
||||
if (!confirm('确定删除 ' + date + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||||
apiDeleteCheckin(date, function(err){
|
||||
window.deleteHistory = function(week) {
|
||||
if (!confirm('确定删除 ' + week + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||||
apiDeleteCheckin(week, function(err){
|
||||
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
||||
showToast('已删除', 'info');
|
||||
loadHistory();
|
||||
loadStats(); // 更新统计
|
||||
loadStats();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -983,35 +960,114 @@
|
||||
|
||||
window.renderWishes = renderWishes;
|
||||
|
||||
/* ================================================================
|
||||
Blueprint — 蓝图六板块
|
||||
================================================================ */
|
||||
|
||||
var blueprintData = null;
|
||||
var blueprintFilter = 'all';
|
||||
|
||||
function loadBlueprint() {
|
||||
apiGetStats(function(err, data) {
|
||||
if (err || !data || !data.pillar_breakdown) return;
|
||||
blueprintData = data.pillar_breakdown;
|
||||
renderBlueprintList();
|
||||
});
|
||||
}
|
||||
|
||||
function renderBlueprintPillars(pb) {
|
||||
var container = document.getElementById('blueprint-pillars');
|
||||
if (!container) return;
|
||||
var html = '';
|
||||
PILLARS.forEach(function(p){
|
||||
html += '<div class="blueprint-pillar" data-pillar="' + p.name + '">' +
|
||||
'<div class="bp-icon">' + p.emoji + '</div>' +
|
||||
'<div class="bp-name">' + p.name + '</div>' +
|
||||
'</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderBlueprintList() {
|
||||
var list = document.getElementById('bp-list');
|
||||
var empty = document.getElementById('bp-list-empty');
|
||||
if (!list || !blueprintData) return;
|
||||
var items = [];
|
||||
PILLARS.forEach(function(p){
|
||||
var info = blueprintData[p.name] || {};
|
||||
if (blueprintFilter === 'all' || blueprintFilter === 'morning') {
|
||||
(info.morning_items || []).forEach(function(mi){ items.push({pillar: p, text: mi.text, type: 'morning'}); });
|
||||
}
|
||||
if (blueprintFilter === 'all' || blueprintFilter === 'evening') {
|
||||
(info.evening_items || []).forEach(function(ei){ items.push({pillar: p, text: ei.mistake || ei.improvement, improvement: ei.improvement, type: 'evening'}); });
|
||||
}
|
||||
if (blueprintFilter === 'all' || blueprintFilter === 'study') {
|
||||
(info.study_items || []).forEach(function(si){ items.push({pillar: p, text: si.name, type: 'study', done: si.done, note: si.note}); });
|
||||
}
|
||||
});
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '';
|
||||
if (empty) empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
if (empty) empty.style.display = 'none';
|
||||
var typeLabels = {morning: '立志', evening: '责善', study: '勤学'};
|
||||
var pillarColors = {
|
||||
'医疗服务': '#4A6CF7', '医药营销': '#D97706', '医疗支付': '#059669',
|
||||
'AI 智能': '#7C3AED', '公司治理': '#DC2626', '个人修养': '#0891B2'
|
||||
};
|
||||
var html = '';
|
||||
items.forEach(function(item){
|
||||
var dot = '<span class="bp-pillar-dot" style="background:' + (pillarColors[item.pillar.name] || '#94A3B8') + '"></span>';
|
||||
var extra = '';
|
||||
if (item.type === 'study' && item.note) extra = '<span class="bp-item-note">' + esc(item.note) + '</span>';
|
||||
if (item.type === 'evening' && item.improvement) extra = '<span class="bp-item-note evening">→ ' + esc(item.improvement) + '</span>';
|
||||
html += '<div class="bp-list-item">' +
|
||||
dot +
|
||||
'<span class="bp-item-pillar">' + item.pillar.name + '</span>' +
|
||||
'<span class="bp-item-type ' + item.type + '">' + (typeLabels[item.type] || item.type) + '</span>' +
|
||||
'<span class="bp-item-text">' + esc(item.text) + '</span>' +
|
||||
extra +
|
||||
'</div>';
|
||||
});
|
||||
list.innerHTML = html;
|
||||
}
|
||||
|
||||
window.filterBlueprint = function(btn) {
|
||||
blueprintFilter = btn.dataset.filter;
|
||||
document.querySelectorAll('.bp-tab').forEach(function(t){ t.classList.remove('active'); });
|
||||
btn.classList.add('active');
|
||||
renderBlueprintList();
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
Init
|
||||
================================================================ */
|
||||
function init() {
|
||||
var today = new Date();
|
||||
calYear = today.getFullYear();
|
||||
calMonth = today.getMonth() + 1;
|
||||
|
||||
var cd = document.getElementById('check-date');
|
||||
var todayStr = fmtDate(today);
|
||||
if (cd) cd.value = todayStr;
|
||||
var todayWk = fmtWeek(today);
|
||||
if (cd) cd.value = todayWk;
|
||||
|
||||
selectedDate = todayStr;
|
||||
selectedWeek = todayWk;
|
||||
initStudyPresets();
|
||||
bindAutoSave();
|
||||
lastSavedDate = todayStr;
|
||||
lastSavedWeek = todayWk;
|
||||
|
||||
// 从页面嵌入数据获取初始统计(0 延迟)
|
||||
if (window.__INITIAL_STATS__) {
|
||||
var s = window.__INITIAL_STATS__;
|
||||
document.getElementById('stat-days').textContent = s.total_days;
|
||||
document.getElementById('stat-days').textContent = s.total_weeks;
|
||||
document.getElementById('stat-morning').textContent = s.total_morning;
|
||||
document.getElementById('stat-study').textContent = s.total_study;
|
||||
calendarStatus = s.calendar || {};
|
||||
}
|
||||
|
||||
loadCheckin();
|
||||
loadStats(); // 异步刷新最新数据
|
||||
renderCalendar(); // 同步渲染 — calendarStatus 已有值
|
||||
loadStats();
|
||||
renderCalendar();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
|
||||
397
static/style.css
397
static/style.css
@@ -18,7 +18,7 @@
|
||||
--radius-sm: 8px;
|
||||
--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);
|
||||
--sidebar-w: 280px;
|
||||
--sidebar-w: 100px;
|
||||
}
|
||||
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
@@ -66,14 +66,29 @@ body {
|
||||
|
||||
.sidebar-user {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 20px 20px 16px;
|
||||
gap: 8px;
|
||||
padding: 24px 12px 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.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 {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
color: #FFF;
|
||||
@@ -131,6 +146,10 @@ body {
|
||||
gap: 8px;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
.daily-sidebar .sidebar-stats {
|
||||
padding: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
background: var(--bg);
|
||||
@@ -162,6 +181,73 @@ body {
|
||||
.calendar-widget {
|
||||
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-weeks {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
padding: 8px 14px 10px;
|
||||
}
|
||||
.cal-week-cell {
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1.5px solid var(--border);
|
||||
background: var(--card);
|
||||
}
|
||||
.cal-week-cell:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.cal-week-cell.pass { background: var(--success-light); border-color: var(--success); color: var(--success); }
|
||||
.cal-week-cell.fail { background: var(--danger-light); border-color: var(--danger); color: var(--danger); }
|
||||
.cal-week-cell.today { border-color: var(--primary); box-shadow: 0 0 0 2px var(--primary-light); }
|
||||
.cal-week-cell.selected { background: var(--primary); color: #FFF; border-color: var(--primary); }
|
||||
|
||||
/* 旧样式清理 */
|
||||
.cal-week-list { display: none; }
|
||||
.cal-week-cell .wk-num, .cal-week-cell .wk-dot { display: none; }
|
||||
|
||||
.cal-header {
|
||||
display: flex;
|
||||
@@ -207,59 +293,86 @@ body {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.cal-grid {
|
||||
.cal-week-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 3px;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 6px 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.cal-cell {
|
||||
aspect-ratio: 1;
|
||||
.cal-week-cell {
|
||||
aspect-ratio: 1.4;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
border: 1.5px solid var(--border);
|
||||
background: var(--card);
|
||||
padding: 2px 0;
|
||||
position: relative;
|
||||
border: 1.5px solid transparent;
|
||||
}
|
||||
|
||||
.cal-cell:hover {
|
||||
.cal-week-cell:hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.cal-cell.other-month {
|
||||
color: var(--border);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cal-cell.other-month:hover {
|
||||
.cal-week-cell .wk-num { font-weight: 600; line-height: 1; }
|
||||
.cal-week-cell .wk-dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: var(--border);
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.cal-cell.today {
|
||||
font-weight: 700;
|
||||
color: var(--primary);
|
||||
.cal-week-cell.pass {
|
||||
background: var(--success-light);
|
||||
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);
|
||||
background: var(--primary-light);
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 0 2px var(--primary-light);
|
||||
}
|
||||
|
||||
.cal-cell.selected {
|
||||
font-weight: 600;
|
||||
color: var(--primary-dark);
|
||||
border-color: var(--primary-dark);
|
||||
background: #DDE3FD;
|
||||
.cal-week-cell.selected {
|
||||
background: var(--primary);
|
||||
color: #FFF;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
.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;
|
||||
color: #FFF;
|
||||
border-color: var(--primary-dark);
|
||||
@@ -332,34 +445,45 @@ body {
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
gap: 4px;
|
||||
padding: 10px 4px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-label { line-height: 1.2; }
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--bg);
|
||||
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 {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-item.active .icon-md { color: var(--primary); }
|
||||
|
||||
.nav-item .icon-sm {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.icon-md { width: 20px; height: 20px; flex-shrink: 0; }
|
||||
|
||||
/* ── Sidebar Footer ── */
|
||||
|
||||
@@ -428,6 +552,12 @@ body {
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.panel-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.check-date-input {
|
||||
padding: 8px 14px;
|
||||
border: 1.5px solid var(--border);
|
||||
@@ -446,33 +576,78 @@ body {
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Daily Grid — 三栏
|
||||
Daily Panel — 左右两栏布局
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
.daily-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
.daily-layout {
|
||||
display: flex;
|
||||
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;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 立志 — 左上 */
|
||||
.card-morning {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
.daily-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
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); }
|
||||
|
||||
/* 改过 — 左下 */
|
||||
.card-evening {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
/* 勤学 — 右列跨2行 */
|
||||
.card-study {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 3;
|
||||
}
|
||||
.daily-grid { display: block; }
|
||||
.card.daily-card { display: none; }
|
||||
.card.daily-card.active { display: flex; }
|
||||
|
||||
/* ── Cards ── */
|
||||
|
||||
@@ -540,18 +715,17 @@ body {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
padding: 8px 14px;
|
||||
border: 1.5px solid var(--border);
|
||||
background: var(--card);
|
||||
color: var(--text-dim);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
font-family: inherit;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.btn-cal-sync:hover {
|
||||
border-color: var(--primary);
|
||||
@@ -1396,11 +1570,9 @@ body {
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sidebar { width: 240px; min-width: 240px; }
|
||||
:root { --sidebar-w: 240px; }
|
||||
.main-content { padding: 20px 24px; }
|
||||
.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; }
|
||||
@@ -1418,3 +1590,98 @@ body {
|
||||
}
|
||||
.main-content { height: auto; overflow: visible; padding: 16px; }
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Blueprint Panel
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
/* Tab 筛选 */
|
||||
.bp-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1.5px solid var(--border);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.bp-tab {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 8px 16px;
|
||||
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;
|
||||
}
|
||||
.bp-tab .icon-sm { opacity: 0.6; }
|
||||
.bp-tab:hover { color: var(--text); }
|
||||
.bp-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||||
.bp-tab.active .icon-sm { opacity: 1; }
|
||||
|
||||
/* 明细列表 */
|
||||
.bp-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
}
|
||||
.bp-list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: var(--card);
|
||||
border-bottom: 0.5px solid var(--border);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.bp-list-item:hover { background: var(--bg); }
|
||||
.bp-pillar-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bp-item-pillar {
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-item-type {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-item-type.morning { background: var(--warning-light); color: #D97706; }
|
||||
.bp-item-type.evening { background: var(--danger-light); color: var(--danger); }
|
||||
.bp-item-type.study { background: var(--success-light); color: var(--success); }
|
||||
.bp-item-text {
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-item-note {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-left: 4px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bp-item-note.evening { color: var(--danger); }
|
||||
.bp-list-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; }
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</div>
|
||||
<div class="sync-modal-loading" id="sync-loading">
|
||||
<div class="sync-spinner"></div>
|
||||
<p>正在从钉钉同步过去15天~未来15天日程…</p>
|
||||
<p>正在从钉钉同步过去4周~未来4周日程…</p>
|
||||
</div>
|
||||
<div class="sync-modal-results" id="sync-results" style="display:none">
|
||||
<div class="sync-summary" id="sync-summary"></div>
|
||||
@@ -42,96 +42,105 @@
|
||||
<!-- 用户信息 -->
|
||||
<div class="sidebar-user">
|
||||
<div class="user-avatar">{{ username[0] }}</div>
|
||||
<div class="user-info">
|
||||
<span class="user-name">{{ username }}</span>
|
||||
<span class="user-tag">道阁 · 紫微</span>
|
||||
</div>
|
||||
<span class="user-name">{{ username }}</span>
|
||||
<a href="/logout" class="btn-logout-icon" title="退出登录" onclick="return confirm('确定退出登录?')">
|
||||
<svg class="icon-sm"><use href="#icon-logout"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="sidebar-stats" id="sidebar-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-days">--</span>
|
||||
<span class="stat-label">打卡天</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-morning">--</span>
|
||||
<span class="stat-label">立志</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-study">--</span>
|
||||
<span class="stat-label">勤学</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历 -->
|
||||
<div class="calendar-widget">
|
||||
<div class="cal-header">
|
||||
<button class="cal-nav" onclick="changeCalMonth(-1)">
|
||||
<svg class="icon-sm"><use href="#icon-chevron-left"/></svg>
|
||||
</button>
|
||||
<span class="cal-month-label" id="cal-month-label">2026年 6月</span>
|
||||
<button class="cal-nav" onclick="changeCalMonth(1)">
|
||||
<svg class="icon-sm"><use href="#icon-chevron-right"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="cal-weekdays">
|
||||
<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>
|
||||
|
||||
<!-- 功能入口 -->
|
||||
<nav class="sidebar-nav">
|
||||
<a class="nav-item active" data-panel="daily" onclick="switchPanel('daily')">
|
||||
<svg class="icon-sm"><use href="#icon-calendar"/></svg>
|
||||
每日打卡
|
||||
<svg class="icon-md"><use href="#icon-calendar"/></svg>
|
||||
<span class="nav-label">打卡</span>
|
||||
</a>
|
||||
<a class="nav-item" data-panel="weekly" onclick="switchPanel('weekly')">
|
||||
<svg class="icon-sm"><use href="#icon-chart-bar"/></svg>
|
||||
每周评分
|
||||
<svg class="icon-md"><use href="#icon-chart-bar"/></svg>
|
||||
<span class="nav-label">评分</span>
|
||||
</a>
|
||||
<a class="nav-item" data-panel="wishes" onclick="switchPanel('wishes')">
|
||||
<svg class="icon-sm"><use href="#icon-star"/></svg>
|
||||
心愿清单
|
||||
<svg class="icon-md"><use href="#icon-star"/></svg>
|
||||
<span class="nav-label">心愿</span>
|
||||
</a>
|
||||
<a class="nav-item" data-panel="history" onclick="switchPanel('history')">
|
||||
<svg class="icon-sm"><use href="#icon-list-bullet"/></svg>
|
||||
历史记录
|
||||
<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>
|
||||
|
||||
<!-- 底部新建按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn-new-day" onclick="goToday()">
|
||||
<svg class="icon-sm"><use href="#icon-sun"/></svg>
|
||||
回到今天
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- ═══ 右侧主内容区 ═══ -->
|
||||
<main class="main-content">
|
||||
|
||||
<!-- ── 每日打卡面板 ── -->
|
||||
<!-- ── 每周打卡面板 ── -->
|
||||
<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>
|
||||
<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-grid">
|
||||
<div class="card card-morning">
|
||||
<div class="daily-layout">
|
||||
<!-- 左侧:统计 + 日历 -->
|
||||
<div class="daily-sidebar">
|
||||
<div class="sidebar-stats" id="sidebar-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-days">--</span>
|
||||
<span class="stat-label">打卡周</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-morning">--</span>
|
||||
<span class="stat-label">立志</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-study">--</span>
|
||||
<span class="stat-label">勤学</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-widget inline">
|
||||
<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)">
|
||||
<svg class="icon-sm"><use href="#icon-chevron-left"/></svg>
|
||||
</button>
|
||||
<span class="cal-month-label" id="cal-month-label">7月</span>
|
||||
<button class="cal-nav" onclick="changeCalMonth(1)">
|
||||
<svg class="icon-sm"><use href="#icon-chevron-right"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="cal-weeks" id="cal-weeks"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧:Tab + 内容 -->
|
||||
<div class="daily-main">
|
||||
<div class="daily-tabs">
|
||||
<button class="daily-tab active" data-tab="morning" onclick="switchDailyTab(this)">
|
||||
<svg class="icon-sm"><use href="#icon-sun"/></svg> 早间立志
|
||||
</button>
|
||||
<button class="daily-tab" data-tab="evening" onclick="switchDailyTab(this)">
|
||||
<svg class="icon-sm"><use href="#icon-magnifying-glass"/></svg> 责善改过
|
||||
</button>
|
||||
<button class="daily-tab" data-tab="study" onclick="switchDailyTab(this)">
|
||||
<svg class="icon-sm"><use href="#icon-book-open"/></svg> 勤学打卡
|
||||
</button>
|
||||
</div>
|
||||
<div class="daily-grid">
|
||||
<div class="card card-morning daily-card active" data-card="morning">
|
||||
<div class="card-head">
|
||||
<div class="card-head-left">
|
||||
<svg class="icon-h2"><use href="#icon-sun"/></svg>
|
||||
@@ -141,13 +150,13 @@
|
||||
<svg class="icon-sm"><use href="#icon-pencil"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="card-desc">今天最重要的 1~3 件事</p>
|
||||
<p class="card-desc">本周最重要的 1~3 件事</p>
|
||||
<div id="morning-list"></div>
|
||||
<button class="btn-add edit-only" onclick="addMorning()">
|
||||
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
|
||||
</button>
|
||||
</div>
|
||||
<div class="card card-evening">
|
||||
<div class="card card-evening daily-card" data-card="evening">
|
||||
<div class="card-head">
|
||||
<div class="card-head-left">
|
||||
<svg class="icon-h2"><use href="#icon-magnifying-glass"/></svg>
|
||||
@@ -157,21 +166,23 @@
|
||||
<svg class="icon-sm"><use href="#icon-pencil"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<p class="card-desc">今天犯的错 & 改进方案(最多5条)</p>
|
||||
<p class="card-desc">本周反思改正(最多5条)</p>
|
||||
<div id="evening-list"></div>
|
||||
<button class="btn-add edit-only" onclick="addEvening()">
|
||||
<svg class="icon-sm"><use href="#icon-plus"/></svg> 增加一条
|
||||
</button>
|
||||
</div>
|
||||
<div class="card card-study">
|
||||
<div class="card card-study daily-card" data-card="study">
|
||||
<div class="card-head">
|
||||
<svg class="icon-h2"><use href="#icon-book-open"/></svg>
|
||||
<h3>勤学打卡</h3>
|
||||
</div>
|
||||
<p class="card-desc">今日修行清单</p>
|
||||
<p class="card-desc">本周修行清单</p>
|
||||
<div id="study-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /daily-main -->
|
||||
</div><!-- /daily-layout -->
|
||||
</section>
|
||||
|
||||
<!-- ── 每周评分面板 ── -->
|
||||
@@ -267,6 +278,31 @@
|
||||
<div class="wishes-empty" id="wishes-empty" style="display:none">暂无心愿,点击上方按钮添加</div>
|
||||
</section>
|
||||
|
||||
<!-- ── 蓝图面板 ── -->
|
||||
<section class="panel" id="panel-blueprint">
|
||||
<div class="panel-header">
|
||||
<h2>人生蓝图</h2>
|
||||
</div>
|
||||
<!-- 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>
|
||||
</div>
|
||||
<!-- 明细列表 -->
|
||||
<div class="bp-list" id="bp-list"></div>
|
||||
<div class="bp-list-empty" id="bp-list-empty" style="display:none">暂无记录</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user