feat: 本機新增頁面與導覽調整

This commit is contained in:
2026-07-27 17:45:20 +08:00
parent 14a0724679
commit 295f18d567
27 changed files with 2967 additions and 9 deletions
+5
View File
@@ -344,6 +344,11 @@
一部是把 1997 年的青春寫得又甜又疼的<span class="mark_b">韓劇</span>
點進去,進度與入口都在;先別急著一次追完,慢慢養也很好看。
</p>
<p>
此頁為線上靜態預覽。完整資料庫與後台 CRUD(乙級第三題骨架)請本機 XAMPP 匯入
<code>variety/vedio-list/dramas.sql</code> 後,開啟
<a href="vedio-list/"><code>variety/vedio-list/</code></a>
</p>
</section>
<section class="list-section" aria-labelledby="vedio-list-heading">
+44
View File
@@ -0,0 +1,44 @@
<?php include_once "./api/db.php"; ?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>深夜清單|管理後台</title>
<link rel="stylesheet" href="../../assets/css/style.css">
<link rel="stylesheet" href="css/vedio-list.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body class="vedio-admin-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">深夜清單後台</h1>
<div class="subtitle-wrap">
<span class="sub-title">ADMIN · CRUD</span>
</div>
</div>
</header>
<div class="container admin-wrap">
<nav class="admin-nav">
<a href="index.php">回前台</a>
<a href="admin.php">後台首頁</a>
<a href="?do=drama">追劇管理</a>
<a href="?do=add">新增作品</a>
</nav>
<?php
$do = $_GET['do'] ?? 'main';
$file = "back/$do.php";
if (file_exists($file)) {
include $file;
} else {
include "back/main.php";
}
?>
</div>
</body>
</html>
+125
View File
@@ -0,0 +1,125 @@
<?php
session_start();
date_default_timezone_set("Asia/Taipei");
class DB
{
protected $dsn = "mysql:host=localhost;charset=utf8;dbname=vedio_list";
protected $pdo;
protected $table;
function __construct($table)
{
$this->table = $table;
$this->pdo = new PDO($this->dsn, 'root', '');
}
function all(...$arg)
{
$sql = "SELECT * FROM `$this->table` ";
if (isset($arg[0])) {
if (is_array($arg[0])) {
$tmp = $this->a2s($arg[0]);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= $arg[0];
}
}
if (isset($arg[1])) {
$sql .= $arg[1];
}
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
function count(...$arg)
{
$sql = "SELECT count(*) FROM `$this->table`";
if (isset($arg[0])) {
if (is_array($arg[0])) {
$tmp = $this->a2s($arg[0]);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= $arg[0];
}
}
if (isset($arg[1])) {
$sql .= $arg[1];
}
return $this->pdo->query($sql)->fetchColumn();
}
function find($arg)
{
$sql = "SELECT * FROM `$this->table` ";
if (is_array($arg)) {
$tmp = $this->a2s($arg);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= " WHERE `id`='$arg'";
}
return $this->pdo->query($sql)->fetch(PDO::FETCH_ASSOC);
}
function save($arg)
{
if (isset($arg['id'])) {
$tmp = $this->a2s($arg);
$sql = "UPDATE `$this->table` SET " . join(" , ", $tmp);
$sql .= " WHERE `id`='{$arg['id']}'";
} else {
$keys = array_keys($arg);
$sql = "INSERT INTO `$this->table` (`" . join("`,`", $keys) . "`) VALUES('" . join("','", $arg) . "');";
}
return $this->pdo->exec($sql);
}
function del($arg)
{
$sql = "DELETE FROM `$this->table` ";
if (is_array($arg)) {
$tmp = $this->a2s($arg);
$sql .= " WHERE " . join(" AND ", $tmp);
} else {
$sql .= " WHERE `id`='$arg'";
}
return $this->pdo->exec($sql);
}
protected function a2s($array)
{
$tmp = [];
foreach ($array as $key => $val) {
$tmp[] = "`$key`='$val'";
}
return $tmp;
}
function q($sql)
{
return $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}
}
function dd($array)
{
echo "<pre>";
print_r($array);
echo "</pre>";
}
function to($url)
{
header("location:$url");
}
$Drama = new DB('dramas');
+5
View File
@@ -0,0 +1,5 @@
<?php
include_once "db.php";
$table = ${$_POST['table']};
$table->del($_POST['id']);
+6
View File
@@ -0,0 +1,6 @@
<?php
include_once "db.php";
$row = $Drama->find($_POST['id']);
$row['sh'] = ((int)$row['sh'] + 1) % 2;
$Drama->save($row);
+13
View File
@@ -0,0 +1,13 @@
<?php
include_once "db.php";
$table = ${$_POST['table']};
$row1 = $table->find($_POST['ids'][0]);
$row2 = $table->find($_POST['ids'][1]);
$tmp = $row1['rank'];
$row1['rank'] = $row2['rank'];
$row2['rank'] = $tmp;
$table->save($row1);
$table->save($row2);
+96
View File
@@ -0,0 +1,96 @@
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$maxRank = $Drama->q("SELECT MAX(`rank`) AS m FROM `dramas`");
$nextRank = ((int)($maxRank[0]['m'] ?? 0)) + 1;
$data = [
'title' => trim($_POST['title'] ?? ''),
'type' => $_POST['type'] ?? 'drama',
'platform' => $_POST['platform'] ?? '其他',
'watch_url' => trim($_POST['watch_url'] ?? ''),
'current_season' => max(1, (int)($_POST['current_season'] ?? 1)),
'current_episode' => max(0, (int)($_POST['current_episode'] ?? 0)),
'current_position' => trim($_POST['current_position'] ?? ''),
'progress_note' => trim($_POST['progress_note'] ?? ''),
'status' => $_POST['status'] ?? 'want',
'rating' => $_POST['rating'] !== '' ? $_POST['rating'] : null,
'intro' => trim($_POST['intro'] ?? ''),
'note' => trim($_POST['note'] ?? ''),
'sh' => 1,
'rank' => $nextRank,
];
// DB::save 用字串拼接;null 改空字串避免 SQL 破掉
if ($data['rating'] === null) {
$data['rating'] = '';
}
$Drama->save($data);
to("?do=drama");
exit;
}
?>
<section>
<h2 class="section-title">新增作品</h2>
<form class="admin-form" method="post" action="?do=add">
<label for="title">劇名/節目名</label>
<input type="text" id="title" name="title" required>
<label for="type">類型</label>
<select id="type" name="type">
<option value="variety">綜藝</option>
<option value="drama" selected>戲劇</option>
</select>
<label for="platform">上架平台</label>
<select id="platform" name="platform">
<option value="Netflix">Netflix</option>
<option value="Disney+">Disney+</option>
<option value="friDay影音">friDay影音</option>
<option value="愛奇藝">愛奇藝</option>
<option value="LINE TV">LINE TV</option>
<option value="YouTube">YouTube</option>
<option value="其他">其他</option>
</select>
<label for="watch_url">直接觀看網址</label>
<input type="url" id="watch_url" name="watch_url" placeholder="https://...">
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1" value="1">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0" value="0">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position" placeholder="例如 35:20">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note" placeholder="例如:女主角剛看到手機裡的影片">
<label for="status">狀態</label>
<select id="status" name="status">
<option value="want">想看</option>
<option value="watching" selected>追劇中</option>
<option value="done">已追完</option>
<option value="paused">暫停</option>
<option value="dropped">棄劇</option>
</select>
<label for="rating">評分</label>
<input type="number" id="rating" name="rating" min="0" max="10" step="0.1" placeholder="例如 8.5">
<label for="intro">簡介</label>
<textarea id="intro" name="intro" rows="4"></textarea>
<label for="note">心得備註</label>
<textarea id="note" name="note" rows="3"></textarea>
<div class="form-actions" style="margin-top:1.25rem;display:flex;gap:10px;justify-content:center;">
<button type="submit" class="is-primary" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid var(--color-brand);background:var(--color-brand);font-weight:700;cursor:pointer;">儲存</button>
<a href="?do=drama" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid rgba(255,204,0,0.35);background:#fff;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;">取消</a>
</div>
</form>
</section>
+71
View File
@@ -0,0 +1,71 @@
<?php
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$rows = $Drama->all(" ORDER BY `rank` ASC, `id` ASC");
?>
<section>
<h2 class="section-title">追劇管理</h2>
<div class="admin-toolbar">
<a href="?do=add">新增作品</a>
</div>
<div class="drama-list">
<?php foreach ($rows as $idx => $row): ?>
<?php
$prev = ($idx === 0) ? $row['id'] : $rows[$idx - 1]['id'];
$next = ($idx === count($rows) - 1) ? $row['id'] : $rows[$idx + 1]['id'];
$typeText = $typeLabels[$row['type']] ?? $row['type'];
?>
<div class="drama-row">
<div class="drama-row__info">
<strong><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></strong><br>
<?= htmlspecialchars($typeText . '' . $row['platform'], ENT_QUOTES, 'UTF-8') ?><br>
進度:第 <?= (int)$row['current_season'] ?> 季・第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
・<?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</div>
<div class="drama-row__actions">
<button type="button" class="show" data-id="<?= (int)$row['id'] ?>">
<?= ((int)$row['sh'] === 1) ? '顯示' : '隱藏' ?>
</button>
<button type="button" class="switch-rank" data-ids="<?= (int)$row['id'] . '-' . (int)$prev ?>">往上</button>
<button type="button" class="switch-rank" data-ids="<?= (int)$row['id'] . '-' . (int)$next ?>">往下</button>
<a class="btn-like" href="?do=edit&amp;id=<?= (int)$row['id'] ?>">編輯</a>
<button type="button" class="del-drama" data-id="<?= (int)$row['id'] ?>">刪除</button>
</div>
</div>
<?php endforeach; ?>
<?php if (count($rows) === 0): ?>
<p class="admin-lead">尚無資料,先新增一部作品吧。</p>
<?php endif; ?>
</div>
</section>
<script>
$(".switch-rank").on("click", function () {
let ids = $(this).data("ids").toString().split("-");
$.post("./api/sw.php", { ids, table: "Drama" }, function () {
location.reload();
});
});
$(".show").on("click", function () {
let id = $(this).data("id");
$.post("./api/show.php", { id }, function () {
location.reload();
});
});
$(".del-drama").on("click", function () {
if (!confirm("確定刪除這部作品?")) return;
let id = $(this).data("id");
$.post("./api/del.php", { id, table: "Drama" }, function () {
location.reload();
});
});
</script>
+107
View File
@@ -0,0 +1,107 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row) {
echo '<p class="admin-lead">找不到這筆資料。</p>';
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row['title'] = trim($_POST['title'] ?? $row['title']);
$row['type'] = $_POST['type'] ?? $row['type'];
$row['platform'] = $_POST['platform'] ?? $row['platform'];
$row['watch_url'] = trim($_POST['watch_url'] ?? '');
$row['current_season'] = max(1, (int)($_POST['current_season'] ?? 1));
$row['current_episode'] = max(0, (int)($_POST['current_episode'] ?? 0));
$row['current_position'] = trim($_POST['current_position'] ?? '');
$row['progress_note'] = trim($_POST['progress_note'] ?? '');
$row['status'] = $_POST['status'] ?? $row['status'];
$row['rating'] = ($_POST['rating'] !== '') ? $_POST['rating'] : '';
$row['intro'] = trim($_POST['intro'] ?? '');
$row['note'] = trim($_POST['note'] ?? '');
$Drama->save($row);
to("?do=drama");
exit;
}
?>
<section>
<h2 class="section-title">編輯作品</h2>
<form class="admin-form" method="post" action="?do=edit&amp;id=<?= (int)$row['id'] ?>">
<label for="title">劇名/節目名</label>
<input type="text" id="title" name="title" required
value="<?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?>">
<label for="type">類型</label>
<select id="type" name="type">
<option value="variety"<?= $row['type'] === 'variety' ? ' selected' : '' ?>>綜藝</option>
<option value="drama"<?= $row['type'] === 'drama' ? ' selected' : '' ?>>戲劇</option>
</select>
<label for="platform">上架平台</label>
<select id="platform" name="platform">
<?php
$platforms = ['Netflix', 'Disney+', 'friDay影音', '愛奇藝', 'LINE TV', 'YouTube', '其他'];
foreach ($platforms as $p):
?>
<option value="<?= htmlspecialchars($p, ENT_QUOTES, 'UTF-8') ?>"<?= $row['platform'] === $p ? ' selected' : '' ?>>
<?= htmlspecialchars($p, ENT_QUOTES, 'UTF-8') ?>
</option>
<?php endforeach; ?>
</select>
<label for="watch_url">直接觀看網址</label>
<input type="url" id="watch_url" name="watch_url"
value="<?= htmlspecialchars($row['watch_url'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="https://...">
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1"
value="<?= (int)$row['current_season'] ?>">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0"
value="<?= (int)$row['current_episode'] ?>">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position"
value="<?= htmlspecialchars($row['current_position'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note"
value="<?= htmlspecialchars($row['progress_note'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="status">狀態</label>
<select id="status" name="status">
<?php
$statuses = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
foreach ($statuses as $key => $label):
?>
<option value="<?= $key ?>"<?= $row['status'] === $key ? ' selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
<label for="rating">評分</label>
<input type="number" id="rating" name="rating" min="0" max="10" step="0.1"
value="<?= htmlspecialchars($row['rating'] ?? '', ENT_QUOTES, 'UTF-8') ?>">
<label for="intro">簡介</label>
<textarea id="intro" name="intro" rows="4"><?= htmlspecialchars($row['intro'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
<label for="note">心得備註</label>
<textarea id="note" name="note" rows="3"><?= htmlspecialchars($row['note'] ?? '', ENT_QUOTES, 'UTF-8') ?></textarea>
<div class="form-actions" style="margin-top:1.25rem;display:flex;gap:10px;justify-content:center;">
<button type="submit" class="is-primary" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid var(--color-brand);background:var(--color-brand);font-weight:700;cursor:pointer;">儲存</button>
<a href="?do=drama" style="min-height:40px;padding:0.4rem 1.05rem;border-radius:999px;border:2px solid rgba(255,204,0,0.35);background:#fff;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;">取消</a>
</div>
</form>
</section>
+7
View File
@@ -0,0 +1,7 @@
<section>
<h2 class="section-title">後台首頁</h2>
<p class="admin-lead">乙級第三題骨架:清單、詳情、進度、後台 CRUD。訂票不做。</p>
<div class="admin-toolbar">
<a href="?do=drama">進入追劇管理</a>
</div>
</section>
+429
View File
@@ -0,0 +1,429 @@
/* ── 追劇清單/後台專用樣式 ── */
.vedio-list-page .vedio-intro {
text-align: left;
}
.vedio-list-page .vedio-intro p {
color: var(--color-text-muted);
font-size: 1rem;
line-height: 1.8;
margin: 0 0 0.85rem;
text-align: left;
}
.vedio-list-page .vedio-intro p:last-child {
margin-bottom: 0;
}
.vedio-list-page .list-lead {
color: var(--color-text-muted);
font-size: 1rem;
margin: 0 0 1.25rem;
text-align: left;
}
.vedio-list-page .type-tabs {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin: 0 0 1.75rem;
}
.vedio-list-page .type-tab {
display: inline-flex;
align-items: center;
justify-content: center;
border: 2px solid rgba(255, 204, 0, 0.22);
background: var(--color-surface-soft);
color: var(--color-text-dark);
font-weight: 700;
font-size: 0.92rem;
padding: 0.45rem 1.1rem;
border-radius: 999px;
text-decoration: none;
white-space: nowrap;
transition: border-color 0.18s ease, background 0.18s ease, color 0.18s ease;
}
.vedio-list-page .type-tab:hover {
border-color: var(--color-brand);
}
.vedio-list-page .type-tab.is-active {
border-color: var(--color-brand);
background: #fffce8;
color: var(--color-brand-dark);
}
.vedio-list-page .drama-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
margin: 0 0 2rem;
}
.vedio-list-page .drama-card {
display: flex;
flex-direction: column;
align-items: stretch;
text-align: left;
padding: 1.15rem 1.2rem 1.25rem;
background: var(--color-surface-soft);
border: 2px solid rgba(255, 204, 0, 0.18);
border-radius: 12px;
box-sizing: border-box;
min-height: 100%;
}
.vedio-list-page .drama-card:hover {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__poster {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
margin: 0 0 0.95rem;
border-radius: 8px;
background:
linear-gradient(145deg, rgba(255, 204, 0, 0.22), rgba(0, 0, 0, 0.08)),
var(--color-surface-soft);
color: var(--color-brand-dark);
font-weight: 800;
font-size: 0.95rem;
letter-spacing: 0.04em;
text-align: center;
padding: 0.75rem;
box-sizing: border-box;
}
.vedio-list-page .drama-card__meta {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 0 0 0.55rem;
}
.vedio-list-page .drama-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.18rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.03em;
white-space: nowrap;
background: rgba(255, 204, 0, 0.18);
color: var(--color-brand-dark);
}
.vedio-list-page .drama-tag--platform {
background: rgba(0, 0, 0, 0.06);
color: var(--color-text-muted);
}
.vedio-list-page .drama-card__title {
margin: 0 0 0.45rem;
font-size: 1.15rem;
font-weight: 800;
color: var(--color-text-dark);
line-height: 1.35;
text-align: center;
}
.vedio-list-page .drama-card__progress,
.vedio-list-page .drama-card__note {
margin: 0 0 0.35rem;
color: var(--color-text-muted);
font-size: 0.9rem;
line-height: 1.65;
text-align: left;
}
.vedio-list-page .drama-card__note {
font-size: 0.84rem;
margin-bottom: 0.85rem;
}
.vedio-list-page .drama-card__actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: auto;
padding-top: 0.35rem;
}
.vedio-list-page .drama-card__actions a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 38px;
padding: 0.35rem 0.9rem;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
}
.vedio-list-page .drama-card__actions a:hover {
border-color: var(--color-brand);
background: #fffce8;
}
.vedio-list-page .drama-card__actions a.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
.vedio-list-page .detail-block {
padding: 1.35rem 1.4rem 1.5rem;
border-radius: 12px;
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.25);
margin-bottom: 1.5rem;
}
.vedio-list-page .detail-block__eyebrow {
display: block;
text-align: center;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--color-text-muted);
margin: 0 0 0.45rem;
}
.vedio-list-page .detail-block__title {
margin: 0 0 0.75rem;
text-align: center;
font-size: 1.35rem;
font-weight: 800;
color: var(--color-brand-dark);
line-height: 1.35;
}
.vedio-list-page .detail-block p {
margin: 0 0 0.75rem;
color: var(--color-text-muted);
font-size: 0.98rem;
line-height: 1.8;
text-align: left;
}
.vedio-list-page .detail-block__progress-box {
margin: 1rem 0;
padding: 0.9rem 1rem;
border-radius: 10px;
background: #fffce8;
border: 1px solid rgba(255, 204, 0, 0.35);
}
.vedio-list-page .detail-block__progress-box p {
margin: 0 0 0.35rem;
}
.vedio-list-page .detail-block__progress-box p:last-child {
margin-bottom: 0;
}
.vedio-list-page .detail-block__actions,
.vedio-list-page .form-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-top: 1.1rem;
}
.vedio-list-page .detail-block__actions a,
.vedio-list-page .form-actions a,
.vedio-list-page .form-actions button,
.vedio-list-page .progress-form button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 40px;
padding: 0.4rem 1.05rem;
border-radius: 999px;
font-size: 0.9rem;
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fff;
color: var(--color-brand-dark);
}
.vedio-list-page .detail-block__actions a.is-primary,
.vedio-list-page .form-actions button.is-primary,
.vedio-list-page .progress-form button.is-primary {
background: var(--color-brand);
border-color: var(--color-brand);
color: #1a1a1a;
}
.vedio-list-page .empty-hint {
text-align: center;
color: var(--color-text-muted);
padding: 1.5rem 1rem;
margin: 0 0 1.5rem;
}
.vedio-list-page .progress-form,
.vedio-admin-page .admin-form {
max-width: 560px;
margin: 0 auto 2rem;
padding: 1.25rem 1.35rem;
background: var(--color-surface-soft);
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 12px;
}
.vedio-list-page .progress-form label,
.vedio-admin-page .admin-form label {
display: block;
margin: 0.85rem 0 0.35rem;
font-weight: 700;
color: var(--color-text-dark);
text-align: left;
}
.vedio-list-page .progress-form input,
.vedio-list-page .progress-form select,
.vedio-list-page .progress-form textarea,
.vedio-admin-page .admin-form input,
.vedio-admin-page .admin-form select,
.vedio-admin-page .admin-form textarea {
width: 100%;
box-sizing: border-box;
padding: 0.55rem 0.7rem;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 8px;
font-size: 0.95rem;
}
.vedio-list-page .vedio-admin-link,
.vedio-admin-page .admin-nav {
text-align: center;
margin: 1rem 0;
}
.vedio-list-page .vedio-admin-link a {
color: var(--color-brand-dark);
font-weight: 700;
}
.vedio-admin-page .admin-wrap {
padding-top: 1.5rem;
padding-bottom: 2.5rem;
}
.vedio-admin-page .admin-nav {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: center;
margin-bottom: 1.5rem;
}
.vedio-admin-page .admin-nav a {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.4rem 1rem;
border-radius: 999px;
border: 2px solid rgba(255, 204, 0, 0.35);
background: #fffce8;
color: var(--color-brand-dark);
font-weight: 700;
text-decoration: none;
white-space: nowrap;
}
.vedio-admin-page .admin-lead {
text-align: center;
color: var(--color-text-muted);
margin: 0 0 1.25rem;
}
.vedio-admin-page .drama-row {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
align-items: center;
padding: 12px 14px;
margin: 0 0 10px;
background: #fff;
border: 1px solid rgba(255, 204, 0, 0.25);
border-radius: 10px;
color: #222;
}
.vedio-admin-page .drama-row__info {
flex: 1 1 220px;
text-align: left;
}
.vedio-admin-page .drama-row__actions {
display: flex;
flex-wrap: wrap;
gap: 6px;
justify-content: flex-end;
}
.vedio-admin-page .drama-row__actions button,
.vedio-admin-page .drama-row__actions a.btn-like {
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0.25rem 0.7rem;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 6px;
background: #fffce8;
color: #222;
font-size: 0.85rem;
font-weight: 700;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
}
.vedio-admin-page .admin-toolbar {
text-align: center;
margin: 0 0 1rem;
}
.vedio-admin-page .admin-toolbar a {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.45rem 1.1rem;
border-radius: 999px;
background: var(--color-brand);
color: #1a1a1a;
font-weight: 800;
text-decoration: none;
}
@media (max-width: 640px) {
.vedio-list-page .drama-grid {
grid-template-columns: 1fr;
}
}
+62
View File
@@ -0,0 +1,62 @@
-- 深夜清單 / 追劇資料庫
-- 本機 XAMPPphpMyAdmin 匯入此檔即可
CREATE DATABASE IF NOT EXISTS `vedio_list` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE `vedio_list`;
DROP TABLE IF EXISTS `dramas`;
CREATE TABLE `dramas` (
`id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`title` varchar(100) NOT NULL COMMENT '劇名/節目名',
`type` varchar(20) NOT NULL DEFAULT 'drama' COMMENT 'variety=綜藝, drama=戲劇',
`platform` varchar(50) NOT NULL COMMENT '上架平台',
`watch_url` varchar(500) DEFAULT NULL COMMENT '作品直接觀看網址',
`current_season` int(10) UNSIGNED NOT NULL DEFAULT 1 COMMENT '目前第幾季',
`current_episode` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '目前第幾集',
`current_position` varchar(50) DEFAULT NULL COMMENT '該集看到的位置',
`progress_note` varchar(255) DEFAULT NULL COMMENT '進度情節備註',
`status` varchar(20) NOT NULL DEFAULT 'watching' COMMENT 'want/watching/done/paused/dropped',
`rating` decimal(3,1) DEFAULT NULL COMMENT '評分',
`intro` text COMMENT '簡介',
`note` text COMMENT '心得備註',
`sh` tinyint(1) UNSIGNED NOT NULL DEFAULT 1 COMMENT '是否顯示',
`rank` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排列順序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `dramas`
(`title`, `type`, `platform`, `watch_url`, `current_season`, `current_episode`, `current_position`, `progress_note`, `status`, `rating`, `intro`, `note`, `sh`, `rank`)
VALUES
(
'三傻遊肯亞',
'variety',
'Netflix',
'https://www.netflix.com/title/82031207',
1,
2,
'28:40',
'三人剛被扔進野外,表情比獅子還慌。',
'watching',
8.5,
'三個人、一座肯亞,以及無數個「這也能出事?」的現場。笑點來得又急又野,像把旅行節目的安全感整袋拆掉,只留下腎上腺素與默契。',
'羅PD 首登 Netflix;志源在裡面,小黃更沒有藉口不補。',
1,
1
),
(
'請回答1997',
'drama',
'Disney+',
'https://www.disneyplus.com/en-tw/browse/entity-3979fb82-8681-44e8-b7be-e313091f0d73',
1,
5,
'41:12',
'宿舍夜談剛結束,收音機還在響,心卻已經開始亂。',
'watching',
9.0,
'把 1997 寫進宿舍、收音機與那一聲來不及說出口的喜歡。甜的時候甜到發亮,疼的時候又老實得讓人不敢快轉。',
'下一集容易哭,衛生紙先備好。',
1,
2
);
+65
View File
@@ -0,0 +1,65 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row || (int)$row['sh'] !== 1) {
echo '<p class="empty-hint">找不到這部作品,或它目前未公開顯示。</p>';
echo '<p class="empty-hint"><a href="?do=main">回清單</a></p>';
return;
}
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$statusLabels = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
$typeText = $typeLabels[$row['type']] ?? $row['type'];
$statusText = $statusLabels[$row['status']] ?? $row['status'];
?>
<section class="detail-block">
<span class="detail-block__eyebrow"><?= htmlspecialchars($typeText . '' . $row['platform'], ENT_QUOTES, 'UTF-8') ?></span>
<h2 class="detail-block__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h2>
<?php if (!empty($row['intro'])): ?>
<p><?= nl2br(htmlspecialchars($row['intro'], ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<div class="detail-block__progress-box">
<p>
觀看進度:第 <?= (int)$row['current_season'] ?> 季|第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
<?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['progress_note'])): ?>
<p><?= htmlspecialchars($row['progress_note'], ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
</div>
<p>狀態:<?= htmlspecialchars($statusText, ENT_QUOTES, 'UTF-8') ?>
<?php if ($row['rating'] !== null && $row['rating'] !== ''): ?>
 · 評分:<?= htmlspecialchars($row['rating'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['note'])): ?>
<p><?= nl2br(htmlspecialchars($row['note'], ENT_QUOTES, 'UTF-8')) ?></p>
<?php endif; ?>
<div class="detail-block__actions">
<?php if (!empty($row['watch_url'])): ?>
<a class="is-primary" href="<?= htmlspecialchars($row['watch_url'], ENT_QUOTES, 'UTF-8') ?>"
target="_blank" rel="noopener noreferrer">繼續觀看</a>
<?php endif; ?>
<a href="?do=progress&amp;id=<?= (int)$row['id'] ?>">更新進度</a>
<a href="?do=main">回清單</a>
</div>
</section>
+84
View File
@@ -0,0 +1,84 @@
<?php
$typeLabels = [
'variety' => '綜藝',
'drama' => '戲劇',
];
$statusLabels = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
$filter = $_GET['type'] ?? 'all';
$where = ['sh' => 1];
if ($filter === 'variety' || $filter === 'drama') {
$where['type'] = $filter;
}
$rows = $Drama->all($where, " ORDER BY `rank` ASC, `id` ASC");
?>
<section class="vedio-intro">
<h2 class="section-title">正在追的那些夜晚</h2>
<p>
有些夜晚適合考古黃色奇蹟,有些夜晚則是——螢幕一亮,人就栽進另一個世界。
綜藝裡的失控笑聲、戲劇裡的心跳停拍,都值得被好好記下來:看到哪一季、哪一集、哪一句台詞剛把你釘在沙發上。
</p>
<p>
這裡的清單來自資料庫:類型先分<span class="mark_y">綜藝</span>與<span class="mark_b">戲劇</span>
進度與「繼續觀看」入口都能改。慢慢養也很好看。
</p>
</section>
<section class="list-section" aria-labelledby="vedio-list-heading">
<h2 class="section-title" id="vedio-list-heading">追劇清單</h2>
<p class="list-lead">先用類型找——綜藝還是戲劇,跟 OTT 首頁那排分類一樣直覺。</p>
<div class="type-tabs" role="tablist" aria-label="作品類型">
<a class="type-tab<?= $filter === 'all' ? ' is-active' : '' ?>" href="?do=main&amp;type=all">全部</a>
<a class="type-tab<?= $filter === 'variety' ? ' is-active' : '' ?>" href="?do=main&amp;type=variety">綜藝</a>
<a class="type-tab<?= $filter === 'drama' ? ' is-active' : '' ?>" href="?do=main&amp;type=drama">戲劇</a>
</div>
<?php if (count($rows) === 0): ?>
<p class="empty-hint">這個類型目前還沒有作品——換「全部」看看,或到後台新增一筆。</p>
<?php else: ?>
<div class="drama-grid">
<?php foreach ($rows as $row): ?>
<?php
$typeText = $typeLabels[$row['type']] ?? $row['type'];
$statusText = $statusLabels[$row['status']] ?? $row['status'];
$posterLabel = htmlspecialchars(strtoupper($row['platform']) . ' · ' . $typeText, ENT_QUOTES, 'UTF-8');
?>
<article class="drama-card" data-type="<?= htmlspecialchars($row['type'], ENT_QUOTES, 'UTF-8') ?>">
<div class="drama-card__poster" aria-hidden="true"><?= $posterLabel ?></div>
<div class="drama-card__meta">
<span class="drama-tag"><?= htmlspecialchars($typeText, ENT_QUOTES, 'UTF-8') ?></span>
<span class="drama-tag drama-tag--platform"><?= htmlspecialchars($row['platform'], ENT_QUOTES, 'UTF-8') ?></span>
</div>
<h3 class="drama-card__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h3>
<p class="drama-card__progress">
觀看進度:第 <?= (int)$row['current_season'] ?> 季・第 <?= (int)$row['current_episode'] ?> 集
<?php if (!empty($row['current_position'])): ?>
・看到 <?= htmlspecialchars($row['current_position'], ENT_QUOTES, 'UTF-8') ?>
<?php endif; ?>
</p>
<?php if (!empty($row['progress_note'])): ?>
<p class="drama-card__note"><?= htmlspecialchars($row['progress_note'], ENT_QUOTES, 'UTF-8') ?></p>
<?php else: ?>
<p class="drama-card__note">狀態:<?= htmlspecialchars($statusText, ENT_QUOTES, 'UTF-8') ?></p>
<?php endif; ?>
<div class="drama-card__actions">
<?php if (!empty($row['watch_url'])): ?>
<a class="is-primary" href="<?= htmlspecialchars($row['watch_url'], ENT_QUOTES, 'UTF-8') ?>"
target="_blank" rel="noopener noreferrer">繼續觀看</a>
<?php endif; ?>
<a href="?do=detail&amp;id=<?= (int)$row['id'] ?>">查看詳情</a>
<a href="?do=progress&amp;id=<?= (int)$row['id'] ?>">更新進度</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php endif; ?>
</section>
+69
View File
@@ -0,0 +1,69 @@
<?php
$id = $_GET['id'] ?? 0;
$row = $Drama->find($id);
if (!$row) {
echo '<p class="empty-hint">找不到這部作品。</p>';
echo '<p class="empty-hint"><a href="?do=main">回清單</a></p>';
return;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$row['current_season'] = max(1, (int)($_POST['current_season'] ?? 1));
$row['current_episode'] = max(0, (int)($_POST['current_episode'] ?? 0));
$row['current_position'] = trim($_POST['current_position'] ?? '');
$row['progress_note'] = trim($_POST['progress_note'] ?? '');
$row['status'] = $_POST['status'] ?? $row['status'];
$Drama->save($row);
to("?do=detail&id=" . (int)$row['id']);
exit;
}
?>
<section class="vedio-intro">
<h2 class="section-title">更新進度</h2>
<p>記下你停在哪一季、哪一集、哪一秒——以及當時螢幕上正在發生什麼,之後才找得回那個心跳瞬間。</p>
</section>
<form class="progress-form" method="post" action="?do=progress&amp;id=<?= (int)$row['id'] ?>">
<h3 class="drama-card__title"><?= htmlspecialchars($row['title'], ENT_QUOTES, 'UTF-8') ?></h3>
<label for="current_season">目前看到第幾季</label>
<input type="number" id="current_season" name="current_season" min="1"
value="<?= (int)$row['current_season'] ?>">
<label for="current_episode">目前看到第幾集</label>
<input type="number" id="current_episode" name="current_episode" min="0"
value="<?= (int)$row['current_episode'] ?>">
<label for="current_position">這一集看到哪裡</label>
<input type="text" id="current_position" name="current_position"
value="<?= htmlspecialchars($row['current_position'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="例如 35:20、剩下 10 分鐘、片尾前">
<label for="progress_note">進度情節備註</label>
<input type="text" id="progress_note" name="progress_note"
value="<?= htmlspecialchars($row['progress_note'] ?? '', ENT_QUOTES, 'UTF-8') ?>"
placeholder="例如:女主角剛看到手機裡的影片">
<label for="status">狀態</label>
<select id="status" name="status">
<?php
$statuses = [
'want' => '想看',
'watching' => '追劇中',
'done' => '已追完',
'paused' => '暫停',
'dropped' => '棄劇',
];
foreach ($statuses as $key => $label):
?>
<option value="<?= $key ?>"<?= $row['status'] === $key ? ' selected' : '' ?>><?= $label ?></option>
<?php endforeach; ?>
</select>
<div class="form-actions">
<button type="submit" class="is-primary">儲存進度</button>
<a href="?do=detail&amp;id=<?= (int)$row['id'] ?>">取消</a>
</div>
</form>
+54
View File
@@ -0,0 +1,54 @@
<?php include_once "./api/db.php"; ?>
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水晶男孩|深夜清單</title>
<link rel="stylesheet" href="../../assets/css/style.css">
<link rel="stylesheet" href="../../assets/css/index.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<link rel="stylesheet" href="../../assets/css/tablet.css">
<link rel="stylesheet" href="css/vedio-list.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
</head>
<body class="vedio-list-page">
<header class="inner-page-header">
<div class="header-inner site-header-inner">
<h1 class="main-title">深夜清單</h1>
<div class="subtitle-wrap">
<span class="sub-title">DRAMA &amp; VARIETY WATCH LIST</span>
</div>
</div>
</header>
<nav class="portal-nav" aria-label="水晶男孩推廣部導覽" data-portal-nav></nav>
<canvas id="particles"></canvas>
<div class="container">
<?php
$do = $_GET['do'] ?? 'main';
$file = "front/$do.php";
if (file_exists($file)) {
include $file;
} else {
include "front/main.php";
}
?>
</div>
<footer>
<p>SECHSKIES Fanpage &copy; 2026 - 黃色氣球永遠飄揚</p>
<a href="https://jekkinoopy.github.io/Portfolio/" class="studio-link" target="_blank" rel="noopener noreferrer">
Design by <span class="mark_b">Jekkinoopy Studio</span>
</a>
<p class="vedio-admin-link"><a href="admin.php">管理後台</a></p>
</footer>
<script src="../../assets/js/particles.js"></script>
<script src="../../assets/js/portal-nav.js"></script>
</body>
</html>