Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99bc6d3c6c | ||
|
|
5d3192dfaf | ||
|
|
9c9511e817 | ||
|
|
b5dbb13956 | ||
|
|
5ef340cb08 | ||
|
|
073b570681 | ||
|
|
ce0e374e86 | ||
|
|
f7bbc505b9 | ||
|
|
351e1f18ae | ||
|
|
0e75326b5f | ||
|
|
cd90c2d799 | ||
|
|
c2cbbdb12a | ||
|
|
8223f72544 | ||
|
|
36b58a8b73 | ||
|
|
5b73fc0eae | ||
|
|
5b637e7fed | ||
|
|
350c9fb877 | ||
|
|
19ae81ae6e | ||
|
|
535af35911 | ||
|
|
fc338f426a | ||
|
|
5300ee426d | ||
|
|
8ac6f1af6b | ||
|
|
8c81fae334 | ||
|
|
67505414d9 |
145
app.py
145
app.py
@@ -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, get_wishes, save_wish, update_wish, delete_wish, reorder_wishes
|
||||||
|
|
||||||
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
|
||||||
@@ -137,6 +137,7 @@ def compute_stats():
|
|||||||
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,
|
'done': si.get('done', False) if isinstance(si, dict) else False,
|
||||||
|
'note': si.get('note', '') if isinstance(si, dict) else '',
|
||||||
'pillar': pillar
|
'pillar': pillar
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -186,7 +187,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,
|
||||||
@@ -213,10 +214,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})
|
||||||
|
|
||||||
|
|
||||||
@@ -224,21 +225,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():
|
||||||
@@ -246,6 +255,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():
|
||||||
@@ -317,15 +337,23 @@ CALENDAR_CACHE = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'zi
|
|||||||
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({
|
||||||
@@ -342,8 +370,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:
|
||||||
@@ -354,61 +381,67 @@ 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
|
||||||
|
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')
|
DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db')
|
||||||
|
|
||||||
# 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑
|
# 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑
|
||||||
CURRENT_SCHEMA_VERSION = 3
|
CURRENT_SCHEMA_VERSION = 4
|
||||||
|
|
||||||
|
|
||||||
def get_conn():
|
def get_conn():
|
||||||
@@ -74,6 +74,29 @@ def init_db():
|
|||||||
conn.execute("ALTER TABLE wishes ADD COLUMN quadrant TEXT NOT NULL DEFAULT '重要不紧急'")
|
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 = '重要不紧急'")
|
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:
|
# if current < 2:
|
||||||
# conn.execute('ALTER TABLE checkins ADD COLUMN tags TEXT DEFAULT ""')
|
# 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.execute('UPDATE wishes SET sort_order = ? WHERE id = ?', (idx, wid))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
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)
|
||||||
|
)
|
||||||
|
|||||||
452
static/app.js
452
static/app.js
@@ -5,13 +5,14 @@
|
|||||||
/* ================================================================
|
/* ================================================================
|
||||||
State
|
State
|
||||||
================================================================ */
|
================================================================ */
|
||||||
var calendarStatus = {}; // { "2026-06-01": "pass", ... }
|
var calendarStatus = {}; // { "2026-W30": "pass", ... }
|
||||||
var calYear, calMonth; // 日历显示的月份
|
var calYear;
|
||||||
var activePanel = 'daily'; // 当前面板
|
var calMonth = new Date().getMonth(); // 月份索引 0-11
|
||||||
var weekOffset = 0; // 每周评分偏移
|
var activePanel = 'daily';
|
||||||
var lastSavedData = null; // 上次保存快照
|
var weekOffset = 0;
|
||||||
var lastSavedDate = null; // 上次保存日期
|
var lastSavedData = null;
|
||||||
var selectedDate = null; // 日历中选中日期
|
var lastSavedWeek = null;
|
||||||
|
var selectedWeek = null;
|
||||||
|
|
||||||
/* 勤学预设项目 */
|
/* 勤学预设项目 */
|
||||||
var PRESET_STUDY_ITEMS = [
|
var PRESET_STUDY_ITEMS = [
|
||||||
@@ -51,18 +52,18 @@
|
|||||||
/* ================================================================
|
/* ================================================================
|
||||||
API
|
API
|
||||||
================================================================ */
|
================================================================ */
|
||||||
function apiGetCheckin(date, cb) {
|
function apiGetCheckin(week, cb) {
|
||||||
fetch('/api/checkin?date=' + encodeURIComponent(date))
|
fetch('/api/checkin?week=' + encodeURIComponent(week))
|
||||||
.then(function(r){ return r.json(); })
|
.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.data); else cb(res.error); })
|
||||||
.catch(function(e){ cb(e.message); });
|
.catch(function(e){ cb(e.message); });
|
||||||
}
|
}
|
||||||
|
|
||||||
function apiSaveCheckin(date, data, cb) {
|
function apiSaveCheckin(week, data, cb) {
|
||||||
fetch('/api/checkin', {
|
fetch('/api/checkin', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
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(r){ return r.json(); })
|
||||||
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
.then(function(res){ if (res.ok) cb(null); else cb(res.error); })
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
function apiGetStats(cb) {
|
function apiGetStats(cb) {
|
||||||
fetch('/api/stats')
|
fetch('/api/stats')
|
||||||
.then(function(r){ return r.json(); })
|
.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); });
|
.catch(function(e){ cb(e.message); });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,91 +132,86 @@
|
|||||||
return y + '-' + m + '-' + dd;
|
return y + '-' + m + '-' + dd;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 判断两个日期是否同一天 */
|
/** 获取当前周的 YYYY-WW 格式 */
|
||||||
function isSameDay(a, b) {
|
function fmtWeek(d) {
|
||||||
return a.getFullYear() === b.getFullYear() &&
|
var jan1 = new Date(d.getFullYear(), 0, 1);
|
||||||
a.getMonth() === b.getMonth() &&
|
var dayOfYear = Math.floor((d - jan1) / 86400000);
|
||||||
a.getDate() === b.getDate();
|
var week = Math.ceil((dayOfYear + jan1.getDay() + 1) / 7);
|
||||||
|
return d.getFullYear() + '-W' + ('0' + week).slice(-2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 切换日历月份 */
|
/** 切换日历年 */
|
||||||
window.changeCalMonth = function(dir) {
|
window.changeCalYear = function(dir) {
|
||||||
calMonth += dir;
|
calYear += dir;
|
||||||
if (calMonth > 12) { calMonth = 1; calYear++; }
|
|
||||||
if (calMonth < 1) { calMonth = 12; calYear--; }
|
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 渲染日历 */
|
/** 切换月份 */
|
||||||
function renderCalendar() {
|
window.changeCalMonth = function(dir) {
|
||||||
var today = new Date();
|
calMonth += dir;
|
||||||
var firstDay = new Date(calYear, calMonth - 1, 1);
|
if (calMonth > 11) { calMonth = 0; calYear++; }
|
||||||
var lastDay = new Date(calYear, calMonth, 0);
|
if (calMonth < 0) { calMonth = 11; calYear--; }
|
||||||
var startDayOfWeek = firstDay.getDay(); // 0=Sun
|
|
||||||
var totalDays = lastDay.getDate();
|
|
||||||
var prevMonthLastDay = new Date(calYear, calMonth - 1, 0).getDate();
|
|
||||||
|
|
||||||
// 月份标签
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当月
|
|
||||||
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>';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 后月填充 — 始终 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) {
|
|
||||||
cell.addEventListener('click', function() {
|
|
||||||
var date = this.dataset.date;
|
|
||||||
if (date) navigateToDate(date);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 导航到指定日期 */
|
|
||||||
function navigateToDate(date) {
|
|
||||||
selectedDate = date;
|
|
||||||
switchPanel('daily');
|
|
||||||
document.getElementById('check-date').value = date;
|
|
||||||
loadCheckin();
|
|
||||||
renderCalendar(); // 刷新日历高亮
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 回到今天 */
|
|
||||||
window.goToday = function() {
|
|
||||||
var today = fmtDate(new Date());
|
|
||||||
calYear = new Date().getFullYear();
|
|
||||||
calMonth = new Date().getMonth() + 1;
|
|
||||||
renderCalendar();
|
renderCalendar();
|
||||||
navigateToDate(today);
|
};
|
||||||
|
|
||||||
|
/** 渲染年-月-周选择器 */
|
||||||
|
function renderCalendar() {
|
||||||
|
document.getElementById('cal-year-label').textContent = calYear + '年';
|
||||||
|
document.getElementById('cal-month-label').textContent = (calMonth + 1) + '月';
|
||||||
|
|
||||||
|
// 计算该月包含的周
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
var we = document.getElementById('cal-weeks');
|
||||||
|
we.innerHTML = wHtml;
|
||||||
|
we.querySelectorAll('.cal-week-cell').forEach(function(cell) {
|
||||||
|
cell.addEventListener('click', function() {
|
||||||
|
var wk = this.dataset.week;
|
||||||
|
if (wk) navigateToWeek(wk);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateToWeek(week) {
|
||||||
|
selectedWeek = week;
|
||||||
|
switchPanel('daily');
|
||||||
|
document.getElementById('check-date').value = week;
|
||||||
|
loadCheckin();
|
||||||
|
renderCalendar();
|
||||||
|
}
|
||||||
|
|
||||||
|
window.goToCurrentWeek = function() {
|
||||||
|
var now = new Date();
|
||||||
|
calYear = now.getFullYear();
|
||||||
|
calMonth = now.getMonth();
|
||||||
|
renderCalendar();
|
||||||
|
navigateToWeek(fmtWeek(now));
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
@@ -224,7 +220,7 @@
|
|||||||
function loadStats() {
|
function loadStats() {
|
||||||
apiGetStats(function(err, data) {
|
apiGetStats(function(err, data) {
|
||||||
if (!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-morning').textContent = data.total_morning;
|
||||||
document.getElementById('stat-study').textContent = data.total_study;
|
document.getElementById('stat-study').textContent = data.total_study;
|
||||||
calendarStatus = data.calendar || {};
|
calendarStatus = data.calendar || {};
|
||||||
@@ -318,7 +314,7 @@
|
|||||||
var div = document.createElement('div');
|
var div = document.createElement('div');
|
||||||
div.className = 'item-row';
|
div.className = 'item-row';
|
||||||
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
div.innerHTML = '<span class="idx">' + (idx+1) + '.</span>' +
|
||||||
'<input type="text" value="' + esc(text) + '" 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>';
|
'<button class="btn-del" onclick="this.parentElement.remove();renumberMorning()"><svg class="icon-sm"><use href="#icon-x"/></svg></button>';
|
||||||
container.appendChild(div);
|
container.appendChild(div);
|
||||||
renumberMorning();
|
renumberMorning();
|
||||||
@@ -410,7 +406,7 @@
|
|||||||
var totalDays = data.length;
|
var totalDays = data.length;
|
||||||
var totalEvents = 0;
|
var totalEvents = 0;
|
||||||
data.forEach(function(d){ totalEvents += d.events.length; });
|
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 = '';
|
var html = '';
|
||||||
@@ -428,8 +424,8 @@
|
|||||||
});
|
});
|
||||||
list.innerHTML = html;
|
list.innerHTML = html;
|
||||||
|
|
||||||
// 自动填充今天到选中日期的日程
|
// 刷新当前页
|
||||||
autoFillTodayEvents(data);
|
loadCheckin();
|
||||||
})
|
})
|
||||||
.catch(function(){
|
.catch(function(){
|
||||||
loading.style.display = 'none';
|
loading.style.display = 'none';
|
||||||
@@ -439,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() {
|
window.closeSyncModal = function() {
|
||||||
var overlay = document.getElementById('sync-modal-overlay');
|
var overlay = document.getElementById('sync-modal-overlay');
|
||||||
if (overlay) overlay.style.display = 'none';
|
if (overlay) overlay.style.display = 'none';
|
||||||
@@ -487,29 +459,37 @@
|
|||||||
btn.classList.toggle('active', editing);
|
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
|
Load & Auto Save Checkin
|
||||||
================================================================ */
|
================================================================ */
|
||||||
|
|
||||||
window.loadCheckin = function() {
|
window.loadCheckin = function() {
|
||||||
var date = document.getElementById('check-date').value;
|
var week = document.getElementById('check-date').value;
|
||||||
if (!date) return;
|
if (!week) return;
|
||||||
|
|
||||||
// 切换日期前先保存当前数据
|
|
||||||
var currentData = buildData();
|
var currentData = buildData();
|
||||||
var currentStr = JSON.stringify(currentData);
|
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();}) ||
|
var hasContent = currentData.morning.some(function(x){return x && x.trim();}) ||
|
||||||
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
|
currentData.evening.some(function(x){return (x.mistake||'').trim() || (x.improvement||'').trim();}) ||
|
||||||
currentData.study.some(function(x){return x.done;});
|
currentData.study.some(function(x){return x.done;});
|
||||||
if (hasContent) {
|
if (hasContent) {
|
||||||
apiSaveCheckin(lastSavedDate, currentData, function(){}); // 静默保存
|
apiSaveCheckin(lastSavedWeek, currentData, function(){});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSavedDate = date;
|
lastSavedWeek = week;
|
||||||
apiGetCheckin(date, function(err, row){
|
apiGetCheckin(week, function(err, row){
|
||||||
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
if (err) { initStudyPresets(); lastSavedData = '{}'; return; }
|
||||||
fillForm(row ? row.data : null);
|
fillForm(row ? row.data : null);
|
||||||
lastSavedData = JSON.stringify(buildData());
|
lastSavedData = JSON.stringify(buildData());
|
||||||
@@ -518,26 +498,26 @@
|
|||||||
|
|
||||||
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
||||||
function triggerAutoSave() {
|
function triggerAutoSave() {
|
||||||
var date = document.getElementById('check-date').value;
|
var week = document.getElementById('check-date').value;
|
||||||
if (!date) return;
|
if (!week) return;
|
||||||
var data = buildData();
|
var data = buildData();
|
||||||
var dataStr = JSON.stringify(data);
|
var dataStr = JSON.stringify(data);
|
||||||
if (dataStr === lastSavedData) return;
|
if (dataStr === lastSavedData) return;
|
||||||
apiSaveCheckin(date, data, function(err){
|
apiSaveCheckin(week, data, function(err){
|
||||||
if (err) { showToast('保存失败', 'error'); return; }
|
if (err) { showToast('保存失败', 'error'); return; }
|
||||||
lastSavedData = dataStr;
|
lastSavedData = dataStr;
|
||||||
loadStats();
|
loadStats();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 为每日打卡面板绑定自动保存事件 */
|
/** 为每周打卡面板绑定自动保存事件 */
|
||||||
function bindAutoSave() {
|
function bindAutoSave() {
|
||||||
var panel = document.getElementById('panel-daily');
|
var panel = document.getElementById('panel-daily');
|
||||||
if (!panel) return;
|
if (!panel) return;
|
||||||
|
|
||||||
// 输入框 & 文本域 & 下拉选择器:失去焦点时保存
|
// 输入框 & 文本域 & 下拉选择器:失去焦点时保存
|
||||||
panel.addEventListener('blur', function(e) {
|
panel.addEventListener('blur', function(e) {
|
||||||
if (e.target.matches('input[type="text"], textarea, input[type="date"], select')) {
|
if (e.target.matches('input[type="text"], textarea, input[type="week"], select')) {
|
||||||
triggerAutoSave();
|
triggerAutoSave();
|
||||||
}
|
}
|
||||||
}, true);
|
}, true);
|
||||||
@@ -586,33 +566,30 @@
|
|||||||
function loadWeekly() {
|
function loadWeekly() {
|
||||||
var today = new Date();
|
var today = new Date();
|
||||||
today.setDate(today.getDate() + weekOffset * 7);
|
today.setDate(today.getDate() + weekOffset * 7);
|
||||||
var monday = getMonday(today);
|
var wk = fmtWeek(today);
|
||||||
var sunday = new Date(monday);
|
document.getElementById('week-label').textContent = wk;
|
||||||
sunday.setDate(monday.getDate() + 6);
|
|
||||||
|
|
||||||
document.getElementById('week-label').textContent =
|
apiGetCheckin(wk, function(err, row){
|
||||||
fmtDate(monday) + ' ~ ' + fmtDate(sunday);
|
|
||||||
|
|
||||||
apiGetHistory(function(err, rows){
|
|
||||||
if (err) { showToast('加载失败', 'error'); return; }
|
if (err) { showToast('加载失败', 'error'); return; }
|
||||||
var map = {};
|
var dd = row ? row.data : null;
|
||||||
for (var i=0; i<rows.length; i++) map[rows[i].date] = rows[i].data;
|
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 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 morning = dd.morning || [];
|
||||||
var evening = dd.evening || [];
|
var evening = dd.evening || [];
|
||||||
var study = dd.study || [];
|
var study = dd.study || [];
|
||||||
|
|
||||||
var morningCount = morning.filter(function(x){ return x && typeof x === 'string' && x.trim(); }).length;
|
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 eveningCount = evening.filter(function(x){
|
||||||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||||
return mst && typeof mst === 'string' && mst.trim();
|
return mst && typeof mst === 'string' && mst.trim();
|
||||||
@@ -620,63 +597,39 @@
|
|||||||
var studyCount = study.filter(function(x){ return x.done; }).length;
|
var studyCount = study.filter(function(x){ return x.done; }).length;
|
||||||
|
|
||||||
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
|
var allThree = morningCount > 0 && eveningCount > 0 && studyCount > 0;
|
||||||
var dayScore, status;
|
var score, status;
|
||||||
if (allThree) {
|
if (allThree) {
|
||||||
dayScore = 60 + (morningCount - 1 + eveningCount - 1 + studyCount - 1) * 5;
|
score = 60 + (morningCount - 1) * 5 + (eveningCount - 1) * 3 + studyCount * 3;
|
||||||
status = 'pass';
|
status = 'pass';
|
||||||
} else {
|
} else {
|
||||||
dayScore = (morningCount + eveningCount + studyCount) * 5;
|
score = 0;
|
||||||
status = 'fail';
|
status = 'fail';
|
||||||
}
|
}
|
||||||
totalScore += dayScore;
|
|
||||||
|
|
||||||
dayCells.push({
|
var grade = score >= 90 ? '优秀' : score >= 75 ? '良好' : score >= 60 ? '达标' : '未达标';
|
||||||
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 score = totalScore;
|
|
||||||
document.getElementById('weekly-score').textContent = score;
|
document.getElementById('weekly-score').textContent = score;
|
||||||
|
|
||||||
var ring = document.querySelector('.score-ring');
|
var ring = document.querySelector('.score-ring');
|
||||||
if (ring) {
|
if (ring) {
|
||||||
var R = 50, C = 2 * Math.PI * R;
|
var R = 50, C = 2 * Math.PI * R;
|
||||||
ring.setAttribute('stroke-dasharray', C);
|
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 = '';
|
var txt = grade + ' · ' + score + ' 分';
|
||||||
if (totalDays >= 7) txt = '全勤!本周每天都有记录';
|
|
||||||
else if (totalDays >= 5) txt = '良好,保持了大部分记录';
|
|
||||||
else if (totalDays >= 3) txt = '一般,需要更规律打卡';
|
|
||||||
else txt = '本周记录偏少,加油!';
|
|
||||||
document.getElementById('score-text').textContent = txt;
|
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 =
|
document.getElementById('score-stats').textContent =
|
||||||
'本周填卡 ' + totalFilled + '/7 天 · 达标 ' + completedDays + ' 天';
|
'立志 ' + morningCount + ' · 责善 ' + eveningCount + ' · 勤学 ' + studyCount + '/8';
|
||||||
|
|
||||||
var gridHtml = '';
|
// 渲染周统计网格(简化版)
|
||||||
for (var g=0; g<dayCells.length; g++) {
|
var gridHtml = '<div class="day-cell ' + status + '">' +
|
||||||
var cell = dayCells[g];
|
'<div class="day-label">' + wk + '</div>' +
|
||||||
var dateParts = cell.ds.split('-');
|
'<div class="day-date">' + grade + '</div>' +
|
||||||
var shortDate = dateParts[1] + '/' + dateParts[2];
|
'<div class="day-score">' + score + '<span style="font-size:11px;font-weight:400">分</span></div>' +
|
||||||
var badgeText = cell.status === 'pass' ? '达标' : (cell.status === 'fail' ? '未达标' : '未打卡');
|
'<div class="day-badge">' + (status === 'pass' ? '达标' : '未达标') + '</div>' +
|
||||||
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>';
|
'</div>';
|
||||||
}
|
|
||||||
document.getElementById('week-days-grid').innerHTML = gridHtml;
|
document.getElementById('week-days-grid').innerHTML = gridHtml;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -699,7 +652,9 @@
|
|||||||
var html = '';
|
var html = '';
|
||||||
for (var i=0; i<rows.length; i++) {
|
for (var i=0; i<rows.length; i++) {
|
||||||
var r = rows[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 evening = (r.data.evening || []).filter(function(x){
|
||||||
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
var mst = typeof x === 'string' ? x : (x.mistake || '');
|
||||||
return mst && typeof mst === 'string' && mst.trim();
|
return mst && typeof mst === 'string' && mst.trim();
|
||||||
@@ -707,9 +662,9 @@
|
|||||||
var study = (r.data.study || []).filter(function(x){ return x.done; });
|
var study = (r.data.study || []).filter(function(x){ return x.done; });
|
||||||
|
|
||||||
var tags = [];
|
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 (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 (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 (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">' +
|
html += '<div class="history-card">' +
|
||||||
'<div class="info">' +
|
'<div class="info">' +
|
||||||
@@ -723,13 +678,13 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.deleteHistory = function(date) {
|
window.deleteHistory = function(week) {
|
||||||
if (!confirm('确定删除 ' + date + ' 的打卡记录吗?此操作不可恢复!')) return;
|
if (!confirm('确定删除 ' + week + ' 的打卡记录吗?此操作不可恢复!')) return;
|
||||||
apiDeleteCheckin(date, function(err){
|
apiDeleteCheckin(week, function(err){
|
||||||
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
if (err) { showToast('删除失败: ' + err, 'error'); return; }
|
||||||
showToast('已删除', 'info');
|
showToast('已删除', 'info');
|
||||||
loadHistory();
|
loadHistory();
|
||||||
loadStats(); // 更新统计
|
loadStats();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1009,33 +964,81 @@
|
|||||||
Blueprint — 蓝图六板块
|
Blueprint — 蓝图六板块
|
||||||
================================================================ */
|
================================================================ */
|
||||||
|
|
||||||
|
var blueprintData = null;
|
||||||
|
var blueprintFilter = 'all';
|
||||||
|
|
||||||
function loadBlueprint() {
|
function loadBlueprint() {
|
||||||
apiGetStats(function(err, data) {
|
apiGetStats(function(err, data) {
|
||||||
if (err || !data || !data.pillar_breakdown) return;
|
if (err || !data || !data.pillar_breakdown) return;
|
||||||
var pb = data.pillar_breakdown;
|
blueprintData = data.pillar_breakdown;
|
||||||
PILLARS.forEach(function(p){
|
renderBlueprintList();
|
||||||
var el = document.getElementById('bp-count-' + p.name);
|
});
|
||||||
var info = pb[p.name] || {};
|
}
|
||||||
var m = info.morning || 0, e = info.evening || 0, s = info.study || 0;
|
|
||||||
if (el) el.textContent = '立志' + m + ' · 责善' + e + ' · 勤学' + s;
|
function renderBlueprintPillars(pb) {
|
||||||
// 显示详情列表
|
var container = document.getElementById('blueprint-pillars');
|
||||||
var detailEl = document.getElementById('bp-detail-' + p.name);
|
if (!container) return;
|
||||||
if (detailEl) {
|
|
||||||
var html = '';
|
var html = '';
|
||||||
(info.morning_items || []).forEach(function(mi){
|
PILLARS.forEach(function(p){
|
||||||
html += '<div class="bp-item" data-type="morning">🥼 ' + esc(mi.text) + '</div>';
|
html += '<div class="blueprint-pillar" data-pillar="' + p.name + '">' +
|
||||||
|
'<div class="bp-icon">' + p.emoji + '</div>' +
|
||||||
|
'<div class="bp-name">' + p.name + '</div>' +
|
||||||
|
'</div>';
|
||||||
});
|
});
|
||||||
(info.evening_items || []).forEach(function(ei){
|
container.innerHTML = html;
|
||||||
html += '<div class="bp-item" data-type="evening">🔍 ' + esc(ei.mistake || ei.improvement) + '</div>';
|
}
|
||||||
});
|
|
||||||
(info.study_items || []).forEach(function(si){
|
function renderBlueprintList() {
|
||||||
html += '<div class="bp-item" data-type="study">📚 ' + esc(si.name) + '</div>';
|
var list = document.getElementById('bp-list');
|
||||||
});
|
var empty = document.getElementById('bp-list-empty');
|
||||||
detailEl.innerHTML = html || '<div class="bp-empty">暂无记录</div>';
|
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
|
Init
|
||||||
@@ -1043,29 +1046,28 @@
|
|||||||
function init() {
|
function init() {
|
||||||
var today = new Date();
|
var today = new Date();
|
||||||
calYear = today.getFullYear();
|
calYear = today.getFullYear();
|
||||||
calMonth = today.getMonth() + 1;
|
|
||||||
|
|
||||||
var cd = document.getElementById('check-date');
|
var cd = document.getElementById('check-date');
|
||||||
var todayStr = fmtDate(today);
|
var todayWk = fmtWeek(today);
|
||||||
if (cd) cd.value = todayStr;
|
if (cd) cd.value = todayWk;
|
||||||
|
|
||||||
selectedDate = todayStr;
|
selectedWeek = todayWk;
|
||||||
initStudyPresets();
|
initStudyPresets();
|
||||||
bindAutoSave();
|
bindAutoSave();
|
||||||
lastSavedDate = todayStr;
|
lastSavedWeek = todayWk;
|
||||||
|
|
||||||
// 从页面嵌入数据获取初始统计(0 延迟)
|
// 从页面嵌入数据获取初始统计(0 延迟)
|
||||||
if (window.__INITIAL_STATS__) {
|
if (window.__INITIAL_STATS__) {
|
||||||
var s = 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-morning').textContent = s.total_morning;
|
||||||
document.getElementById('stat-study').textContent = s.total_study;
|
document.getElementById('stat-study').textContent = s.total_study;
|
||||||
calendarStatus = s.calendar || {};
|
calendarStatus = s.calendar || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCheckin();
|
loadCheckin();
|
||||||
loadStats(); // 异步刷新最新数据
|
loadStats();
|
||||||
renderCalendar(); // 同步渲染 — calendarStatus 已有值
|
renderCalendar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
|
|||||||
448
static/style.css
448
static/style.css
@@ -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,29 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 +146,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 +174,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 +220,73 @@ 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-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 {
|
.cal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -207,59 +332,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 +484,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 +615,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 ── */
|
||||||
|
|
||||||
@@ -1401,11 +1609,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; }
|
.weekly-overview { flex-direction: column; gap: 16px; text-align: center; }
|
||||||
.week-days-grid { grid-template-columns: repeat(4, 1fr); }
|
.week-days-grid { grid-template-columns: repeat(4, 1fr); }
|
||||||
.history-grid { grid-template-columns: 1fr; }
|
.history-grid { grid-template-columns: 1fr; }
|
||||||
@@ -1428,59 +1634,93 @@ body {
|
|||||||
Blueprint Panel
|
Blueprint Panel
|
||||||
═══════════════════════════════════════════ */
|
═══════════════════════════════════════════ */
|
||||||
|
|
||||||
.blueprint-mission {
|
/* Tab 筛选 */
|
||||||
font-size: 15px;
|
.bp-tabs {
|
||||||
font-weight: 600;
|
display: flex;
|
||||||
color: var(--primary);
|
gap: 4px;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 20px;
|
||||||
padding: 12px 16px;
|
border-bottom: 1.5px solid var(--border);
|
||||||
background: var(--primary-light);
|
padding-bottom: 0;
|
||||||
border-radius: var(--radius);
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
.bp-tab {
|
||||||
.blueprint-pillars {
|
display: inline-flex;
|
||||||
display: grid;
|
align-items: center;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
gap: 5px;
|
||||||
gap: 16px;
|
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; }
|
||||||
|
|
||||||
.blueprint-pillar {
|
/* 明细列表 */
|
||||||
|
.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);
|
background: var(--card);
|
||||||
border: 1.5px solid var(--border);
|
border-bottom: 0.5px solid var(--border);
|
||||||
border-radius: var(--radius);
|
transition: background 0.15s;
|
||||||
padding: 20px 16px;
|
|
||||||
text-align: center;
|
|
||||||
transition: border-color 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
}
|
||||||
.blueprint-pillar:hover {
|
.bp-list-item:hover { background: var(--bg); }
|
||||||
border-color: var(--primary);
|
.bp-pillar-dot {
|
||||||
box-shadow: 0 4px 16px rgba(74,108,247,0.08);
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.bp-item-pillar {
|
||||||
.bp-icon { font-size: 32px; margin-bottom: 8px; }
|
font-size: 12px;
|
||||||
.bp-name { font-size: 14px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
|
||||||
.bp-desc { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; line-height: 1.5; }
|
|
||||||
.bp-count { font-size: 13px; font-weight: 600; color: var(--primary); }
|
|
||||||
|
|
||||||
.bp-detail {
|
|
||||||
margin-top: 10px;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 11px;
|
|
||||||
max-height: 120px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
.bp-item {
|
|
||||||
padding: 2px 0;
|
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
white-space: nowrap;
|
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;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
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;
|
||||||
}
|
}
|
||||||
.bp-empty { color: var(--text-muted); text-align: center; padding: 6px 0; }
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.blueprint-pillars { grid-template-columns: repeat(2, 1fr); }
|
.quad-grid { grid-template-columns: 1fr; grid-template-rows: auto; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,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>
|
||||||
@@ -42,20 +42,58 @@
|
|||||||
<!-- 用户信息 -->
|
<!-- 用户信息 -->
|
||||||
<div class="sidebar-user">
|
<div class="sidebar-user">
|
||||||
<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>
|
|
||||||
<a href="/logout" class="btn-logout-icon" title="退出登录" 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>
|
||||||
|
|
||||||
<!-- 统计信息 -->
|
<!-- 功能入口 -->
|
||||||
|
<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="weekly" onclick="switchPanel('weekly')">
|
||||||
|
<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-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-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,78 +104,43 @@
|
|||||||
<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-weeks" 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>
|
||||||
@@ -147,13 +150,13 @@
|
|||||||
<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">本周最重要的 1~3 件事</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>
|
</div>
|
||||||
<div class="card card-evening">
|
<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,21 +166,23 @@
|
|||||||
<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">今天犯的错 & 改进方案(最多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">
|
||||||
<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>
|
<p class="card-desc">本周修行清单</p>
|
||||||
<div id="study-list"></div>
|
<div id="study-list"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div><!-- /daily-main -->
|
||||||
|
</div><!-- /daily-layout -->
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── 每周评分面板 ── -->
|
<!-- ── 每周评分面板 ── -->
|
||||||
@@ -276,55 +281,26 @@
|
|||||||
<!-- ── 蓝图面板 ── -->
|
<!-- ── 蓝图面板 ── -->
|
||||||
<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 class="blueprint-mission">
|
|
||||||
一辈子,干成一件事 — 用 AI 重构医疗全链路
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillars" id="blueprint-pillars">
|
|
||||||
<div class="blueprint-pillar" data-pillar="医疗服务">
|
|
||||||
<div class="bp-icon">🥼</div>
|
|
||||||
<div class="bp-name">医疗服务</div>
|
|
||||||
<div class="bp-desc">诊疗能力 · 临床路径 · 医疗质量</div>
|
|
||||||
<div class="bp-count" id="bp-count-医疗服务">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-医疗服务"></div>
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillar" data-pillar="医药营销">
|
|
||||||
<div class="bp-icon">💊</div>
|
|
||||||
<div class="bp-name">医药营销</div>
|
|
||||||
<div class="bp-desc">科普 · 科研 · 学术会议 · 患者管理</div>
|
|
||||||
<div class="bp-count" id="bp-count-医药营销">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-医药营销"></div>
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillar" data-pillar="医疗支付">
|
|
||||||
<div class="bp-icon">🛡️</div>
|
|
||||||
<div class="bp-name">医疗支付</div>
|
|
||||||
<div class="bp-desc">医保 · 商保 · 支付闭环</div>
|
|
||||||
<div class="bp-count" id="bp-count-医疗支付">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-医疗支付"></div>
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillar" data-pillar="AI 智能">
|
|
||||||
<div class="bp-icon">🤖</div>
|
|
||||||
<div class="bp-name">AI 智能</div>
|
|
||||||
<div class="bp-desc">数字人 · 智能平台 · AI 赋能</div>
|
|
||||||
<div class="bp-count" id="bp-count-AI 智能">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-AI 智能"></div>
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillar" data-pillar="公司治理">
|
|
||||||
<div class="bp-icon">🏛️</div>
|
|
||||||
<div class="bp-name">公司治理</div>
|
|
||||||
<div class="bp-desc">组织 · 人才 · 制度 · 文化</div>
|
|
||||||
<div class="bp-count" id="bp-count-公司治理">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-公司治理"></div>
|
|
||||||
</div>
|
|
||||||
<div class="blueprint-pillar" data-pillar="个人修养">
|
|
||||||
<div class="bp-icon">🎯</div>
|
|
||||||
<div class="bp-name">个人修养</div>
|
|
||||||
<div class="bp-desc">立志 · 责善 · 改过 · 勤学</div>
|
|
||||||
<div class="bp-count" id="bp-count-个人修养">—</div>
|
|
||||||
<div class="bp-detail" id="bp-detail-个人修养"></div>
|
|
||||||
</div>
|
</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>
|
||||||
|
<!-- 明细列表 -->
|
||||||
|
<div class="bp-list" id="bp-list"></div>
|
||||||
|
<div class="bp-list-empty" id="bp-list-empty" style="display:none">暂无记录</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
Reference in New Issue
Block a user