This repository has been archived on 2026-06-20. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
carwash_order/index.php
T
wsh5485 00fc854a64 feat(预约系统): 添加会员类型和客户来源功能
在预约系统中新增会员类型(VIP/普通)和客户来源字段,包含数据库修改、表单添加和展示优化
2025-11-19 01:45:49 +08:00

637 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
session_start();
require_once 'db_connect.php';
$message = '';
$success_message = '';
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$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'] ?? '');
$member_type = $_POST['member_type'];
$source = $_POST['source'];
// 验证必填字段
if (empty($customer_name) || empty($phone) || empty($car_model) ||
empty($car_number) || empty($appointment_date) || empty($appointment_time)) {
throw new Exception('请填写所有必填字段');
}
// 验证套餐
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, member_type, source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$customer_name, $phone, $car_model, $car_number, $package_id, $custom_services,
$start_time, $end_time, $duration, $total_price, $notes, $member_type, $source]);
$success_message = "预约提交成功!我们会尽快联系您确认。";
} catch (Exception $e) {
$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 NOT IN ('已完成', '已取消')
GROUP BY DATE(start_time)
ORDER BY date");
$stmt->execute([$start_date, $end_date]);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 将结果转换为键值对格式(日期 => 预约数量)
$booking_schedule = [];
$booking_details = []; // 存储详细的预约信息
foreach ($results as $row) {
$booking_schedule[$row['date']] = $row['booking_count'];
$booking_details[$row['date']] = $row['bookings'];
}
// 获取具体的时间段预约信息供JavaScript使用
$stmt2 = $pdo->prepare("SELECT DATE(start_time) as date,
TIME_FORMAT(start_time, '%H:%i') as start_time,
TIME_FORMAT(end_time, '%H:%i') as end_time,
status,
customer_name,
car_model,
car_number
FROM bookings
WHERE DATE(start_time) BETWEEN ? AND ?
AND status NOT IN ('已完成', '已取消')
ORDER BY date, start_time");
$stmt2->execute([$start_date, $end_date]);
$all_bookings = $stmt2->fetchAll(PDO::FETCH_ASSOC);
// 按日期组织预约数据
$bookings_by_date = [];
foreach ($all_bookings as $booking) {
$bookings_by_date[$booking['date']][] = $booking;
}
// 获取套餐信息用于JavaScript
$packages_json = json_encode(array_map(function($package) {
$package['services'] = array_filter(array_map('trim', explode(',', $package['services'])));
return $package;
}, $packages));
?>
<!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 active">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="packages.php" class="nav-link">套餐管理</a>
</nav>
</header>
<?php if ($message): ?>
<div class="message error-message" style="background-color: #fee; color: #c33; border-color: #fcc;">
<?= htmlspecialchars($message) ?>
</div>
<?php endif; ?>
<?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 ?>"
data-booking-count="<?= $booking_count ?>"
onclick="showDateDetails('<?= $date ?>')">
<div class="day-number"><?= $date_display ?></div>
<div class="day-week">周<?= $weekday ?></div>
<div class="day-status"><?= $status_text ?></div>
<div class="booking-count"><?= $booking_count ?>个预约</div>
</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 id="bookingDetails" class="booking-details" style="display: none;">
<h3 id="detailsDateTitle"></h3>
<div id="bookingDetailsContent"></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>
<input type="text" id="customer_name" name="customer_name" required>
</div>
<div class="form-group">
<label for="phone">联系电话 *</label>
<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>
</div>
<div class="form-group">
<label for="car_number">车牌号 *</label>
<input type="text" id="car_number" name="car_number" placeholder="如:京A12345" required>
</div>
</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" required readonly>
</div>
<div class="form-group">
<label for="appointment_time">预约时间 *</label>
<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-row">
<div class="form-group">
<label for="member_type">会员类型 *</label>
<select id="member_type" name="member_type" required>
<option value="普通客户">普通客户</option>
<option value="VIP会员">VIP会员</option>
</select>
</div>
<div class="form-group">
<label for="source">客户来源 *</label>
<select id="source" name="source" required>
<option value="抖音">抖音</option>
<option value="微信">微信</option>
<option value="快手">快手</option>
<option value="朋友介绍">朋友介绍</option>
<option value="其他">其他</option>
</select>
</div>
</div>
<div class="form-group">
<label for="notes">备注</label>
<textarea id="notes" name="notes" rows="3" placeholder="其他备注信息..."></textarea>
</div>
<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>
<script>
const packages = <?= $packages_json ?>;
const bookingSchedule = <?= json_encode($booking_schedule) ?>;
const bookingsByDate = <?= json_encode($bookings_by_date) ?>;
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 today = new Date();
today.setDate(today.getDate());
const todayStr = today.toISOString().split('T')[0];
document.getElementById('appointment_date').value = todayStr;
selectedDate = todayStr;
// 默认选择今天的日期并显示时间段
selectDate(todayStr);
// 移动端优化
if (/Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent)) {
document.body.classList.add('mobile-device');
// 自动聚焦到姓名输入框
setTimeout(() => {
document.getElementById('customer_name').focus();
}, 500);
}
});
function showDateDetails(date) {
// 获取预约详情容器
const detailsDiv = document.getElementById('bookingDetails');
const detailsDateTitle = document.getElementById('detailsDateTitle');
const detailsContent = document.getElementById('bookingDetailsContent');
// 设置日期标题
const dateObj = new Date(date);
const dateStr = dateObj.toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
});
detailsDateTitle.textContent = dateStr + ' - 预约详情';
// 获取并显示预约详情
const details = getBookingDetailsForDate(date);
detailsContent.innerHTML = details;
// 显示预约详情区域
detailsDiv.style.display = 'block';
// 同时选择该日期用于预约
selectDate(date);
// 滚动到预约详情区域
setTimeout(() => {
detailsDiv.scrollIntoView({ behavior: 'smooth' });
}, 100);
}
function hideDateDetails() {
document.getElementById('bookingDetails').style.display = 'none';
}
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);
}
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) {
// 检查指定时间段是否已被预约
const bookings = bookingsByDate[date] || [];
for (let booking of bookings) {
const bookingStart = booking.start_time;
const bookingEnd = booking.end_time;
// 检查当前时间段是否与已有预约重叠
if (time >= bookingStart && time < bookingEnd) {
return true; // 该时间段已被预约
}
}
return false; // 该时间段可用
}
function getBookingDetailsForDate(date) {
// 获取指定日期的所有预约详情
const bookings = bookingsByDate[date] || [];
let details = '';
if (bookings.length === 0) {
return '<p>该日期暂无预约</p>';
}
details += `<h4>当日预约情况 (共${bookings.length}个预约):</h4>`;
bookings.forEach((booking, index) => {
details += `
<div class="booking-detail-item">
<div class="booking-time">${booking.start_time} - ${booking.end_time}</div>
<div class="booking-info">
<strong>客户:</strong> ${booking.customer_name} |
<strong>车辆:</strong> ${booking.car_model} (${booking.car_number}) |
<strong>状态:</strong> <span class="status-${booking.status}">${booking.status}</span>
</div>
</div>
`;
});
return details;
}
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();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
</script>
</body>
</html>