| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- (function () {
- const data = window.CASE_DATA;
- const timeline = document.querySelector("#timeline");
- const filters = document.querySelector("#filters");
- let activeFilter = "all";
- let timelineView = "compact";
- let editing = false;
- const editStoreKey = "saigon-leverage-edits-v1";
- const themeStoreKey = "saigon-leverage-theme";
- document.querySelector("#episodeLabel").textContent = `CASE TIMELINE · ${data.episode}`;
- document.querySelector("#heroEyebrow").textContent = data.eyebrow;
- document.querySelector("#caseTitle").innerHTML = `${data.title.split(":")[0]}:<br><span>${data.title.split(":").slice(1).join(":")}</span>`;
- document.querySelector("#heroDeck").textContent = data.deck;
- document.querySelector("#heroDeck").dataset.edit = "hero-deck";
- document.querySelector("#heroMetrics").innerHTML = data.metrics.map((metric) => `
- <div class="metric"><strong data-edit="metric-${metric.label}-value">${metric.value}</strong><span data-edit="metric-${metric.label}-label">${metric.label}</span></div>
- `).join("");
- document.querySelector("#participantsGrid").innerHTML = data.participants.map((party) => `
- <article class="participant-card">
- <div class="participant-title"><span aria-hidden="true">${party.mark}</span><div><h3 data-edit="party-${party.mark}-name">${party.name}</h3><p data-edit="party-${party.mark}-role">${party.role}</p></div></div>
- <p data-edit="party-${party.mark}-stance">${party.stance}</p>
- <strong data-edit="party-${party.mark}-asset">${party.asset}</strong>
- </article>
- `).join("");
- function applyTimelineView() {
- timeline.classList.toggle("compact", timelineView === "compact");
- document.querySelectorAll("[data-view]").forEach((button) => {
- const active = button.dataset.view === timelineView;
- button.classList.toggle("active", active);
- button.setAttribute("aria-pressed", String(active));
- });
- }
- function renderFilters() {
- filters.innerHTML = data.categories.map((item) => `
- <button type="button" class="filter ${item.id === activeFilter ? "active" : ""}" data-filter="${item.id}">
- <i aria-hidden="true"></i>${item.label}
- </button>
- `).join("");
- }
- function renderTimeline() {
- const visible = activeFilter === "all" ? data.events : data.events.filter((event) => event.category === activeFilter);
- timeline.innerHTML = visible.map((event, position) => `
- <article class="event ${event.featured ? "featured" : ""}" id="${event.id}" data-category="${event.category}" style="--delay:${position * 35}ms">
- <div class="event-date"><span data-edit="event-${event.id}-date">${event.date}</span><small>${event.index}</small></div>
- <button class="event-card" type="button" aria-expanded="false" aria-controls="detail-${event.id}">
- <div class="event-card-top"><span class="tag">${categoryLabel(event.category)}</span><span class="expand-icon" aria-hidden="true">+</span></div>
- <h3 data-edit="event-${event.id}-title">${event.title}</h3>
- <p data-edit="event-${event.id}-summary">${event.summary}</p>
- <strong class="event-stat" data-edit="event-${event.id}-stat">${event.stat}</strong>
- <div class="event-detail" id="detail-${event.id}">
- <div><span>商业含义</span><p data-edit="event-${event.id}-detail">${event.detail}</p></div>
- <div><span>关键影响</span><p data-edit="event-${event.id}-impact">${event.impact}</p></div>
- </div>
- </button>
- </article>
- `).join("");
- bindCards();
- applyTimelineView();
- restoreEdits();
- if (editing) setEditing(true);
- }
- function categoryLabel(id) {
- return data.categories.find((item) => item.id === id)?.label || id;
- }
- function bindCards() {
- document.querySelectorAll(".event-card").forEach((card) => {
- card.addEventListener("click", () => {
- if (editing) return;
- const expanded = card.getAttribute("aria-expanded") === "true";
- card.setAttribute("aria-expanded", String(!expanded));
- card.closest(".event").classList.toggle("open", !expanded);
- });
- });
- }
- filters.addEventListener("click", (event) => {
- const button = event.target.closest("[data-filter]");
- if (!button) return;
- activeFilter = button.dataset.filter;
- renderFilters();
- renderTimeline();
- });
- document.querySelector(".view-switch").addEventListener("click", (event) => {
- const button = event.target.closest("[data-view]");
- if (!button) return;
- timelineView = button.dataset.view;
- applyTimelineView();
- });
- document.querySelector("#chapterNav").innerHTML = data.chapters.map((chapter) => {
- const target = data.events.find((event) => event.chapter === chapter.id)?.id;
- return `<a href="#${target}"><span>${chapter.label.slice(0, 2)}</span>${chapter.label.slice(3)}</a>`;
- }).join("");
- document.querySelector("#modelGrid").innerHTML = data.model.map((item) => `
- <article class="model-card ${item.category}"><span>${item.num}</span><h3 data-edit="model-${item.num}-title">${item.title}</h3><strong data-edit="model-${item.num}-subtitle">${item.subtitle}</strong><p data-edit="model-${item.num}-body">${item.body}</p></article>
- `).join("");
- function storedEdits() {
- try { return JSON.parse(localStorage.getItem(editStoreKey)) || {}; } catch (_) { return {}; }
- }
- function restoreEdits() {
- const saved = storedEdits();
- document.querySelectorAll("[data-edit]").forEach((element) => {
- if (saved[element.dataset.edit] !== undefined) element.innerHTML = saved[element.dataset.edit];
- });
- }
- function saveEdit(element) {
- const saved = storedEdits();
- saved[element.dataset.edit] = element.innerHTML;
- localStorage.setItem(editStoreKey, JSON.stringify(saved));
- }
- function setEditing(next) {
- editing = next;
- document.body.classList.toggle("is-editing", editing);
- const editButton = document.querySelector("#editToggle");
- editButton.querySelector(".icon-glyph").textContent = editing ? "✓" : "✎";
- editButton.setAttribute("aria-label", editing ? "完成编辑" : "编辑页面内容");
- editButton.title = editing ? "完成编辑" : "编辑页面内容";
- document.querySelectorAll("[data-edit]").forEach((element) => {
- element.contentEditable = editing ? "true" : "false";
- element.spellcheck = editing;
- });
- }
- document.addEventListener("input", (event) => {
- const editable = event.target.closest("[data-edit]");
- if (editing && editable) saveEdit(editable);
- });
- document.querySelector("#editToggle").addEventListener("click", () => setEditing(!editing));
- document.querySelector("#resetEdits").addEventListener("click", () => {
- if (!confirm("确定恢复全部原稿吗?当前编辑内容将被清除。")) return;
- localStorage.removeItem(editStoreKey);
- location.reload();
- });
- function setTheme(theme) {
- document.documentElement.dataset.theme = theme;
- const themeButton = document.querySelector("#themeToggle");
- const nextTheme = theme === "dark" ? "浅色" : "深色";
- themeButton.querySelector(".icon-glyph").textContent = theme === "dark" ? "☼" : "☾";
- themeButton.setAttribute("aria-label", `切换至${nextTheme}模式`);
- themeButton.title = `切换至${nextTheme}模式`;
- document.querySelectorAll("img[data-dark-src]").forEach((image) => {
- image.src = theme === "dark" ? image.dataset.darkSrc : image.dataset.lightSrc;
- });
- localStorage.setItem(themeStoreKey, theme);
- }
- document.querySelector("#themeToggle").addEventListener("click", () => {
- setTheme(document.documentElement.dataset.theme === "dark" ? "light" : "dark");
- });
- document.querySelector("#copyLink").addEventListener("click", async (event) => {
- const button = event.currentTarget;
- try {
- await navigator.clipboard.writeText(location.href);
- button.classList.add("is-copied");
- button.querySelector(".icon-glyph").textContent = "✓";
- button.setAttribute("aria-label", "链接已复制");
- button.title = "链接已复制";
- setTimeout(() => {
- button.classList.remove("is-copied");
- button.querySelector(".icon-glyph").textContent = "⤴";
- button.setAttribute("aria-label", "复制当前页面链接");
- button.title = "复制当前页面链接";
- }, 1600);
- } catch (_) {
- button.querySelector(".icon-glyph").textContent = "!";
- button.setAttribute("aria-label", "复制失败");
- button.title = "复制失败";
- }
- });
- function updateProgress() {
- const story = document.querySelector("#story");
- const start = story.offsetTop;
- const distance = Math.max(story.offsetHeight - innerHeight, 1);
- const percent = Math.max(0, Math.min(100, ((scrollY - start) / distance) * 100));
- document.querySelector("#progressBar").style.width = `${percent}%`;
- document.querySelector("#progressText").textContent = `${Math.round(percent)}%`;
- }
- addEventListener("scroll", updateProgress, { passive: true });
- renderFilters();
- renderTimeline();
- restoreEdits();
- setTheme(localStorage.getItem(themeStoreKey) || "dark");
- updateProgress();
- })();
|