Codex commited on
Commit ·
0855e12
1
Parent(s): 3b5b6d8
Release v0.2.0 weekly planner polish
Browse files- app.py +110 -3
- static/app.js +2 -6
- static/planner.js +541 -43
- static/v020.css +332 -72
- templates/index.html +15 -15
app.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
import hmac
|
| 2 |
import os
|
| 3 |
-
from datetime import date, datetime
|
| 4 |
from pathlib import Path
|
| 5 |
from urllib.parse import urlparse
|
| 6 |
from zoneinfo import ZoneInfo
|
|
@@ -68,6 +68,7 @@ WEEKDAYS = [
|
|
| 68 |
"星期六",
|
| 69 |
"星期日",
|
| 70 |
]
|
|
|
|
| 71 |
|
| 72 |
|
| 73 |
def get_password() -> str:
|
|
@@ -260,6 +261,15 @@ def parse_task_schedule_payload(payload: dict, settings: dict) -> dict | None:
|
|
| 260 |
}
|
| 261 |
|
| 262 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
def course_occurs_on(course: dict, current_week: int, selected_date: date) -> bool:
|
| 264 |
if current_week < 1:
|
| 265 |
return False
|
|
@@ -341,6 +351,103 @@ def build_planner_payload(selected_date: date) -> dict:
|
|
| 341 |
}
|
| 342 |
|
| 343 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
@app.context_processor
|
| 345 |
def inject_globals():
|
| 346 |
return {
|
|
@@ -352,7 +459,7 @@ def inject_globals():
|
|
| 352 |
def index():
|
| 353 |
login_required = request.args.get("login") == "required"
|
| 354 |
next_path = safe_next_path(request.args.get("next"))
|
| 355 |
-
planner_payload =
|
| 356 |
return render_template(
|
| 357 |
"index.html",
|
| 358 |
categories=store.list_categories(),
|
|
@@ -381,7 +488,7 @@ def planner():
|
|
| 381 |
selected_date = parse_iso_date(request.args.get("date"))
|
| 382 |
except ValueError as exc:
|
| 383 |
return jsonify({"ok": False, "error": str(exc)}), 400
|
| 384 |
-
return jsonify({"ok": True, "planner":
|
| 385 |
|
| 386 |
|
| 387 |
@app.post("/api/login")
|
|
|
|
| 1 |
import hmac
|
| 2 |
import os
|
| 3 |
+
from datetime import date, datetime, timedelta
|
| 4 |
from pathlib import Path
|
| 5 |
from urllib.parse import urlparse
|
| 6 |
from zoneinfo import ZoneInfo
|
|
|
|
| 68 |
"星期六",
|
| 69 |
"星期日",
|
| 70 |
]
|
| 71 |
+
WEEKDAY_SHORT = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
|
| 72 |
|
| 73 |
|
| 74 |
def get_password() -> str:
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
|
| 264 |
+
def get_week_start(target_date: date) -> date:
|
| 265 |
+
return target_date - timedelta(days=target_date.isoweekday() - 1)
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
def get_academic_week(target_date: date, semester_start: date) -> int:
|
| 269 |
+
delta_days = (target_date - semester_start).days
|
| 270 |
+
return (delta_days // 7) + 1 if delta_days >= 0 else 0
|
| 271 |
+
|
| 272 |
+
|
| 273 |
def course_occurs_on(course: dict, current_week: int, selected_date: date) -> bool:
|
| 274 |
if current_week < 1:
|
| 275 |
return False
|
|
|
|
| 351 |
}
|
| 352 |
|
| 353 |
|
| 354 |
+
def build_week_planner_payload(selected_date: date) -> dict:
|
| 355 |
+
settings = store.get_schedule_settings()
|
| 356 |
+
semester_start = date.fromisoformat(settings["semester_start"])
|
| 357 |
+
week_start = get_week_start(selected_date)
|
| 358 |
+
week_end = week_start + timedelta(days=6)
|
| 359 |
+
academic_week = get_academic_week(selected_date, semester_start)
|
| 360 |
+
task_list = [serialize_task(task) for task in store.list_tasks()]
|
| 361 |
+
|
| 362 |
+
week_days = []
|
| 363 |
+
week_day_set = set()
|
| 364 |
+
today = beijing_now().date()
|
| 365 |
+
for offset in range(7):
|
| 366 |
+
current_day = week_start + timedelta(days=offset)
|
| 367 |
+
iso_day = current_day.isoformat()
|
| 368 |
+
week_day_set.add(iso_day)
|
| 369 |
+
week_days.append(
|
| 370 |
+
{
|
| 371 |
+
"iso": iso_day,
|
| 372 |
+
"label": WEEKDAYS[current_day.weekday()],
|
| 373 |
+
"short_label": WEEKDAY_SHORT[current_day.weekday()],
|
| 374 |
+
"month_day": current_day.strftime("%m/%d"),
|
| 375 |
+
"day_of_month": current_day.day,
|
| 376 |
+
"is_today": current_day == today,
|
| 377 |
+
}
|
| 378 |
+
)
|
| 379 |
+
|
| 380 |
+
scheduled_items: list[dict] = []
|
| 381 |
+
for task in task_list:
|
| 382 |
+
schedule = task.get("schedule")
|
| 383 |
+
if schedule and schedule.get("date") in week_day_set:
|
| 384 |
+
scheduled_items.append(
|
| 385 |
+
{
|
| 386 |
+
"id": f"planner_{task['id']}",
|
| 387 |
+
"kind": "task",
|
| 388 |
+
"task_id": task["id"],
|
| 389 |
+
"title": task["title"],
|
| 390 |
+
"category_name": task["category_name"],
|
| 391 |
+
"completed": task.get("completed", False),
|
| 392 |
+
"due_at": task["due_at"],
|
| 393 |
+
"progress_percent": task["progress_percent"],
|
| 394 |
+
"date": schedule["date"],
|
| 395 |
+
"start_time": schedule["start_time"],
|
| 396 |
+
"end_time": schedule["end_time"],
|
| 397 |
+
"locked": False,
|
| 398 |
+
}
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
courses = store.list_courses()
|
| 402 |
+
for offset in range(7):
|
| 403 |
+
current_day = week_start + timedelta(days=offset)
|
| 404 |
+
current_week = get_academic_week(current_day, semester_start)
|
| 405 |
+
for course in courses:
|
| 406 |
+
if course_occurs_on(course, current_week, current_day):
|
| 407 |
+
scheduled_items.append(
|
| 408 |
+
{
|
| 409 |
+
"id": f"planner_{course['id']}_{current_day.isoformat()}",
|
| 410 |
+
"kind": "course",
|
| 411 |
+
"course_id": course["id"],
|
| 412 |
+
"title": course["title"],
|
| 413 |
+
"location": course.get("location", ""),
|
| 414 |
+
"date": current_day.isoformat(),
|
| 415 |
+
"start_time": course["start_time"],
|
| 416 |
+
"end_time": course["end_time"],
|
| 417 |
+
"locked": True,
|
| 418 |
+
"week_pattern": course["week_pattern"],
|
| 419 |
+
"color": course["color"],
|
| 420 |
+
"start_week": course["start_week"],
|
| 421 |
+
"end_week": course["end_week"],
|
| 422 |
+
}
|
| 423 |
+
)
|
| 424 |
+
|
| 425 |
+
scheduled_items.sort(
|
| 426 |
+
key=lambda item: (
|
| 427 |
+
item.get("date", ""),
|
| 428 |
+
item["start_time"],
|
| 429 |
+
item["end_time"],
|
| 430 |
+
item["kind"],
|
| 431 |
+
)
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
return {
|
| 435 |
+
"selected_date": selected_date.isoformat(),
|
| 436 |
+
"weekday": WEEKDAYS[selected_date.weekday()],
|
| 437 |
+
"week_start": week_start.isoformat(),
|
| 438 |
+
"week_end": week_end.isoformat(),
|
| 439 |
+
"week_days": week_days,
|
| 440 |
+
"week_range_label": f"{week_start.strftime('%m/%d')} - {week_end.strftime('%m/%d')}",
|
| 441 |
+
"academic_week": academic_week,
|
| 442 |
+
"academic_label": f"第 {academic_week} 周" if academic_week > 0 else "开学前",
|
| 443 |
+
"settings": settings,
|
| 444 |
+
"time_slots": CLASS_PERIODS,
|
| 445 |
+
"major_blocks": MAJOR_BLOCKS,
|
| 446 |
+
"tasks": task_list,
|
| 447 |
+
"scheduled_items": scheduled_items,
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
|
| 451 |
@app.context_processor
|
| 452 |
def inject_globals():
|
| 453 |
return {
|
|
|
|
| 459 |
def index():
|
| 460 |
login_required = request.args.get("login") == "required"
|
| 461 |
next_path = safe_next_path(request.args.get("next"))
|
| 462 |
+
planner_payload = build_week_planner_payload(beijing_now().date())
|
| 463 |
return render_template(
|
| 464 |
"index.html",
|
| 465 |
categories=store.list_categories(),
|
|
|
|
| 488 |
selected_date = parse_iso_date(request.args.get("date"))
|
| 489 |
except ValueError as exc:
|
| 490 |
return jsonify({"ok": False, "error": str(exc)}), 400
|
| 491 |
+
return jsonify({"ok": True, "planner": build_week_planner_payload(selected_date)})
|
| 492 |
|
| 493 |
|
| 494 |
@app.post("/api/login")
|
static/app.js
CHANGED
|
@@ -551,12 +551,8 @@
|
|
| 551 |
logoutButton.addEventListener("click", handleLogout);
|
| 552 |
}
|
| 553 |
|
| 554 |
-
if (!state.authenticated) {
|
| 555 |
-
|
| 556 |
-
if (state.loginRequired || !window.sessionStorage.getItem(promptedKey)) {
|
| 557 |
-
openModal(loginModal);
|
| 558 |
-
window.sessionStorage.setItem(promptedKey, "1");
|
| 559 |
-
}
|
| 560 |
}
|
| 561 |
|
| 562 |
renderClock();
|
|
|
|
| 551 |
logoutButton.addEventListener("click", handleLogout);
|
| 552 |
}
|
| 553 |
|
| 554 |
+
if (!state.authenticated && state.loginRequired) {
|
| 555 |
+
openModal(loginModal);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
}
|
| 557 |
|
| 558 |
renderClock();
|
static/planner.js
CHANGED
|
@@ -8,16 +8,16 @@
|
|
| 8 |
dragTaskId: null,
|
| 9 |
interaction: null,
|
| 10 |
suppressClickUntil: 0,
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
-
const
|
| 14 |
-
const
|
| 15 |
-
const
|
| 16 |
-
const
|
|
|
|
| 17 |
const SNAP_MINUTES = 5;
|
| 18 |
const MIN_DURATION = 15;
|
| 19 |
-
const MIN_BLOCK_HEIGHT = MIN_DURATION * PIXELS_PER_MINUTE;
|
| 20 |
-
const AXIS_LABEL_MIN_GAP = 28;
|
| 21 |
const CLICK_SUPPRESS_MS = 260;
|
| 22 |
|
| 23 |
const pageTrack = document.getElementById("pageTrack");
|
|
@@ -33,10 +33,11 @@
|
|
| 33 |
const plannerTaskCount = document.getElementById("plannerTaskCount");
|
| 34 |
const plannerTaskPool = document.getElementById("plannerTaskPool");
|
| 35 |
const plannerTimeline = document.getElementById("plannerTimeline");
|
|
|
|
| 36 |
const loginModal = document.getElementById("loginModal");
|
| 37 |
const toastStack = document.getElementById("toastStack");
|
| 38 |
|
| 39 |
-
if (!pageTrack || !plannerDateInput || !plannerPrevDay || !plannerNextDay || !plannerTaskPool || !plannerTimeline) {
|
| 40 |
return;
|
| 41 |
}
|
| 42 |
|
|
@@ -139,9 +140,13 @@
|
|
| 139 |
}).format(date).replace(",", "");
|
| 140 |
}
|
| 141 |
|
| 142 |
-
function
|
| 143 |
-
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
|
| 147 |
function getPlannerConfig() {
|
|
@@ -161,8 +166,34 @@
|
|
| 161 |
};
|
| 162 |
}
|
| 163 |
|
| 164 |
-
function
|
| 165 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
function getCanvasRect() {
|
|
@@ -177,10 +208,26 @@
|
|
| 177 |
return dayStart;
|
| 178 |
}
|
| 179 |
const offsetY = clamp(clientY - rect.top, 0, rect.height);
|
| 180 |
-
const minutes = dayStart + (offsetY /
|
| 181 |
return clamp(snapMinutes(minutes), dayStart, dayEnd);
|
| 182 |
}
|
| 183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
function getTaskById(taskId) {
|
| 185 |
return (state.planner.tasks || []).find((task) => task.id === taskId) || null;
|
| 186 |
}
|
|
@@ -193,15 +240,17 @@
|
|
| 193 |
}
|
| 194 |
|
| 195 |
function minutesToPixels(minutes) {
|
| 196 |
-
return minutes *
|
| 197 |
}
|
| 198 |
|
| 199 |
function timelinePixels(minutes) {
|
| 200 |
-
return
|
| 201 |
}
|
| 202 |
|
| 203 |
function getBlockHeight(startMinutes, endMinutes) {
|
| 204 |
-
|
|
|
|
|
|
|
| 205 |
}
|
| 206 |
|
| 207 |
function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
|
|
@@ -211,15 +260,31 @@
|
|
| 211 |
return height;
|
| 212 |
}
|
| 213 |
|
| 214 |
-
function
|
| 215 |
-
const
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return height;
|
| 219 |
}
|
| 220 |
|
| 221 |
function getPointerDeltaMinutes(pointerStartY, clientY) {
|
| 222 |
-
return snapMinutes((clientY - pointerStartY) /
|
| 223 |
}
|
| 224 |
|
| 225 |
function formatLessonLabel(index) {
|
|
@@ -230,6 +295,7 @@
|
|
| 230 |
const axisMinutes = new Set([getPlannerConfig().dayStart, getPlannerConfig().dayEnd]);
|
| 231 |
(state.planner.time_slots || []).forEach((slot) => {
|
| 232 |
axisMinutes.add(toMinutes(slot.start));
|
|
|
|
| 233 |
});
|
| 234 |
return Array.from(axisMinutes).sort((left, right) => left - right);
|
| 235 |
}
|
|
@@ -248,7 +314,19 @@
|
|
| 248 |
suppressRecentClicks();
|
| 249 |
}
|
| 250 |
|
| 251 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
state.activePage = clamp(index, 0, 1);
|
| 253 |
pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
|
| 254 |
pageSlides.forEach((slide, slideIndex) => {
|
|
@@ -257,11 +335,81 @@
|
|
| 257 |
document.querySelectorAll(".story-tab").forEach((tab) => {
|
| 258 |
tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
|
| 259 |
});
|
|
|
|
|
|
|
|
|
|
| 260 |
if (state.activePage === 1) {
|
| 261 |
loadPlanner(state.selectedDate, true);
|
| 262 |
}
|
| 263 |
}
|
| 264 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
function decorateItems(items) {
|
| 266 |
const prepared = (items || [])
|
| 267 |
.map((item) => ({
|
|
@@ -321,7 +469,7 @@
|
|
| 321 |
if (!line) {
|
| 322 |
return;
|
| 323 |
}
|
| 324 |
-
const { dayStart, dayEnd
|
| 325 |
const parts = new Intl.DateTimeFormat("en-CA", {
|
| 326 |
timeZone: "Asia/Shanghai",
|
| 327 |
year: "numeric",
|
|
@@ -341,27 +489,31 @@
|
|
| 341 |
|
| 342 |
const today = `${map.year}-${map.month}-${map.day}`;
|
| 343 |
const currentMinutes = (Number(map.hour) * 60) + Number(map.minute);
|
|
|
|
| 344 |
|
| 345 |
-
if (
|
| 346 |
line.style.display = "none";
|
| 347 |
return;
|
| 348 |
}
|
| 349 |
|
|
|
|
| 350 |
line.style.display = "block";
|
| 351 |
-
line.style.left = `${
|
| 352 |
-
line.style.
|
| 353 |
line.style.top = `${timelinePixels(currentMinutes - dayStart)}px`;
|
| 354 |
}
|
| 355 |
|
| 356 |
function renderTaskPool() {
|
| 357 |
-
const tasks = (state.planner.tasks || [])
|
|
|
|
|
|
|
| 358 |
plannerTaskCount.textContent = `${tasks.length} 项`;
|
| 359 |
|
| 360 |
if (!tasks.length) {
|
| 361 |
plannerTaskPool.innerHTML = `
|
| 362 |
<div class="planner-empty">
|
| 363 |
<p>目前没有可安排的任务</p>
|
| 364 |
-
<span>先回到第一页添加待办,再把它拖到
|
| 365 |
</div>
|
| 366 |
`;
|
| 367 |
return;
|
|
@@ -376,9 +528,9 @@
|
|
| 376 |
<div class="planner-task-tags">
|
| 377 |
<span>截止 ${formatLocalDateTime(task.due_at)}</span>
|
| 378 |
<span>进度 ${Math.round(task.progress_percent || 0)}%</span>
|
| 379 |
-
<span>${task.schedule ? `${task.schedule.date} · ${task.schedule.start_time}-${task.schedule.end_time}` : "未
|
| 380 |
</div>
|
| 381 |
-
${task.schedule && state.authenticated ? `<button class="planner-task-clear" type="button" data-clear-schedule="${task.id}">移出
|
| 382 |
</article>
|
| 383 |
`).join("");
|
| 384 |
}
|
|
@@ -626,6 +778,323 @@
|
|
| 626 |
updateNowLine();
|
| 627 |
}
|
| 628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
function renderPlanner() {
|
| 630 |
state.selectedDate = state.planner.selected_date || state.selectedDate;
|
| 631 |
plannerDateInput.value = state.selectedDate;
|
|
@@ -645,7 +1114,7 @@
|
|
| 645 |
});
|
| 646 |
state.planner = payload.planner;
|
| 647 |
state.selectedDate = payload.planner.selected_date;
|
| 648 |
-
|
| 649 |
} catch (error) {
|
| 650 |
if (!silent) {
|
| 651 |
showToast(error.message, "error");
|
|
@@ -721,7 +1190,9 @@
|
|
| 721 |
const deltaMinutes = getPointerDeltaMinutes(state.interaction.pointerStartY, event.clientY);
|
| 722 |
|
| 723 |
if (state.interaction.mode === "move") {
|
|
|
|
| 724 |
const duration = state.interaction.initialEndMinutes - state.interaction.initialStartMinutes;
|
|
|
|
| 725 |
const startMinutes = clamp(
|
| 726 |
state.interaction.initialStartMinutes + deltaMinutes,
|
| 727 |
dayStart,
|
|
@@ -736,6 +1207,7 @@
|
|
| 736 |
dayStart,
|
| 737 |
state.interaction.initialEndMinutes - MIN_DURATION
|
| 738 |
);
|
|
|
|
| 739 |
state.interaction.startMinutes = startMinutes;
|
| 740 |
state.interaction.endMinutes = state.interaction.initialEndMinutes;
|
| 741 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
|
@@ -745,17 +1217,17 @@
|
|
| 745 |
state.interaction.initialStartMinutes + MIN_DURATION,
|
| 746 |
dayEnd
|
| 747 |
);
|
|
|
|
| 748 |
state.interaction.startMinutes = state.interaction.initialStartMinutes;
|
| 749 |
state.interaction.endMinutes = endMinutes;
|
| 750 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 751 |
}
|
| 752 |
|
| 753 |
-
updateEventLayout(
|
| 754 |
-
state.interaction.
|
| 755 |
-
state.interaction.startMinutes,
|
| 756 |
-
state.interaction.endMinutes,
|
| 757 |
-
|
| 758 |
-
);
|
| 759 |
updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
|
| 760 |
});
|
| 761 |
|
|
@@ -789,6 +1261,20 @@
|
|
| 789 |
.catch((error) => showToast(error.message, "error"));
|
| 790 |
}
|
| 791 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
function finishInteraction(event) {
|
| 793 |
if (!state.interaction) {
|
| 794 |
return;
|
|
@@ -805,13 +1291,15 @@
|
|
| 805 |
finishPlannerInteraction();
|
| 806 |
|
| 807 |
if (
|
|
|
|
|
|
|
| 808 |
current.startMinutes === current.initialStartMinutes
|
| 809 |
&& current.endMinutes === current.initialEndMinutes
|
| 810 |
) {
|
| 811 |
return;
|
| 812 |
}
|
| 813 |
|
| 814 |
-
|
| 815 |
}
|
| 816 |
|
| 817 |
document.addEventListener("pointerup", finishInteraction);
|
|
@@ -825,11 +1313,11 @@
|
|
| 825 |
});
|
| 826 |
|
| 827 |
plannerPrevDay.addEventListener("click", () => {
|
| 828 |
-
loadPlanner(shiftDate(state.selectedDate, -
|
| 829 |
});
|
| 830 |
|
| 831 |
plannerNextDay.addEventListener("click", () => {
|
| 832 |
-
loadPlanner(shiftDate(state.selectedDate,
|
| 833 |
});
|
| 834 |
|
| 835 |
window.setInterval(() => {
|
|
@@ -839,7 +1327,13 @@
|
|
| 839 |
}, 45000);
|
| 840 |
|
| 841 |
window.setInterval(updateNowLine, 60000);
|
| 842 |
-
window.addEventListener("resize",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 843 |
|
| 844 |
document.addEventListener("visibilitychange", () => {
|
| 845 |
if (document.visibilityState === "visible" && state.activePage === 1) {
|
|
@@ -847,6 +1341,10 @@
|
|
| 847 |
}
|
| 848 |
});
|
| 849 |
|
| 850 |
-
|
| 851 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 852 |
})();
|
|
|
|
| 8 |
dragTaskId: null,
|
| 9 |
interaction: null,
|
| 10 |
suppressClickUntil: 0,
|
| 11 |
+
pixelsPerMinute: 0.58,
|
| 12 |
};
|
| 13 |
|
| 14 |
+
const DEFAULT_PIXELS_PER_MINUTE = 0.58;
|
| 15 |
+
const WEEK_HEADER_HEIGHT = 54;
|
| 16 |
+
const AXIS_WIDTH = 78;
|
| 17 |
+
const SLOT_WIDTH = 108;
|
| 18 |
+
const CANVAS_GAP = 12;
|
| 19 |
const SNAP_MINUTES = 5;
|
| 20 |
const MIN_DURATION = 15;
|
|
|
|
|
|
|
| 21 |
const CLICK_SUPPRESS_MS = 260;
|
| 22 |
|
| 23 |
const pageTrack = document.getElementById("pageTrack");
|
|
|
|
| 33 |
const plannerTaskCount = document.getElementById("plannerTaskCount");
|
| 34 |
const plannerTaskPool = document.getElementById("plannerTaskPool");
|
| 35 |
const plannerTimeline = document.getElementById("plannerTimeline");
|
| 36 |
+
const timelineScroll = document.getElementById("timelineScroll");
|
| 37 |
const loginModal = document.getElementById("loginModal");
|
| 38 |
const toastStack = document.getElementById("toastStack");
|
| 39 |
|
| 40 |
+
if (!pageTrack || !plannerDateInput || !plannerPrevDay || !plannerNextDay || !plannerTaskPool || !plannerTimeline || !timelineScroll) {
|
| 41 |
return;
|
| 42 |
}
|
| 43 |
|
|
|
|
| 140 |
}).format(date).replace(",", "");
|
| 141 |
}
|
| 142 |
|
| 143 |
+
function formatWeekRange(weekStart, weekEnd) {
|
| 144 |
+
if (!weekStart || !weekEnd) {
|
| 145 |
+
return "";
|
| 146 |
+
}
|
| 147 |
+
const [startYear, startMonth, startDay] = String(weekStart).split("-").map(Number);
|
| 148 |
+
const [endYear, endMonth, endDay] = String(weekEnd).split("-").map(Number);
|
| 149 |
+
return `${startYear}.${String(startMonth).padStart(2, "0")}.${String(startDay).padStart(2, "0")} - ${endYear}.${String(endMonth).padStart(2, "0")}.${String(endDay).padStart(2, "0")}`;
|
| 150 |
}
|
| 151 |
|
| 152 |
function getPlannerConfig() {
|
|
|
|
| 166 |
};
|
| 167 |
}
|
| 168 |
|
| 169 |
+
function getWeekDays() {
|
| 170 |
+
return state.planner.week_days || [];
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function getWeekDayIndex(dateIso) {
|
| 174 |
+
return getWeekDays().findIndex((day) => day.iso === dateIso);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
function getWeekDayMeta(dateIso) {
|
| 178 |
+
return getWeekDays().find((day) => day.iso === dateIso) || null;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function computePlannerScale() {
|
| 182 |
+
const { totalMinutes } = getPlannerConfig();
|
| 183 |
+
const rect = timelineScroll.getBoundingClientRect();
|
| 184 |
+
const viewportAvailable = Math.max(Math.floor(window.innerHeight - rect.top - 18), 360);
|
| 185 |
+
const measuredHeight = Math.floor(timelineScroll.clientHeight || viewportAvailable);
|
| 186 |
+
const frameHeight = Math.max(Math.min(measuredHeight, viewportAvailable), 360);
|
| 187 |
+
const bodyHeight = Math.max(frameHeight - WEEK_HEADER_HEIGHT - 4, 300);
|
| 188 |
+
state.pixelsPerMinute = bodyHeight / totalMinutes;
|
| 189 |
+
return {
|
| 190 |
+
frameHeight,
|
| 191 |
+
bodyHeight,
|
| 192 |
+
};
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
function getPixelsPerMinute() {
|
| 196 |
+
return state.pixelsPerMinute || DEFAULT_PIXELS_PER_MINUTE;
|
| 197 |
}
|
| 198 |
|
| 199 |
function getCanvasRect() {
|
|
|
|
| 208 |
return dayStart;
|
| 209 |
}
|
| 210 |
const offsetY = clamp(clientY - rect.top, 0, rect.height);
|
| 211 |
+
const minutes = dayStart + (offsetY / getPixelsPerMinute());
|
| 212 |
return clamp(snapMinutes(minutes), dayStart, dayEnd);
|
| 213 |
}
|
| 214 |
|
| 215 |
+
function clientPointToSchedule(clientX, clientY) {
|
| 216 |
+
const rect = getCanvasRect();
|
| 217 |
+
const days = getWeekDays();
|
| 218 |
+
if (!rect || !days.length) {
|
| 219 |
+
return null;
|
| 220 |
+
}
|
| 221 |
+
const relativeX = clamp(clientX - rect.left, 0, Math.max(rect.width - 1, 0));
|
| 222 |
+
const columnWidth = rect.width / days.length;
|
| 223 |
+
const dayIndex = clamp(Math.floor(relativeX / columnWidth), 0, days.length - 1);
|
| 224 |
+
return {
|
| 225 |
+
date: days[dayIndex].iso,
|
| 226 |
+
minutes: clientYToMinutes(clientY),
|
| 227 |
+
dayIndex,
|
| 228 |
+
};
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
function getTaskById(taskId) {
|
| 232 |
return (state.planner.tasks || []).find((task) => task.id === taskId) || null;
|
| 233 |
}
|
|
|
|
| 240 |
}
|
| 241 |
|
| 242 |
function minutesToPixels(minutes) {
|
| 243 |
+
return Math.round(minutes * getPixelsPerMinute());
|
| 244 |
}
|
| 245 |
|
| 246 |
function timelinePixels(minutes) {
|
| 247 |
+
return minutesToPixels(minutes);
|
| 248 |
}
|
| 249 |
|
| 250 |
function getBlockHeight(startMinutes, endMinutes) {
|
| 251 |
+
const scaledHeight = timelinePixels(endMinutes - startMinutes);
|
| 252 |
+
const minimumHeight = Math.max(28, Math.round(getPixelsPerMinute() * MIN_DURATION));
|
| 253 |
+
return Math.max(scaledHeight, minimumHeight);
|
| 254 |
}
|
| 255 |
|
| 256 |
function setBlockBounds(block, startMinutes, endMinutes, dayStart) {
|
|
|
|
| 260 |
return height;
|
| 261 |
}
|
| 262 |
|
| 263 |
+
function setBlockHorizontalBounds(block, dateIso, overlapIndex = 0, overlapCount = 1) {
|
| 264 |
+
const days = getWeekDays();
|
| 265 |
+
const dayIndex = getWeekDayIndex(dateIso);
|
| 266 |
+
if (dayIndex < 0 || !days.length) {
|
| 267 |
+
return;
|
| 268 |
+
}
|
| 269 |
+
const dayWidthPercent = 100 / days.length;
|
| 270 |
+
const segmentWidth = dayWidthPercent / overlapCount;
|
| 271 |
+
const leftPercent = (dayIndex * dayWidthPercent) + (overlapIndex * segmentWidth);
|
| 272 |
+
block.style.left = `calc(${leftPercent}% + 4px)`;
|
| 273 |
+
block.style.width = `calc(${segmentWidth}% - 8px)`;
|
| 274 |
+
block.dataset.eventDate = dateIso;
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
function updateEventLayout(block, item, dayStart, overlapIndex = 0, overlapCount = 1) {
|
| 278 |
+
const height = setBlockBounds(block, item.startMinutes, item.endMinutes, dayStart);
|
| 279 |
+
setBlockHorizontalBounds(block, item.date, overlapIndex, overlapCount);
|
| 280 |
+
block.classList.toggle("is-compact", height < 66);
|
| 281 |
+
block.classList.toggle("is-tight", height < 40);
|
| 282 |
+
block.classList.toggle("is-micro", height < 24);
|
| 283 |
return height;
|
| 284 |
}
|
| 285 |
|
| 286 |
function getPointerDeltaMinutes(pointerStartY, clientY) {
|
| 287 |
+
return snapMinutes((clientY - pointerStartY) / getPixelsPerMinute());
|
| 288 |
}
|
| 289 |
|
| 290 |
function formatLessonLabel(index) {
|
|
|
|
| 295 |
const axisMinutes = new Set([getPlannerConfig().dayStart, getPlannerConfig().dayEnd]);
|
| 296 |
(state.planner.time_slots || []).forEach((slot) => {
|
| 297 |
axisMinutes.add(toMinutes(slot.start));
|
| 298 |
+
axisMinutes.add(toMinutes(slot.end));
|
| 299 |
});
|
| 300 |
return Array.from(axisMinutes).sort((left, right) => left - right);
|
| 301 |
}
|
|
|
|
| 314 |
suppressRecentClicks();
|
| 315 |
}
|
| 316 |
|
| 317 |
+
function getPageIndexFromHash() {
|
| 318 |
+
const hash = String(window.location.hash || "").toLowerCase();
|
| 319 |
+
return hash === "#planner" || hash === "#page2" || hash === "#week" ? 1 : 0;
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
function syncPageHash(index) {
|
| 323 |
+
const nextHash = index === 1 ? "#planner" : "#home";
|
| 324 |
+
if (window.location.hash !== nextHash) {
|
| 325 |
+
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}${nextHash}`);
|
| 326 |
+
}
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
function setActivePage(index, options = {}) {
|
| 330 |
state.activePage = clamp(index, 0, 1);
|
| 331 |
pageTrack.style.transform = `translateX(-${state.activePage * 100}%)`;
|
| 332 |
pageSlides.forEach((slide, slideIndex) => {
|
|
|
|
| 335 |
document.querySelectorAll(".story-tab").forEach((tab) => {
|
| 336 |
tab.classList.toggle("is-active", Number(tab.dataset.goPage) === state.activePage);
|
| 337 |
});
|
| 338 |
+
if (!options.skipHash) {
|
| 339 |
+
syncPageHash(state.activePage);
|
| 340 |
+
}
|
| 341 |
if (state.activePage === 1) {
|
| 342 |
loadPlanner(state.selectedDate, true);
|
| 343 |
}
|
| 344 |
}
|
| 345 |
|
| 346 |
+
function formatTimelineSlotLabel(index) {
|
| 347 |
+
return `第${String(index + 1).padStart(2, "0")}节`;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function decorateScheduleItems(items) {
|
| 351 |
+
const grouped = new Map();
|
| 352 |
+
|
| 353 |
+
(items || []).forEach((item) => {
|
| 354 |
+
const prepared = {
|
| 355 |
+
...item,
|
| 356 |
+
date: item.date || state.selectedDate,
|
| 357 |
+
startMinutes: toMinutes(item.start_time),
|
| 358 |
+
endMinutes: toMinutes(item.end_time),
|
| 359 |
+
};
|
| 360 |
+
if (!grouped.has(prepared.date)) {
|
| 361 |
+
grouped.set(prepared.date, []);
|
| 362 |
+
}
|
| 363 |
+
grouped.get(prepared.date).push(prepared);
|
| 364 |
+
});
|
| 365 |
+
|
| 366 |
+
const result = [];
|
| 367 |
+
getWeekDays().forEach((day) => {
|
| 368 |
+
const prepared = (grouped.get(day.iso) || [])
|
| 369 |
+
.sort((left, right) => (
|
| 370 |
+
left.startMinutes - right.startMinutes
|
| 371 |
+
|| left.endMinutes - right.endMinutes
|
| 372 |
+
|| left.kind.localeCompare(right.kind)
|
| 373 |
+
));
|
| 374 |
+
|
| 375 |
+
let active = [];
|
| 376 |
+
let currentGroup = [];
|
| 377 |
+
let currentGroupWidth = 0;
|
| 378 |
+
|
| 379 |
+
function finalizeGroup() {
|
| 380 |
+
currentGroup.forEach((item) => {
|
| 381 |
+
item.columnCount = currentGroupWidth || 1;
|
| 382 |
+
});
|
| 383 |
+
currentGroup = [];
|
| 384 |
+
currentGroupWidth = 0;
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
prepared.forEach((item) => {
|
| 388 |
+
active = active.filter((activeItem) => activeItem.endMinutes > item.startMinutes);
|
| 389 |
+
if (!active.length && currentGroup.length) {
|
| 390 |
+
finalizeGroup();
|
| 391 |
+
}
|
| 392 |
+
const usedColumns = new Set(active.map((activeItem) => activeItem.column));
|
| 393 |
+
let column = 0;
|
| 394 |
+
while (usedColumns.has(column)) {
|
| 395 |
+
column += 1;
|
| 396 |
+
}
|
| 397 |
+
item.column = column;
|
| 398 |
+
active.push(item);
|
| 399 |
+
currentGroup.push(item);
|
| 400 |
+
currentGroupWidth = Math.max(currentGroupWidth, active.length);
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
if (currentGroup.length) {
|
| 404 |
+
finalizeGroup();
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
result.push(...prepared);
|
| 408 |
+
});
|
| 409 |
+
|
| 410 |
+
return result;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
function decorateItems(items) {
|
| 414 |
const prepared = (items || [])
|
| 415 |
.map((item) => ({
|
|
|
|
| 469 |
if (!line) {
|
| 470 |
return;
|
| 471 |
}
|
| 472 |
+
const { dayStart, dayEnd } = getPlannerConfig();
|
| 473 |
const parts = new Intl.DateTimeFormat("en-CA", {
|
| 474 |
timeZone: "Asia/Shanghai",
|
| 475 |
year: "numeric",
|
|
|
|
| 489 |
|
| 490 |
const today = `${map.year}-${map.month}-${map.day}`;
|
| 491 |
const currentMinutes = (Number(map.hour) * 60) + Number(map.minute);
|
| 492 |
+
const dayIndex = getWeekDayIndex(today);
|
| 493 |
|
| 494 |
+
if (dayIndex < 0 || currentMinutes < dayStart || currentMinutes > dayEnd) {
|
| 495 |
line.style.display = "none";
|
| 496 |
return;
|
| 497 |
}
|
| 498 |
|
| 499 |
+
const dayWidthPercent = 100 / getWeekDays().length;
|
| 500 |
line.style.display = "block";
|
| 501 |
+
line.style.left = `calc(${dayIndex * dayWidthPercent}% + 4px)`;
|
| 502 |
+
line.style.width = `calc(${dayWidthPercent}% - 8px)`;
|
| 503 |
line.style.top = `${timelinePixels(currentMinutes - dayStart)}px`;
|
| 504 |
}
|
| 505 |
|
| 506 |
function renderTaskPool() {
|
| 507 |
+
const tasks = (state.planner.tasks || [])
|
| 508 |
+
.filter((task) => !task.completed)
|
| 509 |
+
.sort((left, right) => Number(!!left.schedule) - Number(!!right.schedule));
|
| 510 |
plannerTaskCount.textContent = `${tasks.length} 项`;
|
| 511 |
|
| 512 |
if (!tasks.length) {
|
| 513 |
plannerTaskPool.innerHTML = `
|
| 514 |
<div class="planner-empty">
|
| 515 |
<p>目前没有可安排的任务</p>
|
| 516 |
+
<span>先回到第一页添加待办,再把它拖到本周课表里。</span>
|
| 517 |
</div>
|
| 518 |
`;
|
| 519 |
return;
|
|
|
|
| 528 |
<div class="planner-task-tags">
|
| 529 |
<span>截止 ${formatLocalDateTime(task.due_at)}</span>
|
| 530 |
<span>进度 ${Math.round(task.progress_percent || 0)}%</span>
|
| 531 |
+
<span>${task.schedule ? `${task.schedule.date} · ${task.schedule.start_time}-${task.schedule.end_time}` : "尚未排入周表"}</span>
|
| 532 |
</div>
|
| 533 |
+
${task.schedule && state.authenticated ? `<button class="planner-task-clear" type="button" data-clear-schedule="${task.id}">移出排程</button>` : ""}
|
| 534 |
</article>
|
| 535 |
`).join("");
|
| 536 |
}
|
|
|
|
| 778 |
updateNowLine();
|
| 779 |
}
|
| 780 |
|
| 781 |
+
function renderWeekTimeline() {
|
| 782 |
+
const { dayStart, dayEnd, canvasLeft } = getPlannerConfig();
|
| 783 |
+
const { frameHeight, bodyHeight } = computePlannerScale();
|
| 784 |
+
const days = getWeekDays();
|
| 785 |
+
|
| 786 |
+
plannerTimeline.innerHTML = "";
|
| 787 |
+
plannerTimeline.style.height = `${frameHeight}px`;
|
| 788 |
+
plannerTimeline.style.setProperty("--timeline-axis-width", `${AXIS_WIDTH}px`);
|
| 789 |
+
plannerTimeline.style.setProperty("--timeline-slot-width", `${SLOT_WIDTH}px`);
|
| 790 |
+
plannerTimeline.style.setProperty("--timeline-canvas-left", `${canvasLeft}px`);
|
| 791 |
+
plannerTimeline.style.setProperty("--timeline-week-header-height", `${WEEK_HEADER_HEIGHT}px`);
|
| 792 |
+
|
| 793 |
+
if (!days.length) {
|
| 794 |
+
return;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
+
const headerLayer = document.createElement("div");
|
| 798 |
+
headerLayer.className = "timeline-week-header";
|
| 799 |
+
headerLayer.style.left = `${canvasLeft}px`;
|
| 800 |
+
headerLayer.style.right = "12px";
|
| 801 |
+
headerLayer.style.height = `${WEEK_HEADER_HEIGHT - 8}px`;
|
| 802 |
+
|
| 803 |
+
days.forEach((day) => {
|
| 804 |
+
const head = document.createElement("button");
|
| 805 |
+
head.type = "button";
|
| 806 |
+
head.className = `timeline-day-head ${day.is_today ? "is-today" : ""} ${day.iso === state.selectedDate ? "is-selected" : ""}`;
|
| 807 |
+
head.innerHTML = `
|
| 808 |
+
<strong>${escapeHtml(day.short_label)}</strong>
|
| 809 |
+
<span>${escapeHtml(day.month_day)}</span>
|
| 810 |
+
`;
|
| 811 |
+
head.addEventListener("click", () => {
|
| 812 |
+
if (day.iso !== state.selectedDate) {
|
| 813 |
+
loadPlanner(day.iso, true);
|
| 814 |
+
}
|
| 815 |
+
});
|
| 816 |
+
headerLayer.appendChild(head);
|
| 817 |
+
});
|
| 818 |
+
|
| 819 |
+
const axisLayer = document.createElement("div");
|
| 820 |
+
axisLayer.className = "timeline-axis-layer";
|
| 821 |
+
axisLayer.style.top = `${WEEK_HEADER_HEIGHT}px`;
|
| 822 |
+
axisLayer.style.height = `${bodyHeight}px`;
|
| 823 |
+
|
| 824 |
+
const slotLayer = document.createElement("div");
|
| 825 |
+
slotLayer.className = "timeline-slot-layer";
|
| 826 |
+
slotLayer.style.top = `${WEEK_HEADER_HEIGHT}px`;
|
| 827 |
+
slotLayer.style.height = `${bodyHeight}px`;
|
| 828 |
+
|
| 829 |
+
const canvasLayer = document.createElement("div");
|
| 830 |
+
canvasLayer.className = "timeline-canvas-layer";
|
| 831 |
+
canvasLayer.style.top = `${WEEK_HEADER_HEIGHT}px`;
|
| 832 |
+
canvasLayer.style.height = `${bodyHeight}px`;
|
| 833 |
+
|
| 834 |
+
const axisRail = document.createElement("div");
|
| 835 |
+
axisRail.className = "timeline-axis-rail";
|
| 836 |
+
axisLayer.appendChild(axisRail);
|
| 837 |
+
|
| 838 |
+
const lineMarkers = new Set([dayStart, dayEnd]);
|
| 839 |
+
const timeSlots = state.planner.time_slots || [];
|
| 840 |
+
const dayWidthPercent = 100 / days.length;
|
| 841 |
+
|
| 842 |
+
days.forEach((day, dayIndex) => {
|
| 843 |
+
const dayColumn = document.createElement("div");
|
| 844 |
+
dayColumn.className = `timeline-day-column ${day.is_today ? "is-today" : ""} ${day.iso === state.selectedDate ? "is-selected" : ""}`;
|
| 845 |
+
dayColumn.style.left = `${dayIndex * dayWidthPercent}%`;
|
| 846 |
+
dayColumn.style.width = `${dayWidthPercent}%`;
|
| 847 |
+
canvasLayer.appendChild(dayColumn);
|
| 848 |
+
|
| 849 |
+
if (dayIndex > 0) {
|
| 850 |
+
const divider = document.createElement("div");
|
| 851 |
+
divider.className = "timeline-day-divider";
|
| 852 |
+
divider.style.left = `${dayIndex * dayWidthPercent}%`;
|
| 853 |
+
canvasLayer.appendChild(divider);
|
| 854 |
+
}
|
| 855 |
+
});
|
| 856 |
+
|
| 857 |
+
timeSlots.forEach((slot, slotIndex) => {
|
| 858 |
+
const slotStart = toMinutes(slot.start);
|
| 859 |
+
const slotEnd = toMinutes(slot.end);
|
| 860 |
+
lineMarkers.add(slotStart);
|
| 861 |
+
lineMarkers.add(slotEnd);
|
| 862 |
+
|
| 863 |
+
const band = document.createElement("div");
|
| 864 |
+
band.className = "timeline-slot-band";
|
| 865 |
+
band.style.top = `${timelinePixels(slotStart - dayStart)}px`;
|
| 866 |
+
band.style.height = `${timelinePixels(slotEnd - slotStart)}px`;
|
| 867 |
+
band.innerHTML = `
|
| 868 |
+
<strong>${formatTimelineSlotLabel(slotIndex)}</strong>
|
| 869 |
+
<span>${escapeHtml(slot.start)} - ${escapeHtml(slot.end)}</span>
|
| 870 |
+
`;
|
| 871 |
+
slotLayer.appendChild(band);
|
| 872 |
+
});
|
| 873 |
+
|
| 874 |
+
Array.from(lineMarkers)
|
| 875 |
+
.sort((left, right) => left - right)
|
| 876 |
+
.forEach((minute) => {
|
| 877 |
+
const line = document.createElement("div");
|
| 878 |
+
line.className = "timeline-line is-slot";
|
| 879 |
+
line.style.top = `${timelinePixels(minute - dayStart)}px`;
|
| 880 |
+
canvasLayer.appendChild(line);
|
| 881 |
+
});
|
| 882 |
+
|
| 883 |
+
getTimelineAxisMinutes().forEach((minute, axisIndex, axisMinutes) => {
|
| 884 |
+
const tick = document.createElement("div");
|
| 885 |
+
tick.className = "timeline-axis-tick";
|
| 886 |
+
if (axisIndex === 0) {
|
| 887 |
+
tick.classList.add("is-leading");
|
| 888 |
+
} else if (axisIndex === axisMinutes.length - 1) {
|
| 889 |
+
tick.classList.add("is-terminal");
|
| 890 |
+
}
|
| 891 |
+
tick.style.top = `${timelinePixels(minute - dayStart)}px`;
|
| 892 |
+
tick.textContent = minutesToTime(minute);
|
| 893 |
+
axisLayer.appendChild(tick);
|
| 894 |
+
});
|
| 895 |
+
|
| 896 |
+
const majorMap = new Map();
|
| 897 |
+
(state.planner.major_blocks || []).forEach((block) => {
|
| 898 |
+
if (!majorMap.has(block.label)) {
|
| 899 |
+
majorMap.set(block.label, block);
|
| 900 |
+
}
|
| 901 |
+
});
|
| 902 |
+
|
| 903 |
+
Array.from(majorMap.values()).forEach((block, blockIndex) => {
|
| 904 |
+
const startMinutes = toMinutes(block.start);
|
| 905 |
+
const endMinutes = toMinutes(block.end);
|
| 906 |
+
const overlay = document.createElement("div");
|
| 907 |
+
overlay.className = "timeline-major-block";
|
| 908 |
+
overlay.style.top = `${timelinePixels(startMinutes - dayStart)}px`;
|
| 909 |
+
overlay.style.height = `${timelinePixels(endMinutes - startMinutes)}px`;
|
| 910 |
+
overlay.innerHTML = `<span>${escapeHtml(block.label || `第${blockIndex + 1}大节`)}</span>`;
|
| 911 |
+
canvasLayer.appendChild(overlay);
|
| 912 |
+
});
|
| 913 |
+
|
| 914 |
+
decorateScheduleItems(state.planner.scheduled_items).forEach((item) => {
|
| 915 |
+
const block = document.createElement("article");
|
| 916 |
+
block.className = `planner-event ${item.kind === "course" ? "course-event" : "task-event"} ${item.completed ? "is-complete" : ""}`;
|
| 917 |
+
updateEventLayout(block, item, dayStart, item.column || 0, item.columnCount || 1);
|
| 918 |
+
|
| 919 |
+
if (item.kind === "course") {
|
| 920 |
+
if (item.color) {
|
| 921 |
+
block.style.setProperty("--event-accent", item.color);
|
| 922 |
+
}
|
| 923 |
+
block.innerHTML = `
|
| 924 |
+
<div class="planner-event-top">
|
| 925 |
+
<strong>${escapeHtml(item.title)}</strong>
|
| 926 |
+
<span class="planner-lock-badge">固定课程</span>
|
| 927 |
+
</div>
|
| 928 |
+
<div class="planner-event-meta">
|
| 929 |
+
<span class="planner-event-time">${escapeHtml(item.start_time)} - ${escapeHtml(item.end_time)}</span>
|
| 930 |
+
<span>${escapeHtml(item.location || "")}</span>
|
| 931 |
+
</div>
|
| 932 |
+
`;
|
| 933 |
+
} else {
|
| 934 |
+
block.dataset.taskId = item.task_id;
|
| 935 |
+
block.style.setProperty("--event-accent", item.completed ? "#66d0ff" : mixColor(item.progress_percent || 0));
|
| 936 |
+
block.innerHTML = `
|
| 937 |
+
<div class="planner-event-top">
|
| 938 |
+
<strong>${escapeHtml(item.title)}</strong>
|
| 939 |
+
<span class="planner-event-time">${escapeHtml(item.start_time)} - ${escapeHtml(item.end_time)}</span>
|
| 940 |
+
</div>
|
| 941 |
+
<div class="planner-event-meta">
|
| 942 |
+
<span>${escapeHtml(item.category_name)}</span>
|
| 943 |
+
<span>进度 ${Math.round(item.progress_percent || 0)}%</span>
|
| 944 |
+
</div>
|
| 945 |
+
${state.authenticated ? `<button class="planner-event-clear" type="button" data-clear-schedule="${item.task_id}">移出</button>` : ""}
|
| 946 |
+
${state.authenticated ? `<div class="planner-event-resize planner-event-resize-top" data-resize-task-start="${item.task_id}"></div>` : ""}
|
| 947 |
+
${state.authenticated ? `<div class="planner-event-resize planner-event-resize-bottom" data-resize-task-end="${item.task_id}"></div>` : ""}
|
| 948 |
+
`;
|
| 949 |
+
|
| 950 |
+
if (state.authenticated) {
|
| 951 |
+
block.addEventListener("pointerdown", (event) => {
|
| 952 |
+
if (event.button !== 0 || event.target.closest("[data-clear-schedule]")) {
|
| 953 |
+
return;
|
| 954 |
+
}
|
| 955 |
+
|
| 956 |
+
event.preventDefault();
|
| 957 |
+
event.stopPropagation();
|
| 958 |
+
|
| 959 |
+
const point = clientPointToSchedule(event.clientX, event.clientY);
|
| 960 |
+
const mode = event.target.closest("[data-resize-task-start]")
|
| 961 |
+
? "resize-start"
|
| 962 |
+
: event.target.closest("[data-resize-task-end]")
|
| 963 |
+
? "resize-end"
|
| 964 |
+
: "move";
|
| 965 |
+
|
| 966 |
+
beginPlannerInteraction();
|
| 967 |
+
state.interaction = {
|
| 968 |
+
mode,
|
| 969 |
+
block,
|
| 970 |
+
taskId: item.task_id,
|
| 971 |
+
pointerId: event.pointerId,
|
| 972 |
+
pointerStartX: event.clientX,
|
| 973 |
+
pointerStartY: event.clientY,
|
| 974 |
+
initialDate: item.date,
|
| 975 |
+
currentDate: item.date,
|
| 976 |
+
initialStartMinutes: item.startMinutes,
|
| 977 |
+
initialEndMinutes: item.endMinutes,
|
| 978 |
+
startMinutes: item.startMinutes,
|
| 979 |
+
endMinutes: item.endMinutes,
|
| 980 |
+
duration: item.endMinutes - item.startMinutes,
|
| 981 |
+
pointerOffsetMinutes: point ? point.minutes - item.startMinutes : 0,
|
| 982 |
+
};
|
| 983 |
+
|
| 984 |
+
if (typeof block.setPointerCapture === "function") {
|
| 985 |
+
try {
|
| 986 |
+
block.setPointerCapture(event.pointerId);
|
| 987 |
+
} catch (error) {
|
| 988 |
+
// Ignore browsers that reject capture for synthetic pointer sequences.
|
| 989 |
+
}
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
block.classList.add("is-dragging");
|
| 993 |
+
});
|
| 994 |
+
}
|
| 995 |
+
}
|
| 996 |
+
|
| 997 |
+
canvasLayer.appendChild(block);
|
| 998 |
+
});
|
| 999 |
+
|
| 1000 |
+
const preview = document.createElement("div");
|
| 1001 |
+
preview.className = "timeline-drop-preview";
|
| 1002 |
+
preview.id = "timelineDropPreview";
|
| 1003 |
+
preview.style.display = "none";
|
| 1004 |
+
canvasLayer.appendChild(preview);
|
| 1005 |
+
|
| 1006 |
+
const nowLine = document.createElement("div");
|
| 1007 |
+
nowLine.className = "timeline-now-line";
|
| 1008 |
+
nowLine.id = "timelineNowLine";
|
| 1009 |
+
canvasLayer.appendChild(nowLine);
|
| 1010 |
+
|
| 1011 |
+
canvasLayer.addEventListener("dragover", (event) => {
|
| 1012 |
+
if (!state.dragTaskId) {
|
| 1013 |
+
return;
|
| 1014 |
+
}
|
| 1015 |
+
event.preventDefault();
|
| 1016 |
+
const task = getTaskById(state.dragTaskId);
|
| 1017 |
+
const point = clientPointToSchedule(event.clientX, event.clientY);
|
| 1018 |
+
if (!task || !point) {
|
| 1019 |
+
preview.style.display = "none";
|
| 1020 |
+
return;
|
| 1021 |
+
}
|
| 1022 |
+
const duration = getTaskDuration(task);
|
| 1023 |
+
const startMinutes = clamp(point.minutes, dayStart, dayEnd - duration);
|
| 1024 |
+
const dayMeta = getWeekDayMeta(point.date);
|
| 1025 |
+
preview.style.display = "grid";
|
| 1026 |
+
setBlockBounds(preview, startMinutes, startMinutes + duration, dayStart);
|
| 1027 |
+
setBlockHorizontalBounds(preview, point.date, 0, 1);
|
| 1028 |
+
preview.innerHTML = `
|
| 1029 |
+
<strong>${escapeHtml(dayMeta ? dayMeta.short_label : "")}</strong>
|
| 1030 |
+
<span>${minutesToTime(startMinutes)} - ${minutesToTime(startMinutes + duration)}</span>
|
| 1031 |
+
`;
|
| 1032 |
+
});
|
| 1033 |
+
|
| 1034 |
+
canvasLayer.addEventListener("dragleave", (event) => {
|
| 1035 |
+
if (!canvasLayer.contains(event.relatedTarget)) {
|
| 1036 |
+
preview.style.display = "none";
|
| 1037 |
+
}
|
| 1038 |
+
});
|
| 1039 |
+
|
| 1040 |
+
canvasLayer.addEventListener("drop", async (event) => {
|
| 1041 |
+
if (!state.dragTaskId) {
|
| 1042 |
+
return;
|
| 1043 |
+
}
|
| 1044 |
+
event.preventDefault();
|
| 1045 |
+
preview.style.display = "none";
|
| 1046 |
+
if (!requireAuth()) {
|
| 1047 |
+
state.dragTaskId = null;
|
| 1048 |
+
return;
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
const task = getTaskById(state.dragTaskId);
|
| 1052 |
+
const point = clientPointToSchedule(event.clientX, event.clientY);
|
| 1053 |
+
if (!task || !point) {
|
| 1054 |
+
state.dragTaskId = null;
|
| 1055 |
+
return;
|
| 1056 |
+
}
|
| 1057 |
+
|
| 1058 |
+
try {
|
| 1059 |
+
const duration = getTaskDuration(task);
|
| 1060 |
+
const startMinutes = clamp(point.minutes, dayStart, dayEnd - duration);
|
| 1061 |
+
await requestJSON(`/api/tasks/${task.id}/schedule`, {
|
| 1062 |
+
method: "PATCH",
|
| 1063 |
+
body: JSON.stringify({
|
| 1064 |
+
date: point.date,
|
| 1065 |
+
start_time: minutesToTime(startMinutes),
|
| 1066 |
+
end_time: minutesToTime(startMinutes + duration),
|
| 1067 |
+
}),
|
| 1068 |
+
});
|
| 1069 |
+
await loadPlanner(point.date, true);
|
| 1070 |
+
showToast("任务已拖入本周课表");
|
| 1071 |
+
} catch (error) {
|
| 1072 |
+
showToast(error.message, "error");
|
| 1073 |
+
} finally {
|
| 1074 |
+
state.dragTaskId = null;
|
| 1075 |
+
}
|
| 1076 |
+
});
|
| 1077 |
+
|
| 1078 |
+
plannerTimeline.appendChild(headerLayer);
|
| 1079 |
+
plannerTimeline.appendChild(axisLayer);
|
| 1080 |
+
plannerTimeline.appendChild(slotLayer);
|
| 1081 |
+
plannerTimeline.appendChild(canvasLayer);
|
| 1082 |
+
|
| 1083 |
+
updateNowLine();
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
function renderWeekPlanner() {
|
| 1087 |
+
state.selectedDate = state.planner.selected_date || state.selectedDate;
|
| 1088 |
+
plannerDateInput.value = state.selectedDate;
|
| 1089 |
+
plannerDateLabel.textContent = formatWeekRange(state.planner.week_start, state.planner.week_end);
|
| 1090 |
+
plannerWeekday.textContent = state.planner.week_range_label || "";
|
| 1091 |
+
plannerAcademicWeek.textContent = state.planner.academic_label || "";
|
| 1092 |
+
plannerWindow.textContent = `${state.planner.settings.day_start} - ${state.planner.settings.day_end}`;
|
| 1093 |
+
plannerHeadlineNote.textContent = "课程会按学期周次自动固定显示,任务可拖入本周任意一天,并通过上下边缘拉伸;每项任务最短 15 分钟。";
|
| 1094 |
+
renderTaskPool();
|
| 1095 |
+
renderWeekTimeline();
|
| 1096 |
+
}
|
| 1097 |
+
|
| 1098 |
function renderPlanner() {
|
| 1099 |
state.selectedDate = state.planner.selected_date || state.selectedDate;
|
| 1100 |
plannerDateInput.value = state.selectedDate;
|
|
|
|
| 1114 |
});
|
| 1115 |
state.planner = payload.planner;
|
| 1116 |
state.selectedDate = payload.planner.selected_date;
|
| 1117 |
+
renderWeekPlanner();
|
| 1118 |
} catch (error) {
|
| 1119 |
if (!silent) {
|
| 1120 |
showToast(error.message, "error");
|
|
|
|
| 1190 |
const deltaMinutes = getPointerDeltaMinutes(state.interaction.pointerStartY, event.clientY);
|
| 1191 |
|
| 1192 |
if (state.interaction.mode === "move") {
|
| 1193 |
+
const point = clientPointToSchedule(event.clientX, event.clientY);
|
| 1194 |
const duration = state.interaction.initialEndMinutes - state.interaction.initialStartMinutes;
|
| 1195 |
+
state.interaction.currentDate = point ? point.date : state.interaction.initialDate;
|
| 1196 |
const startMinutes = clamp(
|
| 1197 |
state.interaction.initialStartMinutes + deltaMinutes,
|
| 1198 |
dayStart,
|
|
|
|
| 1207 |
dayStart,
|
| 1208 |
state.interaction.initialEndMinutes - MIN_DURATION
|
| 1209 |
);
|
| 1210 |
+
state.interaction.currentDate = state.interaction.initialDate;
|
| 1211 |
state.interaction.startMinutes = startMinutes;
|
| 1212 |
state.interaction.endMinutes = state.interaction.initialEndMinutes;
|
| 1213 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
|
|
|
| 1217 |
state.interaction.initialStartMinutes + MIN_DURATION,
|
| 1218 |
dayEnd
|
| 1219 |
);
|
| 1220 |
+
state.interaction.currentDate = state.interaction.initialDate;
|
| 1221 |
state.interaction.startMinutes = state.interaction.initialStartMinutes;
|
| 1222 |
state.interaction.endMinutes = endMinutes;
|
| 1223 |
state.interaction.duration = state.interaction.endMinutes - state.interaction.startMinutes;
|
| 1224 |
}
|
| 1225 |
|
| 1226 |
+
updateEventLayout(state.interaction.block, {
|
| 1227 |
+
date: state.interaction.currentDate,
|
| 1228 |
+
startMinutes: state.interaction.startMinutes,
|
| 1229 |
+
endMinutes: state.interaction.endMinutes,
|
| 1230 |
+
}, dayStart);
|
|
|
|
| 1231 |
updateEventTimeLabel(state.interaction.block, state.interaction.startMinutes, state.interaction.endMinutes);
|
| 1232 |
});
|
| 1233 |
|
|
|
|
| 1261 |
.catch((error) => showToast(error.message, "error"));
|
| 1262 |
}
|
| 1263 |
|
| 1264 |
+
function persistWeekInteraction(current) {
|
| 1265 |
+
requestJSON(`/api/tasks/${current.taskId}/schedule`, {
|
| 1266 |
+
method: "PATCH",
|
| 1267 |
+
body: JSON.stringify({
|
| 1268 |
+
date: current.currentDate || current.initialDate || state.selectedDate,
|
| 1269 |
+
start_time: minutesToTime(current.startMinutes),
|
| 1270 |
+
end_time: minutesToTime(current.endMinutes),
|
| 1271 |
+
}),
|
| 1272 |
+
})
|
| 1273 |
+
.then(() => loadPlanner(current.currentDate || state.selectedDate, true))
|
| 1274 |
+
.then(() => showToast("规划时间已更新"))
|
| 1275 |
+
.catch((error) => showToast(error.message, "error"));
|
| 1276 |
+
}
|
| 1277 |
+
|
| 1278 |
function finishInteraction(event) {
|
| 1279 |
if (!state.interaction) {
|
| 1280 |
return;
|
|
|
|
| 1291 |
finishPlannerInteraction();
|
| 1292 |
|
| 1293 |
if (
|
| 1294 |
+
(current.currentDate || current.initialDate) === current.initialDate
|
| 1295 |
+
&&
|
| 1296 |
current.startMinutes === current.initialStartMinutes
|
| 1297 |
&& current.endMinutes === current.initialEndMinutes
|
| 1298 |
) {
|
| 1299 |
return;
|
| 1300 |
}
|
| 1301 |
|
| 1302 |
+
persistWeekInteraction(current);
|
| 1303 |
}
|
| 1304 |
|
| 1305 |
document.addEventListener("pointerup", finishInteraction);
|
|
|
|
| 1313 |
});
|
| 1314 |
|
| 1315 |
plannerPrevDay.addEventListener("click", () => {
|
| 1316 |
+
loadPlanner(shiftDate(state.selectedDate, -7));
|
| 1317 |
});
|
| 1318 |
|
| 1319 |
plannerNextDay.addEventListener("click", () => {
|
| 1320 |
+
loadPlanner(shiftDate(state.selectedDate, 7));
|
| 1321 |
});
|
| 1322 |
|
| 1323 |
window.setInterval(() => {
|
|
|
|
| 1327 |
}, 45000);
|
| 1328 |
|
| 1329 |
window.setInterval(updateNowLine, 60000);
|
| 1330 |
+
window.addEventListener("resize", () => {
|
| 1331 |
+
if (state.activePage === 1) {
|
| 1332 |
+
renderWeekPlanner();
|
| 1333 |
+
} else {
|
| 1334 |
+
updateNowLine();
|
| 1335 |
+
}
|
| 1336 |
+
});
|
| 1337 |
|
| 1338 |
document.addEventListener("visibilitychange", () => {
|
| 1339 |
if (document.visibilityState === "visible" && state.activePage === 1) {
|
|
|
|
| 1341 |
}
|
| 1342 |
});
|
| 1343 |
|
| 1344 |
+
window.addEventListener("hashchange", () => {
|
| 1345 |
+
setActivePage(getPageIndexFromHash(), { skipHash: true });
|
| 1346 |
+
});
|
| 1347 |
+
|
| 1348 |
+
renderWeekPlanner();
|
| 1349 |
+
setActivePage(getPageIndexFromHash(), { skipHash: true });
|
| 1350 |
})();
|
static/v020.css
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
.story-layout {
|
| 2 |
-
padding-top:
|
|
|
|
| 3 |
}
|
| 4 |
|
| 5 |
.story-shell {
|
| 6 |
display: grid;
|
| 7 |
-
gap:
|
| 8 |
min-width: 0;
|
|
|
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
.story-toolbar {
|
| 12 |
-
border-radius:
|
| 13 |
-
padding:
|
| 14 |
display: grid;
|
| 15 |
grid-template-columns: auto 1fr auto;
|
| 16 |
-
gap:
|
| 17 |
align-items: center;
|
| 18 |
}
|
| 19 |
|
|
@@ -44,12 +57,15 @@
|
|
| 44 |
position: relative;
|
| 45 |
width: 100%;
|
| 46 |
overflow: hidden;
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
|
| 49 |
.page-track {
|
| 50 |
display: flex;
|
| 51 |
width: 100%;
|
| 52 |
-
|
|
|
|
| 53 |
transition: transform 720ms cubic-bezier(0.22, 1, 0.36, 1);
|
| 54 |
will-change: transform;
|
| 55 |
}
|
|
@@ -61,6 +77,8 @@
|
|
| 61 |
min-width: 100%;
|
| 62 |
overflow: hidden;
|
| 63 |
pointer-events: none;
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
.page-slide.is-active {
|
|
@@ -73,32 +91,145 @@ body.planner-interacting * {
|
|
| 73 |
-webkit-user-select: none !important;
|
| 74 |
}
|
| 75 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
.page-home {
|
| 77 |
display: grid;
|
| 78 |
-
gap:
|
| 79 |
-
grid-template-rows: minmax(
|
| 80 |
-
padding-right:
|
|
|
|
| 81 |
}
|
| 82 |
|
| 83 |
.page-planner {
|
| 84 |
-
padding-left:
|
|
|
|
| 85 |
}
|
| 86 |
|
| 87 |
.inline-link {
|
| 88 |
border: 0;
|
| 89 |
}
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
.hero-copy {
|
| 92 |
-
margin:
|
| 93 |
color: var(--muted);
|
| 94 |
}
|
| 95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
.planner-shell {
|
| 97 |
-
border-radius:
|
| 98 |
-
padding:
|
| 99 |
-
min-height:
|
|
|
|
| 100 |
display: grid;
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
}
|
| 103 |
|
| 104 |
.planner-head,
|
|
@@ -116,17 +247,17 @@ body.planner-interacting * {
|
|
| 116 |
.planner-head,
|
| 117 |
.planner-sidebar-head {
|
| 118 |
justify-content: space-between;
|
| 119 |
-
gap:
|
| 120 |
}
|
| 121 |
|
| 122 |
.planner-controls {
|
| 123 |
-
gap:
|
| 124 |
}
|
| 125 |
|
| 126 |
.planner-date-picker {
|
| 127 |
display: grid;
|
| 128 |
-
gap:
|
| 129 |
-
min-width:
|
| 130 |
}
|
| 131 |
|
| 132 |
.planner-date-picker span {
|
|
@@ -136,9 +267,9 @@ body.planner-interacting * {
|
|
| 136 |
|
| 137 |
.planner-date-picker input,
|
| 138 |
.modal-form select {
|
| 139 |
-
min-height:
|
| 140 |
-
padding: 0
|
| 141 |
-
border-radius:
|
| 142 |
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 143 |
background: rgba(255, 255, 255, 0.04);
|
| 144 |
color: var(--text);
|
|
@@ -147,7 +278,7 @@ body.planner-interacting * {
|
|
| 147 |
|
| 148 |
.planner-meta {
|
| 149 |
flex-wrap: wrap;
|
| 150 |
-
gap:
|
| 151 |
}
|
| 152 |
|
| 153 |
.planner-meta span,
|
|
@@ -155,17 +286,19 @@ body.planner-interacting * {
|
|
| 155 |
.planner-task-category {
|
| 156 |
display: inline-flex;
|
| 157 |
align-items: center;
|
| 158 |
-
padding:
|
| 159 |
border-radius: 999px;
|
| 160 |
background: rgba(255, 255, 255, 0.06);
|
| 161 |
color: var(--muted-strong);
|
|
|
|
| 162 |
}
|
| 163 |
|
| 164 |
.planner-layout {
|
| 165 |
display: grid;
|
| 166 |
-
grid-template-columns: minmax(0, 7fr) minmax(
|
| 167 |
-
gap:
|
| 168 |
min-height: 0;
|
|
|
|
| 169 |
}
|
| 170 |
|
| 171 |
.timeline-surface,
|
|
@@ -176,19 +309,24 @@ body.planner-interacting * {
|
|
| 176 |
}
|
| 177 |
|
| 178 |
.timeline-surface {
|
| 179 |
-
padding:
|
|
|
|
|
|
|
| 180 |
}
|
| 181 |
|
| 182 |
.timeline-scroll {
|
| 183 |
-
overflow:
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
| 186 |
background: rgba(3, 11, 20, 0.46);
|
| 187 |
}
|
| 188 |
|
| 189 |
.timeline-grid {
|
| 190 |
position: relative;
|
| 191 |
-
min-height:
|
|
|
|
| 192 |
}
|
| 193 |
|
| 194 |
.timeline-axis-layer,
|
|
@@ -201,6 +339,14 @@ body.planner-interacting * {
|
|
| 201 |
bottom: 0;
|
| 202 |
}
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
.timeline-axis-layer {
|
| 205 |
left: 0;
|
| 206 |
width: var(--timeline-axis-width);
|
|
@@ -218,6 +364,74 @@ body.planner-interacting * {
|
|
| 218 |
right: 12px;
|
| 219 |
}
|
| 220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
.timeline-axis-rail {
|
| 222 |
position: absolute;
|
| 223 |
top: 0;
|
|
@@ -234,11 +448,11 @@ body.planner-interacting * {
|
|
| 234 |
display: flex;
|
| 235 |
justify-content: flex-end;
|
| 236 |
align-items: center;
|
| 237 |
-
gap:
|
| 238 |
transform: translateY(-50%);
|
| 239 |
-
padding-right:
|
| 240 |
color: rgba(238, 244, 251, 0.76);
|
| 241 |
-
font-size: 0.
|
| 242 |
font-variant-numeric: tabular-nums;
|
| 243 |
letter-spacing: 0.02em;
|
| 244 |
line-height: 1;
|
|
@@ -247,8 +461,8 @@ body.planner-interacting * {
|
|
| 247 |
|
| 248 |
.timeline-axis-tick::after {
|
| 249 |
content: "";
|
| 250 |
-
width:
|
| 251 |
-
height:
|
| 252 |
border-radius: 999px;
|
| 253 |
background: rgba(123, 231, 234, 0.78);
|
| 254 |
box-shadow: 0 0 0 4px rgba(123, 231, 234, 0.12);
|
|
@@ -265,29 +479,29 @@ body.planner-interacting * {
|
|
| 265 |
|
| 266 |
.timeline-slot-band {
|
| 267 |
position: absolute;
|
| 268 |
-
left:
|
| 269 |
-
right:
|
| 270 |
box-sizing: border-box;
|
| 271 |
overflow: hidden;
|
| 272 |
-
padding:
|
| 273 |
-
border-radius:
|
| 274 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.025));
|
| 275 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 276 |
display: flex;
|
| 277 |
flex-direction: column;
|
| 278 |
justify-content: center;
|
| 279 |
-
gap:
|
| 280 |
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
| 281 |
}
|
| 282 |
|
| 283 |
.timeline-slot-band strong {
|
| 284 |
-
font-size: 0.
|
| 285 |
line-height: 1.1;
|
| 286 |
}
|
| 287 |
|
| 288 |
.timeline-slot-band span {
|
| 289 |
color: var(--muted);
|
| 290 |
-
font-size: 0.
|
| 291 |
line-height: 1.1;
|
| 292 |
font-variant-numeric: tabular-nums;
|
| 293 |
}
|
|
@@ -308,17 +522,17 @@ body.planner-interacting * {
|
|
| 308 |
left: 0;
|
| 309 |
right: 0;
|
| 310 |
box-sizing: border-box;
|
| 311 |
-
border-radius:
|
| 312 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
|
| 313 |
pointer-events: none;
|
| 314 |
}
|
| 315 |
|
| 316 |
.timeline-major-block span {
|
| 317 |
position: absolute;
|
| 318 |
-
top:
|
| 319 |
-
right:
|
| 320 |
font-family: "Sora", "Noto Sans SC", sans-serif;
|
| 321 |
-
font-size: clamp(
|
| 322 |
color: rgba(238, 244, 251, 0.16);
|
| 323 |
}
|
| 324 |
|
|
@@ -327,8 +541,8 @@ body.planner-interacting * {
|
|
| 327 |
position: absolute;
|
| 328 |
box-sizing: border-box;
|
| 329 |
overflow: hidden;
|
| 330 |
-
border-radius:
|
| 331 |
-
padding:
|
| 332 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 333 |
background: linear-gradient(180deg, rgba(13, 28, 44, 0.96) 0%, rgba(8, 20, 32, 0.92) 100%);
|
| 334 |
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
|
@@ -336,6 +550,7 @@ body.planner-interacting * {
|
|
| 336 |
|
| 337 |
.planner-event {
|
| 338 |
touch-action: none;
|
|
|
|
| 339 |
}
|
| 340 |
|
| 341 |
.planner-event::before,
|
|
@@ -356,14 +571,15 @@ body.planner-interacting * {
|
|
| 356 |
}
|
| 357 |
|
| 358 |
.planner-event-top strong {
|
| 359 |
-
max-width:
|
| 360 |
-
line-height: 1.
|
|
|
|
| 361 |
}
|
| 362 |
|
| 363 |
.planner-event-meta {
|
| 364 |
-
margin-top:
|
| 365 |
color: var(--muted);
|
| 366 |
-
font-size: 0.
|
| 367 |
}
|
| 368 |
|
| 369 |
.planner-lock-badge,
|
|
@@ -387,14 +603,14 @@ body.planner-interacting * {
|
|
| 387 |
.planner-task-clear {
|
| 388 |
background: rgba(255, 107, 92, 0.14);
|
| 389 |
color: #ff9388;
|
| 390 |
-
margin-top:
|
| 391 |
}
|
| 392 |
|
| 393 |
.planner-event-resize {
|
| 394 |
position: absolute;
|
| 395 |
-
left:
|
| 396 |
-
right:
|
| 397 |
-
height:
|
| 398 |
border-radius: 999px;
|
| 399 |
background: linear-gradient(90deg, rgba(123, 231, 234, 0.9), rgba(97, 210, 159, 0.72));
|
| 400 |
cursor: ns-resize;
|
|
@@ -402,11 +618,11 @@ body.planner-interacting * {
|
|
| 402 |
}
|
| 403 |
|
| 404 |
.planner-event-resize-top {
|
| 405 |
-
top:
|
| 406 |
}
|
| 407 |
|
| 408 |
.planner-event-resize-bottom {
|
| 409 |
-
bottom:
|
| 410 |
}
|
| 411 |
|
| 412 |
.planner-event.is-dragging,
|
|
@@ -414,9 +630,13 @@ body.planner-interacting * {
|
|
| 414 |
opacity: 0.8;
|
| 415 |
}
|
| 416 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
.planner-event.is-compact {
|
| 418 |
border-radius: 16px;
|
| 419 |
-
padding:
|
| 420 |
}
|
| 421 |
|
| 422 |
.planner-event.is-compact::before {
|
|
@@ -447,7 +667,7 @@ body.planner-interacting * {
|
|
| 447 |
}
|
| 448 |
|
| 449 |
.planner-event.is-tight {
|
| 450 |
-
padding: 6px
|
| 451 |
}
|
| 452 |
|
| 453 |
.planner-event.is-tight .planner-event-meta,
|
|
@@ -473,35 +693,41 @@ body.planner-interacting * {
|
|
| 473 |
}
|
| 474 |
|
| 475 |
.planner-sidebar {
|
| 476 |
-
padding:
|
| 477 |
display: grid;
|
| 478 |
-
gap:
|
| 479 |
align-content: start;
|
|
|
|
|
|
|
| 480 |
}
|
| 481 |
|
| 482 |
.planner-sidebar-note {
|
| 483 |
display: grid;
|
| 484 |
-
gap:
|
| 485 |
color: var(--muted);
|
|
|
|
| 486 |
}
|
| 487 |
|
| 488 |
.planner-task-pool {
|
| 489 |
display: grid;
|
| 490 |
-
gap:
|
|
|
|
|
|
|
|
|
|
| 491 |
}
|
| 492 |
|
| 493 |
.planner-task-card {
|
| 494 |
-
padding:
|
| 495 |
-
border-radius:
|
| 496 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 497 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.03) 100%);
|
| 498 |
display: grid;
|
| 499 |
-
gap:
|
| 500 |
}
|
| 501 |
|
| 502 |
.planner-task-card h4 {
|
| 503 |
margin: 0;
|
| 504 |
-
font-size:
|
| 505 |
}
|
| 506 |
|
| 507 |
.planner-task-top {
|
|
@@ -515,11 +741,11 @@ body.planner-interacting * {
|
|
| 515 |
}
|
| 516 |
|
| 517 |
.planner-task-tags span {
|
| 518 |
-
padding:
|
| 519 |
border-radius: 999px;
|
| 520 |
background: rgba(255, 255, 255, 0.05);
|
| 521 |
color: var(--muted-strong);
|
| 522 |
-
font-size: 0.
|
| 523 |
}
|
| 524 |
|
| 525 |
.planner-empty,
|
|
@@ -533,9 +759,22 @@ body.planner-interacting * {
|
|
| 533 |
|
| 534 |
.timeline-drop-preview {
|
| 535 |
display: none;
|
|
|
|
|
|
|
| 536 |
color: var(--muted-strong);
|
| 537 |
border-style: dashed;
|
| 538 |
background: rgba(123, 231, 234, 0.1);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}
|
| 540 |
|
| 541 |
.timeline-now-line {
|
|
@@ -543,6 +782,8 @@ body.planner-interacting * {
|
|
| 543 |
height: 2px;
|
| 544 |
background: linear-gradient(90deg, rgba(255, 107, 92, 0.95), rgba(255, 200, 87, 0.72));
|
| 545 |
box-shadow: 0 0 18px rgba(255, 107, 92, 0.38);
|
|
|
|
|
|
|
| 546 |
}
|
| 547 |
|
| 548 |
.timeline-now-line::before {
|
|
@@ -578,6 +819,18 @@ body.planner-interacting * {
|
|
| 578 |
}
|
| 579 |
|
| 580 |
@media (max-width: 1180px) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
.story-toolbar,
|
| 582 |
.planner-head,
|
| 583 |
.planner-layout {
|
|
@@ -590,6 +843,13 @@ body.planner-interacting * {
|
|
| 590 |
justify-self: stretch;
|
| 591 |
justify-content: center;
|
| 592 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
}
|
| 594 |
|
| 595 |
@media (max-width: 840px) {
|
|
@@ -604,7 +864,7 @@ body.planner-interacting * {
|
|
| 604 |
}
|
| 605 |
|
| 606 |
.timeline-scroll {
|
| 607 |
-
|
| 608 |
}
|
| 609 |
}
|
| 610 |
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
overflow: hidden;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
.layout.story-layout {
|
| 6 |
+
width: min(1560px, calc(100% - 18px));
|
| 7 |
+
min-height: 100vh;
|
| 8 |
+
padding: 14px 0;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
.story-layout {
|
| 12 |
+
padding-top: 0;
|
| 13 |
+
overflow: hidden;
|
| 14 |
}
|
| 15 |
|
| 16 |
.story-shell {
|
| 17 |
display: grid;
|
| 18 |
+
gap: 12px;
|
| 19 |
min-width: 0;
|
| 20 |
+
min-height: calc(100vh - 28px);
|
| 21 |
+
grid-template-rows: auto minmax(0, 1fr);
|
| 22 |
}
|
| 23 |
|
| 24 |
.story-toolbar {
|
| 25 |
+
border-radius: 24px;
|
| 26 |
+
padding: 12px 16px;
|
| 27 |
display: grid;
|
| 28 |
grid-template-columns: auto 1fr auto;
|
| 29 |
+
gap: 14px;
|
| 30 |
align-items: center;
|
| 31 |
}
|
| 32 |
|
|
|
|
| 57 |
position: relative;
|
| 58 |
width: 100%;
|
| 59 |
overflow: hidden;
|
| 60 |
+
min-height: 0;
|
| 61 |
+
height: 100%;
|
| 62 |
}
|
| 63 |
|
| 64 |
.page-track {
|
| 65 |
display: flex;
|
| 66 |
width: 100%;
|
| 67 |
+
height: 100%;
|
| 68 |
+
align-items: stretch;
|
| 69 |
transition: transform 720ms cubic-bezier(0.22, 1, 0.36, 1);
|
| 70 |
will-change: transform;
|
| 71 |
}
|
|
|
|
| 77 |
min-width: 100%;
|
| 78 |
overflow: hidden;
|
| 79 |
pointer-events: none;
|
| 80 |
+
min-height: 0;
|
| 81 |
+
height: 100%;
|
| 82 |
}
|
| 83 |
|
| 84 |
.page-slide.is-active {
|
|
|
|
| 91 |
-webkit-user-select: none !important;
|
| 92 |
}
|
| 93 |
|
| 94 |
+
body.planner-interacting .page-home {
|
| 95 |
+
pointer-events: none;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
body.planner-interacting .page-planner {
|
| 99 |
+
pointer-events: auto;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
.page-home {
|
| 103 |
display: grid;
|
| 104 |
+
gap: 12px;
|
| 105 |
+
grid-template-rows: minmax(216px, 27vh) minmax(0, 1fr);
|
| 106 |
+
padding-right: 10px;
|
| 107 |
+
min-height: 0;
|
| 108 |
}
|
| 109 |
|
| 110 |
.page-planner {
|
| 111 |
+
padding-left: 10px;
|
| 112 |
+
min-height: 0;
|
| 113 |
}
|
| 114 |
|
| 115 |
.inline-link {
|
| 116 |
border: 0;
|
| 117 |
}
|
| 118 |
|
| 119 |
+
.page-home .hero-card {
|
| 120 |
+
min-height: 0;
|
| 121 |
+
padding: 22px 24px;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.clock-wrap {
|
| 125 |
+
gap: 6px;
|
| 126 |
+
padding: 6px 0 4px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.clock-display {
|
| 130 |
+
font-size: clamp(2.7rem, 7vw, 4.9rem);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
.clock-meta {
|
| 134 |
+
gap: 8px 14px;
|
| 135 |
+
font-size: 0.94rem;
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
.hero-copy {
|
| 139 |
+
margin: 6px 0 0;
|
| 140 |
color: var(--muted);
|
| 141 |
}
|
| 142 |
|
| 143 |
+
.board-section {
|
| 144 |
+
display: grid;
|
| 145 |
+
gap: 12px;
|
| 146 |
+
grid-template-rows: auto minmax(0, 1fr);
|
| 147 |
+
min-height: 0;
|
| 148 |
+
overflow: hidden;
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
.board-section .section-header {
|
| 152 |
+
margin-bottom: 0;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.board-section .section-header h2 {
|
| 156 |
+
font-size: clamp(1.1rem, 1.7vw, 1.5rem);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.board-grid {
|
| 160 |
+
display: grid;
|
| 161 |
+
gap: 12px;
|
| 162 |
+
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
| 163 |
+
align-items: stretch;
|
| 164 |
+
align-content: start;
|
| 165 |
+
min-height: 0;
|
| 166 |
+
height: auto;
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.todo-column {
|
| 170 |
+
padding: 16px;
|
| 171 |
+
min-height: 0;
|
| 172 |
+
display: grid;
|
| 173 |
+
grid-template-rows: auto minmax(0, 1fr);
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
.column-header {
|
| 177 |
+
gap: 12px;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.column-title {
|
| 181 |
+
font-size: 1.2rem;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
.task-list {
|
| 185 |
+
gap: 10px;
|
| 186 |
+
margin-top: 12px;
|
| 187 |
+
min-height: 0;
|
| 188 |
+
overflow: auto;
|
| 189 |
+
padding-right: 4px;
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
.task-card {
|
| 193 |
+
padding: 14px;
|
| 194 |
+
border-radius: 18px;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.task-copy h4 {
|
| 198 |
+
margin: 0 0 8px;
|
| 199 |
+
font-size: 0.98rem;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
.meta-line {
|
| 203 |
+
gap: 6px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.meta-badge {
|
| 207 |
+
padding: 6px 9px;
|
| 208 |
+
font-size: 0.76rem;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.progress-shell {
|
| 212 |
+
height: 8px;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
.planner-shell {
|
| 216 |
+
border-radius: 26px;
|
| 217 |
+
padding: 16px;
|
| 218 |
+
min-height: 0;
|
| 219 |
+
height: 100%;
|
| 220 |
display: grid;
|
| 221 |
+
grid-template-rows: auto auto minmax(0, 1fr);
|
| 222 |
+
gap: 10px;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
.planner-head h2 {
|
| 226 |
+
margin: 4px 0 0;
|
| 227 |
+
font-size: clamp(1.34rem, 1.85vw, 1.72rem);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.planner-head .section-note {
|
| 231 |
+
margin: 6px 0 0;
|
| 232 |
+
font-size: 0.92rem;
|
| 233 |
}
|
| 234 |
|
| 235 |
.planner-head,
|
|
|
|
| 247 |
.planner-head,
|
| 248 |
.planner-sidebar-head {
|
| 249 |
justify-content: space-between;
|
| 250 |
+
gap: 12px;
|
| 251 |
}
|
| 252 |
|
| 253 |
.planner-controls {
|
| 254 |
+
gap: 8px;
|
| 255 |
}
|
| 256 |
|
| 257 |
.planner-date-picker {
|
| 258 |
display: grid;
|
| 259 |
+
gap: 6px;
|
| 260 |
+
min-width: 172px;
|
| 261 |
}
|
| 262 |
|
| 263 |
.planner-date-picker span {
|
|
|
|
| 267 |
|
| 268 |
.planner-date-picker input,
|
| 269 |
.modal-form select {
|
| 270 |
+
min-height: 42px;
|
| 271 |
+
padding: 0 12px;
|
| 272 |
+
border-radius: 14px;
|
| 273 |
border: 1px solid rgba(255, 255, 255, 0.1);
|
| 274 |
background: rgba(255, 255, 255, 0.04);
|
| 275 |
color: var(--text);
|
|
|
|
| 278 |
|
| 279 |
.planner-meta {
|
| 280 |
flex-wrap: wrap;
|
| 281 |
+
gap: 8px;
|
| 282 |
}
|
| 283 |
|
| 284 |
.planner-meta span,
|
|
|
|
| 286 |
.planner-task-category {
|
| 287 |
display: inline-flex;
|
| 288 |
align-items: center;
|
| 289 |
+
padding: 6px 10px;
|
| 290 |
border-radius: 999px;
|
| 291 |
background: rgba(255, 255, 255, 0.06);
|
| 292 |
color: var(--muted-strong);
|
| 293 |
+
font-size: 0.76rem;
|
| 294 |
}
|
| 295 |
|
| 296 |
.planner-layout {
|
| 297 |
display: grid;
|
| 298 |
+
grid-template-columns: minmax(0, 7.7fr) minmax(250px, 2.3fr);
|
| 299 |
+
gap: 12px;
|
| 300 |
min-height: 0;
|
| 301 |
+
overflow: hidden;
|
| 302 |
}
|
| 303 |
|
| 304 |
.timeline-surface,
|
|
|
|
| 309 |
}
|
| 310 |
|
| 311 |
.timeline-surface {
|
| 312 |
+
padding: 10px;
|
| 313 |
+
min-height: 0;
|
| 314 |
+
height: 100%;
|
| 315 |
}
|
| 316 |
|
| 317 |
.timeline-scroll {
|
| 318 |
+
overflow: hidden;
|
| 319 |
+
height: 100%;
|
| 320 |
+
max-height: none;
|
| 321 |
+
min-height: 0;
|
| 322 |
+
border-radius: 18px;
|
| 323 |
background: rgba(3, 11, 20, 0.46);
|
| 324 |
}
|
| 325 |
|
| 326 |
.timeline-grid {
|
| 327 |
position: relative;
|
| 328 |
+
min-height: 0;
|
| 329 |
+
height: 100%;
|
| 330 |
}
|
| 331 |
|
| 332 |
.timeline-axis-layer,
|
|
|
|
| 339 |
bottom: 0;
|
| 340 |
}
|
| 341 |
|
| 342 |
+
.timeline-axis-layer,
|
| 343 |
+
.timeline-slot-layer,
|
| 344 |
+
.timeline-canvas-layer,
|
| 345 |
+
.timeline-drop-preview,
|
| 346 |
+
.timeline-now-line {
|
| 347 |
+
bottom: auto;
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
.timeline-axis-layer {
|
| 351 |
left: 0;
|
| 352 |
width: var(--timeline-axis-width);
|
|
|
|
| 364 |
right: 12px;
|
| 365 |
}
|
| 366 |
|
| 367 |
+
.timeline-week-header {
|
| 368 |
+
position: absolute;
|
| 369 |
+
top: 0;
|
| 370 |
+
display: grid;
|
| 371 |
+
grid-template-columns: repeat(7, minmax(0, 1fr));
|
| 372 |
+
gap: 6px;
|
| 373 |
+
z-index: 7;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.timeline-day-head {
|
| 377 |
+
min-height: 46px;
|
| 378 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 379 |
+
border-radius: 16px;
|
| 380 |
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.08) 0%, rgba(255, 255, 255, 0.03) 100%);
|
| 381 |
+
color: var(--muted-strong);
|
| 382 |
+
display: grid;
|
| 383 |
+
place-items: center;
|
| 384 |
+
gap: 2px;
|
| 385 |
+
padding: 5px 4px;
|
| 386 |
+
text-align: center;
|
| 387 |
+
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
|
| 388 |
+
}
|
| 389 |
+
|
| 390 |
+
.timeline-day-head strong {
|
| 391 |
+
font-size: 0.82rem;
|
| 392 |
+
line-height: 1;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.timeline-day-head span {
|
| 396 |
+
font-size: 0.68rem;
|
| 397 |
+
color: var(--muted);
|
| 398 |
+
font-variant-numeric: tabular-nums;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.timeline-day-head.is-selected {
|
| 402 |
+
border-color: rgba(123, 231, 234, 0.44);
|
| 403 |
+
background: linear-gradient(180deg, rgba(123, 231, 234, 0.18) 0%, rgba(255, 255, 255, 0.05) 100%);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.timeline-day-head.is-today {
|
| 407 |
+
box-shadow: inset 0 0 0 1px rgba(255, 200, 87, 0.22);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
.timeline-day-column,
|
| 411 |
+
.timeline-day-divider {
|
| 412 |
+
position: absolute;
|
| 413 |
+
top: 0;
|
| 414 |
+
bottom: 0;
|
| 415 |
+
pointer-events: none;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.timeline-day-column {
|
| 419 |
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.008) 100%);
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
.timeline-day-column.is-selected {
|
| 423 |
+
background: linear-gradient(180deg, rgba(92, 225, 230, 0.06) 0%, rgba(92, 225, 230, 0.015) 100%);
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.timeline-day-column.is-today {
|
| 427 |
+
box-shadow: inset 0 0 0 1px rgba(255, 200, 87, 0.12);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.timeline-day-divider {
|
| 431 |
+
width: 1px;
|
| 432 |
+
background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.02));
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
.timeline-axis-rail {
|
| 436 |
position: absolute;
|
| 437 |
top: 0;
|
|
|
|
| 448 |
display: flex;
|
| 449 |
justify-content: flex-end;
|
| 450 |
align-items: center;
|
| 451 |
+
gap: 10px;
|
| 452 |
transform: translateY(-50%);
|
| 453 |
+
padding-right: 20px;
|
| 454 |
color: rgba(238, 244, 251, 0.76);
|
| 455 |
+
font-size: 0.75rem;
|
| 456 |
font-variant-numeric: tabular-nums;
|
| 457 |
letter-spacing: 0.02em;
|
| 458 |
line-height: 1;
|
|
|
|
| 461 |
|
| 462 |
.timeline-axis-tick::after {
|
| 463 |
content: "";
|
| 464 |
+
width: 7px;
|
| 465 |
+
height: 7px;
|
| 466 |
border-radius: 999px;
|
| 467 |
background: rgba(123, 231, 234, 0.78);
|
| 468 |
box-shadow: 0 0 0 4px rgba(123, 231, 234, 0.12);
|
|
|
|
| 479 |
|
| 480 |
.timeline-slot-band {
|
| 481 |
position: absolute;
|
| 482 |
+
left: 6px;
|
| 483 |
+
right: 8px;
|
| 484 |
box-sizing: border-box;
|
| 485 |
overflow: hidden;
|
| 486 |
+
padding: 5px 8px;
|
| 487 |
+
border-radius: 14px;
|
| 488 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.025));
|
| 489 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 490 |
display: flex;
|
| 491 |
flex-direction: column;
|
| 492 |
justify-content: center;
|
| 493 |
+
gap: 3px;
|
| 494 |
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
| 495 |
}
|
| 496 |
|
| 497 |
.timeline-slot-band strong {
|
| 498 |
+
font-size: 0.74rem;
|
| 499 |
line-height: 1.1;
|
| 500 |
}
|
| 501 |
|
| 502 |
.timeline-slot-band span {
|
| 503 |
color: var(--muted);
|
| 504 |
+
font-size: 0.64rem;
|
| 505 |
line-height: 1.1;
|
| 506 |
font-variant-numeric: tabular-nums;
|
| 507 |
}
|
|
|
|
| 522 |
left: 0;
|
| 523 |
right: 0;
|
| 524 |
box-sizing: border-box;
|
| 525 |
+
border-radius: 16px;
|
| 526 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.045) 0%, rgba(255, 255, 255, 0.015) 100%);
|
| 527 |
pointer-events: none;
|
| 528 |
}
|
| 529 |
|
| 530 |
.timeline-major-block span {
|
| 531 |
position: absolute;
|
| 532 |
+
top: 10px;
|
| 533 |
+
right: 12px;
|
| 534 |
font-family: "Sora", "Noto Sans SC", sans-serif;
|
| 535 |
+
font-size: clamp(1rem, 1.35vw, 1.45rem);
|
| 536 |
color: rgba(238, 244, 251, 0.16);
|
| 537 |
}
|
| 538 |
|
|
|
|
| 541 |
position: absolute;
|
| 542 |
box-sizing: border-box;
|
| 543 |
overflow: hidden;
|
| 544 |
+
border-radius: 16px;
|
| 545 |
+
padding: 10px 12px 12px;
|
| 546 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 547 |
background: linear-gradient(180deg, rgba(13, 28, 44, 0.96) 0%, rgba(8, 20, 32, 0.92) 100%);
|
| 548 |
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.2);
|
|
|
|
| 550 |
|
| 551 |
.planner-event {
|
| 552 |
touch-action: none;
|
| 553 |
+
z-index: 5;
|
| 554 |
}
|
| 555 |
|
| 556 |
.planner-event::before,
|
|
|
|
| 571 |
}
|
| 572 |
|
| 573 |
.planner-event-top strong {
|
| 574 |
+
max-width: 100%;
|
| 575 |
+
line-height: 1.18;
|
| 576 |
+
font-size: 0.86rem;
|
| 577 |
}
|
| 578 |
|
| 579 |
.planner-event-meta {
|
| 580 |
+
margin-top: 8px;
|
| 581 |
color: var(--muted);
|
| 582 |
+
font-size: 0.74rem;
|
| 583 |
}
|
| 584 |
|
| 585 |
.planner-lock-badge,
|
|
|
|
| 603 |
.planner-task-clear {
|
| 604 |
background: rgba(255, 107, 92, 0.14);
|
| 605 |
color: #ff9388;
|
| 606 |
+
margin-top: 8px;
|
| 607 |
}
|
| 608 |
|
| 609 |
.planner-event-resize {
|
| 610 |
position: absolute;
|
| 611 |
+
left: 8px;
|
| 612 |
+
right: 8px;
|
| 613 |
+
height: 8px;
|
| 614 |
border-radius: 999px;
|
| 615 |
background: linear-gradient(90deg, rgba(123, 231, 234, 0.9), rgba(97, 210, 159, 0.72));
|
| 616 |
cursor: ns-resize;
|
|
|
|
| 618 |
}
|
| 619 |
|
| 620 |
.planner-event-resize-top {
|
| 621 |
+
top: -1px;
|
| 622 |
}
|
| 623 |
|
| 624 |
.planner-event-resize-bottom {
|
| 625 |
+
bottom: -1px;
|
| 626 |
}
|
| 627 |
|
| 628 |
.planner-event.is-dragging,
|
|
|
|
| 630 |
opacity: 0.8;
|
| 631 |
}
|
| 632 |
|
| 633 |
+
.planner-event.is-dragging {
|
| 634 |
+
z-index: 8;
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
.planner-event.is-compact {
|
| 638 |
border-radius: 16px;
|
| 639 |
+
padding: 7px 10px 8px;
|
| 640 |
}
|
| 641 |
|
| 642 |
.planner-event.is-compact::before {
|
|
|
|
| 667 |
}
|
| 668 |
|
| 669 |
.planner-event.is-tight {
|
| 670 |
+
padding: 6px 8px;
|
| 671 |
}
|
| 672 |
|
| 673 |
.planner-event.is-tight .planner-event-meta,
|
|
|
|
| 693 |
}
|
| 694 |
|
| 695 |
.planner-sidebar {
|
| 696 |
+
padding: 14px;
|
| 697 |
display: grid;
|
| 698 |
+
gap: 12px;
|
| 699 |
align-content: start;
|
| 700 |
+
min-height: 0;
|
| 701 |
+
grid-template-rows: auto auto minmax(0, 1fr);
|
| 702 |
}
|
| 703 |
|
| 704 |
.planner-sidebar-note {
|
| 705 |
display: grid;
|
| 706 |
+
gap: 6px;
|
| 707 |
color: var(--muted);
|
| 708 |
+
font-size: 0.84rem;
|
| 709 |
}
|
| 710 |
|
| 711 |
.planner-task-pool {
|
| 712 |
display: grid;
|
| 713 |
+
gap: 10px;
|
| 714 |
+
min-height: 0;
|
| 715 |
+
overflow: auto;
|
| 716 |
+
padding-right: 4px;
|
| 717 |
}
|
| 718 |
|
| 719 |
.planner-task-card {
|
| 720 |
+
padding: 12px;
|
| 721 |
+
border-radius: 18px;
|
| 722 |
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 723 |
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06) 0%, rgba(255, 255, 255, 0.03) 100%);
|
| 724 |
display: grid;
|
| 725 |
+
gap: 10px;
|
| 726 |
}
|
| 727 |
|
| 728 |
.planner-task-card h4 {
|
| 729 |
margin: 0;
|
| 730 |
+
font-size: 0.95rem;
|
| 731 |
}
|
| 732 |
|
| 733 |
.planner-task-top {
|
|
|
|
| 741 |
}
|
| 742 |
|
| 743 |
.planner-task-tags span {
|
| 744 |
+
padding: 6px 9px;
|
| 745 |
border-radius: 999px;
|
| 746 |
background: rgba(255, 255, 255, 0.05);
|
| 747 |
color: var(--muted-strong);
|
| 748 |
+
font-size: 0.76rem;
|
| 749 |
}
|
| 750 |
|
| 751 |
.planner-empty,
|
|
|
|
| 759 |
|
| 760 |
.timeline-drop-preview {
|
| 761 |
display: none;
|
| 762 |
+
gap: 4px;
|
| 763 |
+
align-content: center;
|
| 764 |
color: var(--muted-strong);
|
| 765 |
border-style: dashed;
|
| 766 |
background: rgba(123, 231, 234, 0.1);
|
| 767 |
+
z-index: 4;
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
.timeline-drop-preview strong {
|
| 771 |
+
font-size: 0.78rem;
|
| 772 |
+
line-height: 1.1;
|
| 773 |
+
}
|
| 774 |
+
|
| 775 |
+
.timeline-drop-preview span {
|
| 776 |
+
color: var(--muted);
|
| 777 |
+
font-size: 0.72rem;
|
| 778 |
}
|
| 779 |
|
| 780 |
.timeline-now-line {
|
|
|
|
| 782 |
height: 2px;
|
| 783 |
background: linear-gradient(90deg, rgba(255, 107, 92, 0.95), rgba(255, 200, 87, 0.72));
|
| 784 |
box-shadow: 0 0 18px rgba(255, 107, 92, 0.38);
|
| 785 |
+
z-index: 6;
|
| 786 |
+
pointer-events: none;
|
| 787 |
}
|
| 788 |
|
| 789 |
.timeline-now-line::before {
|
|
|
|
| 819 |
}
|
| 820 |
|
| 821 |
@media (max-width: 1180px) {
|
| 822 |
+
body {
|
| 823 |
+
overflow: auto;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.layout.story-layout {
|
| 827 |
+
min-height: auto;
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.story-shell {
|
| 831 |
+
min-height: auto;
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
.story-toolbar,
|
| 835 |
.planner-head,
|
| 836 |
.planner-layout {
|
|
|
|
| 843 |
justify-self: stretch;
|
| 844 |
justify-content: center;
|
| 845 |
}
|
| 846 |
+
|
| 847 |
+
.page-viewport,
|
| 848 |
+
.page-track,
|
| 849 |
+
.page-slide,
|
| 850 |
+
.planner-shell {
|
| 851 |
+
height: auto;
|
| 852 |
+
}
|
| 853 |
}
|
| 854 |
|
| 855 |
@media (max-width: 840px) {
|
|
|
|
| 864 |
}
|
| 865 |
|
| 866 |
.timeline-scroll {
|
| 867 |
+
height: auto;
|
| 868 |
}
|
| 869 |
}
|
| 870 |
|
templates/index.html
CHANGED
|
@@ -14,7 +14,7 @@
|
|
| 14 |
|
| 15 |
<nav class="story-nav" aria-label="页面导航">
|
| 16 |
<button class="story-tab is-active" type="button" data-go-page="0">第一页 · 提醒板</button>
|
| 17 |
-
<button class="story-tab" type="button" data-go-page="1">第二页 ·
|
| 18 |
</nav>
|
| 19 |
|
| 20 |
<div class="action-group">
|
|
@@ -34,9 +34,9 @@
|
|
| 34 |
<div class="hero-topbar">
|
| 35 |
<div>
|
| 36 |
<p class="eyebrow">北京时间</p>
|
| 37 |
-
<p class="hero-copy">第一页保持轻量
|
| 38 |
</div>
|
| 39 |
-
<button class="ghost-link inline-link" type="button" data-go-page="1">切换到
|
| 40 |
</div>
|
| 41 |
<div class="clock-wrap">
|
| 42 |
<h1 class="clock-display" id="clockDisplay">00:00:00</h1>
|
|
@@ -52,11 +52,11 @@
|
|
| 52 |
<div class="section-header">
|
| 53 |
<div>
|
| 54 |
<p class="section-kicker">今日待办板</p>
|
| 55 |
-
<h2>每个分类一列,
|
| 56 |
</div>
|
| 57 |
<p class="section-note">
|
| 58 |
{% if authenticated %}
|
| 59 |
-
已登录,可添加、勾选、重命名,并
|
| 60 |
{% else %}
|
| 61 |
当前为只读模式,登录后可进行编辑与拖拽排程。
|
| 62 |
{% endif %}
|
|
@@ -124,23 +124,23 @@
|
|
| 124 |
<section class="planner-shell card-surface">
|
| 125 |
<div class="planner-head">
|
| 126 |
<div>
|
| 127 |
-
<p class="section-kicker">第二页 ·
|
| 128 |
-
<h2>
|
| 129 |
-
<p class="section-note" id="plannerHeadlineNote">固定课程会按
|
| 130 |
</div>
|
| 131 |
<div class="planner-controls">
|
| 132 |
-
<button class="icon-button" id="plannerPrevDay" type="button" aria-label="
|
| 133 |
<label class="planner-date-picker">
|
| 134 |
-
<span>选择日期</span>
|
| 135 |
<input id="plannerDateInput" type="date">
|
| 136 |
</label>
|
| 137 |
-
<button class="icon-button" id="plannerNextDay" type="button" aria-label="
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
<div class="planner-meta">
|
| 142 |
-
<span id="plannerDateLabel">{{ planner_payload.
|
| 143 |
-
<span id="plannerWeekday">{{ planner_payload.
|
| 144 |
<span id="plannerAcademicWeek">{{ planner_payload.academic_label }}</span>
|
| 145 |
<span id="plannerWindow">{{ planner_payload.settings.day_start }} - {{ planner_payload.settings.day_end }}</span>
|
| 146 |
</div>
|
|
@@ -159,12 +159,12 @@
|
|
| 159 |
<div class="planner-sidebar-head">
|
| 160 |
<div>
|
| 161 |
<p class="column-label">Todolist Drag Zone</p>
|
| 162 |
-
<h3>拖
|
| 163 |
</div>
|
| 164 |
<span class="planner-count" id="plannerTaskCount">0 项</span>
|
| 165 |
</div>
|
| 166 |
<div class="planner-sidebar-note">
|
| 167 |
-
<span>课程
|
| 168 |
<span>任务块可拖动、拉伸,也可移出排程。</span>
|
| 169 |
</div>
|
| 170 |
<div class="planner-task-pool" id="plannerTaskPool"></div>
|
|
|
|
| 14 |
|
| 15 |
<nav class="story-nav" aria-label="页面导航">
|
| 16 |
<button class="story-tab is-active" type="button" data-go-page="0">第一页 · 提醒板</button>
|
| 17 |
+
<button class="story-tab" type="button" data-go-page="1">第二页 · 周课表</button>
|
| 18 |
</nav>
|
| 19 |
|
| 20 |
<div class="action-group">
|
|
|
|
| 34 |
<div class="hero-topbar">
|
| 35 |
<div>
|
| 36 |
<p class="eyebrow">北京时间</p>
|
| 37 |
+
<p class="hero-copy">第一页保持轻量提醒视图,第二页进入整周课程与任务排布。</p>
|
| 38 |
</div>
|
| 39 |
+
<button class="ghost-link inline-link" type="button" data-go-page="1">切换到周课表</button>
|
| 40 |
</div>
|
| 41 |
<div class="clock-wrap">
|
| 42 |
<h1 class="clock-display" id="clockDisplay">00:00:00</h1>
|
|
|
|
| 52 |
<div class="section-header">
|
| 53 |
<div>
|
| 54 |
<p class="section-kicker">今日待办板</p>
|
| 55 |
+
<h2>每个分类一列,压缩留白后在一屏内保持更高信息密度</h2>
|
| 56 |
</div>
|
| 57 |
<p class="section-note">
|
| 58 |
{% if authenticated %}
|
| 59 |
+
已登录,可添加、勾选、重命名,并把任务拖进第二页的周课表。
|
| 60 |
{% else %}
|
| 61 |
当前为只读模式,登录后可进行编辑与拖拽排程。
|
| 62 |
{% endif %}
|
|
|
|
| 124 |
<section class="planner-shell card-surface">
|
| 125 |
<div class="planner-head">
|
| 126 |
<div>
|
| 127 |
+
<p class="section-kicker">第二页 · 周课表</p>
|
| 128 |
+
<h2>按周查看课程与任务,把整周安排收进一屏</h2>
|
| 129 |
+
<p class="section-note" id="plannerHeadlineNote">固定课程会按学期周次自动出现,待办可拖入本周任意一天,并支持上下边缘拉伸;每项任务最短 15 分钟。</p>
|
| 130 |
</div>
|
| 131 |
<div class="planner-controls">
|
| 132 |
+
<button class="icon-button planner-shift-button" id="plannerPrevDay" type="button" aria-label="上一周">←</button>
|
| 133 |
<label class="planner-date-picker">
|
| 134 |
+
<span>选择本周任意日期</span>
|
| 135 |
<input id="plannerDateInput" type="date">
|
| 136 |
</label>
|
| 137 |
+
<button class="icon-button planner-shift-button" id="plannerNextDay" type="button" aria-label="下一周">→</button>
|
| 138 |
</div>
|
| 139 |
</div>
|
| 140 |
|
| 141 |
<div class="planner-meta">
|
| 142 |
+
<span id="plannerDateLabel">{{ planner_payload.week_start }} - {{ planner_payload.week_end }}</span>
|
| 143 |
+
<span id="plannerWeekday">{{ planner_payload.week_range_label }}</span>
|
| 144 |
<span id="plannerAcademicWeek">{{ planner_payload.academic_label }}</span>
|
| 145 |
<span id="plannerWindow">{{ planner_payload.settings.day_start }} - {{ planner_payload.settings.day_end }}</span>
|
| 146 |
</div>
|
|
|
|
| 159 |
<div class="planner-sidebar-head">
|
| 160 |
<div>
|
| 161 |
<p class="column-label">Todolist Drag Zone</p>
|
| 162 |
+
<h3>把待办拖进本周课表</h3>
|
| 163 |
</div>
|
| 164 |
<span class="planner-count" id="plannerTaskCount">0 项</span>
|
| 165 |
</div>
|
| 166 |
<div class="planner-sidebar-note">
|
| 167 |
+
<span>课程是固定块,只能在后台修改。</span>
|
| 168 |
<span>任务块可拖动、拉伸,也可移出排程。</span>
|
| 169 |
</div>
|
| 170 |
<div class="planner-task-pool" id="plannerTaskPool"></div>
|