昨天我們把篩選 UI 做好了,今天就讓它真的動起來!雖然只是基本邏輯,但這是之後加上「搜尋、排序、收藏、LocalStorage 記憶」的關鍵基礎。
js/app.js
在首頁檔 </body>
前,加上這行(若已存在就跳過):
<script src="js/app.js"></script>
js/app.js
(合併版:渲染 + 篩選互動)把下面這段 整份貼到 js/app.js
(會同時包含:假資料、渲染、chips 同步、清除鍵、鍵盤操作與過濾邏輯):
// js/app.js — Day 8:用 JS 動態渲染 + 依類型過濾
(function () {
// ====== 假資料(之後可替換成 API)======
const SHOWS = [
{ id: "tv_0001", title: "我們的戀愛日記", genres: ["Romance","Comedy"], rating: 8.2, cover: "https://picsum.photos/seed/drama1/640/360" },
{ id: "tv_0002", title: "霧中真相", genres: ["Mystery","Drama"], rating: 8.8, cover: "https://picsum.photos/seed/drama2/640/360" },
{ id: "tv_0003", title: "熱血行動", genres: ["Action"], rating: 7.9, cover: "https://picsum.photos/seed/drama3/640/360" },
{ id: "tv_0004", title: "笑到最後一刻", genres: ["Comedy"], rating: 7.6, cover: "https://picsum.photos/seed/drama4/640/360" },
{ id: "tv_0005", title: "青春課間", genres: ["Drama"], rating: 7.4, cover: "https://picsum.photos/seed/drama5/640/360" },
{ id: "tv_0006", title: "答案在遠方", genres: ["Romance","Drama"], rating: 8.5, cover: "https://picsum.photos/seed/drama6/640/360" },
{ id: "tv_0007", title: "夜行者", genres: ["Mystery","Action"], rating: 8.1, cover: "https://picsum.photos/seed/drama7/640/360" },
{ id: "tv_0008", title: "辦公室日常", genres: ["Comedy","Drama"], rating: 7.8, cover: "https://picsum.photos/seed/drama8/640/360" },
{ id: "tv_0009", title: "追風少年", genres: ["Action","Drama"], rating: 7.7, cover: "https://picsum.photos/seed/drama9/640/360" },
];
// 英文代碼 → 中文顯示
const LABELS = { Romance: "愛情", Comedy: "喜劇", Mystery: "懸疑", Action: "動作", Drama: "劇情" };
// ====== DOM 快取 ======
const $cards = $("#cards");
const $buttons = $(".filter-btn");
const $chips = $("#selectedTags");
const $clear = $("#btnClear");
// ====== 渲染卡片 ======
function cardTemplate(s) {
const tags = s.genres.map(g => LABELS[g] || g).join(" / ");
return `
<article class="card" data-id="${s.id}">
<img class="cover" src="${s.cover}" alt="${s.title} 封面">
<div class="card-body">
<h3 class="title">${s.title}</h3>
<p class="meta">${tags} · ⭐ ${s.rating}</p>
<button class="fav" type="button" aria-pressed="false">★ 收藏</button>
</div>
</article>
`;
}
function render(list) {
if (!list.length) {
$cards.html(`<div class="empty">沒有符合條件的劇集</div>`);
return;
}
$cards.html(list.map(cardTemplate).join(""));
}
// ====== 篩選邏輯 ======
function getActiveGenres() {
return $(".filter-btn.active").map((_, el) => $(el).data("genre")).get();
}
function applyFilters() {
const active = new Set(getActiveGenres());
if (!active.size) return SHOWS.slice(); // 沒選任何類型 → 全部
return SHOWS.filter(s => s.genres.some(g => active.has(g)));
}
// ====== 已選 chips 與清除鍵 ======
function updateClearState() {
const any = $(".filter-btn.active").length > 0;
$clear.attr("aria-disabled", any ? "false" : "true");
}
function addChip(genre) {
const text = LABELS[genre] || genre;
if ($chips.find(`[data-genre="${genre}"]`).length) return;
$chips.append(`
<span class="chip" data-genre="${genre}">
${text}
<button class="remove" type="button" aria-label="移除 ${text}">
×<span class="visually-hidden"> 移除 ${text}</span>
</button>
</span>
`);
}
function removeChip(genre) {
$chips.find(`[data-genre="${genre}"]`).remove();
}
// ====== 綁定互動 ======
function bindUI() {
// 切換按鈕(點擊/鍵盤)
$(document).on("click", ".filter-btn", function () {
const $btn = $(this);
const genre = $btn.data("genre");
const active = !$btn.hasClass("active");
$btn.toggleClass("active", active).attr("aria-pressed", active);
active ? addChip(genre) : removeChip(genre);
updateClearState();
render(applyFilters());
}).on("keydown", ".filter-btn", function (e) {
if (e.key === " " || e.key === "Enter") { e.preventDefault(); $(this).click(); }
});
// 移除單一 chip
$(document).on("click", ".chip .remove", function () {
const $chip = $(this).closest(".chip");
const genre = $chip.data("genre");
$chip.remove();
const $btn = $buttons.filter(`[data-genre="${genre}"]`);
$btn.removeClass("active").attr("aria-pressed", "false");
updateClearState();
render(applyFilters());
});
// 一鍵清除
$clear.on("click", function () {
if ($clear.attr("aria-disabled") === "true") return;
$buttons.removeClass("active").attr("aria-pressed", "false");
$chips.empty();
updateClearState();
render(applyFilters());
});
}
// ====== 啟動 ======
$(function () {
render(SHOWS); // 先渲染全部
bindUI(); // 接上互動
updateClearState(); // 初始化清除鍵狀態
});
})();
style.css
末尾加「空狀態」樣式/* 空狀態 */
.empty{
text-align: center; color: var(--muted);
padding: 24px 8px; border: 1px dashed var(--border);
border-radius: var(--radius); background: var(--surface);
}