feat: 重构洗车预约系统,新增套餐管理和时间段选择功能
- 新增套餐管理模块,支持套餐的增删改查 - 重构预约表结构,支持时间段选择和套餐关联 - 实现日历视图和时间段网格选择界面 - 更新数据库结构,添加套餐表和相关字段 - 优化移动端体验,增强触摸交互 - 更新文档和样式,匹配新功能
This commit is contained in:
@@ -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
@@ -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, '需要特别清洗内饰');
|
||||
@@ -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()">×</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
@@ -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>
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user