14 Commits

Author SHA1 Message Date
mac
cd90c2d799 v2.0.5 — 侧边栏菜单美化:大图标+蓝色激活态 2026-07-24 17:11:46 +08:00
mac
c2cbbdb12a v2.0.4 — 修复Tab切换:提高daily-card优先级覆盖card的display:flex 2026-07-24 17:10:37 +08:00
mac
8223f72544 v2.0.3 — 周历改4周一行,月起始加月份标签 2026-07-24 16:53:06 +08:00
mac
36b58a8b73 v2.0.2 — 每周打卡三卡片改为Tab切换 2026-07-24 16:34:20 +08:00
mac
5b73fc0eae v2.0.1 — 修复 database.py 缺少聚合函数 2026-07-24 16:23:07 +08:00
mac
5b637e7fed v2.0.0 — 日打卡彻底改为周打卡 2026-07-24 16:20:40 +08:00
mac
350c9fb877 v1.5.3 — 日历同步按日期自动填充
- 后端 api_calendar_sync_all 查询后自动去重保存到各日期 checkin
- 不再只填充当前选中的日期
- 弹窗显示已填充天数
2026-06-05 12:19:22 +08:00
mac
19ae81ae6e v1.5.2 — 责善显示改进方案
- 责善列表项追加 → 反思内容
- 红色标注区分改进方案
2026-06-04 17:46:44 +08:00
mac
535af35911 v1.5.1 — 蓝图重命名为人生蓝图,图标改为太阳 2026-06-04 16:49:27 +08:00
mac
fc338f426a v1.5.0 — 蓝图 VI 统一
- 移除头部 mission 文字和六板块卡片
- Tab 改用 SVG 图标(bolt/放大镜/书本/列表)
- 板块用彩色圆点标识(六色 VI)
- 整体风格对齐天机阁设计系统
2026-06-04 16:43:08 +08:00
mac
5300ee426d v1.4.6 — 备注左对齐 2026-06-04 16:39:58 +08:00
mac
8ac6f1af6b v1.4.5 — 蓝图去统计数+勤学显示备注
- 六板块卡片移除统计数字,仅保留 emoji+名称
- 勤学项在列表中显示备注 note
2026-06-04 16:38:54 +08:00
mac
8c81fae334 v1.4.4 — 蓝图改为 Tab 筛选+明细列表
- 顶栏六板块卡片只显示统计数
- 新增立志/责善/勤学/全部四个 Tab
- 明细列表显示每条记录的板块+类型+内容
- 类型标签颜色区分(立志黄/责善红/勤学绿)
2026-06-04 16:19:50 +08:00
mac
67505414d9 v1.4.3 — 修复蓝图无数据: apiGetStats 取错字段 2026-06-04 16:01:09 +08:00
5 changed files with 675 additions and 485 deletions

195
app.py
View File

@@ -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})
# ── 启动 ──────────────────────────────────────────────

View File

@@ -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)
)

View File

@@ -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') {

View File

@@ -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; }
}

View File

@@ -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">今天犯的错 &amp; 改进方案最多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>