This repository has been archived on 2026-06-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
carwash_order/packages.php
T
wsh5485 c64651d6c7 feat(移动端): 添加移动端导航菜单并优化响应式设计
refactor(预约统计): 修改查询逻辑只计算有效预约
2025-12-12 03:39:01 +08:00

1331 lines
49 KiB
PHP

<?php
require_once 'db_connect.php';
$message = '';
$success_message = '';
// 处理套餐操作
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
try {
if ($action === 'add') {
$package_name = trim($_POST['package_name']);
$description = trim($_POST['description']);
$base_duration = (int)$_POST['base_duration'];
$price = (float)$_POST['price'];
$services = implode(',', array_filter(array_map('trim', $_POST['services'] ?? [])));
$package_reminder = trim($_POST['package_reminder']);
if (empty($package_name)) {
throw new Exception('套餐名称不能为空');
}
if ($base_duration <= 0) {
throw new Exception('基础时长必须大于0');
}
if ($price < 0) {
throw new Exception('价格不能为负数');
}
$stmt = $pdo->prepare("INSERT INTO packages (package_name, description, base_duration, price, services, package_reminder) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$package_name, $description, $base_duration, $price, $services, $package_reminder]);
$success_message = "套餐添加成功!";
} elseif ($action === 'update') {
$id = (int)$_POST['id'];
$package_name = trim($_POST['package_name']);
$description = trim($_POST['description']);
$base_duration = (int)$_POST['base_duration'];
$price = (float)$_POST['price'];
$services = implode(',', array_filter(array_map('trim', $_POST['services'] ?? [])));
$is_active = isset($_POST['is_active']) ? 1 : 0;
if (empty($package_name)) {
throw new Exception('套餐名称不能为空');
}
if ($base_duration <= 0) {
throw new Exception('基础时长必须大于0');
}
if ($price < 0) {
throw new Exception('价格不能为负数');
}
// 获取当前套餐的专属预约信息,避免更新时丢失
$stmt = $pdo->prepare("SELECT package_reminder FROM packages WHERE id = ?");
$stmt->execute([$id]);
$current_package = $stmt->fetch();
$package_reminder = $current_package['package_reminder'] ?? '';
// 更新套餐信息,保留现有的package_reminder
$stmt = $pdo->prepare("UPDATE packages SET package_name = ?, description = ?, base_duration = ?, price = ?, services = ?, package_reminder = ?, is_active = ? WHERE id = ?");
$stmt->execute([$package_name, $description, $base_duration, $price, $services, $package_reminder, $is_active, $id]);
$success_message = "套餐更新成功!";
} elseif ($action === 'delete') {
$id = (int)$_POST['id'];
// 检查是否有预约使用此套餐
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings WHERE package_id = ?");
$stmt->execute([$id]);
$booking_count = $stmt->fetchColumn();
if ($booking_count > 0) {
throw new Exception("无法删除此套餐,因为有 {$booking_count} 个预约正在使用此套餐。建议禁用此套餐而不是删除。");
}
$stmt = $pdo->prepare("DELETE FROM packages WHERE id = ?");
$stmt->execute([$id]);
$success_message = "套餐删除成功!";
} elseif ($action === 'update_reminder') {
// 处理套餐专属预约信息的单独更新
$id = (int)$_POST['id'];
$package_reminder = trim($_POST['package_reminder']);
$stmt = $pdo->prepare("UPDATE packages SET package_reminder = ? WHERE id = ?");
$stmt->execute([$package_reminder, $id]);
// 返回JSON响应
header('Content-Type: application/json');
echo json_encode(['success' => true, 'message' => '套餐专属预约信息更新成功!'], JSON_UNESCAPED_UNICODE);
exit;
} elseif ($action === 'toggle_active') {
// 快速切换启用/禁用状态
$id = (int)$_POST['id'];
$stmt = $pdo->prepare("UPDATE packages SET is_active = NOT is_active WHERE id = ?");
$stmt->execute([$id]);
header('Content-Type: application/json');
echo json_encode(['success' => true, 'message' => '状态更新成功!'], JSON_UNESCAPED_UNICODE);
exit;
}
} catch (Exception $e) {
$message = "操作失败:" . $e->getMessage();
}
}
// 获取搜索和筛选参数
$search = isset($_GET['search']) ? trim($_GET['search']) : '';
$status_filter = isset($_GET['status']) ? $_GET['status'] : 'all'; // all, active, inactive
$sort_by = isset($_GET['sort']) ? $_GET['sort'] : 'created_at'; // created_at, price, duration, name
$sort_order = isset($_GET['order']) ? $_GET['order'] : 'desc'; // asc, desc
// 构建查询
$query = "SELECT p.*,
COUNT(b.id) as booking_count,
SUM(CASE WHEN b.status NOT IN ('已完成', '已取消') THEN 1 ELSE 0 END) as active_booking_count
FROM packages p
LEFT JOIN bookings b ON p.id = b.package_id
WHERE 1=1";
$params = [];
// 搜索条件
if (!empty($search)) {
$query .= " AND (p.package_name LIKE ? OR p.description LIKE ?)";
$search_param = "%{$search}%";
$params[] = $search_param;
$params[] = $search_param;
}
// 状态筛选
if ($status_filter === 'active') {
$query .= " AND p.is_active = 1";
} elseif ($status_filter === 'inactive') {
$query .= " AND p.is_active = 0";
}
$query .= " GROUP BY p.id";
// 排序
$allowed_sorts = ['created_at', 'price', 'base_duration', 'package_name', 'booking_count'];
$sort_by = in_array($sort_by, $allowed_sorts) ? $sort_by : 'created_at';
$sort_order = in_array($sort_order, ['asc', 'desc']) ? $sort_order : 'desc';
if ($sort_by === 'package_name') {
$query .= " ORDER BY p.package_name " . strtoupper($sort_order);
} elseif ($sort_by === 'booking_count') {
$query .= " ORDER BY booking_count " . strtoupper($sort_order) . ", p.created_at DESC";
} else {
$query .= " ORDER BY p.{$sort_by} " . strtoupper($sort_order);
}
$stmt = $pdo->prepare($query);
$stmt->execute($params);
$packages = $stmt->fetchAll();
// 统计信息
$total_packages = count($packages);
$active_packages = count(array_filter($packages, function($p) { return $p['is_active']; }));
$inactive_packages = $total_packages - $active_packages;
$total_bookings = array_sum(array_column($packages, 'booking_count'));
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>张老师撸车(私家车库)工作室 - 套餐管理</title>
<link rel="stylesheet" href="style.css">
<style>
/* 套餐管理页面优化样式 */
.packages-container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
/* 统计卡片 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
}
.stat-card.active {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.inactive {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.stat-card.bookings {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.stat-card .stat-label {
font-size: 14px;
opacity: 0.9;
margin-bottom: 8px;
}
.stat-card .stat-value {
font-size: 32px;
font-weight: bold;
margin-bottom: 4px;
}
.stat-card .stat-unit {
font-size: 14px;
opacity: 0.8;
}
/* 搜索和筛选区域 */
.filter-section {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 24px;
}
.filter-row {
display: flex;
gap: 16px;
align-items: center;
flex-wrap: wrap;
}
.search-box {
flex: 1;
min-width: 200px;
position: relative;
}
.search-box input {
width: 100%;
padding: 12px 16px 12px 40px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.search-box input:focus {
outline: none;
border-color: #409EFF;
}
.search-box::before {
content: '🔍';
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
font-size: 18px;
}
.filter-group {
display: flex;
gap: 8px;
align-items: center;
}
.filter-group label {
font-weight: 600;
color: #333;
white-space: nowrap;
}
.filter-group select {
padding: 10px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
background: white;
cursor: pointer;
transition: border-color 0.3s;
}
.filter-group select:focus {
outline: none;
border-color: #409EFF;
}
/* 套餐卡片优化 */
.packages-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-top: 24px;
}
.package-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.package-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #409EFF, #67C23A);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.package-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.package-card:hover::before {
transform: scaleX(1);
}
.package-card.inactive {
opacity: 0.7;
background: #f5f5f5;
}
.package-card.inactive::before {
background: linear-gradient(90deg, #909399, #C0C4CC);
}
.package-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 2px solid #f0f0f0;
}
.package-title {
font-size: 20px;
font-weight: bold;
color: #303133;
margin: 0;
flex: 1;
}
.package-status {
display: flex;
gap: 8px;
align-items: center;
}
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
}
.status-badge.active {
background: #e1f3d8;
color: #67C23A;
}
.status-badge.inactive {
background: #fef0f0;
color: #F56C6C;
}
.booking-badge {
background: #e6f7ff;
color: #409EFF;
padding: 4px 10px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
}
.package-description {
color: #606266;
font-size: 14px;
line-height: 1.6;
margin-bottom: 16px;
min-height: 44px;
}
.package-details {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.detail-item {
display: flex;
flex-direction: column;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
}
.detail-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.detail-value {
font-size: 18px;
font-weight: bold;
color: #303133;
}
.detail-value.price {
color: #F56C6C;
}
.package-services {
margin-bottom: 16px;
}
.services-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.service-tag {
display: inline-block;
padding: 6px 12px;
background: #e6f7ff;
color: #409EFF;
border-radius: 16px;
font-size: 12px;
font-weight: 500;
}
.package-reminder {
margin-top: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #409EFF;
}
.reminder-editor {
width: 100%;
padding: 12px;
background: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
line-height: 1.5;
min-height: 80px;
resize: vertical;
font-family: inherit;
}
.reminder-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
.package-actions {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.package-actions button,
.package-actions form {
flex: 1;
}
.btn-toggle-status {
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-toggle-status.active {
background: #67C23A;
color: white;
}
.btn-toggle-status.inactive {
background: #F56C6C;
color: white;
}
.btn-toggle-status:hover {
opacity: 0.9;
transform: scale(1.05);
}
/* 编辑模态框 */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
overflow-y: auto;
padding: 20px;
}
.modal-content {
background: white;
margin: 20px auto;
padding: 30px;
border-radius: 12px;
max-width: 700px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 2px solid #f0f0f0;
}
.modal-header h3 {
margin: 0;
font-size: 24px;
color: #303133;
}
.close-modal {
font-size: 28px;
color: #909399;
cursor: pointer;
transition: color 0.3s;
line-height: 1;
}
.close-modal:hover {
color: #303133;
}
/* 表单样式优化 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
font-size: 14px;
}
.form-group label.required::after {
content: ' *';
color: #F56C6C;
}
.form-control {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
font-family: inherit;
}
.form-control:focus {
outline: none;
border-color: #409EFF;
}
.input-group {
display: flex;
align-items: stretch;
}
.input-group-addon {
padding: 12px 16px;
background: #f5f5f5;
border: 2px solid #e0e0e0;
display: flex;
align-items: center;
font-size: 14px;
color: #606266;
}
.input-group .form-control:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-right: none;
}
.input-group-addon:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: none;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.services-container {
margin-bottom: 12px;
}
.service-item {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.btn-outline {
padding: 8px 16px;
background: white;
color: #409EFF;
border: 2px solid #409EFF;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-outline:hover {
background: #409EFF;
color: white;
}
.btn-primary {
padding: 10px 20px;
background: #409EFF;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary:hover {
background: #66B1FF;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
}
.btn-danger {
padding: 8px 16px;
background: #F56C6C;
color: white;
border: none;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-danger:hover {
background: #f78989;
}
.btn-sm {
padding: 6px 12px;
font-size: 12px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #909399;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-message {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
color: #606266;
}
.empty-submessage {
font-size: 14px;
color: #909399;
}
/* 响应式设计 */
@media (max-width: 768px) {
.packages-grid {
grid-template-columns: 1fr;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
.filter-row {
flex-direction: column;
}
.search-box,
.filter-group {
width: 100%;
}
.modal-content {
margin: 10px;
padding: 20px;
}
}
/* 折叠表单 */
.collapsible-form {
margin-bottom: 24px;
}
.form-toggle {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 0;
}
.form-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
}
.form-toggle h2 {
margin: 0;
font-size: 18px;
}
.form-toggle-icon {
font-size: 20px;
transition: transform 0.3s;
}
.form-toggle.active .form-toggle-icon {
transform: rotate(180deg);
}
.form-content {
display: none;
background: white;
padding: 24px;
border-radius: 0 0 12px 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.form-content.active {
display: block;
animation: slideDown 0.3s ease;
}
</style>
<script src="mobile-nav.js" defer></script>
</head>
<body>
<div class="packages-container">
<header class="header">
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()" aria-label="菜单">
<span></span>
<span></span>
<span></span>
</button>
<h1>🚗 张老师撸车工作室 - 套餐管理</h1>
<nav class="nav">
<a href="index.php" class="nav-link">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
<a href="packages.php" class="nav-link active">套餐管理</a>
<a href="vip.php" class="nav-link">VIP管理</a>
<a href="announcement.php" class="nav-link">今日待办</a>
</nav>
</header>
<!-- 移动端导航菜单 -->
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
<nav class="mobile-nav">
<a href="index.php" class="nav-link">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="pending_bookings.php" class="nav-link">待处理预约</a>
<a href="packages.php" class="nav-link active">套餐管理</a>
<a href="vip.php" class="nav-link">VIP管理</a>
<a href="announcement.php" class="nav-link">今日待办</a>
</nav>
<?php if ($message): ?>
<div class="message error-message" style="background-color: #fee; color: #c33; border-color: #fcc; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px;">
<?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<?php if ($success_message): ?>
<div class="message success-message" style="background-color: #d4edda; color: #155724; border-color: #c3e6cb; padding: 12px 16px; border-radius: 8px; margin-bottom: 20px;">
<?= htmlspecialchars($success_message) ?>
</div>
<?php endif; ?>
<!-- 统计信息 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-label">总套餐数</div>
<div class="stat-value"><?= $total_packages ?></div>
<div class="stat-unit">个套餐</div>
</div>
<div class="stat-card active">
<div class="stat-label">启用中</div>
<div class="stat-value"><?= $active_packages ?></div>
<div class="stat-unit">个套餐</div>
</div>
<div class="stat-card inactive">
<div class="stat-label">已禁用</div>
<div class="stat-value"><?= $inactive_packages ?></div>
<div class="stat-unit">个套餐</div>
</div>
<div class="stat-card bookings">
<div class="stat-label">总预约数</div>
<div class="stat-value"><?= $total_bookings ?></div>
<div class="stat-unit">次预约</div>
</div>
</div>
<!-- 搜索和筛选 -->
<div class="filter-section">
<form method="GET" class="filter-row">
<div class="search-box">
<input type="text" name="search" placeholder="搜索套餐名称或描述..." value="<?= htmlspecialchars($search) ?>">
</div>
<div class="filter-group">
<label>状态:</label>
<select name="status" onchange="this.form.submit()">
<option value="all" <?= $status_filter === 'all' ? 'selected' : '' ?>>全部</option>
<option value="active" <?= $status_filter === 'active' ? 'selected' : '' ?>>启用</option>
<option value="inactive" <?= $status_filter === 'inactive' ? 'selected' : '' ?>>禁用</option>
</select>
</div>
<div class="filter-group">
<label>排序:</label>
<select name="sort" onchange="this.form.submit()">
<option value="created_at" <?= $sort_by === 'created_at' ? 'selected' : '' ?>>创建时间</option>
<option value="package_name" <?= $sort_by === 'package_name' ? 'selected' : '' ?>>套餐名称</option>
<option value="price" <?= $sort_by === 'price' ? 'selected' : '' ?>>价格</option>
<option value="base_duration" <?= $sort_by === 'base_duration' ? 'selected' : '' ?>>时长</option>
<option value="booking_count" <?= $sort_by === 'booking_count' ? 'selected' : '' ?>>预约次数</option>
</select>
</div>
<div class="filter-group">
<label>顺序:</label>
<select name="order" onchange="this.form.submit()">
<option value="desc" <?= $sort_order === 'desc' ? 'selected' : '' ?>>降序</option>
<option value="asc" <?= $sort_order === 'asc' ? 'selected' : '' ?>>升序</option>
</select>
</div>
<?php if (!empty($search) || $status_filter !== 'all'): ?>
<a href="packages.php" class="btn-outline" style="text-decoration: none; display: inline-flex; align-items: center;">重置</a>
<?php endif; ?>
</form>
</div>
<!-- 添加套餐表单(可折叠) -->
<div class="collapsible-form">
<div class="form-toggle" onclick="toggleAddForm()">
<h2>📋 添加新套餐</h2>
<span class="form-toggle-icon">▼</span>
</div>
<div class="form-content" id="addFormContent">
<form method="POST" class="package-form">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label for="package_name" class="required">套餐名称</label>
<input type="text" id="package_name" name="package_name" required placeholder="如:标准洗车套餐" class="form-control">
</div>
<div class="form-group">
<label for="description">套餐描述</label>
<textarea id="description" name="description" rows="3" placeholder="详细描述套餐内容和特点" class="form-control"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="base_duration" class="required">基础时长</label>
<div class="input-group">
<input type="number" id="base_duration" name="base_duration" min="15" step="15" value="60" required class="form-control">
<span class="input-group-addon">分钟</span>
</div>
</div>
<div class="form-group">
<label for="price" class="required">价格</label>
<div class="input-group">
<span class="input-group-addon">¥</span>
<input type="number" id="price" name="price" min="0" step="0.01" required placeholder="0.00" class="form-control">
</div>
</div>
</div>
<div class="form-group">
<label>服务项目</label>
<div class="services-container" id="addServicesContainer">
<div class="service-item">
<input type="text" name="services[]" placeholder="如:外观清洗" value="外观清洗" class="form-control" required>
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)">删除</button>
</div>
<div class="service-item">
<input type="text" name="services[]" placeholder="如:内饰清洁" value="内饰清洁" class="form-control" required>
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)">删除</button>
</div>
</div>
<button type="button" class="btn-outline btn-sm" onclick="addService('addServicesContainer')">+ 添加服务项目</button>
</div>
<div class="form-group">
<label for="package_reminder">套餐专属预约信息</label>
<textarea id="package_reminder" name="package_reminder" rows="4" placeholder="输入此套餐的专属预约信息,将在预约确认时显示" class="form-control"></textarea>
</div>
<div class="form-actions" style="display: flex; justify-content: flex-end; gap: 12px; margin-top: 24px;">
<button type="button" class="btn-outline" onclick="toggleAddForm()">取消</button>
<button type="submit" class="btn-primary">✨ 添加套餐</button>
</div>
</form>
</div>
</div>
<!-- 套餐列表 -->
<div class="packages-list">
<h2 style="margin-bottom: 20px; color: #303133;">📊 套餐列表 (<?= $total_packages ?> 个)</h2>
<?php if (empty($packages)): ?>
<div class="empty-state">
<div class="empty-icon">📦</div>
<p class="empty-message">暂无套餐数据</p>
<p class="empty-submessage">点击上方"添加新套餐"创建第一个套餐吧!</p>
</div>
<?php else: ?>
<div class="packages-grid">
<?php foreach ($packages as $package): ?>
<div class="package-card <?= $package['is_active'] ? '' : 'inactive' ?>" data-package-id="<?= $package['id'] ?>">
<div class="package-header">
<h3 class="package-title"><?= htmlspecialchars($package['package_name']) ?></h3>
<div class="package-status">
<?php if ($package['booking_count'] > 0): ?>
<span class="booking-badge"><?= $package['booking_count'] ?>次预约</span>
<?php endif; ?>
<span class="status-badge <?= $package['is_active'] ? 'active' : 'inactive' ?>">
<?= $package['is_active'] ? '✓ 启用' : '✗ 禁用' ?>
</span>
</div>
</div>
<?php if ($package['description']): ?>
<p class="package-description"><?= htmlspecialchars($package['description']) ?></p>
<?php endif; ?>
<div class="package-details">
<div class="detail-item">
<span class="detail-label">⏱️ 时长</span>
<span class="detail-value"><?= $package['base_duration'] ?>分钟</span>
</div>
<div class="detail-item">
<span class="detail-label">💰 价格</span>
<span class="detail-value price">¥<?= number_format($package['price'], 2) ?></span>
</div>
</div>
<?php
$services = explode(',', $package['services']);
if ($services && !empty(trim($services[0]))):
?>
<div class="package-services">
<span class="detail-label" style="display: block; margin-bottom: 8px; font-weight: 600; color: #606266;">✨ 包含服务</span>
<div class="services-tags">
<?php foreach ($services as $service): ?>
<?php if (trim($service)): ?>
<span class="service-tag"><?= htmlspecialchars(trim($service)) ?></span>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="package-reminder">
<div class="detail-label" style="font-weight: 600; margin-bottom: 8px; color: #333;">📝 套餐专属预约信息</div>
<textarea class="reminder-editor" data-package-id="<?= $package['id'] ?>" oninput="autoResizeTextarea(this)" placeholder="输入此套餐的专属预约信息,将在预约确认时显示"><?= htmlspecialchars($package['package_reminder'] ?? '') ?></textarea>
<div class="reminder-actions">
<button type="button" class="btn-primary btn-sm" onclick="saveReminder(this)">💾 保存</button>
<button type="button" class="btn-outline btn-sm" onclick="cancelReminderEdit(this)">取消</button>
</div>
</div>
<div class="package-actions">
<button type="button" class="btn-primary btn-sm" onclick="editPackage(<?= $package['id'] ?>)">✏️ 编辑</button>
<button type="button" class="btn-toggle-status <?= $package['is_active'] ? 'active' : 'inactive' ?>"
onclick="togglePackageStatus(<?= $package['id'] ?>, this)">
<?= $package['is_active'] ? '禁用' : '启用' ?>
</button>
<form method="POST" style="display: inline;" onsubmit="return confirmDelete(<?= $package['booking_count'] ?>)">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $package['id'] ?>">
<button type="submit" class="btn-danger btn-sm">🗑️ 删除</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- 编辑套餐模态框 -->
<div id="editModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>✏️ 编辑套餐</h3>
<span class="close-modal" onclick="closeEditModal()">&times;</span>
</div>
<form method="POST" id="editForm">
<input type="hidden" name="action" value="update">
<input type="hidden" name="id" id="edit_package_id">
<div class="form-group">
<label class="required">套餐名称</label>
<input type="text" name="package_name" id="edit_package_name" required placeholder="请输入套餐名称" class="form-control">
</div>
<div class="form-group">
<label>套餐描述</label>
<textarea name="description" id="edit_description" rows="3" placeholder="请输入套餐描述" class="form-control"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label class="required">基础时长</label>
<div class="input-group">
<input type="number" name="base_duration" id="edit_base_duration" min="15" step="15" required class="form-control">
<span class="input-group-addon">分钟</span>
</div>
</div>
<div class="form-group">
<label class="required">价格</label>
<div class="input-group">
<span class="input-group-addon">¥</span>
<input type="number" name="price" id="edit_price" min="0" step="0.01" required placeholder="0.00" class="form-control">
</div>
</div>
</div>
<div class="form-group">
<label>服务项目</label>
<div class="services-container" id="editServicesContainer">
<!-- 动态生成 -->
</div>
<button type="button" class="btn-outline btn-sm" onclick="addService('editServicesContainer')">+ 添加服务项目</button>
</div>
<div class="form-group">
<label class="checkbox-label" style="display: flex; align-items: center; cursor: pointer;">
<input type="checkbox" name="is_active" id="edit_is_active" style="margin-right: 8px; width: auto;">
<span style="font-weight: normal; color: #333;">启用此套餐</span>
</label>
</div>
<div class="form-actions" style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">
<button type="button" class="btn-outline" onclick="closeEditModal()">取消</button>
<button type="submit" class="btn-primary">💾 保存更改</button>
</div>
</form>
</div>
</div>
<script>
// 切换添加表单显示/隐藏
function toggleAddForm() {
const content = document.getElementById('addFormContent');
const toggle = document.querySelector('.form-toggle');
const isActive = content.classList.contains('active');
if (isActive) {
content.classList.remove('active');
toggle.classList.remove('active');
} else {
content.classList.add('active');
toggle.classList.add('active');
}
}
// 添加服务项目
function addService(containerId) {
const container = document.getElementById(containerId);
const serviceItem = document.createElement('div');
serviceItem.className = 'service-item';
serviceItem.innerHTML = `
<input type="text" name="services[]" placeholder="如:外观清洗" class="form-control" required>
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)">删除</button>
`;
container.appendChild(serviceItem);
}
// 删除服务项目
function removeService(button) {
const container = button.closest('.services-container');
const items = container.querySelectorAll('.service-item');
if (items.length > 1) {
button.parentElement.remove();
} else {
alert('至少需要保留一个服务项目');
}
}
// 编辑套餐
function editPackage(id) {
// 获取套餐数据(从页面中提取)
const packageCard = document.querySelector(`[data-package-id="${id}"]`);
if (!packageCard) return;
// 填充表单数据
document.getElementById('edit_package_id').value = id;
// 从页面获取数据(需要从PHP渲染的数据中获取)
// 这里使用AJAX获取完整数据
fetch(`get_package.php?id=${id}`)
.then(response => response.json())
.then(data => {
document.getElementById('edit_package_name').value = data.package_name || '';
document.getElementById('edit_description').value = data.description || '';
document.getElementById('edit_base_duration').value = data.base_duration || 60;
document.getElementById('edit_price').value = data.price || 0;
document.getElementById('edit_is_active').checked = data.is_active == 1;
// 填充服务项目
const servicesContainer = document.getElementById('editServicesContainer');
servicesContainer.innerHTML = '';
const services = (data.services || '').split(',');
services.forEach(service => {
if (trim(service)) {
const serviceItem = document.createElement('div');
serviceItem.className = 'service-item';
serviceItem.innerHTML = `
<input type="text" name="services[]" value="${escapeHtml(trim(service))}" class="form-control" required>
<button type="button" class="btn-danger btn-sm" onclick="removeService(this)">删除</button>
`;
servicesContainer.appendChild(serviceItem);
}
});
// 如果没有服务项目,添加一个空项
if (servicesContainer.children.length === 0) {
addService('editServicesContainer');
}
// 显示模态框
document.getElementById('editModal').style.display = 'block';
})
.catch(error => {
console.error('获取套餐数据失败:', error);
alert('获取套餐数据失败,请刷新页面重试');
});
}
// 关闭编辑模态框
function closeEditModal() {
document.getElementById('editModal').style.display = 'none';
}
// 点击模态框外部关闭
window.onclick = function(event) {
const modal = document.getElementById('editModal');
if (event.target === modal) {
closeEditModal();
}
}
// 切换套餐启用/禁用状态
function togglePackageStatus(id, button) {
const formData = new FormData();
formData.append('action', 'toggle_active');
formData.append('id', id);
fetch('packages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
// 刷新页面以更新状态
window.location.reload();
} else {
alert(data.message || '操作失败');
}
})
.catch(error => {
console.error('切换状态失败:', error);
alert('操作失败,请重试');
});
}
// 确认删除
function confirmDelete(bookingCount) {
if (bookingCount > 0) {
return confirm(`此套餐有 ${bookingCount} 个预约记录,删除后这些记录将无法显示套餐信息。\n\n确定要删除吗?建议禁用套餐而不是删除。`);
}
return confirm('确定要删除这个套餐吗?此操作不可恢复。');
}
// 保存套餐专属预约信息
function saveReminder(button) {
const textarea = button.closest('.package-reminder').querySelector('.reminder-editor');
const packageId = textarea.dataset.packageId;
const reminderText = textarea.value.trim();
const formData = new FormData();
formData.append('action', 'update_reminder');
formData.append('id', packageId);
formData.append('package_reminder', reminderText);
fetch('packages.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('保存成功!');
} else {
alert(data.message || '保存失败,请重试!');
}
})
.catch(error => {
console.error('保存失败:', error);
alert('保存失败,请重试!');
});
}
// 取消编辑提醒
function cancelReminderEdit(button) {
const textarea = button.closest('.package-reminder').querySelector('.reminder-editor');
textarea.blur();
}
// 自适应文本框高度
function autoResizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight + 10, 500) + 'px';
}
// 工具函数
function trim(str) {
return str ? str.trim() : '';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', function() {
const textareas = document.querySelectorAll('.reminder-editor');
textareas.forEach(autoResizeTextarea);
});
</script>
</body>
</html>