9cd2b31648
- 在bookings.php中添加转换为VIP客户的按钮 - 在vip.php中实现从预约记录转换VIP客户的逻辑 - 添加VIP客户转换界面和批量转换功能 - 新增get_package.php用于获取套餐信息 - 优化会员类型自动转换逻辑
1314 lines
48 KiB
PHP
1314 lines
48 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>
|
|
</head>
|
|
<body>
|
|
<div class="packages-container">
|
|
<header class="header">
|
|
<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>
|
|
|
|
<?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()">×</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>
|