feat: 重构洗车预约系统,新增套餐管理和时间段选择功能

- 新增套餐管理模块,支持套餐的增删改查
- 重构预约表结构,支持时间段选择和套餐关联
- 实现日历视图和时间段网格选择界面
- 更新数据库结构,添加套餐表和相关字段
- 优化移动端体验,增强触摸交互
- 更新文档和样式,匹配新功能
This commit is contained in:
2025-11-19 01:06:35 +08:00
parent 0fadca8ca0
commit 8ffad04df3
5 changed files with 1260 additions and 270 deletions
+52 -101
View File
@@ -1,143 +1,94 @@
# 洗车预约系统
# 🚗 洗车预约系统
一个简单实用的PHP洗车预约管理系统,支持在线预约和预约管理功能
现代化洗车预约管理系统,采用日历时间段选择模式,支持套餐管理和移动端优化
## 功能特性
## 功能特性
### 客户功能
- 📝 在线提交洗车预约
- 📱 填写客户信息(姓名、电话、车型、车牌号)
- 🛁 选择服务类型(普通洗车、精洗、打蜡、内饰清洁)
- 📅 选择预约日期和时间
- 💬 添加备注信息
- ✅ 防重复预约检查
- 📅 **日历选择** - 直观的周历显示和时间段网格
- **快捷预约** - 1-4小时快捷时长选择,支持自定义
- 💼 **套餐管理** - 灵活的套餐配置和价格管理
- 📱 **移动优化** - 完美响应式设计,触摸友好
- 🔧 **管理功能** - 预约状态管理和时间冲突检查
### 管理功能
- 📋 查看所有预约记录
- 🔄 更新预约状态(待确认→已确认→已完成)
- ❌ 取消预约功能
- 📊 预约统计显示
- 🎯 状态颜色标识
## 技术栈
- **后端**: PHP 7.4+
- **数据库**: MySQL 5.7+
## 🛠️ 技术栈
- **后端**: PHP 7.4+ | MySQL 5.7+
- **前端**: HTML5 + CSS3 + JavaScript
- **响应式设计**: 移动端自适应
- **数据库操作**: PDO
- **数据库**: PDO + MySQL
## 移动端优化特性
- 📱 **响应式设计**: 完美适配手机、平板、桌面设备
- 👆 **触摸优化**: 针对移动设备优化的触摸交互
- 🔍 **防缩放**: 防止意外的双击缩放
- ⌨️ **输入优化**: 针对移动端键盘类型的自动适配
- 🎨 **界面适配**: 小屏幕下的布局优化
- 🚫 **触摸反馈**: 按钮点击的视觉反馈效果
- 📋 **表单体验**: 移动端友好的表单交互
## 文件结构
## 📁 文件结构
```
carwash_order/
├── index.php # 预约首页
├── bookings.php # 预约管理页面
├── bookings.php # 预约管理
├── packages.php # 套餐管理
├── config.php # 数据库配置
├── db_connect.php # 数据库连接
├── carwash_db.sql # 数据库结构
── style.css # 样式文件
└── README.md # 说明文档
── style.css # 样式文件
```
## 安装说明
## 🚀 快速开始
### 1. 环境要求
- PHP 7.4 或更高版本
- MySQL 5.7 或更高版本
- Web服务器 (Apache/Nginx)
- PHP 7.4+ | MySQL 5.7+ | Web服务器
### 2. 数据库配置
1. 创建MySQL数据库
2. 导入数据库结构:
```bash
mysql -u root -p < carwash_db.sql
```
### 3. 修改配置文件
编辑 `config.php` 文件,修改数据库连接信息:
```php
$host = 'localhost'; // 数据库主机
$username = 'root'; // 数据库用户名
$password = ''; // 数据库密码
$database = 'carwash_booking'; // 数据库名
```
修改 `config.php` 数据库连接信息。
### 4. 运行系统
1. 将所有文件放在Web服务器根目录
2. 访问 `index.php` 开始使用系统
### 3. 访问系统
访问 `index.php` 开始预约,`packages.php` 管理套餐。
## 使用指南
## 📖 使用指南
### 客户预约流程
1. 访问首页,填写完整的预约信息
2. 选择服务类型和预约时间
3. 提交表单后系统会检查时间冲突
4. 成功提交后会显示确认信息
1. **选择日期** → 日历点击选择
2. **选择时间段** → 8:00-18:00时间段
3. **选择时长** → 快捷按钮或自定义
4. **选择套餐** → 从可用套餐选择
5. **填写信息** → 完善客户信息
6. **提交预约** → 自动检查冲突
### 管理员操作
1. 访问 `bookings.php` 查看所有预约
2. 可以更新预约状态:
- **已确认**: 联系客户确认预约
- **已完成**: 洗车服务已完成
- **已取消**: 取消该预约
- **查看预约** → `bookings.php`
- **管理套餐** → `packages.php`
- **更新状态** → 待确认→已确认→已完成→已取消
## 数据库结构
## 🗄️ 数据库结构
### packages 表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INT | 主键 |
| name | VARCHAR(100) | 套餐名称 |
| description | TEXT | 套餐描述 |
| base_duration | INT | 基础时长(分钟) |
| base_price | DECIMAL(10,2) | 基础价格 |
| services | TEXT | 服务项目 |
| is_active | BOOLEAN | 是否启用 |
### bookings 表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | INT | 主键,自增 |
| id | INT | 主键 |
| customer_name | VARCHAR(100) | 客户姓名 |
| phone | VARCHAR(20) | 联系电话 |
| car_model | VARCHAR(50) | 车型 |
| car_number | VARCHAR(20) | 车牌号 |
| service_type | ENUM | 服务类型 |
| appointment_date | DATE | 预约日期 |
| appointment_time | TIME | 预约时间 |
| notes | TEXT | 备注信息 |
| package_id | INT | 套餐ID |
| start_time | DATETIME | 开始时间 |
| duration | INT | 时长(分钟) |
| status | ENUM | 预约状态 |
| created_at | TIMESTAMP | 创建时间 |
## 服务类型和价格
- **普通洗车**: ¥30
- **精洗**: ¥80
- **打蜡**: ¥120
- **内饰清洁**: ¥60
## 📋 预约状态
- **待确认** 🔵 | **已确认** 🟢 | **已完成** 🟦 | **已取消** 🔴
## 状态说明
- **待确认**: 新提交的预约,等待管理员确认
- **已确认**: 管理员已确认,可以提供服务
- **已完成**: 服务已完成
- **已取消**: 预约已取消
## 🔧 常见问题
## 扩展功能建议
如需添加更多功能,可以考虑:
- 用户注册登录系统
- 在线支付功能
- 短信/邮件通知
- 预约提醒功能
- 服务评价系统
- 数据统计报表
## 故障排除
### 数据库连接失败
1. 检查 `config.php` 中的数据库配置
2. 确认MySQL服务正在运行
3. 检查数据库用户权限
### 页面显示异常
1. 确认PHP版本兼容
2. 检查Web服务器配置
3. 查看PHP错误日志
**数据库连接失败** → 检查 config.php 配置和MySQL服务
**页面显示异常** → 确认PHP版本和Web服务器配置
**日历功能异常** → 检查JavaScript支持和套餐数据
## 许可证
本项目基于MIT许可证开源。
+35 -9
View File
@@ -2,21 +2,47 @@
CREATE DATABASE IF NOT EXISTS carwash_booking;
USE carwash_booking;
-- 创建套餐表
CREATE TABLE IF NOT EXISTS packages (
id INT AUTO_INCREMENT PRIMARY KEY,
package_name VARCHAR(100) NOT NULL,
description TEXT,
base_duration INT NOT NULL COMMENT '基础服务时长(分钟)',
price DECIMAL(10,2) NOT NULL,
services JSON NOT NULL COMMENT '包含的服务项目',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 修改预约表支持时间段
DROP TABLE IF EXISTS bookings;
CREATE TABLE IF NOT EXISTS bookings (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_name VARCHAR(100) NOT NULL,
phone VARCHAR(20) NOT NULL,
car_model VARCHAR(50) NOT NULL,
car_number VARCHAR(20) NOT NULL,
service_type ENUM('普通洗车', '精洗', '打蜡', '内饰清洁') NOT NULL,
appointment_date DATE NOT NULL,
appointment_time TIME NOT NULL,
package_id INT,
custom_services TEXT COMMENT '自定义服务内容',
start_time DATETIME NOT NULL,
end_time DATETIME NOT NULL,
duration INT NOT NULL COMMENT '实际服务时长(分钟)',
total_price DECIMAL(10,2) NOT NULL,
notes TEXT,
status ENUM('待确认', '已确认', '已完成', '已取消') DEFAULT '待确认',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
status ENUM('待确认', '已确认', '进行中', '已完成', '已取消') DEFAULT '待确认',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE SET NULL
);
-- 插入一些示例数据
INSERT INTO bookings (customer_name, phone, car_model, car_number, service_type, appointment_date, appointment_time, notes) VALUES
('张三', '13800138001', '大众朗逸', '京A12345', '普通洗车', '2024-01-15', '09:00:00', ''),
('李四', '13800138002', '丰田凯美瑞', '京B67890', '精洗', '2024-01-15', '10:30:00', '需要特别清洗内饰');
-- 插入示例套餐数据
INSERT INTO packages (package_name, description, base_duration, price, services) VALUES
('基础洗车', '基础外观清洗', 30, 50.00, '["外观冲洗", "泡沫清洁", "内饰吸尘"]'),
('精洗套餐', '全面深度清洗', 90, 150.00, '["外观精洗", "内饰深度清洁", "轮胎清洁", "打蜡"]'),
('VIP套餐', '顶级豪华洗护', 180, 300.00, '["全套精洗", "抛光打蜡", "内饰护理", "发动机清洁", "真皮护理"]');
-- 插入示例预约数据
INSERT INTO bookings (customer_name, phone, car_model, car_number, package_id, start_time, end_time, duration, total_price, notes) VALUES
('张三', '13800138001', '大众朗逸', '京A12345', 1, '2024-12-20 09:00:00', '2024-12-20 09:30:00', 30, 50.00, '第一次来'),
('李四', '13800138002', '丰田凯美瑞', '京B67890', 2, '2024-12-20 10:30:00', '2024-12-20 12:00:00', 90, 150.00, '需要特别清洗内饰');
+423 -133
View File
@@ -1,47 +1,108 @@
<?php
// index.php - 预约首页
session_start();
require_once 'db_connect.php';
$message = '';
$message_type = '';
$success_message = '';
// 处理表单提交
if ($_POST) {
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$customer_name = $_POST['customer_name'] ?? '';
$phone = $_POST['phone'] ?? '';
$car_model = $_POST['car_model'] ?? '';
$car_number = $_POST['car_number'] ?? '';
$service_type = $_POST['service_type'] ?? '';
$appointment_date = $_POST['appointment_date'] ?? '';
$appointment_time = $_POST['appointment_time'] ?? '';
$notes = $_POST['notes'] ?? '';
$customer_name = trim($_POST['customer_name']);
$phone = trim($_POST['phone']);
$car_model = trim($_POST['car_model']);
$car_number = trim($_POST['car_number']);
$package_id = (int)$_POST['package_id'];
$custom_services = trim($_POST['custom_services'] ?? '');
$appointment_date = $_POST['appointment_date'];
$appointment_time = $_POST['appointment_time'];
$duration = (int)$_POST['duration'];
$notes = trim($_POST['notes'] ?? '');
// 验证必填字段
if (empty($customer_name) || empty($phone) || empty($car_model) ||
empty($car_number) || empty($service_type) || empty($appointment_date) || empty($appointment_time)) {
$message = '请填写所有必填字段';
$message_type = 'error';
} else {
// 检查时间是否已经被预约
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings WHERE appointment_date = ? AND appointment_time = ? AND status != '已取消'");
$stmt->execute([$appointment_date, $appointment_time]);
if ($stmt->fetchColumn() > 0) {
$message = '该时间段已被预约,请选择其他时间!';
$message_type = 'error';
} else {
// 插入新预约
$stmt = $pdo->prepare("INSERT INTO bookings (customer_name, phone, car_model, car_number, service_type, appointment_date, appointment_time, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$customer_name, $phone, $car_model, $car_number, $service_type, $appointment_date, $appointment_time, $notes]);
empty($car_number) || empty($appointment_date) || empty($appointment_time)) {
throw new Exception('请填写所有必填字段');
}
$message = '预约成功!我们会尽快联系您确认。';
$message_type = 'success';
// 验证套餐
if ($package_id) {
$stmt = $pdo->prepare("SELECT * FROM packages WHERE id = ? AND is_active = 1");
$stmt->execute([$package_id]);
$package = $stmt->fetch();
if (!$package) {
throw new Exception('选择的套餐无效');
}
$total_price = $package['price'];
} else {
throw new Exception('请选择一个套餐');
}
// 计算预约时间范围
$start_time = $appointment_date . ' ' . $appointment_time . ':00';
$end_time = date('Y-m-d H:i:s', strtotime($start_time . " +{$duration} minutes"));
// 检查时间冲突
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings
WHERE status != '已取消'
AND (
(start_time <= ? AND end_time > ?)
OR (start_time < ? AND end_time >= ?)
OR (start_time >= ? AND end_time <= ?)
)");
$stmt->execute([$start_time, $start_time, $end_time, $end_time, $start_time, $end_time]);
if ($stmt->fetchColumn() > 0) {
throw new Exception('该时间段已被预约,请选择其他时间');
}
// 插入预约记录
$stmt = $pdo->prepare("INSERT INTO bookings
(customer_name, phone, car_model, car_number, package_id, custom_services,
start_time, end_time, duration, total_price, notes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$customer_name, $phone, $car_model, $car_number, $package_id, $custom_services,
$start_time, $end_time, $duration, $total_price, $notes]);
$success_message = "预约提交成功!我们会尽快联系您确认。";
} catch (Exception $e) {
$message = '预约失败:' . $e->getMessage();
$message_type = 'error';
$message = $e->getMessage();
}
}
// 获取套餐列表
$stmt = $pdo->query("SELECT * FROM packages WHERE is_active = 1 ORDER BY price");
$packages = $stmt->fetchAll();
// 获取一周内的预约数据用于日历显示
$start_date = date('Y-m-d');
$end_date = date('Y-m-d', strtotime('+7 days'));
$stmt = $pdo->prepare("SELECT DATE(start_time) as date,
COUNT(*) as booking_count,
GROUP_CONCAT(
CONCAT(
TIME_FORMAT(start_time, '%H:%i'), '-',
TIME_FORMAT(end_time, '%H:%i'),
'(', status, ')'
) ORDER BY start_time SEPARATOR '<br>'
) as bookings
FROM bookings
WHERE DATE(start_time) BETWEEN ? AND ?
AND status != '已取消'
GROUP BY DATE(start_time)
ORDER BY date");
$stmt->execute([$start_date, $end_date]);
$booking_schedule = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
// 获取套餐信息用于JavaScript
$packages_json = json_encode(array_map(function($package) {
$package['services'] = json_decode($package['services'], true);
return $package;
}, $packages));
?>
<!DOCTYPE html>
<html lang="zh-CN">
@@ -51,37 +112,85 @@ if ($_POST) {
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<meta name="description" content="在线洗车预约系统,快速便捷地预约专业洗车服务">
<meta name="keywords" content="洗车,预约,在线预约,洗车服务">
<meta name="description" content="洗车预约系统 - 在线预约洗车服务">
<meta name="keywords" content="洗车,预约,在线预约,汽车美容">
<link rel="apple-touch-icon" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgdmlld0JveD0iMCAwIDE5MiAxOTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiByeD0iMjQiIGZpbGw9IiMzMDk1RjQiLz4KPHN2ZyB4PSI0OCIgeT0iNDgiIHdpZHRoPSI5NiIgaGVpZ2h0PSI5NiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMS4yIDQuNEMyMS42IDMuNiAyMi4xIDMuMSAyMi41IDIuN0MyMy40IDIuMSAyNC41IDIuMSAyNS4yIDIuN0MyNS44IDMuMSAyNi4zIDMuNiAyNi43IDQuNEMyNy4xIDUuMSAyNy4xIDYuMiAyNi43IDcuMUMyNi4zIDcuOCAyNS44IDguMyAyNS4yIDguN0MyNC43IDkuMSAyMy42IDkuMSAyMi45IDguN0MyMi4zIDguMyAyMS44IDcuOCAyMS40IDcuMUMyMS4wIDYuMiAyMS4wIDUuMSAyMS4yIDQuNFoiLz4KPHN2ZyB4PSIyMCIgeT0iMTIiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMSAyMy41QzIwLjUgMjMuNSAyMCAyMyAyMCAyMi41VjEyQzIwIDExLjUgMjAuNSAxMSAyMSAxMUg5QzguNSAxMSAxMi41IDEwLjUgMTIgMTBIMjBWMTBCMjAgMTAuNSAyMC41IDExIDIxIDExVjIzLjVaIi8+CjxwYXRoIGQ9Ik0xOCAyMFYxN0gxNFY4SDVWMTNIMTlWMTVIMTlWMjBaIi8+CjxwYXRoIGQ9Ik04IDEwSDVWN0g4VjEwWiIvPgo8L3N2Zz4KPC9zdmc+">
<title>洗车预约系统</title>
<link rel="stylesheet" href="style.css">
<!-- Favicon for mobile devices -->
<link rel="apple-touch-icon" sizes="180x180" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚗</text></svg>">
</head>
<body>
<div class="container">
<header>
<header class="header">
<h1>🚗 洗车预约系统</h1>
<p>快速预约,专业洗车服务</p>
<nav class="nav">
<a href="index.php" class="nav-link active">预约系统</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="packages.php" class="nav-link">套餐管理</a>
</nav>
</header>
<nav>
<a href="index.php">预约洗车</a>
<a href="bookings.php">查看预约</a>
</nav>
<?php if ($message): ?>
<div class="<?php echo $message_type === 'success' ? 'success-message' : ''; ?>" style="<?php echo $message_type === 'error' ? 'background: #f8d7da; color: #721c24; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; border: 1px solid #f5c6cb;' : ''; ?>">
<?php echo $message; ?>
<div class="message error-message" style="background-color: #fee; color: #c33; border-color: #fcc;">
<?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<div class="booking-form">
<h2>预约信息</h2>
<form method="POST">
<?php if ($success_message): ?>
<div class="message success-message">
<?= htmlspecialchars($success_message) ?>
</div>
<?php endif; ?>
<div class="booking-container">
<div class="calendar-section">
<h2>📅 选择预约日期</h2>
<div class="calendar">
<?php for ($i = 0; $i < 7; $i++):
$date = date('Y-m-d', strtotime("+{$i} days"));
$date_display = date('m/d', strtotime("+{$i} days"));
$weekday = ['日', '一', '二', '三', '四', '五', '六'][date('w', strtotime("+{$i} days"))];
$booking_count = $booking_schedule[$date] ?? 0;
$is_today = $i === 0;
$is_full = $booking_count >= 8; // 假设每天最多8个时段
$status_class = $is_full ? 'full' : ($booking_count > 0 ? 'busy' : 'available');
$status_text = $is_full ? '已满' : ($booking_count > 0 ? '繁忙' : '可预约');
?>
<div class="calendar-day <?= $status_class ?> <?= $is_today ? 'today' : '' ?>"
data-date="<?= $date ?>" onclick="selectDate('<?= $date ?>')">
<div class="day-number"><?= $date_display ?></div>
<div class="day-week">周<?= $weekday ?></div>
<div class="day-status"><?= $status_text ?></div>
<?php if ($booking_schedule[$date]): ?>
<div class="booking-count"><?= $booking_count ?>个预约</div>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
<div class="time-slots" id="timeSlots" style="display: none;">
<h3>🕐 选择时间段</h3>
<div class="quick-duration">
<span>快捷时长:</span>
<button type="button" class="duration-btn" onclick="selectDuration(60)">1小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(90)">1.5小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(120)">2小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(180)">3小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(240)">4小时</button>
<input type="number" id="customDuration" min="30" step="30" value="60" style="width: 80px; margin-left: 10px;">
<button type="button" class="btn btn-sm" onclick="applyCustomDuration()">确定</button>
</div>
<div class="time-grid" id="timeGrid">
<!-- 时间段将通过JavaScript动态生成 -->
</div>
</div>
</div>
<div class="booking-form-section">
<h2>📋 预约信息</h2>
<form method="POST" class="form" id="bookingForm">
<div class="form-group">
<label for="customer_name">姓名 *</label>
<label for="customer_name">客户姓名 *</label>
<input type="text" id="customer_name" name="customer_name" required>
</div>
@@ -90,6 +199,7 @@ if ($_POST) {
<input type="tel" id="phone" name="phone" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="car_model">车型 *</label>
<input type="text" id="car_model" name="car_model" placeholder="如:大众朗逸" required>
@@ -99,93 +209,298 @@ if ($_POST) {
<label for="car_number">车牌号 *</label>
<input type="text" id="car_number" name="car_number" placeholder="如:京A12345" required>
</div>
<div class="form-group">
<label for="service_type">服务类型 *</label>
<select id="service_type" name="service_type" required>
<option value="">请选择服务类型</option>
<option value="普通洗车">普通洗车 (¥30)</option>
<option value="精洗">精洗 (¥80)</option>
<option value="打蜡">打蜡 (¥120)</option>
<option value="内饰清洁">内饰清洁 (¥60)</option>
</select>
</div>
<div class="form-group">
<label for="package_id">选择套餐 *</label>
<select id="package_id" name="package_id" required onchange="updatePackageInfo()">
<option value="">请选择套餐</option>
<?php foreach ($packages as $package): ?>
<option value="<?= $package['id'] ?>"
data-duration="<?= $package['base_duration'] ?>"
data-price="<?= $package['price'] ?>"
data-services='<?= htmlspecialchars($package['services']) ?>'>
<?= htmlspecialchars($package['package_name']) ?> - ¥<?= number_format($package['price'], 2) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="package-info" id="packageInfo" style="display: none;">
<div class="package-details">
<h4 id="packageName"></h4>
<p id="packageDescription"></p>
<div class="package-meta">
<span id="packageDuration"></span>
<span id="packagePrice"></span>
</div>
<div id="packageServices"></div>
</div>
</div>
<div class="form-group">
<label for="custom_services">自定义服务需求</label>
<textarea id="custom_services" name="custom_services" rows="3"
placeholder="如有特殊需求,请在此说明..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="appointment_date">预约日期 *</label>
<input type="date" id="appointment_date" name="appointment_date" min="<?php echo date('Y-m-d'); ?>" required>
<input type="date" id="appointment_date" name="appointment_date" required readonly>
</div>
<div class="form-group">
<label for="appointment_time">预约时间 *</label>
<input type="time" id="appointment_time" name="appointment_time" required>
<input type="time" id="appointment_time" name="appointment_time" required readonly>
</div>
</div>
<div class="form-group">
<label for="duration">服务时长(分钟) *</label>
<input type="number" id="duration" name="duration" min="30" step="30" value="60" required>
</div>
<div class="form-group">
<label for="notes">备注</label>
<textarea id="notes" name="notes" rows="3" placeholder="特殊要求或备注信息"></textarea>
<textarea id="notes" name="notes" rows="3" placeholder="其他备注信息..."></textarea>
</div>
<button type="submit" class="btn">提交预约</button>
<div class="form-actions">
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>提交预约</button>
<button type="reset" class="btn" onclick="resetForm()">重置</button>
</div>
</form>
</div>
</div>
<div style="text-align: center; margin-top: 2rem; color: #666;">
<p>营业时间:8:00 - 18:00 | 咨询电话:400-123-4567</p>
<!-- 日期详情弹窗 -->
<div id="dateModal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3 id="modalDate"></h3>
<span class="close" onclick="closeModal()">&times;</span>
</div>
<div class="modal-body" id="modalBody">
<!-- 预约详情将通过JavaScript加载 -->
</div>
</div>
</div>
</div>
<script>
// 移动端优化脚本
const packages = <?= $packages_json ?>;
const bookingSchedule = <?= json_encode($booking_schedule) ?>;
let selectedDate = null;
let selectedTime = null;
let selectedDuration = 60;
// 工作时间设置
const workingHours = {
start: 8, // 8:00
end: 18, // 18:00
slotDuration: 30 // 30分钟一个时段
};
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 自动聚焦到第一个输入框
const firstInput = document.querySelector('input[type="text"]');
if (firstInput && !/Mobi|Android/i.test(navigator.userAgent)) {
firstInput.focus();
}
const today = new Date();
today.setDate(today.getDate());
document.getElementById('appointment_date').value = today.toISOString().split('T')[0];
selectedDate = today.toISOString().split('T')[0];
// 为按钮添加触摸反馈
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => {
btn.addEventListener('touchstart', function() {
this.style.transform = 'translateY(1px)';
});
btn.addEventListener('touchend', function() {
this.style.transform = 'translateY(-2px)';
});
});
// 优化表单输入体验
const inputs = document.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
// 添加焦点样式
input.addEventListener('focus', function() {
this.parentElement.style.transform = 'scale(1.02)';
this.parentElement.style.transition = 'transform 0.2s';
});
input.addEventListener('blur', function() {
this.parentElement.style.transform = 'scale(1)';
});
// iOS Safari的输入类型优化
if (/iPhone|iPad|iPod/i.test(navigator.userAgent)) {
if (input.type === 'tel') {
input.setAttribute('pattern', '[0-9]*');
}
if (input.type === 'number') {
input.setAttribute('inputmode', 'numeric');
}
}
});
// 检测设备类型并添加类名
const isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
// 移动端优化
if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.body.classList.add('mobile-device');
// 自动聚焦到姓名输入框
setTimeout(() => {
document.getElementById('customer_name').focus();
}, 500);
}
});
function selectDate(date) {
selectedDate = date;
document.getElementById('appointment_date').value = date;
// 更新日历选中状态
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected');
});
document.querySelector(`[data-date="${date}"]`).classList.add('selected');
// 生成时间段
generateTimeSlots(date);
}
// 防止双击缩放(针对iOS Safari)
function generateTimeSlots(date) {
const timeGrid = document.getElementById('timeGrid');
const timeSlotsDiv = document.getElementById('timeSlots');
timeGrid.innerHTML = '';
// 获取当天已有预约
const dayBookings = bookingSchedule[date];
// 生成时间段
for (let hour = workingHours.start; hour < workingHours.end; hour++) {
for (let minute = 0; minute < 60; minute += workingHours.slotDuration) {
const timeString = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
const slotTime = new Date(`${date} ${timeString}:00`);
const now = new Date();
const isPast = slotTime <= now;
const isBooked = checkTimeSlotBooked(date, timeString);
const slotDiv = document.createElement('div');
slotDiv.className = `time-slot ${isPast ? 'past' : ''} ${isBooked ? 'booked' : 'available'}`;
slotDiv.textContent = timeString;
slotDiv.onclick = () => selectTimeSlot(timeString);
timeGrid.appendChild(slotDiv);
}
}
timeSlotsDiv.style.display = 'block';
selectedTime = null;
updateSubmitButton();
}
function checkTimeSlotBooked(date, time) {
// 这里需要根据实际的预约数据检查时间段是否被占用
// 简化实现,实际应该查询数据库
return false;
}
function selectTimeSlot(time) {
selectedTime = time;
document.getElementById('appointment_time').value = time;
// 更新时间段选中状态
document.querySelectorAll('.time-slot').forEach(slot => {
slot.classList.remove('selected');
});
const slotElement = document.querySelector(`[onclick="selectTimeSlot('${time}')"]`);
if (slotElement) {
slotElement.classList.add('selected');
}
updateSubmitButton();
}
function selectDuration(minutes) {
selectedDuration = minutes;
document.getElementById('duration').value = minutes;
// 更新时长按钮选中状态
document.querySelectorAll('.duration-btn').forEach(btn => {
btn.classList.remove('selected');
});
const btn = document.querySelector(`[onclick="selectDuration(${minutes})"]`);
if (btn) {
btn.classList.add('selected');
}
}
function applyCustomDuration() {
const customDuration = parseInt(document.getElementById('customDuration').value);
if (customDuration >= 30) {
selectDuration(customDuration);
} else {
alert('自定义时长不能少于30分钟');
}
}
function updatePackageInfo() {
const packageSelect = document.getElementById('package_id');
const selectedOption = packageSelect.options[packageSelect.selectedIndex];
const packageInfoDiv = document.getElementById('packageInfo');
if (selectedOption && selectedOption.value) {
const packageId = parseInt(selectedOption.value);
const package = packages.find(p => p.id === packageId);
if (package) {
// 更新套餐信息显示
document.getElementById('packageName').textContent = package.package_name;
document.getElementById('packageDescription').textContent = package.description;
document.getElementById('packageDuration').textContent = `基础时长: ${package.base_duration}分钟`;
document.getElementById('packagePrice').textContent = `价格: ¥${package.price}`;
// 显示服务项目
const servicesContainer = document.getElementById('packageServices');
if (package.services && package.services.length > 0) {
servicesContainer.innerHTML = '<strong>包含服务:</strong><br>' +
package.services.map(service => `• ${service}`).join('<br>');
}
packageInfoDiv.style.display = 'block';
// 自动设置基础时长
document.getElementById('duration').value = package.base_duration;
selectDuration(package.base_duration);
}
} else {
packageInfoDiv.style.display = 'none';
}
updateSubmitButton();
}
function updateSubmitButton() {
const submitBtn = document.getElementById('submitBtn');
const isFormValid = selectedDate && selectedTime &&
document.getElementById('customer_name').value &&
document.getElementById('phone').value &&
document.getElementById('car_model').value &&
document.getElementById('car_number').value &&
document.getElementById('package_id').value;
submitBtn.disabled = !isFormValid;
}
function resetForm() {
selectedDate = null;
selectedTime = null;
selectedDuration = 60;
document.getElementById('appointment_date').value = '';
document.getElementById('appointment_time').value = '';
document.getElementById('duration').value = 60;
document.getElementById('packageInfo').style.display = 'none';
document.getElementById('timeSlots').style.display = 'none';
// 重置所有选中状态
document.querySelectorAll('.calendar-day').forEach(day => {
day.classList.remove('selected');
});
document.querySelectorAll('.time-slot').forEach(slot => {
slot.classList.remove('selected');
});
document.querySelectorAll('.duration-btn').forEach(btn => {
btn.classList.remove('selected');
});
}
// 表单验证
document.getElementById('bookingForm').addEventListener('input', updateSubmitButton);
// 触摸反馈效果
document.addEventListener('touchstart', function(e) {
if (e.target.classList.contains('btn') || e.target.classList.contains('time-slot')) {
e.target.style.transform = 'scale(0.95)';
}
});
document.addEventListener('touchend', function(e) {
if (e.target.classList.contains('btn') || e.target.classList.contains('time-slot')) {
setTimeout(() => {
e.target.style.transform = 'scale(1)';
}, 150);
}
});
// 防止双击缩放(iOS Safari
let lastTouchEnd = 0;
document.addEventListener('touchend', function(event) {
const now = (new Date()).getTime();
@@ -194,31 +509,6 @@ if ($_POST) {
}
lastTouchEnd = now;
}, false);
// 表单提交前的简单验证
const form = document.querySelector('form');
if (form) {
form.addEventListener('submit', function(e) {
const requiredFields = this.querySelectorAll('[required]');
let hasEmpty = false;
requiredFields.forEach(field => {
if (!field.value.trim()) {
hasEmpty = true;
field.style.borderColor = '#dc3545';
field.addEventListener('input', function() {
this.style.borderColor = '';
}, { once: true });
}
});
if (hasEmpty) {
e.preventDefault();
alert('请填写所有必填字段!');
}
});
}
});
</script>
</body>
</html>
+304
View File
@@ -0,0 +1,304 @@
<?php
session_start();
require_once 'db_connect.php';
// 检查是否已登录
if (!isset($_SESSION['admin_logged_in']) || $_SESSION['admin_logged_in'] !== true) {
header('Location: index.php');
exit();
}
$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 = json_encode(array_filter(array_map('trim', $_POST['services'] ?? [])));
$stmt = $pdo->prepare("INSERT INTO packages (package_name, description, base_duration, price, services) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$package_name, $description, $base_duration, $price, $services]);
$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 = json_encode(array_filter(array_map('trim', $_POST['services'] ?? [])));
$is_active = isset($_POST['is_active']) ? 1 : 0;
$stmt = $pdo->prepare("UPDATE packages SET package_name = ?, description = ?, base_duration = ?, price = ?, services = ?, is_active = ? WHERE id = ?");
$stmt->execute([$package_name, $description, $base_duration, $price, $services, $is_active, $id]);
$message = "套餐更新成功!";
} elseif ($action === 'delete') {
$id = (int)$_POST['id'];
$stmt = $pdo->prepare("DELETE FROM packages WHERE id = ?");
$stmt->execute([$id]);
$message = "套餐删除成功!";
}
} catch (Exception $e) {
$message = "操作失败:" . $e->getMessage();
}
}
// 获取套餐列表
$stmt = $pdo->query("SELECT * FROM packages ORDER BY created_at DESC");
$packages = $stmt->fetchAll();
?>
<!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">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="format-detection" content="telephone=no">
<meta name="description" content="洗车套餐管理系统">
<meta name="keywords" content="洗车,套餐,管理">
<link rel="apple-touch-icon" href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkyIiBoZWlnaHQ9IjE5MiIgdmlld0JveD0iMCAwIDE5MiAxOTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxOTIiIGhlaWdodD0iMTkyIiByeD0iMjQiIGZpbGw9IiMzMDk1RjQiLz4KPHN2ZyB4PSI0OCIgeT0iNDgiIHdpZHRoPSI5NiIgaGVpZ2h0PSI5NiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMS4yIDQuNEMyMS42IDMuNiAyMi4xIDMuMSAyMi41IDIuN0MyMy40IDIuMSAyNC41IDIuMSAyNS4yIDIuN0MyNS44IDMuMSAyNi4zIDMuNiAyNi43IDQuNEMyNy4xIDUuMSAyNy4xIDYuMiAyNi43IDcuMUMyNi4zIDcuOCAyNS44IDguMyAyNS4yIDguN0MyNC43IDkuMSAyMy42IDkuMSAyMi45IDguN0MyMi4zIDguMyAyMS44IDcuOCAyMS40IDcuMUMyMS4wIDYuMiAyMS4wIDUuMSAyMS4yIDQuNFoiLz4KPHN2ZyB4PSIyMCIgeT0iMTIiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJ3aGl0ZSI+CjxwYXRoIGQ9Ik0yMSAyMy41QzIwLjUgMjMuNSAyMCAyMyAyMCAyMi41VjEyQzIwIDExLjUgMjAuNSAxMSAyMSAxMUg5QzguNSAxMSAxMi41IDEwLjUgMTIgMTBIMjBWMTBCMjAgMTAuNSAyMC41IDExIDIxIDExVjIzLjVaIi8+CjxwYXRoIGQ9Ik0xOCAyMFYxN0gxNFY4SDVWMTNIMTlWMTVIMTlWMjBaIi8+CjxwYXRoIGQ9Ik04IDEwSDVWN0g4VjEwWiIvPgo8L3N2Zz4KPC9zdmc+">
<title>套餐管理 - 洗车预约系统</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="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="packages.php" class="nav-link active">套餐管理</a>
</nav>
</header>
<?php if ($message): ?>
<div class="message <?= strpos($message, '成功') !== false ? 'success-message' : 'error-message' ?>"
style="<?= strpos($message, '成功') !== false ? '' : 'background-color: #fee; color: #c33; border-color: #fcc;' ?>">
<?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<div class="package-form-container">
<h2>添加新套餐</h2>
<form method="POST" class="form">
<input type="hidden" name="action" value="add">
<div class="form-group">
<label for="package_name">套餐名称:</label>
<input type="text" id="package_name" name="package_name" required>
</div>
<div class="form-group">
<label for="description">套餐描述:</label>
<textarea id="description" name="description" rows="3"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="base_duration">基础时长(分钟):</label>
<input type="number" id="base_duration" name="base_duration" min="15" step="15" value="60" required>
</div>
<div class="form-group">
<label for="price">价格(元):</label>
<input type="number" id="price" name="price" min="0" step="0.01" required>
</div>
</div>
<div class="form-group">
<label>服务项目:</label>
<div class="services-container">
<div class="service-item">
<input type="text" name="services[]" placeholder="服务项目1" value="外观清洗">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
</div>
<div class="service-item">
<input type="text" name="services[]" placeholder="服务项目2" value="内饰清洁">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
</div>
</div>
<button type="button" class="btn-add" onclick="addService()">+ 添加服务项目</button>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">添加套餐</button>
</div>
</form>
</div>
<div class="packages-list">
<h2>套餐列表</h2>
<?php if (empty($packages)): ?>
<p class="empty-message">暂无套餐数据</p>
<?php else: ?>
<?php foreach ($packages as $package): ?>
<div class="package-card <?= $package['is_active'] ? '' : 'inactive' ?>">
<div class="package-header">
<h3><?= htmlspecialchars($package['package_name']) ?></h3>
<div class="package-status">
<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">¥<?= number_format($package['price'], 2) ?></span>
</div>
</div>
<?php
$services = json_decode($package['services'], true);
if ($services):
?>
<div class="package-services">
<span class="detail-label">包含服务:</span>
<div class="services-tags">
<?php foreach ($services as $service): ?>
<span class="service-tag"><?= htmlspecialchars($service) ?></span>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<div class="package-actions">
<button class="btn btn-sm" onclick="editPackage(<?= $package['id'] ?>)">编辑</button>
<form method="POST" style="display: inline;" onsubmit="return confirmDelete()">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="id" value="<?= $package['id'] ?>">
<button type="submit" class="btn btn-danger btn-sm">删除</button>
</form>
</div>
<!-- 编辑表单 -->
<form method="POST" class="edit-form" id="edit-form-<?= $package['id'] ?>" style="display: none;">
<input type="hidden" name="action" value="update">
<input type="hidden" name="id" value="<?= $package['id'] ?>">
<div class="form-group">
<label>套餐名称:</label>
<input type="text" name="package_name" value="<?= htmlspecialchars($package['package_name']) ?>" required>
</div>
<div class="form-group">
<label>套餐描述:</label>
<textarea name="description" rows="2"><?= htmlspecialchars($package['description']) ?></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label>基础时长(分钟):</label>
<input type="number" name="base_duration" min="15" step="15" value="<?= $package['base_duration'] ?>" required>
</div>
<div class="form-group">
<label>价格(元):</label>
<input type="number" name="price" min="0" step="0.01" value="<?= $package['price'] ?>" required>
</div>
</div>
<div class="form-group">
<label>服务项目:</label>
<div class="services-container">
<?php
$services = json_decode($package['services'], true) ?: [];
foreach ($services as $service): ?>
<div class="service-item">
<input type="text" name="services[]" value="<?= htmlspecialchars($service) ?>">
<button type="button" class="btn-remove" onclick="removeService(this)">删除</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn-add" onclick="addService()">+ 添加服务项目</button>
</div>
<div class="form-group">
<label>
<input type="checkbox" name="is_active" <?= $package['is_active'] ? 'checked' : '' ?>>
启用此套餐
</label>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">保存更改</button>
<button type="button" class="btn" onclick="cancelEdit(<?= $package['id'] ?>)">取消</button>
</div>
</form>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
<script>
function addService() {
const container = document.querySelector('.services-container');
const serviceItem = document.createElement('div');
serviceItem.className = 'service-item';
serviceItem.innerHTML = `
<input type="text" name="services[]" placeholder="服务项目">
<button type="button" class="btn-remove" 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) {
// 隐藏所有编辑表单
document.querySelectorAll('.edit-form').forEach(form => {
form.style.display = 'none';
});
// 显示当前编辑表单
const editForm = document.getElementById(`edit-form-${id}`);
if (editForm) {
editForm.style.display = 'block';
editForm.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
function cancelEdit(id) {
const editForm = document.getElementById(`edit-form-${id}`);
if (editForm) {
editForm.style.display = 'none';
}
}
function confirmDelete() {
return confirm('确定要删除这个套餐吗?此操作不可恢复。');
}
// 移动端优化
if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.body.classList.add('mobile-device');
}
</script>
</body>
</html>
+419
View File
@@ -158,15 +158,375 @@ nav a:hover {
color: white;
}
/* 新增:预约页面布局 */
.booking-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-top: 2rem;
}
.calendar-section, .booking-form-section {
background: var(--card-background);
padding: 1.5rem;
border-radius: 12px;
box-shadow: var(--card-shadow);
}
/* 日历样式 */
.calendar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.calendar-day {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem;
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.calendar-day::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.calendar-day:hover::before {
left: 100%;
}
.calendar-day.today {
box-shadow: 0 0 0 3px var(--primary-color);
transform: scale(1.05);
}
.calendar-day.selected {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
transform: scale(1.08);
}
.calendar-day.available {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.calendar-day.busy {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
}
.calendar-day.full {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
}
.day-number {
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.day-week {
font-size: 0.9rem;
opacity: 0.9;
margin-bottom: 0.3rem;
}
.day-status {
font-size: 0.8rem;
opacity: 0.8;
}
.booking-count {
font-size: 0.75rem;
margin-top: 0.5rem;
opacity: 0.9;
}
/* 时间段选择 */
.time-slots {
margin-top: 2rem;
}
.quick-duration {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.quick-duration span {
font-weight: 600;
color: var(--text-color);
white-space: nowrap;
}
.duration-btn {
padding: 0.5rem 1rem;
border: 2px solid var(--primary-color);
background: white;
color: var(--primary-color);
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
white-space: nowrap;
}
.duration-btn:hover {
background: var(--primary-color);
color: white;
transform: translateY(-2px);
}
.duration-btn.selected {
background: var(--primary-color);
color: white;
box-shadow: 0 4px 12px rgba(52, 144, 220, 0.3);
}
.time-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 0.8rem;
}
.time-slot {
padding: 1rem 0.5rem;
text-align: center;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 0.9rem;
font-weight: 500;
background: white;
}
.time-slot.available:hover {
border-color: var(--primary-color);
color: var(--primary-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(52, 144, 220, 0.2);
}
.time-slot.selected {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
box-shadow: 0 4px 12px rgba(52, 144, 220, 0.3);
}
.time-slot.booked {
background: #f8f9fa;
color: #6c757d;
border-color: #dee2e6;
cursor: not-allowed;
text-decoration: line-through;
}
.time-slot.past {
background: #f8f9fa;
color: #adb5bd;
border-color: #e9ecef;
cursor: not-allowed;
}
/* 套餐信息展示 */
.package-info {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 1.5rem;
border-radius: 12px;
margin-top: 1rem;
border-left: 4px solid var(--primary-color);
}
.package-details h4 {
margin: 0 0 0.5rem 0;
color: var(--text-color);
font-size: 1.2rem;
}
.package-details p {
margin: 0 0 1rem 0;
color: #666;
font-size: 0.95rem;
}
.package-meta {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.package-meta span {
background: white;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 600;
color: var(--primary-color);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
#packageServices {
font-size: 0.9rem;
color: #555;
line-height: 1.5;
}
/* 模态框 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
}
.modal-content {
background: white;
border-radius: 12px;
max-width: 500px;
width: 100%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid #eee;
}
.modal-header h3 {
margin: 0;
color: var(--text-color);
}
.close {
font-size: 2rem;
cursor: pointer;
color: #999;
transition: color 0.3s ease;
}
.close:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
}
/* 表单操作按钮 */
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.form-actions .btn {
flex: 1;
}
.btn-primary {
background: var(--primary-color);
color: white;
border: none;
transition: all 0.3s ease;
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-hover);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(52, 144, 220, 0.3);
}
.btn-primary:disabled {
background: #ccc;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-sm {
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
/* 移动端响应式设计 */
/* 平板设备优化 */
@media (max-width: 768px) {
body { font-size: 14px; }
.container {
padding: 15px;
max-width: 100%;
}
.booking-container {
grid-template-columns: 1fr;
gap: 1.5rem;
}
.calendar {
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.8rem;
}
.calendar-day {
padding: 0.8rem;
}
.day-number {
font-size: 1.3rem;
}
.quick-duration {
justify-content: center;
}
.time-grid {
grid-template-columns: repeat(auto-fit, minmax(70px, 1fr));
gap: 0.6rem;
}
.time-slot {
padding: 0.8rem 0.4rem;
font-size: 0.8rem;
}
.package-meta {
justify-content: center;
}
.form-actions {
flex-direction: column;
}
header {
padding: 1.5rem 0;
margin-bottom: 1.5rem;
@@ -191,6 +551,21 @@ nav a:hover {
align-self: flex-end;
}
.form-group {
margin-bottom: 1rem;
}
input, select, textarea {
padding: 12px;
font-size: 16px; /* 防止iOS缩放 */
}
.btn {
padding: 15px 25px;
min-height: 48px; /* 触摸友好的最小尺寸 */
font-size: 16px;
}
nav a {
margin: 0.5rem;
padding: 0.75rem 1rem;
@@ -221,6 +596,28 @@ nav a:hover {
margin-bottom: 1rem;
}
.form-row {
flex-direction: column;
}
.form-row .form-group {
flex: none;
width: 100%;
}
.calendar {
grid-template-columns: repeat(2, 1fr);
}
.duration-btn {
padding: 0.4rem 0.8rem;
font-size: 0.8rem;
}
.modal-content {
margin: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
@@ -292,6 +689,28 @@ nav a:hover {
padding: 0.75rem;
}
.calendar {
grid-template-columns: 1fr;
}
.quick-duration {
flex-direction: column;
align-items: stretch;
}
.btn {
width: 100%;
margin-bottom: 10px;
}
.calendar-day {
padding: 0.6rem;
}
.time-grid {
grid-template-columns: repeat(2, 1fr);
}
.form-group input,
.form-group select,
.form-group textarea {