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 b963c2b513 feat: 新增预约系统功能及优化
- 添加获取每日预约时长的API接口
- 实现0元订单自动标记为已付款功能
- 优化预约信息复制功能,增加服务时长和备注
- 新增预约信息模板系统
- 在待处理预约页面添加时长提示功能
- 优化移动端触摸反馈和倒计时显示
2025-12-06 05:05:15 +08:00

2127 lines
98 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
date_default_timezone_set('Asia/Shanghai');
session_start();
require_once 'db_connect.php';
$message = '';
$success_message = '';
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$customer_type = $_POST['customer_type'];
$vip_id = isset($_POST['vip_id']) ? (int)$_POST['vip_id'] : 0;
// 如果选择VIP客户,从VIP表获取信息
if ($customer_type === 'vip' && $vip_id > 0) {
$stmt = $pdo->prepare("SELECT * FROM vip_customers WHERE id = ? AND is_active = 1");
$stmt->execute([$vip_id]);
$vip_customer = $stmt->fetch();
if (!$vip_customer) {
throw new Exception('选择的VIP客户无效');
}
// 使用VIP客户信息
$customer_name = $vip_customer['customer_name'];
$phone = $vip_customer['phone'];
$car_model = $vip_customer['car_model'] ?: $car_model; // 允许覆盖
$car_number = $vip_customer['car_number'] ?: $car_number; // 允许覆盖
$member_type = 'VIP会员';
} else {
// 新客户录入
$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('请填写所有必填字段');
}
// 验证VIP客户或新客户的必填字段
if ($customer_type === 'vip') {
if (empty($vip_id)) {
throw new Exception('请选择一个VIP客户');
}
} else {
if (empty($customer_name) || empty($phone)) {
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('该时间段已被预约,请选择其他时间');
}
// 插入预约记录
// 对于0元订单,自动标记为已付款
$payment_status = ($total_price <= 0) ? '已付款' : '未付款';
$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, payment_status)
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, $payment_status]);
$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) {
$start_date = $booking['date'];
$start_time = $booking['start_time'];
$end_time = $booking['end_time'];
// 将预约添加到开始日期
$bookings_by_date[$start_date][] = $booking;
// 检查是否是跨天预约(结束时间早于开始时间)
if (strtotime($end_time) < strtotime($start_time)) {
// 计算第二天的日期
$next_date = date('Y-m-d', strtotime($start_date . ' +1 day'));
// 创建第二天的预约记录副本
$next_day_booking = $booking;
$next_day_booking['is_cross_day'] = true; // 标记为跨天预约
// 将预约添加到第二天
$bookings_by_date[$next_date][] = $next_day_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">
<style>
/* VIP搜索结果样式 */
.vip-search-results {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
z-index: 1000;
max-height: 200px;
overflow-y: auto;
}
.vip-search-item {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid #f0f0f0;
transition: background-color 0.2s ease;
}
.vip-search-item:hover {
background-color: #f8f9fa;
}
.vip-search-item:last-child {
border-bottom: none;
}
.vip-search-item .customer-name {
font-weight: bold;
color: #333;
margin-bottom: 4px;
}
.vip-search-item .customer-phone {
color: #666;
font-size: 0.9em;
}
.vip-search-item .customer-car {
color: #888;
font-size: 0.85em;
margin-top: 4px;
}
.vip-search-item.selected {
background-color: #e3f2fd;
}
#vip_search {
position: relative;
}
.form-group {
position: relative;
}
.form-group #vip_search {
margin-bottom: 10px;
}
.search-tips {
font-size: 0.85em;
color: #666;
margin-top: 5px;
}
.no-results {
text-align: center;
color: #999;
padding: 20px;
font-style: italic;
}
.vip-search-results mark {
background-color: #ffeb3b;
color: #333;
padding: 1px 2px;
border-radius: 2px;
font-weight: bold;
}
.phone-vip-tip {
font-size: 0.85em;
margin-top: 5px;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ddd;
}
.phone-vip-tip.vip-detected {
background-color: #e8f5e8;
border-color: #4caf50;
color: #2e7d32;
}
.phone-vip-tip.suggestion {
background-color: #fff3e0;
border-color: #ff9800;
color: #ef6c00;
}
</style>
</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="pending_bookings.php" class="nav-link">待处理预约</a>
<a href="packages.php" class="nav-link">套餐管理</a>
<a href="vip.php" class="nav-link">VIP管理</a>
<a href="announcement.php" class="nav-link">今日待办</a>
</nav>
</header>
<?php if ($message): ?>
<div class="message error-message" style="background-color: #fee; color: #c33; border-color: #fcc;">
<?= 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="pagination-controls">
<button type="button" id="prevPage" class="btn btn-sm btn-secondary" onclick="changePage(-1)">上一页</button>
<span id="pageInfo" class="page-info">第 1 页 / 共 4 页</span>
<button type="button" id="nextPage" class="btn btn-sm btn-secondary" onclick="changePage(1)">下一页</button>
</div>
<style>
/* 分页控制样式 */
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 20px;
gap: 15px;
}
.page-info {
font-size: 14px;
font-weight: bold;
color: #333;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 日历布局样式 */
.calendar {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 10px;
margin-bottom: 20px;
}
/* 确保响应式布局 */
@media (max-width: 768px) {
.calendar {
grid-template-columns: repeat(4, 1fr);
}
}
@media (max-width: 480px) {
.calendar {
grid-template-columns: repeat(2, 1fr);
}
}
/* 超出范围日期样式 */
.calendar-day.disabled {
background-color: #f5f5f5;
color: #999;
cursor: not-allowed;
}
.calendar-day.disabled:hover {
transform: none;
box-shadow: none;
}
</style>
<div class="calendar" id="calendarContainer">
<!-- 日期将通过JavaScript动态生成 -->
</div>
<script>
// 初始化当前页码(从0开始)
let currentPage = 0;
const totalPages = 4; // 总共4页(28天)
const daysPerPage = 7; // 每页7天
// 获取本周一的日期
function getCurrentMonday() {
const today = new Date();
const dayOfWeek = today.getDay(); // 0是周日,1-6是周一到周六
const diff = today.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); // 调整为周一
const monday = new Date(today);
monday.setDate(diff);
return monday;
}
// 生成日历数据 - 按周一到周日顺序排列
const generateCalendarDays = () => {
const calendarContainer = document.getElementById('calendarContainer');
calendarContainer.innerHTML = '';
// 获取基准周一日期
const baseMonday = getCurrentMonday();
// 根据页码计算当前页的起始周
const weekOffset = currentPage;
// 计算当前页第一周的周一
const pageStartMonday = new Date(baseMonday);
pageStartMonday.setDate(baseMonday.getDate() + weekOffset * 7);
// 按周一到周日的顺序生成日期
for (let dayOffset = 0; dayOffset < 7; dayOffset++) {
const date = new Date(pageStartMonday);
date.setDate(pageStartMonday.getDate() + dayOffset);
// 确保使用本地时区获取日期字符串
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}-${month}-${day}`;
const dateDisplay = `${date.getMonth() + 1}/${date.getDate()}`;
const weekday = ['日', '一', '二', '三', '四', '五', '六'][date.getDay()];
// 获取今天的日期字符串用于比较
const today = new Date();
const todayYear = today.getFullYear();
const todayMonth = String(today.getMonth() + 1).padStart(2, '0');
const todayDay = String(today.getDate()).padStart(2, '0');
const todayStr = `${todayYear}-${todayMonth}-${todayDay}`;
const isToday = dateStr === todayStr;
// 获取预约数量
const bookingCount = bookingsByDate[dateStr] ? bookingsByDate[dateStr].length : 0;
const isFull = bookingCount >= 8; // 假设每天最多8个时段
// 检查日期是否在未来28天内
const currentDate = new Date();
const maxDate = new Date(currentDate);
maxDate.setDate(currentDate.getDate() + 27); // 28天包括今天
const isWithinRange = date >= currentDate && date <= maxDate;
const statusClass = isFull ? 'full' : (bookingCount > 0 ? 'busy' : (isWithinRange ? 'available' : 'disabled'));
const statusText = isFull ? '已满' : (bookingCount > 0 ? '繁忙' : (isWithinRange ? '可预约' : '超出范围'));
const dayDiv = document.createElement('div');
dayDiv.className = `calendar-day ${statusClass} ${isToday ? 'today' : ''}`;
dayDiv.dataset.date = dateStr;
dayDiv.dataset.bookingCount = bookingCount;
// 只有在范围内的日期才可点击
if (isWithinRange) {
dayDiv.onclick = () => showDateDetails(dateStr);
}
dayDiv.innerHTML = `
<div class="day-number">${dateDisplay}</div>
<div class="day-week">周${weekday}</div>
<div class="day-status">${statusText}</div>
<div class="booking-count">${bookingCount}个预约</div>
`;
calendarContainer.appendChild(dayDiv);
}
// 更新分页信息
updatePaginationInfo();
};
// 更新分页信息
const updatePaginationInfo = () => {
const pageInfo = document.getElementById('pageInfo');
pageInfo.textContent = `第 ${currentPage + 1} 页 / 共 ${totalPages} 页`;
// 启用/禁用翻页按钮
document.getElementById('prevPage').disabled = currentPage === 0;
document.getElementById('nextPage').disabled = currentPage === totalPages - 1;
};
// 切换页面
const changePage = (direction) => {
const newPage = currentPage + direction;
if (newPage >= 0 && newPage < totalPages) {
currentPage = newPage;
generateCalendarDays();
}
};
// 页面加载时生成第一页
document.addEventListener('DOMContentLoaded', function() {
generateCalendarDays();
});
</script>
<style>
/* 统一时间选择样式为pending_bookings.php的样式 */
.calendar-day {
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.calendar-day:hover {
background: #e9ecef;
}
.calendar-day.selected {
background: #007bff;
color: white;
}
.time-slots-container {
margin-bottom: 20px;
}
.time-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
}
.time-slot {
padding: 10px;
border-radius: 4px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.time-slot.available {
background: #d4edda;
color: #155724;
}
.time-slot.booked {
background: #f8d7da;
color: #721c24;
cursor: not-allowed;
}
.time-slot.past {
background: #e9ecef;
color: #6c757d;
cursor: not-allowed;
}
.time-slot.selected {
background: #007bff;
color: white;
}
.time-slot:hover.available {
background: #c3e6cb;
}
</style>
<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(240)">4小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(360)">6小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(480)">8小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(600)">10小时</button>
<button type="button" class="duration-btn" onclick="selectDuration(720)">12小时</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-row">
<div class="form-group">
<label for="customer_type">客户类型 *</label>
<select id="customer_type" name="customer_type" required onchange="handleCustomerTypeChange()">
<option value="new">新客户</option>
<option value="vip">VIP客户</option>
</select>
</div>
<div class="form-group" id="vip_select_group" style="display: none;">
<label for="vip_search">搜索VIP客户</label>
<input type="text" id="vip_search" placeholder="输入姓名或手机号搜索"
oninput="searchVIPCustomers()" autocomplete="off"
onfocus="clearSearchResults()">
<div class="search-tips">💡 支持模糊搜索,输入姓名或手机号即可快速定位</div>
<label for="vip_id" style="margin-top: 10px;">选择VIP客户</label>
<select id="vip_id" name="vip_id" onchange="loadVIPInfo()">
<option value="">请选择VIP客户</option>
</select>
<div id="vip_search_results" class="vip-search-results" style="display: none;"></div>
</div>
</div>
<!-- VIP客户上次预约信息显示区域 -->
<div id="vip_last_booking_info" class="vip-last-booking-info" style="display: none;">
<h4>📋 最近预约信息</h4>
<div id="last_booking_content" style="padding: 10px; background-color: #f8f9fa; border-radius: 5px;">
<!-- 这里将显示上次预约信息 -->
</div>
</div>
<div class="form-row" id="new_customer_fields">
<div class="form-group">
<label for="customer_name">客户姓名 *</label>
<input type="text" id="customer_name" name="customer_name" placeholder="请输入客户姓名">
</div>
<div class="form-group">
<label for="phone">联系电话 *</label>
<input type="tel" id="phone" name="phone" placeholder="请输入联系电话"
oninput="checkPhoneForVIP()">
<div id="phone_vip_tip" class="phone-vip-tip" style="display: none;"></div>
</div>
</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 class="form-group" style="margin-top: 15px;">
<label for="total_price">最终价格 (¥)</label>
<input type="number" id="total_price" name="total_price" min="0" step="0.01" value="0" placeholder="输入最终价格" required>
<input type="hidden" id="package_price" value="0">
</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;
// 工作时间设置 - 改为24小时制
const workingHours = {
start: 0, // 00:00
end: 24, // 24:00
slotDuration: 30 // 30分钟一个时段
};
// 页面加载完成时初始化
document.addEventListener('DOMContentLoaded', function() {
// 初始化时间选择
generateTimeSlots();
// 处理当前时间
const now = new Date();
const todayStr = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0');
document.getElementById('appointment_date').value = todayStr;
// 初始化VIP客户全局变量
window.allVIPCustomers = [];
// 加载VIP客户(使用异步加载确保数据完整性)
console.log('开始初始化VIP客户数据...');
loadVIPCustomers()
.then(data => {
console.log('VIP客户数据初始化成功,共', data.length, '条记录');
})
.catch(error => {
console.error('VIP客户数据初始化失败:', error);
// 即使VIP数据加载失败,也要继续页面初始化
});
// 初始化调试面板
setupVIPDebugPanel();
updateDebugStatus('正在加载...');
// 调试信息
console.log('页面初始化完成');
});
// 加载VIP客户列表 - 返回Promise以支持异步调用
function loadVIPCustomers() {
console.log('开始加载VIP客户列表...');
return new Promise((resolve, reject) => {
// 定义多个可能的API路径,优先使用绝对路径
const apiUrls = [
'/get_vip_customers.php', // 推荐的绝对路径
'get_vip_customers.php', // 原始相对路径
'./get_vip_customers.php' // 另一种相对路径
];
let currentAttempt = 0;
// 从本地存储加载或创建测试数据
function getTestVIPData() {
// 先尝试从localStorage加载
const savedVips = localStorage.getItem('temp_vip_customers');
if (savedVips) {
try {
const data = JSON.parse(savedVips);
console.log('从本地存储加载了测试VIP数据:', data.length, '条记录');
return data;
} catch (e) {
console.error('解析本地存储的VIP数据失败:', e);
}
}
// 创建测试数据
const testCustomers = [
{ customer_name: '张三', phone: '18612345678', car_model: '奔驰C级', car_number: '京A12345' },
{ customer_name: '李四', phone: '13987654321', car_model: '宝马3系', car_number: '京B54321' },
{ customer_name: '王五', phone: '18699627661', car_model: '奥迪A4', car_number: '沪A12345' },
{ customer_name: '赵六', phone: '18699627777', car_model: '特斯拉Model 3', car_number: '深A67890' },
{ customer_name: '钱七', phone: '13812345678', car_model: '丰田凯美瑞', car_number: '广A12345' }
];
// 为每个客户添加ID和时间戳
testCustomers.forEach((customer, index) => {
customer.id = index + 1;
customer.created_at = new Date().toISOString();
customer.is_active = 1;
});
// 保存到localStorage
localStorage.setItem('temp_vip_customers', JSON.stringify(testCustomers));
console.log('创建并保存了测试VIP数据,共', testCustomers.length, '条记录');
return testCustomers;
}
function tryNextUrl() {
if (currentAttempt >= apiUrls.length) {
const errorMsg = '所有URL尝试失败,使用测试数据进行演示';
console.error(errorMsg);
updateDebugStatus(errorMsg, 'warning');
// 使用测试数据
const testData = getTestVIPData();
window.allVIPCustomers = testData;
updateVIPSelect(testData);
// 更新调试面板
const historyElem = document.getElementById('debug-search-history');
if (historyElem) {
const previewEntry = document.createElement('div');
previewEntry.style.cssText = 'border-bottom: 1px solid #eee; padding: 5px 0; color: #444;';
previewEntry.innerHTML = `
<div style="font-weight: bold; color: orange;">⚠️ 使用测试VIP数据:</div>
<div>加载了 ${testData.length} 个测试VIP客户</div>
<div style="margin-top: 5px; font-size: 11px;">
${testData.map((vip, idx) =>
`${idx + 1}. ${vip.customer_name}: ${vip.phone}`
).join('<br>')}
</div>
`;
historyElem.innerHTML = '';
historyElem.appendChild(previewEntry);
}
// 返回测试数据
resolve(testData);
return;
}
const currentUrl = apiUrls[currentAttempt];
console.log(`尝试加载VIP数据 (${currentAttempt + 1}/${apiUrls.length}): ${currentUrl}`);
fetch(currentUrl)
.then(response => {
console.log(`URL ${currentUrl} 响应状态: ${response.status} ${response.statusText}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return response.text();
})
.then(text => {
console.log(`URL ${currentUrl} 原始响应文本长度: ${text.length} 字符`);
// 显示部分原始响应以便调试
if (text.length > 0) {
console.log('原始响应前100字符:', text.substring(0, Math.min(100, text.length)));
}
try {
// 先检查是否为空响应
if (!text || text.trim() === '') {
throw new Error('API返回空响应');
}
// 检查是否包含HTML标签(可能是错误页面)
if (text.includes('<html') || text.includes('<body') || text.includes('</html>')) {
throw new Error('响应包含HTML内容而非JSON');
}
// 尝试解析JSON
let data;
try {
data = JSON.parse(text);
} catch (parseError) {
// 尝试修复常见的JSON格式问题
console.warn('JSON解析失败,尝试修复格式...');
// 移除可能的BOM字符
const cleanText = text.replace(/^\uFEFF/, '');
// 移除可能的多余逗号
const fixedText = cleanText.replace(/,\s*}/g, '}').replace(/,\s*\]/g, ']');
if (fixedText !== text) {
console.log('使用修复后的文本重新解析');
try {
data = JSON.parse(fixedText);
console.log('修复后解析成功!');
} catch (fixError) {
// 如果修复失败,继续尝试下一个URL
currentAttempt++;
tryNextUrl();
return;
}
} else {
// 如果没有修复,继续尝试下一个URL
currentAttempt++;
tryNextUrl();
return;
}
}
// 检查是否是错误响应
if (data.error) {
// 如果API返回错误,继续尝试下一个URL
currentAttempt++;
tryNextUrl();
return;
}
console.log('VIP客户数据解析成功:', typeof data);
// 处理可能的嵌套数据结构
let finalData = data;
if (!Array.isArray(data)) {
console.log('数据不是数组,检查是否有嵌套结构...');
if (data && data.customers && Array.isArray(data.customers)) {
console.log('发现嵌套的customers数组');
finalData = data.customers;
} else if (data && data.vip_customers && Array.isArray(data.vip_customers)) {
console.log('发现嵌套的vip_customers数组');
finalData = data.vip_customers;
} else {
// 格式不符合预期,继续尝试下一个URL
currentAttempt++;
tryNextUrl();
return;
}
}
console.log('最终VIP数据类型检查:', Array.isArray(finalData));
console.log('VIP客户数量:', finalData.length);
// 存储VIP客户数据
window.allVIPCustomers = finalData;
updateVIPSelect(finalData); // 更新下拉列表
// 更新调试面板状态
updateDebugStatus('加载成功,共 ' + finalData.length + ' 条记录', 'success');
// 显示前几个VIP客户信息(如果有的话)
if (finalData.length > 0) {
console.log('前3个VIP客户信息:');
finalData.slice(0, 3).forEach((vip, index) => {
console.log(`${index + 1}. ${vip.customer_name || '未知'} - ${vip.phone || '未知'}`);
});
}
// 在调试面板中显示部分数据预览
const historyElem = document.getElementById('debug-search-history');
if (historyElem) {
const previewEntry = document.createElement('div');
previewEntry.style.cssText = 'border-bottom: 1px solid #eee; padding: 5px 0; color: #444;';
previewEntry.innerHTML = `
<div style="font-weight: bold;">📋 VIP数据预览:</div>
<div>加载了 ${finalData.length} 个VIP客户</div>
`;
// 显示前3个VIP客户作为预览
if (finalData.length > 0) {
let previewHtml = '<div style="margin-top: 5px; font-size: 11px;">';
finalData.slice(0, 3).forEach((vip, idx) => {
previewHtml += `${idx + 1}. ${vip.customer_name || '未知'}: ${vip.phone || '未知'}<br>`;
});
previewHtml += '</div>';
previewEntry.innerHTML += previewHtml;
}
historyElem.innerHTML = '';
historyElem.appendChild(previewEntry);
}
// 成功完成,resolve Promise(使用处理后的finalData
resolve(finalData);
} catch(e) {
console.error('VIP客户数据处理过程中发生异常:', e);
// 继续尝试下一个URL
currentAttempt++;
tryNextUrl();
}
})
.catch(error => {
console.error(`URL ${currentUrl} 请求失败:`, error.message);
// 继续尝试下一个URL
currentAttempt++;
tryNextUrl();
});
}
// 开始尝试第一个URL
tryNextUrl();
});
}
// 测试搜索功能
function testSearchFunction() {
if (window.allVIPCustomers.length > 0) {
console.log('当前VIP客户:', window.allVIPCustomers);
// 模拟搜索测试
const testTerm = window.allVIPCustomers[0].customer_name.substring(0, 1);
const results = window.allVIPCustomers.filter(vip =>
vip.customer_name.includes(testTerm) ||
vip.phone.includes(testTerm)
);
console.log('搜索测试:', testTerm, '->', results.length, '个结果');
}
}
// 更新VIP客户下拉列表
function updateVIPSelect(customers) {
const vipSelect = document.getElementById('vip_id');
vipSelect.innerHTML = '<option value="">请选择VIP客户</option>';
customers.forEach(vip => {
const option = document.createElement('option');
option.value = vip.id;
option.textContent = `${vip.customer_name} (${vip.phone})`;
vipSelect.appendChild(option);
});
}
// VIP客户搜索功能 - 修复版本
function searchVIPCustomers() {
console.log('开始搜索VIP客户...');
// 确保allVIPCustomers已初始化
if (!window.allVIPCustomers || !Array.isArray(window.allVIPCustomers)) {
window.allVIPCustomers = [];
console.warn('allVIPCustomers未初始化,已创建空数组');
}
const searchInput = document.getElementById('vip_search');
const searchResultsDiv = document.getElementById('vip_search_results');
const vipSelect = document.getElementById('vip_id');
if (!searchInput || !searchResultsDiv || !vipSelect) {
console.error('找不到必要的DOM元素');
return;
}
const searchTerm = searchInput.value.trim();
console.log('搜索关键词:', searchTerm);
// 检查数据加载状态
console.log('VIP客户总数:', window.allVIPCustomers.length);
// 立即处理空搜索情况
if (searchTerm === '') {
searchResultsDiv.innerHTML = '';
searchResultsDiv.style.display = 'none';
vipSelect.style.display = 'block';
return;
}
// 始终显示搜索结果容器
searchResultsDiv.style.display = 'block';
// 如果数据还没加载完成,先加载数据
if (window.allVIPCustomers.length === 0) {
console.log('VIP客户数据尚未加载,先加载数据...');
searchResultsDiv.innerHTML = '<div class="loading">正在加载VIP客户数据...</div>';
loadVIPCustomers().then(() => {
console.log('数据加载完成,重新执行搜索');
// 数据加载后执行搜索
performSearch(searchTerm);
}).catch(error => {
console.error('加载VIP数据失败:', error);
searchResultsDiv.innerHTML = '<div class="search-error">加载VIP客户数据失败,请重试</div>';
});
} else {
// 直接执行搜索
performSearch(searchTerm);
}
function performSearch(term) {
// 标准化手机号函数
const normalizePhone = (phoneStr) => {
return phoneStr ? phoneStr.replace(/[^0-9]/g, '') : '';
};
// 模糊搜索:支持姓名、手机号和车牌号搜索,增强匹配逻辑
const filteredCustomers = window.allVIPCustomers.filter(vip => {
// 确保vip对象有效
if (!vip || typeof vip !== 'object') return false;
const searchLower = term.toLowerCase();
const name = (vip.customer_name || '').toLowerCase();
const phone = vip.phone || '';
const carModel = (vip.car_model || '').toLowerCase();
const carNumber = (vip.car_number || '').toLowerCase();
// 基本匹配
let isMatch = name.includes(searchLower) ||
phone.includes(term) ||
carModel.includes(searchLower) ||
carNumber.includes(searchLower);
// 手机号标准化匹配(处理数字搜索)
if (!isMatch && /^\d+$/.test(term)) {
const normalizedPhone = normalizePhone(phone);
const normalizedSearch = normalizePhone(term);
isMatch = normalizedPhone.includes(normalizedSearch) ||
normalizedSearch.includes(normalizedPhone);
}
// 调试日志
console.log('搜索项检查:', {
id: vip.id,
name: vip.customer_name,
phone: phone,
match: isMatch
});
return isMatch;
});
console.log('搜索结果数量:', filteredCustomers.length);
// 显示搜索结果
if (filteredCustomers.length === 0) {
searchResultsDiv.innerHTML = '<div class="no-results">未找到匹配的VIP客户<br>请尝试使用姓名、手机号或车牌号搜索</div>';
} else {
let html = '';
filteredCustomers.forEach(vip => {
// 安全获取数据并设置默认值
const name = vip.customer_name || '未知客户';
const phone = vip.phone || '未知手机号';
const id = vip.id || '';
const carModel = vip.car_model || '';
const carNumber = vip.car_number || '';
// 转义引号,防止JS语法错误
const escapedId = id.toString().replace(/'/g, "\\'");
const escapedName = name.replace(/'/g, "\\'");
const escapedPhone = phone.replace(/'/g, "\\'");
// 高亮处理
const highlightedName = name.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>');
const highlightedPhone = phone.replace(new RegExp(`(${term})`, 'gi'), '<span class="highlight">$1</span>');
html += `
<div class="vip-search-item" onclick="selectVIPCustomer('${escapedId}', '${escapedName}', '${escapedPhone}')">
<div class="customer-name">${highlightedName}</div>
<div class="customer-phone">${highlightedPhone}</div>
${carModel || carNumber ? `<div class="customer-car">${carModel} ${carNumber}</div>` : ''}
</div>
`;
});
searchResultsDiv.innerHTML = html;
}
// 隐藏下拉列表
vipSelect.style.display = 'none';
}
}
// 显示搜索结果
function displaySearchResults(results, searchTerm) {
console.log('显示搜索结果...', results.length, '个结果');
const searchResultsDiv = document.getElementById('vip_search_results');
if (!searchResultsDiv) {
console.error('找不到VIP搜索结果容器');
return;
}
if (results.length === 0) {
searchResultsDiv.innerHTML = '<div class="no-results">未找到匹配的VIP客户</div>';
searchResultsDiv.style.display = 'block';
return;
}
let html = '';
results.forEach(vip => {
// 安全地处理数据
const name = vip.customer_name || '';
const phone = vip.phone || '';
const id = vip.id || '';
const highlightedName = highlightSearchTerm(name, searchTerm);
const highlightedPhone = highlightSearchTerm(phone, searchTerm);
html += `
<div class="vip-search-item" onclick="selectVIPCustomer('${id}', '${name}', '${phone}')">
<div class="customer-name">${highlightedName}</div>
<div class="customer-phone">${highlightedPhone}</div>
${vip.car_model || vip.car_number ? `<div class="customer-car">${vip.car_model || ''} ${vip.car_number || ''}</div>` : ''}
</div>
`;
});
searchResultsDiv.innerHTML = html;
searchResultsDiv.style.display = 'block';
console.log('搜索结果已显示');
}
// 高亮搜索关键词
function highlightSearchTerm(text, searchTerm) {
if (!text || !searchTerm) return text || '';
console.log('高亮处理:', text, '关键词:', searchTerm);
const regex = new RegExp(`(${searchTerm})`, 'gi');
const result = text.replace(regex, '<span class="highlight">$1</span>');
console.log('高亮结果:', result);
return result;
}
// 清除搜索结果
function clearSearchResults() {
console.log('清除搜索结果');
const searchResults = document.getElementById('vip_search_results');
if (searchResults) {
searchResults.innerHTML = '';
searchResults.style.display = 'none';
}
const searchInput = document.getElementById('vip_search');
if (searchInput) {
searchInput.value = '';
}
}
// 选择VIP客户 - 修复版本
function selectVIPCustomer(customerId, customerName = '', customerPhone = '') {
console.log('选择VIP客户:', customerId, customerName, customerPhone);
// 设置下拉选择值
const vipSelect = document.getElementById('vip_id');
const searchResultsDiv = document.getElementById('vip_search_results');
const searchInput = document.getElementById('vip_search');
if (vipSelect) {
vipSelect.value = customerId;
vipSelect.style.display = 'block';
console.log('已设置VIP选择值:', customerId);
} else {
console.error('找不到VIP选择元素');
}
// 清除搜索状态
if (searchResultsDiv) {
searchResultsDiv.innerHTML = '';
searchResultsDiv.style.display = 'none';
}
if (searchInput) {
searchInput.value = '';
}
// 加载VIP客户信息
loadVIPInfo();
// 显示选择成功的提示
if (customerName) {
console.log('成功选择VIP客户:', customerName);
}
}
// 检查手机号是否为VIP客户 - 完全重写的可靠版本
function checkPhoneForVIP() {
console.log('--- 开始检查手机号是否为VIP客户 ---');
const phoneInput = document.getElementById('phone');
const tipDiv = document.getElementById('phone_vip_tip');
const phone = phoneInput.value.trim();
// 清除提示
tipDiv.style.display = 'none';
tipDiv.className = 'phone-vip-tip';
// 基本检查
if (phone.length < 3) {
console.log('手机号过短,不进行检查:', phone);
return;
}
// 检查window.allVIPCustomers是否存在且为数组
if (!window.allVIPCustomers || !Array.isArray(window.allVIPCustomers)) {
console.error('错误:window.allVIPCustomers未正确初始化');
tipDiv.innerHTML = '<span style="color:red;">⚠️ VIP客户数据未加载,请刷新页面</span>';
tipDiv.className = 'phone-vip-tip error';
tipDiv.style.display = 'block';
return;
}
console.log('搜索手机号:', phone);
console.log('VIP客户总数:', window.allVIPCustomers.length);
// 创建标准化函数:移除所有非数字字符
const normalizePhone = (phoneStr) => {
return phoneStr ? phoneStr.replace(/[^0-9]/g, '') : '';
};
const normalizedInput = normalizePhone(phone);
console.log('标准化后的输入:', normalizedInput);
// 找出所有可能匹配的VIP客户
const potentialMatches = [];
let matchedVIP = null;
let bestMatchScore = 0;
for (const vip of window.allVIPCustomers) {
if (!vip.phone) continue;
const vipPhone = vip.phone;
const normalizedVIPPhone = normalizePhone(vipPhone);
console.log('检查VIP:', vip.customer_name, '手机号:', vipPhone, '标准化后:', normalizedVIPPhone);
// 计算匹配分数
let matchScore = 0;
// 精确匹配(最高优先级)
if (vipPhone === phone) {
matchScore = 100;
} else if (normalizedVIPPhone === normalizedInput) {
matchScore = 95;
}
// 包含匹配(中等优先级)
else if (vipPhone.includes(phone)) {
matchScore = 90;
} else if (phone.includes(vipPhone)) {
matchScore = 85;
} else if (normalizedVIPPhone.includes(normalizedInput)) {
matchScore = 80;
} else if (normalizedInput.includes(normalizedVIPPhone)) {
matchScore = 75;
}
// 后几位匹配(低优先级)
else if (normalizedInput.length >= 4 && normalizedVIPPhone.endsWith(normalizedInput)) {
matchScore = 70;
}
// 前几位匹配
else if (normalizedInput.length >= 4 && normalizedVIPPhone.startsWith(normalizedInput)) {
matchScore = 65;
}
// 部分匹配(最低优先级)
else if (normalizedVIPPhone.includes(normalizedInput.substring(normalizedInput.length - 4))) {
matchScore = 60;
} else if (normalizedInput.length >= 6 && normalizedVIPPhone.includes(normalizedInput.substring(0, 6))) {
matchScore = 55;
}
// 如果找到更好的匹配,更新匹配结果
if (matchScore > bestMatchScore) {
bestMatchScore = matchScore;
matchedVIP = vip;
console.log('找到更好的匹配:', vip.customer_name, vipPhone, '分数:', matchScore);
}
// 记录潜在匹配供调试
if (matchScore >= 50 && matchScore < bestMatchScore) {
potentialMatches.push({name: vip.customer_name, phone: vipPhone, score: matchScore});
} else if (normalizedVIPPhone.includes(normalizedInput.substring(normalizedInput.length - 3)) ||
normalizedInput.includes(normalizedVIPPhone.substring(normalizedVIPPhone.length - 3))) {
potentialMatches.push({name: vip.customer_name, phone: vipPhone, score: 40});
}
}
// 根据分数过滤潜在匹配,只保留分数较高的
potentialMatches.sort((a, b) => (b.score || 0) - (a.score || 0));
console.log('潜在匹配列表:', potentialMatches);
console.log('最终匹配结果:', matchedVIP, '匹配分数:', bestMatchScore);
// 设置最小匹配分数阈值
const MIN_MATCH_SCORE = 55;
// 只有当匹配分数足够高时才认为是有效匹配
const isEffectiveMatch = matchedVIP && bestMatchScore >= MIN_MATCH_SCORE;
if (isEffectiveMatch) {
console.log('确认有效匹配,分数:', bestMatchScore);
// 显示VIP客户提示
tipDiv.innerHTML = `
<strong>👑 检测到VIP客户!</strong><br>
该手机号属于 VIP客户:${matchedVIP.customer_name}<br>
手机号:${matchedVIP.phone}<br>
车牌号:${matchedVIP.car_number || '暂无'}<br>
匹配度:${bestMatchScore}分<br>
<button type="button" onclick="switchToVIPMode(${matchedVIP.id})" class="switch-to-vip-btn">
切换到VIP客户模式
</button>
`;
tipDiv.className = 'phone-vip-tip vip-detected';
tipDiv.style.display = 'block';
} else if (phone.length >= 8) {
// 显示建议提示(当手机号长度足够时)
let message = '💡 <strong>提示:</strong>未找到该手机号的VIP客户<br>';
message += '如果此客户是VIP客户,请检查手机号是否正确';
// 如果有潜在匹配,添加提示并显示匹配分数
if (potentialMatches.length > 0) {
message += '<br><br><strong>可能相关的VIP客户:</strong><br>';
potentialMatches.slice(0, 3).forEach(vip => {
const scoreText = vip.score ? ` (匹配度: ${vip.score}分)` : '';
message += `- ${vip.name} (${vip.phone})${scoreText}<br>`;
});
} else if (matchedVIP && bestMatchScore > 0) {
// 如果有低分数匹配,也显示出来
message += `<br><br><strong>相似手机号的VIP客户:</strong><br>`;
message += `- ${matchedVIP.customer_name} (${matchedVIP.phone}) (匹配度: ${bestMatchScore}分)<br>`;
}
tipDiv.innerHTML = message;
tipDiv.className = 'phone-vip-tip suggestion';
tipDiv.style.display = 'block';
}
// 增强的调试日志
console.log('--- 结束检查手机号 ---');
console.log('检查总结: 输入手机号=', phone, '标准化后=', normalizedInput, 'VIP客户总数=', window.allVIPCustomers.length, '有效匹配=', isEffectiveMatch);
// 记录到调试面板,包含更详细的信息
logSearchToDebugPanel(phone, matchedVIP, potentialMatches, {
normalizedInput,
matchScore: bestMatchScore,
isEffectiveMatch,
vipCount: window.allVIPCustomers.length
});
}
// VIP客户搜索调试面板
/*
* VIP客户搜索调试面板 - 默认隐藏
*
* 开启方式:
* 1. 页面加载时默认隐藏,点击右上角"显示"按钮可开启调试面板
* 2. 或在控制台执行: document.getElementById('vip-debug-panel').style.display = 'block'
* 3. 或修改上方style.cssText中的'display: none'为'display: block'使其默认显示
*
* 使用方式:
* 1. 查看VIP数据加载状态(成功/失败/数量)
* 2. 查看每次搜索的详细历史记录,包括输入手机号、匹配结果和潜在匹配
* 3. 点击"隐藏"按钮可关闭面板,再次点击"显示"可重新打开
*/
function setupVIPDebugPanel() {
// 检查是否已存在调试面板
if (document.getElementById('vip-debug-panel')) return;
// 创建调试面板HTML
const debugPanel = document.createElement('div');
debugPanel.id = 'vip-debug-panel';
debugPanel.style.cssText = `
position: fixed;
top: 10px;
right: 10px;
width: 350px;
max-height: 400px;
background: #f8f9fa;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 10000;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
display: none;
`;
debugPanel.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<h4 style="margin: 0; font-size: 14px;">VIP客户搜索调试面板</h4>
<button id="toggle-vip-debug" style="padding: 2px 8px; font-size: 10px;">显示</button>
</div>
<div id="debug-status" style="margin-bottom: 10px; padding: 5px; background: #e3f2fd; border-radius: 4px;">
VIP数据加载状态: <span id="vip-data-status">未加载</span>
</div>
<div id="debug-search-history" style="max-height: 250px; overflow-y: auto;">
<p style="color: #666; font-style: italic; text-align: center;">搜索历史将显示在这里</p>
</div>
`;
// 添加到页面
document.body.appendChild(debugPanel);
// 注: 调试面板默认隐藏,可通过点击右上角"显示"按钮打开
// 添加切换显示/隐藏功能
document.getElementById('toggle-vip-debug').addEventListener('click', function() {
const panel = document.getElementById('vip-debug-panel');
const button = document.getElementById('toggle-vip-debug');
if (panel.style.display === 'none') {
panel.style.display = 'block';
button.textContent = '隐藏';
} else {
panel.style.display = 'none';
button.textContent = '显示';
}
});
}
// 更新调试面板状态
function updateDebugStatus(status) {
const statusElem = document.getElementById('vip-data-status');
if (statusElem) {
statusElem.textContent = status;
// 设置不同状态的颜色
if (status.includes('成功')) {
statusElem.style.color = 'green';
} else if (status.includes('错误') || status.includes('未加载')) {
statusElem.style.color = 'red';
} else {
statusElem.style.color = 'black';
}
}
}
// 记录搜索历史到调试面板
function logSearchToDebugPanel(phone, result, potentialMatches, searchInfo = {}) {
const historyElem = document.getElementById('debug-search-history');
if (!historyElem) return;
const timestamp = new Date().toLocaleTimeString();
const { normalizedInput, matchScore, isEffectiveMatch, vipCount } = searchInfo;
const logEntry = document.createElement('div');
logEntry.style.cssText = 'border-bottom: 1px solid #eee; padding: 8px 0; font-size: 12px;';
// 构建日志内容
let logContent = `
<div style="color: #666; font-weight: bold;">[${timestamp}] 搜索: ${phone}</div>
`;
// 添加详细信息
if (normalizedInput) {
logContent += `
<div style="color: #6c757d; margin-left: 10px; font-size: 11px;">标准化: ${normalizedInput}</div>
`;
}
if (vipCount !== undefined) {
logContent += `
<div style="color: #6c757d; margin-left: 10px; font-size: 11px;">数据库中VIP客户数: ${vipCount}</div>
`;
}
// 显示匹配结果
let resultHtml = '<span style="color: red;">未匹配</span>';
if (isEffectiveMatch !== undefined) {
if (isEffectiveMatch && result) {
resultHtml = `<span style="color: green;">匹配成功: ${result.customer_name} (${result.phone})</span>`;
if (matchScore !== undefined) {
logContent += `
<div>结果: ${resultHtml}</div>
<div style="color: #28a745; margin-left: 10px; font-size: 11px;">匹配度: ${matchScore}分</div>
`;
} else {
logContent += `
<div>结果: ${resultHtml}</div>
`;
}
} else {
if (result && matchScore !== undefined) {
resultHtml = `<span style="color: orange;">相似匹配: ${result.customer_name} (${result.phone}) (匹配度: ${matchScore}分)</span>`;
}
logContent += `
<div>结果: ${resultHtml}</div>
`;
}
} else if (result) {
// 兼容旧版本调用
resultHtml = `<span style="color: green;">匹配成功: ${result.customer_name} (${result.phone})</span>`;
logContent += `
<div>结果: ${resultHtml}</div>
`;
} else {
logContent += `
<div>结果: ${resultHtml}</div>
`;
}
// 如果有潜在匹配,也显示出来
if (potentialMatches && potentialMatches.length > 0) {
let potentialHtml = '<div style="margin-top: 2px; font-size: 11px; color: #666;">潜在匹配: ';
potentialHtml += potentialMatches.slice(0, 2).map(v => {
const scoreText = v.score ? ` (${v.score}分)` : '';
return `${v.name} (${v.phone})${scoreText}`;
}).join(', ');
if (potentialMatches.length > 2) potentialHtml += '...';
potentialHtml += '</div>';
logContent += potentialHtml;
}
logEntry.innerHTML = logContent;
// 添加到历史的顶部
if (historyElem.firstChild) {
historyElem.insertBefore(logEntry, historyElem.firstChild);
} else {
historyElem.appendChild(logEntry);
}
// 限制历史记录数量
while (historyElem.children.length > 20) {
historyElem.removeChild(historyElem.lastChild);
}
}
// 在页面加载时初始化调试面板
document.addEventListener('DOMContentLoaded', function() {
setupVIPDebugPanel();
});
// 切换到VIP模式
function switchToVIPMode(vipId = null) {
// 切换到VIP客户模式
document.getElementById('customer_type').value = 'vip';
handleCustomerTypeChange();
// 如果提供了VIP ID,选择该VIP客户
if (vipId) {
// 先等待VIP客户列表加载完成
setTimeout(() => {
document.getElementById('vip_id').value = vipId;
loadVIPInfo();
}, 500);
}
// 隐藏提示
document.getElementById('phone_vip_tip').style.display = 'none';
}
// 处理客户类型变更
function handleCustomerTypeChange() {
const customerType = document.getElementById('customer_type').value;
const vipSelectGroup = document.getElementById('vip_select_group');
const newCustomerFields = document.getElementById('new_customer_fields');
console.log('切换客户类型:', customerType);
if (customerType === 'vip') {
vipSelectGroup.style.display = 'block';
newCustomerFields.style.display = 'none';
// 隐藏新客户字段的必填属性
document.getElementById('customer_name').required = false;
document.getElementById('phone').required = false;
// 确保VIP客户数据已加载
if (window.allVIPCustomers.length === 0) {
console.log('VIP客户数据未加载,开始加载...');
loadVIPCustomers().then(() => {
console.log('VIP客户数据加载完成,可进行搜索和选择');
}).catch(error => {
console.error('加载VIP数据失败:', error);
});
}
} else {
vipSelectGroup.style.display = 'none';
newCustomerFields.style.display = 'flex';
// 显示新客户字段的必填属性
document.getElementById('customer_name').required = true;
document.getElementById('phone').required = true;
// 清除VIP选择
const vipSelect = document.getElementById('vip_id');
if (vipSelect) {
vipSelect.value = '';
}
// 清除搜索结果
const searchResultsDiv = document.getElementById('vip_search_results');
const searchInput = document.getElementById('vip_search');
if (searchResultsDiv) {
searchResultsDiv.innerHTML = '';
searchResultsDiv.style.display = 'none';
}
if (searchInput) {
searchInput.value = '';
}
}
updateSubmitButton();
}
// 加载VIP客户信息
function loadVIPInfo() {
const vipId = document.getElementById('vip_id').value;
if (vipId) {
// 这里将从数据库获取VIP客户详细信息
fetch(`get_vip_customer.php?id=${vipId}`)
.then(response => response.json())
.then(vip => {
if (vip.car_model) {
document.getElementById('car_model').value = vip.car_model;
}
if (vip.car_number) {
document.getElementById('car_number').value = vip.car_number;
}
})
.catch(error => {
console.log('加载VIP信息失败:', error);
});
// 获取并显示VIP客户最近预约记录
fetchLastBookingInfo(vipId);
} else {
// 如果没有选择VIP客户,隐藏上次预约信息区域
document.getElementById('vip_last_booking_info').style.display = 'none';
}
updateSubmitButton();
}
// 获取并显示VIP客户最近预约记录
function fetchLastBookingInfo(vipId) {
const vipName = document.getElementById('vip_id').options[document.getElementById('vip_id').selectedIndex].text;
fetch(`get_vip_last_booking.php?vip_id=${vipId}`)
.then(response => response.json())
.then(data => {
const infoContainer = document.getElementById('vip_last_booking_info');
const contentContainer = document.getElementById('last_booking_content');
infoContainer.style.display = 'block';
if (data.has_booking) {
// 有预约记录,显示上次预约信息
contentContainer.innerHTML = `
<p style="margin-bottom: 8px;"><strong>👤 ${vipName}</strong></p>
<p>📅 <strong>预约时间:</strong> ${data.appointment_date} ${data.appointment_time}</p>
<p>💼 <strong>套餐类型:</strong> ${data.package_name}</p>
<p>⏱️ <strong>施工时长:</strong> ${data.duration} 分钟</p>
`;
} else {
// 没有预约记录,显示首次到店提示
contentContainer.innerHTML = `
<p style="margin-bottom: 8px;"><strong>👤 ${vipName}</strong></p>
<p>这是该VIP客户的首次预约,请为客户提供优质服务体验。</p>
`;
}
})
.catch(error => {
console.error('获取上次预约记录失败:', error);
// 即使失败,也显示一个友好的提示
const infoContainer = document.getElementById('vip_last_booking_info');
const contentContainer = document.getElementById('last_booking_content');
infoContainer.style.display = 'block';
contentContainer.innerHTML = `<p style="color: #dc3545;">无法获取预约记录,请稍后重试。</p>`;
});
}
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.dataset.time = timeString; // 添加数据属性,与pending_bookings.php保持一致
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 (booking.is_cross_day) {
// 如果是跨天预约的第二天记录,所有时间都应该是已被预约的(从00:00到结束时间)
if (time < bookingEnd) {
return true; // 该时间段已被预约
}
} else {
// 正常预约检查
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) => {
// 处理跨天预约的显示
let displayStartTime = booking.start_time;
let displayEndTime = booking.end_time;
let crossDayLabel = '';
if (booking.is_cross_day) {
// 对于跨天预约的第二天记录,显示为从00:00开始
displayStartTime = '00:00';
crossDayLabel = ' <small style="color: #ff6b6b;">(跨天预约延续)</small>';
}
details += `
<div class="booking-detail-item">
<div class="booking-time">${displayStartTime} - ${displayEndTime}${crossDayLabel}</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');
});
// 使用data-time属性查找元素,与pending_bookings.php保持一致
const slotElement = document.querySelector(`[data-time="${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);
const maxDuration = 720; // 最大时长为12小时(720分钟)
if (customDuration < 30) {
alert('自定义时长不能少于30分钟');
} else if (customDuration > maxDuration) {
alert('自定义时长不能超过12小时');
} else {
selectDuration(customDuration);
}
}
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);
// 设置价格输入字段的值为套餐价格
document.getElementById('package_price').value = package.price;
document.getElementById('total_price').value = package.price;
console.log('已设置价格:', package.price);
}
} else {
packageInfoDiv.style.display = 'none';
document.getElementById('package_price').value = 0;
document.getElementById('total_price').value = 0;
}
updateSubmitButton();
}
function updateSubmitButton() {
const submitBtn = document.getElementById('submitBtn');
const customerType = document.getElementById('customer_type').value;
// 根据客户类型进行不同的验证
let isFormValid;
if (customerType === 'vip') {
// VIP模式:验证VIP选择和其他必要字段,但不验证新客户字段
isFormValid = selectedDate && selectedTime &&
document.getElementById('vip_id').value &&
document.getElementById('car_model').value &&
document.getElementById('car_number').value &&
document.getElementById('package_id').value &&
document.getElementById('total_price').value;
} else {
// 新客户模式:验证所有必填字段
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 &&
document.getElementById('total_price').value;
}
submitBtn.disabled = !isFormValid;
console.log('更新提交按钮状态:', 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>