docs: 加入三個原版對照資料夾
This commit is contained in:
@@ -165,22 +165,6 @@ function modules(): array
|
||||
],
|
||||
'list' => ['display_name', 'media_type', 'category', 'file_path', 'status', 'updated_at'],
|
||||
],
|
||||
'dance_practices' => [
|
||||
'label' => '練舞清單', 'item_label' => '練舞項目', 'list_title' => '練舞清單', 'icon' => 'list-check', 'table' => 'dance_practice_items', 'order' => 'sort_order, id', 'group' => 'dance',
|
||||
'fields' => [
|
||||
'title' => ['label' => '歌曲名稱', 'type' => 'text', 'required' => true],
|
||||
'tags' => ['label' => '練習分類', 'type' => 'text'],
|
||||
'practice_focus' => ['label' => '練習重點', 'type' => 'textarea', 'required' => true],
|
||||
'video_url' => ['label' => '參考影片網址', 'type' => 'url'],
|
||||
'difficulty' => ['label' => '難度', 'type' => 'select', 'default' => 'medium', 'required' => true, 'options' => ['easy' => '入門', 'medium' => '中等', 'hard' => '挑戰']],
|
||||
'progress_status' => ['label' => '練習進度', 'type' => 'select', 'default' => 'not_started', 'required' => true, 'options' => ['not_started' => '未開始', 'practicing' => '練習中', 'review' => '待複習', 'completed' => '已完成']],
|
||||
'note_text' => ['label' => '補充說明', 'type' => 'text'],
|
||||
'is_featured' => ['label' => '優先練習', 'type' => 'checkbox'],
|
||||
'sort_order' => ['label' => '排序', 'type' => 'number', 'default' => '0'],
|
||||
'status' => ['label' => '公開狀態', 'type' => 'status', 'required' => true],
|
||||
],
|
||||
'list' => ['title', 'difficulty', 'progress_status', 'is_featured', 'sort_order', 'status', 'updated_at'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -199,51 +183,6 @@ function status_label(string $status): string
|
||||
return ['draft' => '草稿', 'coming_soon' => '籌備中', 'published' => '公開', 'archived' => '封存', 'available' => '可使用'][$status] ?? $status;
|
||||
}
|
||||
|
||||
function dance_application_statuses(): array
|
||||
{
|
||||
return [
|
||||
'new' => '新報名',
|
||||
'contacted' => '已聯絡',
|
||||
'confirmed' => '確認參與',
|
||||
'declined' => '暫不參與',
|
||||
'archived' => '已封存',
|
||||
];
|
||||
}
|
||||
|
||||
function dance_attendance_label(?string $value): string
|
||||
{
|
||||
return [
|
||||
'attended' => '參加過 20TH',
|
||||
'watched_video' => '看過 20TH 影片',
|
||||
'first_time' => '第一次知道',
|
||||
][$value ?? ''] ?? '未填寫';
|
||||
}
|
||||
|
||||
function dance_song_label(?string $value): string
|
||||
{
|
||||
return [
|
||||
'couple' => 'Couple',
|
||||
'comeback' => "Com' Back",
|
||||
'road_fighter' => 'Road Fighter',
|
||||
'pom_saeng_pom_sa' => 'Pom Saeng Pom Sa',
|
||||
'other' => '其他',
|
||||
][$value ?? ''] ?? '未選擇';
|
||||
}
|
||||
|
||||
function dance_content_labels(?string $value): array
|
||||
{
|
||||
$labels = [
|
||||
'cover' => '翻跳',
|
||||
'medley' => '串燒聯舞',
|
||||
'blessing' => '祝福影片',
|
||||
'graphic' => '圖文應援',
|
||||
'editing' => '影片剪輯',
|
||||
'archive' => '資料整理',
|
||||
];
|
||||
$selected = array_filter(array_map('trim', explode(',', (string) $value)));
|
||||
return array_map(static fn (string $item): string => $labels[$item] ?? $item, $selected);
|
||||
}
|
||||
|
||||
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 (?, ?, ?, ?, ?, ?)');
|
||||
@@ -279,15 +218,8 @@ function render_header(string $title, string $active = ''): void
|
||||
<a class="<?= $active === 'dashboard' ? 'active' : '' ?>" href="index.php"><i class="bi bi-speedometer2"></i>控制台</a>
|
||||
<p>內容資料</p>
|
||||
<?php foreach ($mods as $key => $mod): ?>
|
||||
<?php if (($mod['group'] ?? 'content') === 'dance') continue; ?>
|
||||
<a class="<?= $active === $key ? 'active' : '' ?>" href="records.php?module=<?= h($key) ?>"><i class="bi bi-<?= h($mod['icon']) ?>"></i><?= h($mod['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<p>水晶熱舞社</p>
|
||||
<?php foreach ($mods as $key => $mod): ?>
|
||||
<?php if (($mod['group'] ?? 'content') !== 'dance') continue; ?>
|
||||
<a class="<?= $active === $key ? 'active' : '' ?>" href="records.php?module=<?= h($key) ?>"><i class="bi bi-<?= h($mod['icon']) ?>"></i><?= h($mod['label']) ?></a>
|
||||
<?php endforeach; ?>
|
||||
<a class="<?= $active === 'dance-applications' ? 'active' : '' ?>" href="dance-applications.php"><i class="bi bi-person-hearts"></i>30TH 應援報名</a>
|
||||
<p>系統</p>
|
||||
<a class="<?= $active === 'publishing' ? 'active' : '' ?>" href="publishing.php"><i class="bi bi-eye"></i>公開狀態</a>
|
||||
<a class="<?= $active === 'admins' ? 'active' : '' ?>" href="admins.php"><i class="bi bi-shield-lock"></i>管理者帳號</a>
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
require_login();
|
||||
|
||||
$id = (int) ($_GET['id'] ?? 0);
|
||||
$stmt = db()->prepare('SELECT reference_file_name, reference_original_name, reference_mime_type FROM dance_applications WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$file = $stmt->fetch();
|
||||
if (!$file || !$file['reference_file_name']) {
|
||||
http_response_code(404);
|
||||
exit('找不到檔案。');
|
||||
}
|
||||
|
||||
$storedName = basename((string) $file['reference_file_name']);
|
||||
$path = (string) $config['dance_upload_dir'] . DIRECTORY_SEPARATOR . $storedName;
|
||||
if (!is_file($path)) {
|
||||
http_response_code(404);
|
||||
exit('檔案不存在。');
|
||||
}
|
||||
|
||||
$downloadName = basename((string) ($file['reference_original_name'] ?: $storedName));
|
||||
$downloadName = str_replace(["\r", "\n", '"'], '', $downloadName);
|
||||
audit('download', 'dance_applications', $id, ['file' => $downloadName]);
|
||||
|
||||
header('Content-Type: ' . ((string) $file['reference_mime_type'] ?: 'application/octet-stream'));
|
||||
header('Content-Length: ' . filesize($path));
|
||||
header("Content-Disposition: attachment; filename*=UTF-8''" . rawurlencode($downloadName));
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
readfile($path);
|
||||
exit;
|
||||
@@ -1,61 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
require_login();
|
||||
|
||||
$id = (int) ($_GET['id'] ?? $_POST['id'] ?? 0);
|
||||
$statusOptions = dance_application_statuses();
|
||||
$stmt = db()->prepare('SELECT * FROM dance_applications WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$application = $stmt->fetch();
|
||||
if (!$application) {
|
||||
http_response_code(404);
|
||||
exit('找不到這筆報名資料。');
|
||||
}
|
||||
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
verify_csrf();
|
||||
$status = trim((string) ($_POST['status'] ?? ''));
|
||||
$adminNotes = mb_substr(trim((string) ($_POST['admin_notes'] ?? '')), 0, 5000, 'UTF-8');
|
||||
if (!isset($statusOptions[$status])) {
|
||||
$error = '請選擇有效的處理狀態。';
|
||||
} else {
|
||||
$update = db()->prepare('UPDATE dance_applications SET status = ?, admin_notes = ? WHERE id = ?');
|
||||
$update->execute([$status, $adminNotes !== '' ? $adminNotes : null, $id]);
|
||||
audit('update', 'dance_applications', $id, ['status' => $status, 'admin_notes' => $adminNotes]);
|
||||
flash('success', '報名資料的處理狀態已更新。');
|
||||
redirect('dance-application.php?id=' . $id);
|
||||
}
|
||||
}
|
||||
|
||||
render_header('處理熱舞社報名 #' . $id, 'dance-applications');
|
||||
?>
|
||||
<?php if ($error): ?><div class="notice notice--error"><?= h($error) ?></div><?php endif; ?>
|
||||
<div class="toolbar"><a class="button" href="dance-applications.php"><i class="bi bi-arrow-left"></i>回報名清單</a></div>
|
||||
|
||||
<section class="panel application-detail">
|
||||
<div class="panel__head"><div><h2><?= h($application['nickname']) ?></h2><p>送出於 <?= h($application['created_at']) ?></p></div><span class="status status--<?= h($application['status']) ?>"><?= h($statusOptions[$application['status']] ?? $application['status']) ?></span></div>
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item"><span>暱稱</span><strong><?= h($application['nickname']) ?></strong></div>
|
||||
<div class="detail-item"><span>Email</span><a href="mailto:<?= h($application['email']) ?>"><?= h($application['email']) ?></a></div>
|
||||
<div class="detail-item"><span>練舞經驗</span><strong><?= $application['dance_years'] !== null ? (int) $application['dance_years'] . ' 年' : '未填寫' ?></strong></div>
|
||||
<div class="detail-item"><span>可參與日期</span><strong><?= h($application['available_date'] ?: '未填寫') ?></strong></div>
|
||||
<div class="detail-item"><span>20TH 經驗</span><strong><?= h(dance_attendance_label($application['attended_20th'])) ?></strong></div>
|
||||
<div class="detail-item"><span>想練的歌曲</span><strong><?= h(dance_song_label($application['song'])) ?></strong></div>
|
||||
<div class="detail-item detail-item--wide"><span>想參與的內容</span><strong><?= h(implode('、', dance_content_labels($application['participate_content']))) ?></strong></div>
|
||||
<div class="detail-item detail-item--wide"><span>30TH 留言</span><div class="detail-value"><?= $application['message_30th'] ? nl2br(h($application['message_30th'])) : '未填寫' ?></div></div>
|
||||
<div class="detail-item detail-item--wide"><span>參考檔案</span><?php if ($application['reference_file_name']): ?><a class="button detail-download" href="dance-application-file.php?id=<?= $id ?>"><i class="bi bi-download"></i><?= h($application['reference_original_name'] ?: '下載檔案') ?><?php if ($application['reference_file_size']): ?>(<?= h(number_format((int) $application['reference_file_size'] / 1024 / 1024, 2)) ?> MB)<?php endif; ?></a><?php else: ?><strong>未上傳</strong><?php endif; ?></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<form class="panel editor" method="post">
|
||||
<input type="hidden" name="csrf" value="<?= h(csrf_token()) ?>">
|
||||
<input type="hidden" name="id" value="<?= $id ?>">
|
||||
<div class="form-grid">
|
||||
<label class="field"><span>處理狀態 *</span><select name="status" required><?php foreach ($statusOptions as $value => $label): ?><option value="<?= h($value) ?>" <?= $application['status'] === $value ? 'selected' : '' ?>><?= h($label) ?></option><?php endforeach; ?></select></label>
|
||||
<label class="field field--wide"><span>內部備註</span><textarea name="admin_notes" rows="6" maxlength="5000" placeholder="例如:已寄信、等待確認練習日期……"><?= h((string) $application['admin_notes']) ?></textarea></label>
|
||||
</div>
|
||||
<div class="form-actions"><a class="button" href="dance-applications.php">取消</a><button class="button button--primary" type="submit">儲存處理狀態</button></div>
|
||||
</form>
|
||||
<?php render_footer();
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
require __DIR__ . '/bootstrap.php';
|
||||
require_login();
|
||||
|
||||
$page = max(1, (int) ($_GET['page'] ?? 1));
|
||||
$perPage = 15;
|
||||
$query = trim((string) ($_GET['q'] ?? ''));
|
||||
$status = trim((string) ($_GET['status'] ?? ''));
|
||||
$statusOptions = dance_application_statuses();
|
||||
$where = [];
|
||||
$params = [];
|
||||
|
||||
if ($query !== '') {
|
||||
$where[] = '(nickname LIKE ? OR email LIKE ? OR message_30th LIKE ?)';
|
||||
$searchValue = '%' . $query . '%';
|
||||
$params = [$searchValue, $searchValue, $searchValue];
|
||||
}
|
||||
if ($status !== '' && isset($statusOptions[$status])) {
|
||||
$where[] = 'status = ?';
|
||||
$params[] = $status;
|
||||
} else {
|
||||
$status = '';
|
||||
}
|
||||
|
||||
$whereSql = $where ? ' WHERE ' . implode(' AND ', $where) : '';
|
||||
$countStmt = db()->prepare('SELECT COUNT(*) FROM dance_applications' . $whereSql);
|
||||
$countStmt->execute($params);
|
||||
$total = (int) $countStmt->fetchColumn();
|
||||
$pages = max(1, (int) ceil($total / $perPage));
|
||||
$page = min($page, $pages);
|
||||
$offset = ($page - 1) * $perPage;
|
||||
|
||||
$stmt = db()->prepare('SELECT * FROM dance_applications' . $whereSql . " ORDER BY created_at DESC, id DESC LIMIT $perPage OFFSET $offset");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
render_header('30TH 應援報名', 'dance-applications');
|
||||
?>
|
||||
<div class="toolbar">
|
||||
<form class="filters" method="get">
|
||||
<label class="search"><i class="bi bi-search"></i><input name="q" value="<?= h($query) ?>" placeholder="搜尋暱稱、Email 或留言"></label>
|
||||
<select name="status" aria-label="處理狀態">
|
||||
<option value="">全部狀態</option>
|
||||
<?php foreach ($statusOptions as $value => $label): ?>
|
||||
<option value="<?= h($value) ?>" <?= $status === $value ? 'selected' : '' ?>><?= h($label) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="button" type="submit">篩選</button>
|
||||
</form>
|
||||
<a class="button" href="../yellow-note/crystal-dance-survey.html#form-30th" target="_blank" rel="noopener"><i class="bi bi-box-arrow-up-right"></i>查看募集表單</a>
|
||||
</div>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel__head"><div><h2>報名清單</h2><p>共 <?= $total ?> 筆;Email 與上傳檔案僅供企劃聯絡使用</p></div></div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead><tr><th>編號</th><th>暱稱</th><th>Email</th><th>想參與</th><th>歌曲</th><th>狀態</th><th>送出時間</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<td>#<?= (int) $row['id'] ?></td>
|
||||
<td><?= h($row['nickname']) ?></td>
|
||||
<td><?= h(mb_strimwidth((string) $row['email'], 0, 34, '…', 'UTF-8')) ?></td>
|
||||
<td><?= h(implode('、', dance_content_labels($row['participate_content']))) ?></td>
|
||||
<td><?= h(dance_song_label($row['song'])) ?></td>
|
||||
<td><span class="status status--<?= h($row['status']) ?>"><?= h($statusOptions[$row['status']] ?? $row['status']) ?></span></td>
|
||||
<td><?= h($row['created_at']) ?></td>
|
||||
<td class="actions"><a href="dance-application.php?id=<?= (int) $row['id'] ?>">查看/處理</a></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php if (!$rows): ?><tr><td colspan="8" class="empty">目前沒有符合條件的報名資料。</td></tr><?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php if ($pages > 1): ?>
|
||||
<nav class="pagination" aria-label="報名資料分頁">
|
||||
<?php for ($i = 1; $i <= $pages; $i++): ?>
|
||||
<a class="<?= $i === $page ? 'active' : '' ?>" href="?q=<?= urlencode($query) ?>&status=<?= urlencode($status) ?>&page=<?= $i ?>"><?= $i ?></a>
|
||||
<?php endfor; ?>
|
||||
</nav>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
<?php render_footer();
|
||||
@@ -115,63 +115,6 @@ CREATE TABLE IF NOT EXISTS locations (
|
||||
CONSTRAINT fk_location_media FOREIGN KEY (media_id) REFERENCES media_assets(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dance_applications (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
nickname VARCHAR(80) NOT NULL,
|
||||
email VARCHAR(190) NOT NULL,
|
||||
dance_years TINYINT UNSIGNED NULL,
|
||||
available_date DATE NULL,
|
||||
attended_20th ENUM('attended','watched_video','first_time') NULL,
|
||||
participate_content VARCHAR(255) NOT NULL,
|
||||
song VARCHAR(80) NULL,
|
||||
reference_file_name VARCHAR(100) NULL,
|
||||
reference_original_name VARCHAR(255) NULL,
|
||||
reference_mime_type VARCHAR(100) NULL,
|
||||
reference_file_size INT UNSIGNED NULL,
|
||||
message_30th TEXT NULL,
|
||||
status ENUM('new','contacted','confirmed','declined','archived') NOT NULL DEFAULT 'new',
|
||||
admin_notes TEXT NULL,
|
||||
submitted_ip VARCHAR(45) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_dance_applications_status_created (status, created_at),
|
||||
INDEX idx_dance_applications_email (email)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dance_practice_items (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(160) NOT NULL,
|
||||
tags VARCHAR(255) NULL,
|
||||
practice_focus TEXT NOT NULL,
|
||||
video_url VARCHAR(500) NULL,
|
||||
difficulty ENUM('easy','medium','hard') NOT NULL DEFAULT 'medium',
|
||||
progress_status ENUM('not_started','practicing','review','completed') NOT NULL DEFAULT 'not_started',
|
||||
note_text VARCHAR(255) NULL,
|
||||
is_featured TINYINT(1) NOT NULL DEFAULT 0,
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
status ENUM('draft','coming_soon','published','archived') NOT NULL DEFAULT 'draft',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_dance_practice_order (status, sort_order, id),
|
||||
INDEX idx_dance_practice_title (title)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO dance_practice_items
|
||||
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
|
||||
SELECT 'Couple', 'Dance,Vocal', '副歌動作、隊形感、甜感表情', 'easy', 'not_started', '可作為第一首示範歌', 1, 10, 'published'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Couple');
|
||||
|
||||
INSERT INTO dance_practice_items
|
||||
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
|
||||
SELECT 'Com'' Back', 'Dance,Rap', '強拍、Rap 段落、舞台氣勢', 'medium', 'not_started', '未來擴充', 0, 20, 'coming_soon'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Com'' Back');
|
||||
|
||||
INSERT INTO dance_practice_items
|
||||
(title, tags, practice_focus, difficulty, progress_status, note_text, is_featured, sort_order, status)
|
||||
SELECT 'Road Fighter', 'Dance,Rap,Performance', '節奏、力量、舞台感', 'hard', 'not_started', '未來擴充', 0, 30, 'coming_soon'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM dance_practice_items WHERE title = 'Road Fighter');
|
||||
|
||||
CREATE TABLE IF NOT EXISTS audit_logs (
|
||||
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
admin_user_id INT UNSIGNED NULL,
|
||||
|
||||
Reference in New Issue
Block a user