2c5ac2b943
为所有页面卡片添加 enhanced-card 类,实现统一视觉风格 添加卡片悬停动画、顶部装饰条和状态变化效果 优化状态标签、按钮和详情布局的样式
2226 lines
106 KiB
PHP
2226 lines
106 KiB
PHP
<?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 = isset($_POST['customer_type']) ? $_POST['customer_type'] : '';
|
||
if (!in_array($customer_type, ['vip', 'new'])) {
|
||
throw new Exception('无效的客户类型');
|
||
}
|
||
|
||
$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'];
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:28', 'message' => 'VIP customer data', 'data' => ['vip_id' => $vip_id, 'has_car_model' => isset($vip_customer['car_model']), 'has_car_number' => isset($vip_customer['car_number'])], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'A']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
// VIP客户信息优先,但允许通过POST覆盖(如果用户想修改)
|
||
$car_model = isset($_POST['car_model']) && trim($_POST['car_model']) ? trim($_POST['car_model']) : (isset($vip_customer['car_model']) && $vip_customer['car_model'] ? $vip_customer['car_model'] : '');
|
||
$car_number = isset($_POST['car_number']) && trim($_POST['car_number']) ? trim($_POST['car_number']) : (isset($vip_customer['car_number']) && $vip_customer['car_number'] ? $vip_customer['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'] ?? 0);
|
||
$custom_services = trim($_POST['custom_services'] ?? '');
|
||
$appointment_date = $_POST['appointment_date'] ?? '';
|
||
$appointment_time = $_POST['appointment_time'] ?? '';
|
||
$duration = (int)($_POST['duration'] ?? 60);
|
||
$notes = trim($_POST['notes'] ?? '');
|
||
|
||
// 验证member_type和source
|
||
$allowed_member_types = ['普通客户', 'VIP会员'];
|
||
$member_type = isset($_POST['member_type']) && in_array($_POST['member_type'], $allowed_member_types) ? $_POST['member_type'] : '普通客户';
|
||
|
||
$allowed_sources = ['抖音', '微信', '快手', '朋友介绍', '其他'];
|
||
$source = isset($_POST['source']) && in_array($_POST['source'], $allowed_sources) ? $_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('请选择一个套餐');
|
||
}
|
||
|
||
// 验证日期和时间格式
|
||
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $appointment_date)) {
|
||
throw new Exception('预约日期格式不正确');
|
||
}
|
||
if (!preg_match('/^\d{2}:\d{2}$/', $appointment_time)) {
|
||
throw new Exception('预约时间格式不正确');
|
||
}
|
||
|
||
// 计算预约时间范围
|
||
$start_time = $appointment_date . ' ' . $appointment_time . ':00';
|
||
$start_timestamp = strtotime($start_time);
|
||
if ($start_timestamp === false) {
|
||
throw new Exception('预约时间无效,请检查日期和时间');
|
||
}
|
||
$end_time = date('Y-m-d H:i:s', $start_timestamp + $duration * 60);
|
||
|
||
// 验证结束时间是否有效
|
||
if ($end_time === false) {
|
||
throw new Exception('计算结束时间失败');
|
||
}
|
||
|
||
// 检查时间冲突
|
||
// 两个时间段重叠的条件:现有预约的开始时间 < 新预约的结束时间 AND 现有预约的结束时间 > 新预约的开始时间
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:98', 'message' => 'Checking time conflict', 'data' => ['start_time' => $start_time, 'end_time' => $end_time, 'duration' => $duration], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'F']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings
|
||
WHERE status != '已取消'
|
||
AND start_time < ?
|
||
AND end_time > ?");
|
||
$stmt->execute([$end_time, $start_time]);
|
||
$conflict_count = $stmt->fetchColumn();
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:107', 'message' => 'Time conflict check result', 'data' => ['conflict_count' => $conflict_count], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'F']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
|
||
if ($conflict_count > 0) {
|
||
throw new Exception('该时间段已被预约,请选择其他时间');
|
||
}
|
||
|
||
// 检查客户是否有历史预约记录,如果有则自动转为普通客户
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:136', 'message' => 'Checking customer booking history', 'data' => ['phone' => $phone, 'car_number' => $car_number, 'current_member_type' => $member_type], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'I']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
// 检查该手机号和车牌号组合是否有历史预约记录
|
||
$stmt = $pdo->prepare("SELECT COUNT(*) FROM bookings
|
||
WHERE phone = ? AND car_number = ?");
|
||
$stmt->execute([$phone, $car_number]);
|
||
$has_booking_history = $stmt->fetchColumn() > 0;
|
||
|
||
// 如果客户有历史预约记录,自动转为普通客户(除非是VIP客户模式)
|
||
if ($has_booking_history && $member_type === 'VIP会员' && $customer_type !== 'vip') {
|
||
$member_type = '普通客户';
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:144', 'message' => 'Auto converted to regular customer', 'data' => ['reason' => 'has_booking_history', 'phone' => $phone, 'car_number' => $car_number], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'I']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
}
|
||
|
||
// 插入预约记录
|
||
// 对于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) {
|
||
$booking_date = $booking['date']; // 使用不同的变量名避免覆盖外层$start_date
|
||
$booking_start_time = $booking['start_time'];
|
||
$booking_end_time = $booking['end_time'];
|
||
|
||
// 将预约添加到开始日期
|
||
$bookings_by_date[$booking_date][] = $booking;
|
||
|
||
// 检查是否是跨天预约(结束时间早于开始时间,表示跨天)
|
||
// 注意:这里比较的是时间字符串(HH:MM格式),需要转换为可比较的格式
|
||
$start_timestamp = strtotime($booking_start_time);
|
||
$end_timestamp = strtotime($booking_end_time);
|
||
|
||
// #region agent log
|
||
$log_data = json_encode(['location' => 'index.php:196', 'message' => 'Checking cross-day booking', 'data' => ['booking_date' => $booking_date, 'start_time' => $booking_start_time, 'end_time' => $booking_end_time, 'start_ts' => $start_timestamp, 'end_ts' => $end_timestamp], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'G']);
|
||
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
|
||
// #endregion
|
||
|
||
if ($start_timestamp !== false && $end_timestamp !== false && $end_timestamp < $start_timestamp) {
|
||
// 计算第二天的日期
|
||
$next_date = date('Y-m-d', strtotime($booking_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">
|
||
<script src="mobile-nav.js" defer></script>
|
||
|
||
<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">
|
||
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()" aria-label="菜单">
|
||
<span></span>
|
||
<span></span>
|
||
<span></span>
|
||
</button>
|
||
<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>
|
||
|
||
<!-- 移动端导航菜单 -->
|
||
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
|
||
<nav class="mobile-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>
|
||
|
||
<?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 enhanced-card" 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 enhanced-card" 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 enhanced-card" style="display: none;">
|
||
<h3 id="detailsDateTitle"></h3>
|
||
<div id="bookingDetailsContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="booking-form-section enhanced-card">
|
||
<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 || '';
|
||
|
||
// 转义HTML和引号,防止XSS和JS语法错误
|
||
const escapeHtml = (str) => {
|
||
const div = document.createElement('div');
|
||
div.textContent = str;
|
||
return div.innerHTML;
|
||
};
|
||
const escapeJs = (str) => {
|
||
return str.replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\\/g, '\\\\');
|
||
};
|
||
|
||
const escapedId = escapeJs(id.toString());
|
||
const escapedName = escapeJs(name);
|
||
const escapedPhone = escapeJs(phone);
|
||
|
||
// 高亮处理(先转义HTML,再进行高亮)
|
||
const escapedNameHtml = escapeHtml(name);
|
||
const escapedPhoneHtml = escapeHtml(phone);
|
||
const escapedTerm = escapeHtml(term);
|
||
const highlightedName = escapedNameHtml.replace(new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<span class="highlight">$1</span>');
|
||
const highlightedPhone = escapedPhoneHtml.replace(new RegExp(`(${escapedTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'), '<span class="highlight">$1</span>');
|
||
|
||
const escapedCarModel = escapeHtml(carModel);
|
||
const escapedCarNumber = escapeHtml(carNumber);
|
||
|
||
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">${escapedCarModel} ${escapedCarNumber}</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>
|