一个简单的书影音记录应用
以前记录书影音是在豆瓣app里记录,再用第三方应用mythsman/idouban导出,其中要用到mythsman/mouban后端,这个应用经常出问题。本文用php简单实现了书影音的记录,不使用数据库,记录在文本文件中。
代码
<?php
// =============================================================================
// 配置文件
// =============================================================================
// 开启错误显示(调试完成后可注释)
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 会话处理
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// 系统配置
$DATA_FILE = 'books_data.txt';
$DATE_FORMAT = 'Y-m-d';
$UPLOAD_DIR = 'uploads/';
$MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
$ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
// 🔐 密码配置 - 请修改这里的密码!
$ADMIN_PASSWORD = 'admin'; // 将这里的密码改成你想要的
$SESSION_TIMEOUT = 3600; // 会话超时时间(秒),1小时
date_default_timezone_set('Asia/Shanghai');
// 创建必要的目录
if (!file_exists($UPLOAD_DIR)) {
mkdir($UPLOAD_DIR, 0755, true);
}
// =============================================================================
// 核心函数
// =============================================================================
/**
* 检查用户是否已登录
*/
function isLoggedIn() {
global $SESSION_TIMEOUT;
if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
if (isset($_SESSION['login_time']) && (time() - $_SESSION['login_time']) < $SESSION_TIMEOUT) {
return true;
}
}
return false;
}
/**
* 登录验证
*/
function login($password) {
global $ADMIN_PASSWORD;
if ($password === $ADMIN_PASSWORD) {
$_SESSION['logged_in'] = true;
$_SESSION['login_time'] = time();
return true;
}
return false;
}
/**
* 退出登录
*/
function logout() {
$_SESSION = array();
session_destroy();
}
/**
* 处理图片上传
*/
function handleUpload($fileField) {
global $UPLOAD_DIR, $MAX_FILE_SIZE, $ALLOWED_TYPES;
if (!isset($_FILES[$fileField]) || $_FILES[$fileField]['error'] === UPLOAD_ERR_NO_FILE) {
return '';
}
$file = $_FILES[$fileField];
if ($file['error'] !== UPLOAD_ERR_OK) {
return '';
}
// 检查文件大小
if ($file['size'] > $MAX_FILE_SIZE) {
return '';
}
// 检查文件类型
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, $ALLOWED_TYPES)) {
return '';
}
// 生成安全文件名
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$safeName = 'item_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $ext;
$targetPath = $UPLOAD_DIR . $safeName;
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
return $safeName;
}
return '';
}
/**
* 加载所有记录并按月份分组
*/
function loadItemsGroupedByMonth($showAll = false) {
global $DATA_FILE;
$items = ['books' => [], 'movies' => []];
if (file_exists($DATA_FILE)) {
$lines = file($DATA_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(' ||| ', $line, 8);
if (count($parts) >= 7) {
$item = [
'id' => $parts[0],
'type' => $parts[1],
'title' => $parts[2],
'cover_url' => $parts[3],
'uploaded_cover' => $parts[4],
'douban_url' => $parts[5],
'read_date' => $parts[6],
'created' => $parts[7] ?? date('Y-m-d H:i:s')
];
// 添加月份字段
$item['year_month'] = date('Y年m月', strtotime($item['read_date']));
$item['year'] = date('Y', strtotime($item['read_date']));
$item['month'] = date('m', strtotime($item['read_date']));
if ($item['type'] === 'book') {
$items['books'][] = $item;
} elseif ($item['type'] === 'movie') {
$items['movies'][] = $item;
}
}
}
// 按日期排序(最新的在前面)
usort($items['books'], function($a, $b) {
return strtotime($b['read_date']) - strtotime($a['read_date']);
});
usort($items['movies'], function($a, $b) {
return strtotime($b['read_date']) - strtotime($a['read_date']);
});
// 如果不是显示全部,则限制条数
if (!$showAll) {
$items['books'] = array_slice($items['books'], 0, 10);
$items['movies'] = array_slice($items['movies'], 0, 10);
}
// 按月份分组
$groupedItems = [
'books' => groupItemsByMonth($items['books']),
'movies' => groupItemsByMonth($items['movies'])
];
return $groupedItems;
}
return ['books' => [], 'movies' => []];
}
/**
* 将项目按月份分组
*/
function groupItemsByMonth($items) {
$grouped = [];
foreach ($items as $item) {
$yearMonth = $item['year_month'];
if (!isset($grouped[$yearMonth])) {
$grouped[$yearMonth] = [
'month' => $yearMonth,
'year' => $item['year'],
'month_num' => $item['month'],
'items' => []
];
}
$grouped[$yearMonth]['items'][] = $item;
}
// 按月份排序(最新的在前面)
uasort($grouped, function($a, $b) {
return ($b['year'] . $b['month_num']) <=> ($a['year'] . $a['month_num']);
});
return $grouped;
}
/**
* 获取所有项目数量
*/
function getAllItemsCount() {
global $DATA_FILE;
$counts = ['books' => 0, 'movies' => 0];
if (file_exists($DATA_FILE)) {
$lines = file($DATA_FILE, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$parts = explode(' ||| ', $line, 8);
if (count($parts) >= 7) {
$type = $parts[1];
if ($type === 'book') {
$counts['books']++;
} elseif ($type === 'movie') {
$counts['movies']++;
}
}
}
}
return $counts;
}
/**
* 保存记录(需要登录)
*/
function saveItem($type, $title, $cover_url, $uploaded_cover, $douban_url, $read_date) {
if (!isLoggedIn()) {
return false;
}
global $DATA_FILE;
$items = loadItemsGroupedByMonth(true);
$id = time() . '_' . rand(1000, 9999);
$newItem = [
'id' => $id,
'type' => $type,
'title' => trim(str_replace('|||', '︳', $title)),
'cover_url' => trim($cover_url),
'uploaded_cover' => trim($uploaded_cover),
'douban_url' => trim($douban_url),
'read_date' => $read_date,
'created' => date('Y-m-d H:i:s')
];
// 将所有项目保存回文件
$allItems = [];
foreach (['books', 'movies'] as $category) {
foreach ($items[$category] as $monthGroup) {
$allItems = array_merge($allItems, $monthGroup['items']);
}
}
// 添加新项目
$allItems[] = $newItem;
// 重新排序
usort($allItems, function($a, $b) {
return strtotime($b['read_date']) - strtotime($a['read_date']);
});
// 保存到文件
$lines = [];
foreach ($allItems as $item) {
$lines[] = implode(' ||| ', [
$item['id'], $item['type'], $item['title'], $item['cover_url'],
$item['uploaded_cover'], $item['douban_url'], $item['read_date'], $item['created']
]);
}
return file_put_contents($DATA_FILE, implode(PHP_EOL, $lines)) !== false;
}
/**
* 删除记录(需要登录)
*/
function deleteItem($id) {
if (!isLoggedIn()) {
return false;
}
global $DATA_FILE, $UPLOAD_DIR;
$items = loadItemsGroupedByMonth(true);
$itemToDelete = null;
$allItems = [];
// 查找要删除的记录并重新构建所有项目数组
foreach (['books', 'movies'] as $category) {
foreach ($items[$category] as $monthGroup) {
foreach ($monthGroup['items'] as $item) {
if ($item['id'] === $id) {
$itemToDelete = $item;
} else {
$allItems[] = $item;
}
}
}
}
if (!$itemToDelete) return false;
// 删除关联的图片
if (!empty($itemToDelete['uploaded_cover'])) {
$imagePath = $UPLOAD_DIR . $itemToDelete['uploaded_cover'];
if (file_exists($imagePath)) {
@unlink($imagePath);
}
}
// 保存新列表
$lines = [];
foreach ($allItems as $item) {
$lines[] = implode(' ||| ', [
$item['id'], $item['type'], $item['title'], $item['cover_url'],
$item['uploaded_cover'], $item['douban_url'], $item['read_date'], $item['created']
]);
}
return file_put_contents($DATA_FILE, implode(PHP_EOL, $lines)) !== false;
}
// =============================================================================
// 处理请求
// =============================================================================
$action = $_GET['action'] ?? '';
$message = '';
// 检查是否显示全部
$showAllBooks = isset($_GET['show_all_books']);
$showAllMovies = isset($_GET['show_all_movies']);
$showAll = $showAllBooks || $showAllMovies;
// 显示模式:cards(卡片)或 list(列表)
// 优先使用URL参数,其次使用cookie,最后默认cards
if (isset($_GET['view'])) {
$viewMode = $_GET['view'];
// 保存到cookie,30天有效期
setcookie('view_mode', $viewMode, time() + 30*24*60*60, '/');
} elseif (isset($_COOKIE['view_mode']) && in_array($_COOKIE['view_mode'], ['cards', 'list'])) {
$viewMode = $_COOKIE['view_mode'];
} else {
$viewMode = 'cards';
}
// 登录/登出
if ($action === 'login' && isset($_POST['password'])) {
if (login($_POST['password'])) {
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
} else {
$message = '<div class="message error">❌ 密码错误</div>';
}
} elseif ($action === 'logout') {
logout();
header('Location: ' . $_SERVER['PHP_SELF']);
exit;
}
// 添加记录
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['title'])) {
if (!isLoggedIn()) {
$message = '<div class="message error">❌ 请先登录</div>';
} else {
$uploadedCover = '';
if (isset($_FILES['cover_upload']) && $_FILES['cover_upload']['error'] !== UPLOAD_ERR_NO_FILE) {
$uploadedCover = handleUpload('cover_upload');
}
$itemType = $_POST['item_type'] ?? 'book';
$success = saveItem(
$itemType,
$_POST['title'],
$_POST['cover_url'] ?? '',
$uploadedCover,
$_POST['douban_url'] ?? '',
$_POST['read_date'] ?? date($DATE_FORMAT)
);
if ($success) {
$message = '<div class="message success">✅ ' . ($itemType === 'book' ? '书籍' : '电影') . '添加成功!</div>';
} else {
$message = '<div class="message error">❌ 添加失败</div>';
}
}
}
// 删除记录
if ($action === 'delete' && isset($_GET['id'])) {
if (!isLoggedIn()) {
$message = '<div class="message error">❌ 请先登录</div>';
} else {
if (deleteItem($_GET['id'])) {
$message = '<div class="message success">✅ 删除成功!</div>';
}
}
}
// 加载数据
$items = loadItemsGroupedByMonth($showAll);
$books = $items['books'];
$movies = $items['movies'];
$allCounts = getAllItemsCount();
$loggedIn = isLoggedIn();
$showLoginForm = ($action === 'login' || (empty($books) && empty($movies))) && !$loggedIn;
// =============================================================================
// HTML界面 - 优化响应式
// =============================================================================
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
<title><?= $loggedIn ? '📚🎬 书影音管理后台' : '📚🎬 我的书影音' ?></title>
<style>
/* 基础设置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', sans-serif;
line-height: 1.5;
background: #f8f9fa;
color: #333;
padding: 12px;
font-size: 16px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
/* 头部导航 */
header {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
display: flex;
justify-content: space-between;
align-items: center;
}
.header-left h1 {
color: #2c3e50;
font-size: 1.8rem;
font-weight: 700;
}
.header-left .subtitle {
color: #7f8c8d;
font-size: 0.95rem;
}
.header-right {
display: flex;
gap: 12px;
align-items: center;
}
/* 消息提示 */
.message {
padding: 12px;
margin: 15px 0;
border-radius: 8px;
font-size: 0.95rem;
}
.success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
/* 按钮 */
.btn {
padding: 10px 18px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 44px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102,126,234,0.4);
}
.btn-small {
padding: 6px 12px;
font-size: 0.9rem;
border-radius: 6px;
min-height: 36px;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-outline {
background: transparent;
border: 2px solid #667eea;
color: #667eea;
}
.btn-outline:hover {
background: #667eea;
color: white;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
/* 视图切换按钮 */
.view-toggle {
display: flex;
border-radius: 8px;
overflow: hidden;
border: 2px solid #e0e0e0;
background: white;
}
.view-toggle-btn {
padding: 8px 16px;
border: none;
background: none;
cursor: pointer;
font-size: 1.2rem;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
}
.view-toggle-btn:hover {
background: #f8f9ff;
}
.view-toggle-btn.active {
background: #667eea;
color: white;
}
/* 双列布局 */
.two-columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.column {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 3px 10px rgba(0,0,0,0.06);
}
.column-title {
font-size: 1.3rem;
margin-bottom: 20px;
color: #2c3e50;
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.column-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
/* 月份分组样式 */
.month-group {
margin-bottom: 25px;
}
.month-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 12px 20px;
border-radius: 8px;
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
/* 书籍/电影卡片 */
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 18px;
}
.item-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 3px 12px rgba(0,0,0,0.06);
transition: all 0.3s;
display: flex;
flex-direction: column;
height: 100%;
}
.item-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.12);
}
.item-cover {
height: 180px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
position: relative;
}
.item-cover img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.5s;
}
.item-card:hover .item-cover img {
transform: scale(1.05);
}
.book-cover { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
.movie-cover { background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); }
.item-info {
padding: 18px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.item-title {
font-size: 1.05rem;
font-weight: 600;
margin-bottom: 10px;
color: #2c3e50;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: auto;
font-size: 0.9rem;
color: #666;
}
.item-actions {
display: flex;
gap: 8px;
margin-top: 15px;
}
.type-badge {
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
display: inline-block;
margin-bottom: 8px;
}
.book-badge { background: #e3f2fd; color: #1976d2; }
.movie-badge { background: #fce4ec; color: #c2185b; }
/* 列表显示样式 - 优化移动端 */
.items-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.list-item {
display: flex;
align-items: center;
padding: 15px;
background: white;
border-radius: 10px;
border-left: 4px solid #667eea;
box-shadow: 0 2px 6px rgba(0,0,0,0.04);
transition: all 0.3s;
}
.list-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.list-item.book-item {
border-left-color: #1976d2;
}
.list-item.movie-item {
border-left-color: #c2185b;
}
.list-item-cover {
width: 80px;
height: 80px;
border-radius: 8px;
overflow: hidden;
margin-right: 15px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
}
.list-item-cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
.list-item-info {
flex-grow: 1;
min-width: 0; /* 防止文本溢出 */
}
.list-item-title {
font-weight: 600;
font-size: 1.05rem;
margin-bottom: 5px;
color: #2c3e50;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.list-item-meta {
display: flex;
gap: 10px;
font-size: 0.9rem;
color: #666;
flex-wrap: wrap;
align-items: center;
}
.list-item-type {
padding: 3px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.list-item-type.book {
background: #e3f2fd;
color: #1976d2;
}
.list-item-type.movie {
background: #fce4ec;
color: #c2185b;
}
.list-item-actions {
display: flex;
gap: 8px;
margin-left: 10px;
flex-shrink: 0;
}
/* 表单 */
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
label {
margin-bottom: 8px;
font-weight: 600;
color: #2c3e50;
font-size: 0.95rem;
}
input, select, textarea {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
width: 100%;
min-height: 48px;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102,126,234,0.1);
}
/* 类型选择器 */
.type-selector {
display: flex;
gap: 15px;
margin-bottom: 25px;
}
.type-option {
flex: 1;
text-align: center;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s;
}
.type-option:hover {
border-color: #667eea;
background: #f8f9ff;
}
.type-option.selected {
border-color: #667eea;
background: #667eea;
color: white;
}
/* 上传区域 */
.upload-area {
border: 2px dashed #667eea;
border-radius: 10px;
padding: 20px;
text-align: center;
background: #f8f9ff;
cursor: pointer;
transition: all 0.3s;
margin: 15px 0;
}
.upload-area:hover {
background: #eef1ff;
}
.upload-preview {
max-width: 150px;
max-height: 150px;
margin: 15px auto;
border-radius: 8px;
display: none;
}
/* 登录框 */
.login-box {
max-width: 400px;
margin: 50px auto;
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
text-align: center;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #666;
}
/* 显示全部按钮区域 */
.show-all-section {
text-align: center;
margin: 20px 0;
padding: 15px;
background: #f8f9ff;
border-radius: 10px;
border: 2px dashed #667eea;
}
/* 页脚统计 */
footer {
text-align: center;
padding: 20px;
color: #666;
font-size: 0.9rem;
margin-top: 40px;
border-top: 1px solid #eee;
}
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.stat-item {
background: #f8f9fa;
padding: 15px;
border-radius: 10px;
text-align: center;
}
.stat-number {
font-size: 1.6rem;
font-weight: bold;
margin-bottom: 5px;
}
.book-stat { color: #1976d2; }
.movie-stat { color: #c2185b; }
/* 响应式设计 - 针对小屏优化列表显示 */
@media (max-width: 768px) {
body {
padding: 10px;
font-size: 15px;
}
header {
padding: 15px;
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.header-right {
width: 100%;
justify-content: flex-start;
flex-wrap: wrap;
}
.items-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
/* 小屏列表显示优化 */
.list-item {
flex-direction: row;
align-items: flex-start;
padding: 12px;
}
.list-item-cover {
width: 70px;
height: 70px;
margin-right: 12px;
}
.list-item-info {
flex: 1;
min-width: 0;
}
.list-item-title {
font-size: 1rem;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.list-item-meta {
gap: 8px;
font-size: 0.85rem;
flex-wrap: wrap;
}
.list-item-type {
padding: 2px 6px;
font-size: 0.7rem;
}
.list-item-actions {
margin-left: 8px;
flex-direction: column;
gap: 6px;
}
.list-item-actions .btn-small {
padding: 5px 10px;
font-size: 0.8rem;
min-height: 32px;
}
.item-cover {
height: 160px;
}
.form-row {
grid-template-columns: 1fr;
}
.type-selector {
flex-direction: column;
}
.column {
padding: 15px;
}
.column-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.month-header {
font-size: 1rem;
padding: 10px 15px;
}
}
@media (max-width: 480px) {
.items-grid {
grid-template-columns: 1fr;
}
.header-left h1 {
font-size: 1.5rem;
}
/* 超小屏列表显示进一步优化 */
.list-item {
padding: 10px;
gap: 10px;
}
.list-item-cover {
width: 60px;
height: 60px;
margin-right: 10px;
}
.list-item-title {
font-size: 0.95rem;
}
.list-item-meta {
font-size: 0.8rem;
}
.list-item-type {
font-size: 0.65rem;
padding: 1px 5px;
}
.list-item-actions {
flex-direction: column;
gap: 5px;
}
.list-item-actions .btn-small {
padding: 4px 8px;
font-size: 0.75rem;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.item-title {
font-size: 1rem;
}
.show-all-section {
padding: 10px;
}
}
@media (max-width: 992px) {
.two-columns {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 头部导航 -->
<header>
<div class="header-left">
<h1>📚🎬 我的书影音</h1>
<p class="subtitle"><?= $loggedIn ? '管理员模式' : '公开浏览模式' ?></p>
</div>
<div class="header-right">
<?php if ($loggedIn): ?>
<div class="view-toggle">
<a href="?view=cards<?= $showAllBooks ? '&show_all_books=1' : '' ?><?= $showAllMovies ? '&show_all_movies=1' : '' ?>"
class="view-toggle-btn <?= $viewMode === 'cards' ? 'active' : '' ?>"
title="卡片视图">
🗂️
</a>
<a href="?view=list<?= $showAllBooks ? '&show_all_books=1' : '' ?><?= $showAllMovies ? '&show_all_movies=1' : '' ?>"
class="view-toggle-btn <?= $viewMode === 'list' ? 'active' : '' ?>"
title="列表视图">
📋
</a>
</div>
<span style="color: #28a745; font-weight: 600;">✅ 已登录</span>
<a href="?action=logout" class="btn" style="background: #6c757d; color: white;">🚪 退出</a>
<?php else: ?>
<?php if (!empty($books) || !empty($movies)): ?>
<div class="view-toggle">
<a href="?view=cards<?= $showAllBooks ? '&show_all_books=1' : '' ?><?= $showAllMovies ? '&show_all_movies=1' : '' ?>"
class="view-toggle-btn <?= $viewMode === 'cards' ? 'active' : '' ?>"
title="卡片视图">
🗂️
</a>
<a href="?view=list<?= $showAllBooks ? '&show_all_books=1' : '' ?><?= $showAllMovies ? '&show_all_movies=1' : '' ?>"
class="view-toggle-btn <?= $viewMode === 'list' ? 'active' : '' ?>"
title="列表视图">
📋
</a>
</div>
<a href="?action=login" class="btn btn-primary">🔑 登录</a>
<?php endif; ?>
<span style="color: #6c757d;">👤 访客</span>
<?php endif; ?>
</div>
</header>
<?php if ($message): echo $message; endif; ?>
<!-- 登录表单 -->
<?php if ($showLoginForm): ?>
<div class="login-box">
<h2>🔐 管理员登录</h2>
<p>请输入密码进入管理后台</p>
<form method="POST" action="?action=login" class="login-form">
<input type="password" name="password" placeholder="输入管理员密码" required
style="width: 100%; padding: 12px; margin: 15px 0; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 16px;">
<button type="submit" class="btn btn-primary" style="width: 100%;">
🔑 登录
</button>
</form>
<?php if (!empty($books) || !empty($movies)): ?>
<p style="margin-top: 20px; color: #666; font-size: 0.9rem;">
<a href="<?= $_SERVER['PHP_SELF'] ?>" style="color: #667eea; text-decoration: none;">
← 返回浏览书影音
</a>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<!-- 书影音双列布局 -->
<?php if ((!empty($books) || !empty($movies)) && !$showLoginForm): ?>
<div class="two-columns">
<!-- 书籍列 -->
<div class="column">
<div class="column-header">
<h2 class="column-title">
<span style="background: #1976d2; color: white; width: 36px; height: 36px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;">📚</span>
书籍
</h2>
<div>
<span style="color: #666; font-size: 0.9rem;">
共 <?= $allCounts['books'] ?> 本
<?php if (!$showAllBooks && $allCounts['books'] > 10): ?>
(显示最新 10 本)
<?php endif; ?>
</span>
</div>
</div>
<?php if (empty($books)): ?>
<div class="empty-state">
<div style="font-size: 3rem; color: #ddd; margin-bottom: 15px;">📚</div>
<p>暂无书籍记录</p>
<?php if ($loggedIn): ?>
<p style="color: #666; font-size: 0.9rem; margin-top: 10px;">在下方添加你的第一本书</p>
<?php endif; ?>
</div>
<?php else: ?>
<?php foreach ($books as $monthGroup): ?>
<div class="month-group">
<div class="month-header">
📅 <?= $monthGroup['month'] ?>
</div>
<?php if ($viewMode === 'cards'): ?>
<!-- 卡片视图 -->
<div class="items-grid">
<?php foreach ($monthGroup['items'] as $item): ?>
<div class="item-card">
<div class="item-cover book-cover">
<?php if (!empty($item['uploaded_cover']) || !empty($item['cover_url'])): ?>
<?php $imgSrc = !empty($item['uploaded_cover']) ? $UPLOAD_DIR . $item['uploaded_cover'] : $item['cover_url']; ?>
<img src="<?= htmlspecialchars($imgSrc) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div style="display:none; color: white; font-size: 2rem; width:100%; height:100%; align-items:center; justify-content:center;">📚</div>
<?php else: ?>
<div style="color: white; font-size: 2rem;">📚</div>
<?php endif; ?>
</div>
<div class="item-info">
<span class="type-badge book-badge">书籍</span>
<h3 class="item-title"><?= htmlspecialchars($item['title']) ?></h3>
<div class="item-meta">
<span>📅 <?= $item['read_date'] ?></span>
<?php if (!empty($item['uploaded_cover'])): ?>
<span title="本地上传的图片">📤</span>
<?php endif; ?>
</div>
<div class="item-actions">
<?php if (!empty($item['douban_url'])): ?>
<a href="<?= htmlspecialchars($item['douban_url']) ?>"
target="_blank" class="btn btn-primary btn-small">
🔗 豆瓣
</a>
<?php endif; ?>
<?php if ($loggedIn): ?>
<button class="btn btn-danger btn-small"
onclick="if(confirm('确定删除这本书吗?')) location.href='?action=delete&id=<?= $item['id'] ?>'">
🗑️ 删除
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<!-- 列表视图 -->
<div class="items-list">
<?php foreach ($monthGroup['items'] as $item): ?>
<div class="list-item book-item">
<div class="list-item-cover">
<?php if (!empty($item['uploaded_cover']) || !empty($item['cover_url'])): ?>
<?php $imgSrc = !empty($item['uploaded_cover']) ? $UPLOAD_DIR . $item['uploaded_cover'] : $item['cover_url']; ?>
<img src="<?= htmlspecialchars($imgSrc) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div style="display:none; color: #666; font-size: 1.5rem;">📚</div>
<?php else: ?>
<div style="color: #666; font-size: 1.5rem;">📚</div>
<?php endif; ?>
</div>
<div class="list-item-info">
<div class="list-item-title" title="<?= htmlspecialchars($item['title']) ?>">
<?= htmlspecialchars($item['title']) ?>
</div>
<div class="list-item-meta">
<span class="list-item-type book">书籍</span>
<span>📅 <?= $item['read_date'] ?></span>
<?php if (!empty($item['uploaded_cover'])): ?>
<span title="本地上传的图片">📤</span>
<?php endif; ?>
</div>
</div>
<div class="list-item-actions">
<?php if (!empty($item['douban_url'])): ?>
<a href="<?= htmlspecialchars($item['douban_url']) ?>"
target="_blank" class="btn btn-primary btn-small">
🔗 豆瓣
</a>
<?php endif; ?>
<?php if ($loggedIn): ?>
<button class="btn btn-danger btn-small"
onclick="if(confirm('确定删除这本书吗?')) location.href='?action=delete&id=<?= $item['id'] ?>'">
🗑️ 删除
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (!$showAllBooks && $allCounts['books'] > 10): ?>
<div class="show-all-section">
<p style="margin-bottom: 10px; color: #666;">
已显示最新 10 本书,共 <?= $allCounts['books'] ?> 本
</p>
<a href="?view=<?= $viewMode ?>&show_all_books=1" class="btn btn-outline">
📚 显示全部书籍
</a>
</div>
<?php elseif ($showAllBooks): ?>
<div class="show-all-section">
<p style="margin-bottom: 10px; color: #28a745; font-weight: 600;">
✅ 显示全部 <?= $allCounts['books'] ?> 本书
</p>
<a href="?view=<?= $viewMode ?>" class="btn btn-primary">
↩️ 返回首页(显示最新10条)
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<!-- 电影列 -->
<div class="column">
<div class="column-header">
<h2 class="column-title">
<span style="background: #c2185b; color: white; width: 36px; height: 36px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center;">🎬</span>
电影
</h2>
<div>
<span style="color: #666; font-size: 0.9rem;">
共 <?= $allCounts['movies'] ?> 部
<?php if (!$showAllMovies && $allCounts['movies'] > 10): ?>
(显示最新 10 部)
<?php endif; ?>
</span>
</div>
</div>
<?php if (empty($movies)): ?>
<div class="empty-state">
<div style="font-size: 3rem; color: #ddd; margin-bottom: 15px;">🎬</div>
<p>暂无电影记录</p>
<?php if ($loggedIn): ?>
<p style="color: #666; font-size: 0.9rem; margin-top: 10px;">在下方添加你的第一部电影</p>
<?php endif; ?>
</div>
<?php else: ?>
<?php foreach ($movies as $monthGroup): ?>
<div class="month-group">
<div class="month-header">
📅 <?= $monthGroup['month'] ?>
</div>
<?php if ($viewMode === 'cards'): ?>
<!-- 卡片视图 -->
<div class="items-grid">
<?php foreach ($monthGroup['items'] as $item): ?>
<div class="item-card">
<div class="item-cover movie-cover">
<?php if (!empty($item['uploaded_cover']) || !empty($item['cover_url'])): ?>
<?php $imgSrc = !empty($item['uploaded_cover']) ? $UPLOAD_DIR . $item['uploaded_cover'] : $item['cover_url']; ?>
<img src="<?= htmlspecialchars($imgSrc) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div style="display:none; color: white; font-size: 2rem; width:100%; height:100%; align-items:center; justify-content:center;">🎬</div>
<?php else: ?>
<div style="color: white; font-size: 2rem;">🎬</div>
<?php endif; ?>
</div>
<div class="item-info">
<span class="type-badge movie-badge">电影</span>
<h3 class="item-title"><?= htmlspecialchars($item['title']) ?></h3>
<div class="item-meta">
<span>📅 <?= $item['read_date'] ?></span>
<?php if (!empty($item['uploaded_cover'])): ?>
<span title="本地上传的图片">📤</span>
<?php endif; ?>
</div>
<div class="item-actions">
<?php if (!empty($item['douban_url'])): ?>
<a href="<?= htmlspecialchars($item['douban_url']) ?>"
target="_blank" class="btn btn-primary btn-small">
🔗 豆瓣
</a>
<?php endif; ?>
<?php if ($loggedIn): ?>
<button class="btn btn-danger btn-small"
onclick="if(confirm('确定删除这部电影吗?')) location.href='?action=delete&id=<?= $item['id'] ?>'">
🗑️ 删除
</button>
<?php endif; ?>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<!-- 列表视图 -->
<div class="items-list">
<?php foreach ($monthGroup['items'] as $item): ?>
<div class="list-item movie-item">
<div class="list-item-cover">
<?php if (!empty($item['uploaded_cover']) || !empty($item['cover_url'])): ?>
<?php $imgSrc = !empty($item['uploaded_cover']) ? $UPLOAD_DIR . $item['uploaded_cover'] : $item['cover_url']; ?>
<img src="<?= htmlspecialchars($imgSrc) ?>"
alt="<?= htmlspecialchars($item['title']) ?>"
onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
<div style="display:none; color: #666; font-size: 1.5rem;">🎬</div>
<?php else: ?>
<div style="color: #666; font-size: 1.5rem;">🎬</div>
<?php endif; ?>
</div>
<div class="list-item-info">
<div class="list-item-title" title="<?= htmlspecialchars($item['title']) ?>">
<?= htmlspecialchars($item['title']) ?>
</div>
<div class="list-item-meta">
<span class="list-item-type movie">电影</span>
<span>📅 <?= $item['read_date'] ?></span>
<?php if (!empty($item['uploaded_cover'])): ?>
<span title="本地上传的图片">📤</span>
<?php endif; ?>
</div>
</div>
<div class="list-item-actions">
<?php if (!empty($item['douban_url'])): ?>
<a href="<?= htmlspecialchars($item['douban_url']) ?>"
target="_blank" class="btn btn-primary btn-small">
🔗 豆瓣
</a>
<?php endif; ?>
<?php if ($loggedIn): ?>
<button class="btn btn-danger btn-small"
onclick="if(confirm('确定删除这部电影吗?')) location.href='?action=delete&id=<?= $item['id'] ?>'">
🗑️ 删除
</button>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (!$showAllMovies && $allCounts['movies'] > 10): ?>
<div class="show-all-section">
<p style="margin-bottom: 10px; color: #666;">
已显示最新 10 部电影,共 <?= $allCounts['movies'] ?> 部
</p>
<a href="?view=<?= $viewMode ?>&show_all_movies=1" class="btn btn-outline">
🎬 显示全部电影
</a>
</div>
<?php elseif ($showAllMovies): ?>
<div class="show-all-section">
<p style="margin-bottom: 10px; color: #28a745; font-weight: 600;">
✅ 显示全部 <?= $allCounts['movies'] ?> 部电影
</p>
<a href="?view=<?= $viewMode ?>" class="btn btn-primary">
↩️ 返回首页(显示最新10条)
</a>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
</div>
<?php elseif ($loggedIn && !$showLoginForm): ?>
<!-- 空状态 -->
<div class="empty-state">
<div style="font-size: 4rem; color: #ddd; margin-bottom: 20px;">📚🎬</div>
<h3>书影音空空如也</h3>
<p>开始添加你的第一本书或电影吧!</p>
</div>
<?php endif; ?>
<!-- 添加记录表单(仅管理员可见) -->
<?php if ($loggedIn && !$showLoginForm): ?>
<div class="column" style="margin-top: 30px;">
<h2 class="column-title">➕ 添加新记录</h2>
<form method="POST" enctype="multipart/form-data">
<!-- 类型选择器 -->
<div class="type-selector" id="typeSelector">
<div class="type-option selected" data-type="book">
<div style="font-size: 2rem; margin-bottom: 10px;">📚</div>
<div>书籍</div>
</div>
<div class="type-option" data-type="movie">
<div style="font-size: 2rem; margin-bottom: 10px;">🎬</div>
<div>电影</div>
</div>
</div>
<input type="hidden" name="item_type" id="item_type" value="book">
<div class="form-row">
<div class="form-group">
<label>📖 标题 *</label>
<input type="text" name="title" required placeholder="输入书名或电影名">
</div>
<div class="form-group">
<label>📅 日期</label>
<input type="date" name="read_date" value="<?= date($DATE_FORMAT) ?>">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>🌐 豆瓣链接(可选)</label>
<input type="url" name="douban_url" placeholder="豆瓣链接">
</div>
<div class="form-group">
<label>🖼️ 封面图片URL(可选)</label>
<input type="url" name="cover_url" placeholder="图片URL链接">
</div>
</div>
<!-- 图片上传 -->
<div class="form-group">
<label>📤 上传封面图片(可选,最大2MB)</label>
<div class="upload-area" id="uploadArea">
<input type="file" name="cover_upload" id="cover_upload"
accept="image/jpeg,image/png,image/gif,image/webp"
style="display: none;">
<div style="font-size: 4rem; color: #667eea; margin-bottom: 10px;">
📁
</div>
<p>点击或拖放图片到这里</p>
<p style="color: #666; font-size: 0.9rem; margin-top: 5px;">
支持 JPG, PNG, GIF, WebP 格式
</p>
<img id="uploadPreview" class="upload-preview" alt="预览">
</div>
</div>
<button type="submit" class="btn btn-primary" style="width: 100%;">💾 保存记录</button>
</form>
</div>
<?php endif; ?>
<!-- 页脚统计 -->
<footer>
<div class="stats">
<div class="stat-item">
<div class="stat-number book-stat"><?= $allCounts['books'] ?></div>
<div>📚 书籍总数</div>
</div>
<div class="stat-item">
<div class="stat-number movie-stat"><?= $allCounts['movies'] ?></div>
<div>🎬 电影总数</div>
</div>
<div class="stat-item">
<div class="stat-number" style="color: #667eea;"><?= $allCounts['books'] + $allCounts['movies'] ?></div>
<div>📊 总记录数</div>
</div>
<?php if ($loggedIn): ?>
<div class="stat-item">
<div class="stat-number" style="color: #28a745;"><?= round((time() - $_SESSION['login_time']) / 60) ?></div>
<div>🕒 登录时长(分钟)</div>
</div>
<?php endif; ?>
</div>
<p style="margin-top: 15px; color: #999; font-size: 0.85rem;">
<?= $loggedIn ? '🔐 管理员模式' : '👁️ 公开浏览模式' ?> | 系统版本 5.1 | <?= date('Y') ?> ©
</p>
</footer>
</div>
<!-- JavaScript -->
<script>
// 类型选择器
const typeOptions = document.querySelectorAll('.type-option');
const itemTypeInput = document.getElementById('item_type');
typeOptions.forEach(option => {
option.addEventListener('click', function() {
typeOptions.forEach(opt => opt.classList.remove('selected'));
this.classList.add('selected');
itemTypeInput.value = this.dataset.type;
updateFormPlaceholders();
});
});
// 图片上传交互
const uploadArea = document.getElementById('uploadArea');
if (uploadArea) {
uploadArea.addEventListener('click', function() {
document.getElementById('cover_upload').click();
});
// 拖放上传
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadArea.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadArea.addEventListener(eventName, unhighlight, false);
});
function highlight() {
uploadArea.style.background = '#eef1ff';
uploadArea.style.borderColor = '#764ba2';
}
function unhighlight() {
uploadArea.style.background = '#f8f9ff';
uploadArea.style.borderColor = '#667eea';
}
uploadArea.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
document.getElementById('cover_upload').files = files;
previewImage(files[0]);
}
}
// 文件选择预览
document.getElementById('cover_upload').addEventListener('change', function(e) {
if (this.files.length > 0) {
previewImage(this.files[0]);
}
});
function previewImage(file) {
const preview = document.getElementById('uploadPreview');
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
}
}
// 设置今天为默认日期
const dateInput = document.querySelector('input[name="read_date"]');
if (dateInput) {
dateInput.value = '<?= date($DATE_FORMAT) ?>';
}
// 根据类型修改表单占位符
function updateFormPlaceholders() {
const type = itemTypeInput.value;
const titleInput = document.querySelector('input[name="title"]');
const doubanInput = document.querySelector('input[name="douban_url"]');
if (type === 'book') {
titleInput.placeholder = '输入书名';
doubanInput.placeholder = 'https://book.douban.com/subject/...';
} else {
titleInput.placeholder = '输入电影名';
doubanInput.placeholder = 'https://movie.douban.com/subject/...';
}
}
// 初始化
updateFormPlaceholders();
</script>
</body>
</html>
放在支持php的web服务上,登录密码admin。
书影音记录条目:名称,豆瓣链接,图片(可从豆瓣下载,不能直接引用,有防盗链)。
体验
卡片模式

列表模式

添加条目


我的书影音
代码由deepseek生成。对个人来说已经够用了。