Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd90c2d799 | ||
|
|
c2cbbdb12a | ||
|
|
8223f72544 | ||
|
|
36b58a8b73 | ||
|
|
5b73fc0eae | ||
|
|
5b637e7fed | ||
|
|
350c9fb877 | ||
|
|
19ae81ae6e | ||
|
|
535af35911 | ||
|
|
fc338f426a | ||
|
|
5300ee426d | ||
|
|
8ac6f1af6b | ||
|
|
8c81fae334 | ||
|
|
67505414d9 |
195
app.py
195
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,7 +72,7 @@ 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
|
||||
@@ -137,6 +137,7 @@ def compute_stats():
|
||||
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
|
||||
})
|
||||
|
||||
@@ -186,7 +187,7 @@ def compute_stats():
|
||||
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,
|
||||
morning_items=all_morning_items,
|
||||
@@ -213,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})
|
||||
|
||||
|
||||
@@ -224,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():
|
||||
@@ -246,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():
|
||||
@@ -317,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)
|
||||
)
|
||||
|
||||
462
static/app.js
462
static/app.js
@@ -5,13 +5,13 @@
|
||||
/* ================================================================
|
||||
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 activePanel = 'daily';
|
||||
var weekOffset = 0;
|
||||
var lastSavedData = null;
|
||||
var lastSavedWeek = null;
|
||||
var selectedWeek = null;
|
||||
|
||||
/* 勤学预设项目 */
|
||||
var PRESET_STUDY_ITEMS = [
|
||||
@@ -51,18 +51,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); })
|
||||
@@ -86,7 +86,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); });
|
||||
}
|
||||
|
||||
@@ -131,91 +131,79 @@
|
||||
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();
|
||||
};
|
||||
|
||||
/** 渲染日历 */
|
||||
/** 渲染周历(4周一行 + 月份分组) */
|
||||
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-month-label').textContent =
|
||||
calYear + '年 ' + calMonth + '月';
|
||||
|
||||
var grid = document.getElementById('cal-grid');
|
||||
document.getElementById('cal-year-label').textContent = calYear + '年';
|
||||
var yearStart = new Date(calYear, 0, 1);
|
||||
var yearEnd = new Date(calYear, 11, 31);
|
||||
var weeks = [];
|
||||
var d = new Date(yearStart);
|
||||
d.setDate(d.getDate() - d.getDay() + (d.getDay() === 0 ? -6 : 1));
|
||||
var lastMonth = -1;
|
||||
var monthsList = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
while (d <= yearEnd) {
|
||||
var wk = fmtWeek(d);
|
||||
var mon = new Date(d);
|
||||
var mIdx = mon.getMonth();
|
||||
var isMonthStart = (mIdx !== lastMonth && mon.getDate() <= 7);
|
||||
weeks.push({ wk: wk, monthLabel: isMonthStart ? monthsList[mIdx] : '' });
|
||||
lastMonth = mIdx;
|
||||
d.setDate(d.getDate() + 7);
|
||||
}
|
||||
var todayWk = fmtWeek(new Date());
|
||||
var el = document.getElementById('cal-week-list');
|
||||
var html = '';
|
||||
|
||||
// 前月填充
|
||||
for (var p = startDayOfWeek - 1; p >= 0; p--) {
|
||||
html += '<div class="cal-cell other-month">' + (prevMonthLastDay - p) + '</div>';
|
||||
for (var i = 0; i < weeks.length; i++) {
|
||||
var we = weeks[i];
|
||||
var w = we.wk;
|
||||
var cls = 'cal-week-cell';
|
||||
var status = calendarStatus[w];
|
||||
if (status) cls += ' ' + status;
|
||||
if (w === todayWk) cls += ' today';
|
||||
if (w === selectedWeek) cls += ' selected';
|
||||
if (we.monthLabel) cls += ' month-start';
|
||||
var wkNum = w.split('-W')[1].replace(/^0/, '');
|
||||
html += '<div class="' + cls + '" data-week="' + w + '" data-month="' + we.monthLabel + '" title="' + w + '">' +
|
||||
'<span class="wk-num">W' + wkNum + '</span>' +
|
||||
'<span class="wk-dot"></span>' +
|
||||
'</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) {
|
||||
el.innerHTML = html;
|
||||
el.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());
|
||||
window.goToCurrentWeek = function() {
|
||||
var now = fmtWeek(new Date());
|
||||
calYear = new Date().getFullYear();
|
||||
calMonth = new Date().getMonth() + 1;
|
||||
renderCalendar();
|
||||
navigateToDate(today);
|
||||
navigateToWeek(now);
|
||||
};
|
||||
|
||||
/* ================================================================
|
||||
@@ -224,7 +212,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 || {};
|
||||
@@ -318,7 +306,7 @@
|
||||
var div = document.createElement('div');
|
||||
div.className = 'item-row';
|
||||
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>';
|
||||
container.appendChild(div);
|
||||
renumberMorning();
|
||||
@@ -410,7 +398,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 = '';
|
||||
@@ -428,8 +416,8 @@
|
||||
});
|
||||
list.innerHTML = html;
|
||||
|
||||
// 自动填充今天到选中日期的日程
|
||||
autoFillTodayEvents(data);
|
||||
// 刷新当前页
|
||||
loadCheckin();
|
||||
})
|
||||
.catch(function(){
|
||||
loading.style.display = 'none';
|
||||
@@ -439,30 +427,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';
|
||||
@@ -487,29 +451,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());
|
||||
@@ -518,26 +490,26 @@
|
||||
|
||||
/** 自动保存 — 失去焦点/勾选/删除时触发 */
|
||||
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"], select')) {
|
||||
if (e.target.matches('input[type="text"], textarea, input[type="week"], select')) {
|
||||
triggerAutoSave();
|
||||
}
|
||||
}, true);
|
||||
@@ -586,97 +558,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;
|
||||
});
|
||||
}
|
||||
@@ -699,7 +644,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();
|
||||
@@ -707,9 +654,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">' +
|
||||
@@ -723,13 +670,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();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1009,63 +956,110 @@
|
||||
Blueprint — 蓝图六板块
|
||||
================================================================ */
|
||||
|
||||
var blueprintData = null;
|
||||
var blueprintFilter = 'all';
|
||||
|
||||
function loadBlueprint() {
|
||||
apiGetStats(function(err, data) {
|
||||
if (err || !data || !data.pillar_breakdown) return;
|
||||
var pb = data.pillar_breakdown;
|
||||
PILLARS.forEach(function(p){
|
||||
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;
|
||||
// 显示详情列表
|
||||
var detailEl = document.getElementById('bp-detail-' + p.name);
|
||||
if (detailEl) {
|
||||
var html = '';
|
||||
(info.morning_items || []).forEach(function(mi){
|
||||
html += '<div class="bp-item" data-type="morning">🥼 ' + esc(mi.text) + '</div>';
|
||||
});
|
||||
(info.evening_items || []).forEach(function(ei){
|
||||
html += '<div class="bp-item" data-type="evening">🔍 ' + esc(ei.mistake || ei.improvement) + '</div>';
|
||||
});
|
||||
(info.study_items || []).forEach(function(si){
|
||||
html += '<div class="bp-item" data-type="study">📚 ' + esc(si.name) + '</div>';
|
||||
});
|
||||
detailEl.innerHTML = html || '<div class="bp-empty">暂无记录</div>';
|
||||
}
|
||||
});
|
||||
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') {
|
||||
|
||||
270
static/style.css
270
static/style.css
@@ -207,59 +207,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);
|
||||
@@ -333,15 +360,16 @@ body {
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 13px;
|
||||
gap: 14px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: all 0.15s;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
@@ -349,17 +377,21 @@ body {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-item .icon-sm {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
.nav-item:hover .icon-sm { color: var(--primary); }
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-item .icon-sm {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.nav-item.active .icon-sm { color: var(--primary); }
|
||||
|
||||
/* ── Sidebar Footer ── */
|
||||
|
||||
@@ -456,29 +488,38 @@ body {
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
.daily-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
.card.daily-card { display: none; }
|
||||
.card.daily-card.active { display: flex; }
|
||||
|
||||
/* 立志 — 左上 */
|
||||
.card-morning {
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
/* 每周打卡 Tab */
|
||||
.daily-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1.5px solid var(--border);
|
||||
}
|
||||
|
||||
/* 改过 — 左下 */
|
||||
.card-evening {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
/* 勤学 — 右列跨2行 */
|
||||
.card-study {
|
||||
grid-column: 2;
|
||||
grid-row: 1 / 3;
|
||||
.daily-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;
|
||||
}
|
||||
.daily-tab .icon-sm { opacity: 0.6; }
|
||||
.daily-tab:hover { color: var(--text); }
|
||||
.daily-tab.active { color: var(--primary); border-bottom-color: var(--primary); }
|
||||
.daily-tab.active .icon-sm { opacity: 1; }
|
||||
|
||||
/* ── Cards ── */
|
||||
|
||||
@@ -1405,7 +1446,6 @@ body {
|
||||
: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; }
|
||||
@@ -1428,59 +1468,93 @@ body {
|
||||
Blueprint Panel
|
||||
═══════════════════════════════════════════ */
|
||||
|
||||
.blueprint-mission {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 16px;
|
||||
background: var(--primary-light);
|
||||
border-radius: var(--radius);
|
||||
text-align: center;
|
||||
/* Tab 筛选 */
|
||||
.bp-tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1.5px solid var(--border);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.blueprint-pillars {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
.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; }
|
||||
|
||||
.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);
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px 16px;
|
||||
text-align: center;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
border-bottom: 0.5px solid var(--border);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.blueprint-pillar:hover {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 4px 16px rgba(74,108,247,0.08);
|
||||
.bp-list-item:hover { background: var(--bg); }
|
||||
.bp-pillar-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bp-icon { font-size: 32px; margin-bottom: 8px; }
|
||||
.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;
|
||||
.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;
|
||||
}
|
||||
.bp-empty { color: var(--text-muted); text-align: center; padding: 6px 0; }
|
||||
|
||||
@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 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>
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="sidebar-stats" id="sidebar-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-days">--</span>
|
||||
<span class="stat-label">打卡天</span>
|
||||
<span class="stat-label">打卡周</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-num" id="stat-morning">--</span>
|
||||
@@ -67,21 +67,18 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日历 -->
|
||||
<!-- 日历(周视图) -->
|
||||
<div class="calendar-widget">
|
||||
<div class="cal-header">
|
||||
<button class="cal-nav" onclick="changeCalMonth(-1)">
|
||||
<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-month-label">2026年 6月</span>
|
||||
<button class="cal-nav" onclick="changeCalMonth(1)">
|
||||
<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-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-week-list" id="cal-week-list"></div>
|
||||
<div class="cal-legend">
|
||||
<span class="legend-dot pass"></span> 达标
|
||||
<span class="legend-dot fail"></span> 未达标
|
||||
@@ -93,7 +90,7 @@
|
||||
<nav class="sidebar-nav">
|
||||
<a class="nav-item active" data-panel="daily" onclick="switchPanel('daily')">
|
||||
<svg class="icon-sm"><use href="#icon-calendar"/></svg>
|
||||
每日打卡
|
||||
每周打卡
|
||||
</a>
|
||||
<a class="nav-item" data-panel="weekly" onclick="switchPanel('weekly')">
|
||||
<svg class="icon-sm"><use href="#icon-chart-bar"/></svg>
|
||||
@@ -108,16 +105,16 @@
|
||||
历史记录
|
||||
</a>
|
||||
<a class="nav-item" data-panel="blueprint" onclick="switchPanel('blueprint')">
|
||||
<svg class="icon-sm"><use href="#icon-star"/></svg>
|
||||
蓝图
|
||||
<svg class="icon-sm"><use href="#icon-sun"/></svg>
|
||||
人生蓝图
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- 底部新建按钮 -->
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn-new-day" onclick="goToday()">
|
||||
<button class="btn-new-day" onclick="goToCurrentWeek()">
|
||||
<svg class="icon-sm"><use href="#icon-sun"/></svg>
|
||||
回到今天
|
||||
回到本周
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -125,19 +122,30 @@
|
||||
<!-- ═══ 右侧主内容区 ═══ -->
|
||||
<main class="main-content">
|
||||
|
||||
<!-- ── 每日打卡面板 ── -->
|
||||
<!-- ── 每周打卡面板 ── -->
|
||||
<section class="panel active" id="panel-daily">
|
||||
<div class="panel-header">
|
||||
<h2>每日打卡</h2>
|
||||
<h2>每周打卡</h2>
|
||||
<div class="panel-header-right">
|
||||
<input type="date" id="check-date" class="check-date-input" onchange="loadCheckin()">
|
||||
<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-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">
|
||||
<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>
|
||||
@@ -147,13 +155,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>
|
||||
@@ -163,18 +171,18 @@
|
||||
<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>
|
||||
@@ -276,55 +284,26 @@
|
||||
<!-- ── 蓝图面板 ── -->
|
||||
<section class="panel" id="panel-blueprint">
|
||||
<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>
|
||||
<!-- 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>
|
||||
|
||||
Reference in New Issue
Block a user