CONTENT MANAGEMENT
PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); return $pdo; } function h(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function redirect(string $path): never { header('Location: ' . $path); exit; } function csrf_token(): string { if (empty($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(32)); } return $_SESSION['csrf']; } function verify_csrf(): void { $token = (string) ($_POST['csrf'] ?? ''); if (!hash_equals((string) ($_SESSION['csrf'] ?? ''), $token)) { http_response_code(419); exit('表單已過期,請返回後重新操作。'); } } function require_login(): void { if (empty($_SESSION['admin_id'])) { redirect('login.php'); } } function flash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function take_flash(): ?array { $flash = $_SESSION['flash'] ?? null; unset($_SESSION['flash']); return is_array($flash) ? $flash : null; } function modules(): array { return [ 'albums' => [ 'label' => '專輯管理', 'icon' => 'disc', 'table' => 'albums', 'order' => 'release_date DESC, id DESC', 'fields' => [ 'title' => ['label' => '專輯名稱', 'type' => 'text', 'required' => true], 'title_ko' => ['label' => '韓文/英文名稱', 'type' => 'text'], 'release_date' => ['label' => '發行日期', 'type' => 'date'], 'era' => ['label' => '時期', 'type' => 'select', 'options' => ['classic' => '輝煌全盛期', 'reunion' => '啟動新篇章']], 'cover_media_id' => ['label' => '封面媒體編號', 'type' => 'number'], 'summary' => ['label' => '專輯簡介', 'type' => 'textarea'], 'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['title', 'release_date', 'era', 'status', 'updated_at'], ], 'songs' => [ 'label' => '歌曲管理', 'icon' => 'music-note-list', 'table' => 'songs', 'order' => 'album_id, track_no, id', 'fields' => [ 'album_id' => ['label' => '所屬專輯', 'type' => 'relation', 'table' => 'albums', 'required' => true], 'track_no' => ['label' => '曲序', 'type' => 'number'], 'title' => ['label' => '歌名', 'type' => 'text', 'required' => true], 'title_ko' => ['label' => '韓文歌名', 'type' => 'text'], 'lyrics_url' => ['label' => '歌詞頁連結', 'type' => 'url'], 'video_url' => ['label' => '影片連結', 'type' => 'url'], 'is_title_track' => ['label' => '主打歌', 'type' => 'checkbox'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['title', 'album_id', 'track_no', 'is_title_track', 'status', 'updated_at'], ], 'stages' => [ 'label' => '打歌舞台管理', 'icon' => 'mic', 'table' => 'stage_performances', 'order' => 'stage_date DESC, sort_order, id', 'fields' => [ 'song_id' => ['label' => '所屬歌曲', 'type' => 'relation', 'table' => 'songs', 'required' => true], 'stage_date' => ['label' => '播出日期', 'type' => 'date', 'required' => true], 'station' => ['label' => '電視台', 'type' => 'text', 'required' => true], 'program' => ['label' => '節目名稱', 'type' => 'text', 'required' => true], 'title' => ['label' => '舞台標題', 'type' => 'text', 'required' => true], 'youtube_url' => ['label' => 'YouTube 連結', 'type' => 'url'], 'note' => ['label' => '備註', 'type' => 'textarea'], 'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['title', 'song_id', 'stage_date', 'station', 'program', 'status', 'updated_at'], ], 'concerts' => [ 'label' => '演唱會管理', 'icon' => 'ticket-perforated', 'table' => 'concert_events', 'order' => 'event_date DESC, id DESC', 'fields' => [ 'tour_name' => ['label' => '巡演/活動名稱', 'type' => 'text', 'required' => true], 'event_date' => ['label' => '演出日期', 'type' => 'date', 'required' => true], 'country' => ['label' => '國家', 'type' => 'text'], 'city' => ['label' => '城市', 'type' => 'text'], 'venue' => ['label' => '場館', 'type' => 'text'], 'member_names' => ['label' => '演出成員', 'type' => 'text'], 'poster_media_id' => ['label' => '海報媒體編號', 'type' => 'number'], 'source_url' => ['label' => '資料來源', 'type' => 'url'], 'notes' => ['label' => '備註', 'type' => 'textarea'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['tour_name', 'event_date', 'city', 'venue', 'status', 'updated_at'], ], 'variety' => [ 'label' => '綜藝節目管理', 'icon' => 'tv', 'table' => 'variety_shows', 'order' => 'air_start DESC, id DESC', 'fields' => [ 'title' => ['label' => '節目名稱', 'type' => 'text', 'required' => true], 'series_name' => ['label' => '系列/單元', 'type' => 'text'], 'platform' => ['label' => '播出平台', 'type' => 'text'], 'air_start' => ['label' => '開始日期', 'type' => 'date'], 'air_end' => ['label' => '結束日期', 'type' => 'date'], 'member_names' => ['label' => '參與成員', 'type' => 'text'], 'episode_info' => ['label' => '集數', 'type' => 'text'], 'video_url' => ['label' => '影片連結', 'type' => 'url'], 'summary' => ['label' => '節目簡介', 'type' => 'textarea'], 'tags' => ['label' => '標籤', 'type' => 'text'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['title', 'platform', 'air_start', 'member_names', 'status', 'updated_at'], ], 'locations' => [ 'label' => 'Yellow Note', 'icon' => 'geo-alt', 'table' => 'locations', 'order' => 'visit_date DESC, sort_order, id', 'fields' => [ 'title' => ['label' => '地點名稱', 'type' => 'text', 'required' => true], 'visit_date' => ['label' => '行程日期', 'type' => 'date'], 'day_label' => ['label' => '日次', 'type' => 'text'], 'address' => ['label' => '地址', 'type' => 'text'], 'latitude' => ['label' => '緯度', 'type' => 'number', 'step' => 'any'], 'longitude' => ['label' => '經度', 'type' => 'number', 'step' => 'any'], 'member_names' => ['label' => '參與成員', 'type' => 'text'], 'description' => ['label' => '行程說明', 'type' => 'textarea'], 'quote_text' => ['label' => '引言', 'type' => 'textarea'], 'tags' => ['label' => '標籤', 'type' => 'text'], 'media_id' => ['label' => '圖片媒體編號', 'type' => 'number'], 'source_url' => ['label' => '來源連結', 'type' => 'url'], 'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'], 'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true], ], 'list' => ['title', 'visit_date', 'member_names', 'tags', 'status', 'updated_at'], ], 'media' => [ 'label' => '媒體庫', 'icon' => 'images', 'table' => 'media_assets', 'order' => 'id DESC', 'fields' => [ 'display_name' => ['label' => '管理名稱', 'type' => 'text', 'required' => true], 'file_path' => ['label' => '檔案', 'type' => 'file'], 'media_type' => ['label' => '類型', 'type' => 'select', 'default' => 'image', 'options' => ['image' => '圖片', 'video' => '影片', 'pdf' => 'PDF', 'other' => '其他']], 'alt_text' => ['label' => '替代文字', 'type' => 'text'], 'source_note' => ['label' => '來源/授權註記', 'type' => 'textarea'], 'category' => ['label' => '分類', 'type' => 'text'], 'status' => ['label' => '使用狀態', 'type' => 'select', 'default' => 'draft', 'options' => ['available' => '可使用', 'draft' => '待整理', 'archived' => '封存']], ], 'list' => ['display_name', 'media_type', 'category', 'file_path', 'status', 'updated_at'], ], ]; } function module_config(string $key): array { $all = modules(); if (!isset($all[$key])) { http_response_code(404); exit('找不到管理模組。'); } return $all[$key]; } function status_label(string $status): string { return ['draft' => '草稿', 'coming_soon' => '籌備中', 'published' => '公開', 'archived' => '封存', 'available' => '可使用'][$status] ?? $status; } function audit(string $action, string $entityType, ?int $entityId, array $details = []): void { $stmt = db()->prepare('INSERT INTO audit_logs (admin_user_id, action_name, entity_type, entity_id, details_json, ip_address) VALUES (?, ?, ?, ?, ?, ?)'); $stmt->execute([$_SESSION['admin_id'] ?? null, $action, $entityType, $entityId, json_encode($details, JSON_UNESCAPED_UNICODE), $_SERVER['REMOTE_ADDR'] ?? null]); } function render_header(string $title, string $active = ''): void { global $config; $flash = take_flash(); $mods = modules(); ?>
CONTENT MANAGEMENT