chore: variety資料夾改名vedio並同步連結
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
# 打歌舞台後台|乙級練習版
|
||||
|
||||
1. 開啟 XAMPP Control Panel,啟動 Apache 與 MySQL。
|
||||
2. 進入 phpMyAdmin,匯入 `setup.sql`。
|
||||
3. 將本資料夾放入 `C:\xampp\htdocs\`,或在此資料夾執行:
|
||||
`C:\xampp\php\php.exe -S 127.0.0.1:8080`
|
||||
4. 開啟 `http://127.0.0.1:8080/login.php`。
|
||||
5. 練習帳號:`admin`;密碼:`1234`。
|
||||
|
||||
練習內容:Session 登入、PDO、CRUD、預備敘述、防 XSS、CSRF、列表排序、顯示/隱藏與 JSON API。
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
$rows = $pdo->query('SELECT stage_date,youtube_id,station,program,title,note,sort_order FROM stages WHERE is_visible=1 ORDER BY sort_order,stage_date,id')->fetchAll();
|
||||
echo json_encode(['stages'=>$rows], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
session_start();
|
||||
|
||||
const DB_HOST = '127.0.0.1';
|
||||
const DB_NAME = 'three_words_10th';
|
||||
const DB_USER = 'root';
|
||||
const DB_PASS = '';
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
'mysql:host=' . DB_HOST . ';dbname=' . DB_NAME . ';charset=utf8mb4',
|
||||
DB_USER,
|
||||
DB_PASS,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
exit('資料庫連線失敗,請先啟動 XAMPP MySQL 並匯入 setup.sql。');
|
||||
}
|
||||
|
||||
function e(?string $value): string { return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8'); }
|
||||
function loggedIn(): bool { return isset($_SESSION['admin_id']); }
|
||||
function requireLogin(): void { if (!loggedIn()) { header('Location: login.php'); exit; } }
|
||||
function csrf(): string {
|
||||
if (empty($_SESSION['csrf'])) $_SESSION['csrf'] = bin2hex(random_bytes(24));
|
||||
return $_SESSION['csrf'];
|
||||
}
|
||||
function checkCsrf(): void {
|
||||
if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) exit('表單驗證失敗');
|
||||
}
|
||||
function youtubeId(string $url): string {
|
||||
if (preg_match('~(?:youtu\.be/|youtube\.com/(?:watch\?v=|embed/|shorts/))([\w-]{11})~', $url, $m)) return $m[1];
|
||||
return preg_match('/^[\w-]{11}$/', $url) ? $url : '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php'; requireLogin(); checkCsrf();
|
||||
$stmt = $pdo->prepare('DELETE FROM stages WHERE id = ?');
|
||||
$stmt->execute([(int)($_POST['id'] ?? 0)]);
|
||||
header('Location: index.php');
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
requireLogin();
|
||||
$edit = ['id'=>'','stage_date'=>'','youtube_id'=>'','station'=>'','program'=>'','title'=>'','note'=>'','sort_order'=>0,'is_visible'=>1];
|
||||
if (isset($_GET['edit'])) {
|
||||
$stmt = $pdo->prepare('SELECT * FROM stages WHERE id = ?'); $stmt->execute([(int)$_GET['edit']]);
|
||||
$edit = $stmt->fetch() ?: $edit;
|
||||
}
|
||||
$stages = $pdo->query('SELECT * FROM stages ORDER BY sort_order ASC, stage_date ASC, id ASC')->fetchAll();
|
||||
?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>打歌舞台後台</title><link rel="stylesheet" href="style.css"></head><body>
|
||||
<header><div><p class="eyebrow">THREE WORDS · 10TH</p><h1>打歌舞台後台</h1></div><nav><a href="stages.php" target="_blank">查看前台 ↗</a><a href="api.php" target="_blank">JSON API ↗</a><a href="logout.php">登出</a></nav></header>
|
||||
<main class="adminGrid">
|
||||
<section class="panel formPanel"><div class="panelTitle"><span>01</span><h2><?=$edit['id']?'修改舞台':'新增舞台'?></h2></div>
|
||||
<form method="post" action="save.php"><input type="hidden" name="csrf" value="<?=csrf()?>"><input type="hidden" name="id" value="<?=e((string)$edit['id'])?>">
|
||||
<div class="two"><label>播出日期<input type="date" name="stage_date" required value="<?=e($edit['stage_date'])?>"></label><label>顯示順序<input type="number" name="sort_order" value="<?=e((string)$edit['sort_order'])?>"></label></div>
|
||||
<label>YouTube 網址或影片 ID<input name="youtube_url" required placeholder="https://www.youtube.com/watch?v=..." value="<?=e($edit['youtube_id'])?>"></label>
|
||||
<div class="two"><label>電視台<input name="station" required placeholder="SBS" value="<?=e($edit['station'])?>"></label><label>節目名稱<input name="program" required placeholder="人氣歌謠" value="<?=e($edit['program'])?>"></label></div>
|
||||
<label>舞台標題<input name="title" required value="<?=e($edit['title'])?>"></label><label>備註<textarea name="note" rows="3"><?=e($edit['note'])?></textarea></label>
|
||||
<label class="check"><input type="checkbox" name="is_visible" value="1" <?=$edit['is_visible']?'checked':''?>> 前台顯示</label><div class="formActions"><button><?=$edit['id']?'儲存修改':'新增資料'?></button><?php if($edit['id']):?><a href="index.php">取消修改</a><?php endif;?></div></form></section>
|
||||
<section class="panel listPanel"><div class="panelTitle"><span>02</span><h2>資料列表</h2><b><?=count($stages)?> 筆</b></div>
|
||||
<div class="tableWrap"><table><thead><tr><th>順序</th><th>日期/縮圖</th><th>電視台/舞台</th><th>狀態</th><th>操作</th></tr></thead><tbody>
|
||||
<?php foreach($stages as $row):?><tr><td><?=e((string)$row['sort_order'])?></td><td><time><?=e($row['stage_date'])?></time><img src="https://i.ytimg.com/vi/<?=e($row['youtube_id'])?>/mqdefault.jpg" alt="影片縮圖"></td><td><strong><?=e($row['station'])?> · <?=e($row['program'])?></strong><span><?=e($row['title'])?></span></td><td><i class="status <?=$row['is_visible']?'on':''?>"><?=$row['is_visible']?'顯示':'隱藏'?></i></td><td class="ops"><a href="?edit=<?=$row['id']?>">修改</a><form method="post" action="delete.php" onsubmit="return confirm('確定刪除這筆資料?')"><input type="hidden" name="csrf" value="<?=csrf()?>"><input type="hidden" name="id" value="<?=$row['id']?>"><button>刪除</button></form></td></tr><?php endforeach; ?>
|
||||
</tbody></table></div></section></main></body></html>
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
if (loggedIn()) { header('Location: index.php'); exit; }
|
||||
$error = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
checkCsrf();
|
||||
$stmt = $pdo->prepare('SELECT * FROM admins WHERE account = ? AND password = SHA2(?, 256)');
|
||||
$stmt->execute([trim($_POST['account'] ?? ''), $_POST['password'] ?? '']);
|
||||
if ($admin = $stmt->fetch()) {
|
||||
session_regenerate_id(true);
|
||||
$_SESSION['admin_id'] = $admin['id'];
|
||||
$_SESSION['admin_account'] = $admin['account'];
|
||||
header('Location: index.php'); exit;
|
||||
}
|
||||
$error = '帳號或密碼錯誤';
|
||||
}
|
||||
?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>後台登入</title><link rel="stylesheet" href="style.css"></head>
|
||||
<body class="loginPage"><main class="loginCard"><p class="eyebrow">THREE WORDS · ADMIN</p><h1>舞台資料管理</h1><p>乙級練習:Session 登入與資料庫驗證</p><?php if ($error): ?><div class="alert"><?=e($error)?></div><?php endif; ?>
|
||||
<form method="post"><input type="hidden" name="csrf" value="<?=csrf()?>"><label>帳號<input name="account" required autofocus value="admin"></label><label>密碼<input type="password" name="password" required value="1234"></label><button>登入後台</button></form></main></body></html>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php';
|
||||
$_SESSION = [];
|
||||
session_destroy();
|
||||
header('Location: login.php');
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
require __DIR__ . '/config.php'; requireLogin(); checkCsrf();
|
||||
$id = (int)($_POST['id'] ?? 0);
|
||||
$youtube = youtubeId(trim($_POST['youtube_url'] ?? ''));
|
||||
if (!$youtube) exit('YouTube 網址或 ID 格式錯誤');
|
||||
$data = [$_POST['stage_date'] ?? '', $youtube, trim($_POST['station'] ?? ''), trim($_POST['program'] ?? ''), trim($_POST['title'] ?? ''), trim($_POST['note'] ?? ''), (int)($_POST['sort_order'] ?? 0), isset($_POST['is_visible']) ? 1 : 0];
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare('UPDATE stages SET stage_date=?,youtube_id=?,station=?,program=?,title=?,note=?,sort_order=?,is_visible=? WHERE id=?');
|
||||
$data[] = $id; $stmt->execute($data);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('INSERT INTO stages(stage_date,youtube_id,station,program,title,note,sort_order,is_visible) VALUES(?,?,?,?,?,?,?,?)');
|
||||
$stmt->execute($data);
|
||||
}
|
||||
header('Location: index.php');
|
||||
@@ -0,0 +1,27 @@
|
||||
CREATE DATABASE IF NOT EXISTS three_words_10th CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE three_words_10th;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admins (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
account VARCHAR(50) NOT NULL UNIQUE,
|
||||
password CHAR(64) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS stages (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
stage_date DATE NOT NULL,
|
||||
youtube_id VARCHAR(11) NOT NULL,
|
||||
station VARCHAR(30) NOT NULL,
|
||||
program VARCHAR(100) NOT NULL,
|
||||
title VARCHAR(150) NOT NULL,
|
||||
note VARCHAR(255) NOT NULL DEFAULT '',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
is_visible TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT IGNORE INTO admins (account, password) VALUES ('admin', SHA2('1234', 256));
|
||||
INSERT INTO stages (stage_date, youtube_id, station, program, title, note, sort_order)
|
||||
SELECT '2016-12-04', '8HTqr3Wp-R0', 'SBS', '인기가요 · Inkigayo', 'THREE WORDS · Goodbye Stage', '十週年收藏舞台', 10
|
||||
WHERE NOT EXISTS (SELECT 1 FROM stages WHERE youtube_id = '8HTqr3Wp-R0');
|
||||
@@ -0,0 +1,2 @@
|
||||
<?php require __DIR__ . '/config.php'; $stages=$pdo->query('SELECT * FROM stages WHERE is_visible=1 ORDER BY sort_order,stage_date,id')->fetchAll(); ?>
|
||||
<!doctype html><html lang="zh-Hant"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>THREE WORDS 打歌舞台</title><link rel="stylesheet" href="style.css"></head><body class="front"><main><p class="eyebrow">MUSIC SHOW ARCHIVE</p><h1>打歌舞台</h1><div class="frontTable"><div class="frontHead"><span>DATE</span><span>VIDEO</span><span>STATION</span></div><?php foreach($stages as $s):?><article><time><?=e($s['stage_date'])?></time><a href="https://youtu.be/<?=e($s['youtube_id'])?>" target="_blank"><img src="https://i.ytimg.com/vi/<?=e($s['youtube_id'])?>/hqdefault.jpg" alt="<?=e($s['title'])?>"><div><strong><?=e($s['title'])?></strong><small>WATCH ↗</small></div></a><div class="tv"><strong><?=e($s['station'])?></strong><span><?=e($s['program'])?></span></div></article><?php endforeach;?></div><a class="back" href="index.php">回後台</a></main></body></html>
|
||||
@@ -0,0 +1,2 @@
|
||||
:root{--ink:#07101d;--cream:#f2ede2;--gold:#efb53f;--blue:#16334d;--muted:#69727d}*{box-sizing:border-box}body{margin:0;background:#e8e2d6;color:var(--ink);font-family:Arial,"Microsoft JhengHei",sans-serif}a{color:inherit}.eyebrow{font:700 10px Arial;letter-spacing:.3em;color:#8a6c19}header{display:flex;justify-content:space-between;align-items:end;padding:32px 4vw;background:var(--ink);color:var(--cream)}h1{margin:8px 0 0;font:900 clamp(35px,5vw,68px)/1 serif}header nav{display:flex;gap:18px;flex-wrap:wrap}header nav a{text-decoration:none;border-bottom:1px solid var(--gold);padding-bottom:4px}.adminGrid{display:grid;grid-template-columns:minmax(310px,.7fr) minmax(600px,1.6fr);gap:24px;padding:28px 4vw}.panel{background:var(--cream);padding:25px;box-shadow:0 15px 45px rgba(7,16,29,.08)}.panelTitle{display:flex;align-items:center;gap:13px;margin-bottom:24px;border-bottom:1px solid #aaa;padding-bottom:15px}.panelTitle span{display:grid;place-content:center;width:35px;height:35px;border-radius:50%;background:var(--gold);font-weight:900}.panelTitle h2{margin:0;font:800 28px serif}.panelTitle b{margin-left:auto;color:var(--muted)}label{display:grid;gap:8px;margin:0 0 17px;font-size:13px;font-weight:700}input,textarea{width:100%;border:1px solid #aaa;background:#fffdf7;padding:11px;font:inherit}.two{display:grid;grid-template-columns:1fr 1fr;gap:14px}.check{display:flex;align-items:center}.check input{width:auto}.formActions{display:flex;align-items:center;gap:15px}.formActions button,.loginCard button{border:0;background:var(--gold);padding:13px 20px;font-weight:900;cursor:pointer}.tableWrap{overflow:auto}table{width:100%;border-collapse:collapse;min-width:760px}th,td{text-align:left;border-bottom:1px solid #aaa;padding:12px 10px;vertical-align:middle}th{font:700 9px Arial;letter-spacing:.15em;color:#76682f}td img{display:block;width:130px;aspect-ratio:16/9;object-fit:cover;margin-top:8px}td strong,td span{display:block}td span{margin-top:7px;color:var(--muted);font-size:13px}.status{display:inline-block;padding:5px 8px;background:#bbb;font-style:normal;font-size:12px}.status.on{background:var(--gold)}.ops{display:flex;gap:9px;align-items:center}.ops form{margin:0}.ops button{border:0;background:transparent;color:#9b2f22;text-decoration:underline;cursor:pointer}.loginPage{min-height:100vh;display:grid;place-content:center;background:radial-gradient(circle at 80% 10%,#3f5c72,var(--ink) 55%)}.loginCard{width:min(440px,90vw);padding:38px;background:var(--cream);box-shadow:0 30px 90px #0006}.loginCard h1{font-size:48px}.alert{padding:10px;background:#ffd9d2;color:#922;margin:15px 0}.front{background:var(--cream)}.front main{padding:80px 8vw}.front h1{font-size:90px}.frontTable{border-top:2px solid var(--ink);margin-top:50px}.frontHead,.frontTable article{display:grid;grid-template-columns:.55fr 2fr .8fr;gap:25px;align-items:center}.frontHead{padding:12px 0;border-bottom:1px solid #aaa;font:700 9px Arial;letter-spacing:.2em}.frontTable article{padding:22px 0;border-bottom:1px solid #aaa}.frontTable time{font:900 30px Arial}.frontTable article>a{display:grid;grid-template-columns:220px 1fr;gap:20px;align-items:center;text-decoration:none}.frontTable img{width:100%;aspect-ratio:16/9;object-fit:cover}.frontTable small{display:block;margin-top:10px;color:#8a6c19}.tv strong{display:block;font:900 48px Arial;color:#c48f17}.tv span{font-size:12px}.back{display:inline-block;margin-top:30px}.frontTable article>a:hover img{filter:brightness(.75)}
|
||||
@media(max-width:900px){.adminGrid{grid-template-columns:1fr}.frontHead{display:none}.frontTable article{grid-template-columns:1fr}.frontTable article>a{grid-template-columns:1fr}.front h1{font-size:60px}}@media(max-width:560px){header{align-items:start;flex-direction:column;gap:22px}.adminGrid{padding:18px}.two{grid-template-columns:1fr}.front main{padding:55px 6vw}}
|
||||
Reference in New Issue
Block a user