17 Commits

Author SHA1 Message Date
wsh5485 2c0e7cbb5f style: 将容器最大宽度从1200px调整为1400px
统一修改多个页面容器的最大宽度,以提供更宽敞的布局空间
2025-12-12 16:56:04 +08:00
wsh5485 2c5ac2b943 style(UI): 统一卡片样式并增强视觉效果
为所有页面卡片添加 enhanced-card 类,实现统一视觉风格
添加卡片悬停动画、顶部装饰条和状态变化效果
优化状态标签、按钮和详情布局的样式
2025-12-12 03:53:53 +08:00
wsh5485 c64651d6c7 feat(移动端): 添加移动端导航菜单并优化响应式设计
refactor(预约统计): 修改查询逻辑只计算有效预约
2025-12-12 03:39:01 +08:00
wsh5485 9cd2b31648 feat(会员系统): 添加从预约记录转换为VIP客户功能
- 在bookings.php中添加转换为VIP客户的按钮
- 在vip.php中实现从预约记录转换VIP客户的逻辑
- 添加VIP客户转换界面和批量转换功能
- 新增get_package.php用于获取套餐信息
- 优化会员类型自动转换逻辑
2025-12-12 03:11:12 +08:00
wsh5485 89a22c7b11 fix: 确保日志目录存在以避免写入日志失败
在多个文件中添加了检查并创建日志目录的逻辑,防止因目录不存在而导致日志写入失败
2025-12-12 02:50:40 +08:00
wsh5485 5438b944b8 fix(预约系统): 增强日期时间验证并修复跨天预约处理
- 添加日期和时间格式的正则验证
- 改进时间有效性检查逻辑,防止无效时间输入
- 修复跨天预约处理中的变量命名冲突问题
- 优化时间冲突检测SQL查询条件
- 增加XSS防护措施,对VIP客户搜索结果显示进行转义
2025-12-12 02:45:53 +08:00
wsh5485 ae557aa5c2 fix: 增强表单提交的数据验证和冲突检查逻辑
- 添加客户类型验证,只允许'vip'或'new'
- 优化VIP客户信息处理逻辑,允许通过表单覆盖
- 为所有输入字段添加默认值和trim处理
- 添加会员类型和来源渠道的验证
- 简化时间冲突检查逻辑并添加调试日志
- 修复空值可能导致的问题
2025-12-12 02:42:51 +08:00
wsh5485 905bbc5934 feat: 添加调试日志并改进数据库处理逻辑
- 在多个文件中添加调试日志记录功能
- 将数据库连接统一迁移到db_connect.php
- 改进预约时间冲突检测逻辑
- 优化VIP客户数据处理
- 增强套餐查询的健壮性
- 更新预约状态处理流程
2025-12-12 02:38:16 +08:00
wsh5485 58fbb9f1e1 feat(预约系统): 添加套餐专属预约信息功能并优化复制逻辑
- 在SQL查询中添加package_reminder字段
- 重构复制按钮使用data属性存储预约信息
- 实现套餐专属预约信息的单独保存功能
- 优化复制功能以包含套餐专属提醒信息
- 添加套餐专属预约信息的实时编辑功能
2025-12-12 01:06:36 +08:00
wsh5485 31899dfea8 feat(套餐管理): 添加套餐专属预约信息字段
在套餐添加和编辑功能中新增package_reminder字段,用于存储套餐专属预约信息
2025-12-11 18:39:48 +08:00
wsh5485 a6c48fc681 fix(bookings): 修复客服备注功能中的数据库连接和转义问题
将$conn改为$pdo以使用正确的数据库连接
移除addslashes函数调用,仅使用htmlspecialchars进行转义
2025-12-09 17:13:52 +08:00
wsh5485 a714f0a526 feat(预约系统): 添加客服备注功能及样式
- 在公告页面和预约管理页面添加客服备注功能
- 为客服备注添加专用样式区分于客户备注
- 实现通过模态框编辑和保存客服备注的AJAX功能
- 将原备注字段明确标记为客户备注
2025-12-09 16:57:03 +08:00
wsh5485 b963c2b513 feat: 新增预约系统功能及优化
- 添加获取每日预约时长的API接口
- 实现0元订单自动标记为已付款功能
- 优化预约信息复制功能,增加服务时长和备注
- 新增预约信息模板系统
- 在待处理预约页面添加时长提示功能
- 优化移动端触摸反馈和倒计时显示
2025-12-06 05:05:15 +08:00
wsh5485 a672e1d7bc feat(预约管理): 添加预约信息复制功能和改进待处理预约界面
- 在 bookings.php 中添加复制预约信息按钮和功能
- 重构 pending_bookings.php 界面,添加预约记录选择器
- 优化待处理预约的显示逻辑,默认高亮第一条记录
- 移除不再使用的复制信息相关代码
2025-12-06 04:16:38 +08:00
wsh5485 1f0cf4acaa fix: 排除已取消的订单在待处理预约查询中
修改SQL查询条件,添加status != '已取消'过滤,确保已取消的订单不会出现在待处理预约列表中
2025-12-06 03:25:56 +08:00
wsh5485 26dd84bea2 docs: 统一导航菜单中"待处理预约"的文案
将多个页面导航菜单中的"待预约处理"统一修改为"待处理预约",保持文案一致性
2025-12-06 03:19:18 +08:00
wsh5485 a372464299 docs: 添加系统功能文档VERSION_FEATURES.md
添加详细的系统功能文档,包含核心功能模块介绍、技术实现细节、系统架构、界面优化、安装配置说明和使用指南
2025-12-06 03:09:41 +08:00
16 changed files with 3597 additions and 569 deletions
+170
View File
@@ -0,0 +1,170 @@
# 张老师撸车工作室 - 洗车预约管理系统
## 版本功能汇总
### 1. 核心预约管理系统
**功能特点:**
- **普通客户/VIP客户区分预约**:支持选择普通客户或VIP客户进行预约
- **套餐选择**:可选择预设的洗车套餐
- **时间冲突检测**:自动检查并避免预约时间冲突
- **预约状态管理**:支持待确认、已确认、进行中、已完成、已取消等状态
- **预约记录查询与筛选**:可按状态筛选和搜索预约记录
- **预约时间更新**:支持修改已预约的时间
- **付款状态管理**:可标记预约的付款状态(已付款/未付款)
**技术实现:**
- 基于PHP和MySQL开发
- 使用PDO进行数据库操作
- 时间冲突算法确保资源合理分配
- 表单验证和错误处理机制
### 2. 套餐管理系统
**功能特点:**
- **套餐CRUD操作**:支持添加、编辑、删除和查看套餐
- **套餐内容管理**:可定义套餐名称、描述、基础时长、价格和包含的服务项目
- **套餐状态控制**:可启用/禁用套餐
- **服务项目管理**:支持添加多个服务项目到套餐中
- **可视化套餐列表**:卡片式展示,包含套餐详细信息
**界面优化:**
- 现代化卡片式设计
- 服务项目标签展示
- 悬停效果和微动画
- 响应式设计支持移动端
### 3. VIP客户管理系统
**功能特点:**
- **VIP客户信息录入**:支持添加VIP客户基本信息
- **VIP客户查询与管理**:可查看、编辑和删除VIP客户
- **VIP客户识别**:基于手机号和车牌号组合唯一识别
- **VIP客户预约**:支持快速选择VIP客户进行预约
- **数据验证**:防止重复录入VIP客户
**技术实现:**
- 复合唯一索引确保数据完整性
- 表单验证和错误处理
- VIP客户信息自动填充功能
### 4. 待预约处理系统(WPS表单集成)
**功能特点:**
- **WPS表单数据同步**:自动获取WPS表单提交的预约请求
- **预约转换功能**:可将WPS表单提交转换为正式预约
- **套餐选择与价格计算**:支持为WPS表单提交选择套餐和计算价格
- **时间选择与冲突检测**:转换时自动检测时间冲突
- **状态管理**:标记已处理的表单提交
**技术实现:**
- 数据库表设计支持WPS表单数据存储
- 预约转换逻辑确保数据完整性
- 错误处理和日志记录
### 5. 公告与待办列表系统
**功能特点:**
- **今日预约概览**:展示当天所有预约记录
- **状态统计**:显示待处理和已完成的预约数量
- **日期选择**:支持查看不同日期的预约记录
- **预约详情展示**:包含客户信息、套餐、时间和服务项目
- **待办事项管理**:直观展示需要处理的预约
**界面优化:**
- 响应式设计支持移动端
- 统计卡片展示关键指标
- 时间线式预约列表
- 简洁明了的状态标识
### 6. 系统架构与技术栈
**技术栈:**
- **后端语言**PHP 7+
- **数据库**MySQL
- **前端技术**HTML5, CSS3, JavaScript, jQuery
- **数据库连接**PDO (PHP Data Objects)
- **版本控制**Git
**系统特点:**
- 模块化设计,功能分离清晰
- 数据库设计合理,关系明确
- 安全性考虑(输入验证、SQL注入防护)
- 响应式设计,支持桌面和移动设备
- 用户友好的界面和交互体验
### 7. 界面与用户体验优化
**优化内容:**
- **现代化UI设计**:卡片式布局、渐变效果、阴影处理
- **响应式设计**:适配不同屏幕尺寸
- **动画效果**:页面加载淡入、元素过渡动画
- **表单体验**:统一的表单样式、输入验证、占位符提示
- **交互反馈**:按钮悬停效果、操作成功/失败提示
**移动端适配:**
- 响应式布局调整
- 触摸友好的元素大小
- 优化的移动端表单
- 适配不同移动设备
## 版本信息
**当前版本:** v1.0.0
**发布日期:** 2024年
**主要改进:**
1. 完整的预约管理流程
2. 套餐管理功能
3. VIP客户管理系统
4. WPS表单集成
5. 响应式界面设计
6. 用户体验优化
## 系统要求
- PHP 7.0+
- MySQL 5.6+
- Web服务器(Apache/Nginx
- 支持PDO的PHP环境
- 浏览器支持:Chrome、Firefox、Safari、Edge (最新版本)
## 安装与配置
1. 导入数据库脚本 `merged_db.sql`
2. 配置数据库连接信息 `config.php`
3. 将项目部署到Web服务器
4. 访问系统首页开始使用
## 使用说明
1. **添加套餐**:在套餐管理页面添加洗车套餐
2. **管理VIP客户**:录入和管理VIP客户信息
3. **创建预约**:在首页选择客户类型和套餐,填写预约信息
4. **处理待预约**:在待预约页面处理WPS表单提交
5. **查看公告**:在公告页面查看今日待办和统计信息
## 功能流程图
1. **预约流程**:选择客户类型 → 填写/选择客户信息 → 选择套餐 → 选择时间 → 确认预约
2. **套餐管理**:添加套餐 → 编辑套餐 → 启用/禁用套餐 → 查看套餐列表
3. **VIP管理**:录入VIP客户 → 查询/编辑VIP信息 → 使用VIP快速预约
4. **WPS表单处理**:接收表单提交 → 转换为预约 → 标记为已处理
## 数据统计与分析
系统支持通过数据库查询获取以下统计数据:
- 每日/每周/每月预约量
- 不同套餐的销售情况
- VIP客户数量和活跃度
- 客户来源分析
## 未来计划
1. 增加支付功能集成
2. 实现短信通知系统
3. 添加数据可视化图表
4. 完善会员积分系统
5. 支持多门店管理
6. 移动端App开发
+84 -37
View File
@@ -60,10 +60,11 @@ try {
<meta name="keywords" content="公告,今日待办,预约列表,洗车管理">
<title>张老师撸车(私家车库)工作室</title>
<link rel="stylesheet" href="style.css">
<script src="mobile-nav.js" defer></script>
<style>
/* 公告页面特有样式 */
.announcement-container {
max-width: 1200px;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
@@ -245,6 +246,29 @@ try {
font-size: 0.9rem;
}
/* 客服备注样式 */
.service-notes {
margin-top: 12px;
padding: 12px;
background-color: #f8f9fa;
border-left: 4px solid #007bff;
border-radius: 4px;
}
.service-notes h4 {
margin-top: 0;
margin-bottom: 8px;
font-size: 14px;
color: #007bff;
}
.service-notes p {
margin: 0;
font-size: 13px;
color: #495057;
line-height: 1.5;
}
.no-bookings {
text-align: center;
padding: 60px 20px;
@@ -274,26 +298,42 @@ try {
<body>
<div class="container">
<header class="header">
<button class="mobile-menu-toggle" onclick="toggleMobileMenu()">
<span></span>
<span></span>
<span></span>
</button>
<h1>🚗 张老师撸车工作室 - 今日待办</h1>
<nav class="nav">
<a href="index.php" class="nav-link">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="pending_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 active">今日待办</a>
</nav>
</header>
<!-- 移动端导航菜单 -->
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
<nav class="mobile-nav">
<a href="index.php" class="nav-link">预约洗车</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 active">今日待办</a>
</nav>
<div class="announcement-container">
<!-- 页面标题区 -->
<div class="page-header">
<div class="page-header enhanced-card">
<h1>📋 待办列表</h1>
<div class="subtitle"><?php echo date('Y年m月d日', strtotime($selected_date)); ?> - <?php echo date('l', strtotime($selected_date)); ?></div>
</div>
<!-- 日期选择器 -->
<div style="background: white; border-radius: 10px; padding: 20px; margin-bottom: 30px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); text-align: center;">
<div class="enhanced-card" style="text-align: center; padding: 20px; margin-bottom: 30px;">
<form method="GET" style="display: flex; justify-content: center; align-items: center; gap: 15px; flex-wrap: wrap;">
<label for="selected_date" style="font-weight: 600; color: #333;">选择日期:</label>
<input type="date" id="selected_date" name="selected_date" value="<?php echo $selected_date; ?>" style="padding: 10px 15px; border: 2px solid #e0e0e0; border-radius: 5px; font-size: 1rem; width: 200px; transition: border-color 0.3s ease;">
@@ -302,7 +342,7 @@ try {
</div>
<!-- 统计信息卡片 -->
<div class="stats-card">
<div class="stats-card enhanced-card">
<div class="stat-item stat-pending">
<div class="stat-label">待处理</div>
<div class="stat-number"><?php echo $pending_count; ?></div>
@@ -353,48 +393,45 @@ try {
if (!empty($booking['services'])) {
$services = explode(',', $booking['services']);
}
if (!empty($booking['custom_services'])) {
$services[] = $booking['custom_services'];
}
?>
<div class="booking-card <?php echo $card_class; ?>">
<div class="enhanced-card booking-card <?php echo $card_class; ?>">
<div class="booking-time">
🕒 <?php echo date('H:i', strtotime($booking['start_time'])); ?> - <?php echo date('H:i', strtotime($booking['end_time'])); ?>
<span class="booking-status status-<?php echo strtolower($booking['status']); ?>">
<span class="status-badge status-<?php echo strtolower($booking['status']); ?>">
<?php echo $booking['status']; ?>
</span>
</div>
<div class="booking-details">
<div class="detail-row">
<div class="booking-details details-grid">
<div class="detail-item">
<span class="detail-label">客户姓名</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['customer_name']); ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">联系电话</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['phone']); ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">车型</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['car_model']); ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">车牌号</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['car_number']); ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">套餐</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['package_name'] ?: '自定义套餐'); ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">时长</span>
<span class="detail-value"><?php echo $booking['duration']; ?> 分钟</span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">价格</span>
<span class="detail-value">¥<?php echo $booking['total_price']; ?></span>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">会员类型</span>
<span class="detail-value"><?php echo $booking['member_type']; ?></span>
</div>
@@ -410,11 +447,19 @@ try {
</ul>
</div>
<?php endif; ?>
<!-- 客服备注 -->
<?php if (!empty($booking['custom_services'])): ?>
<div class="service-notes">
<h4>客服备注:</h4>
<p><?php echo htmlspecialchars($booking['custom_services']); ?></p>
</div>
<?php endif; ?>
<!-- 备注信息 -->
<?php if (!empty($booking['notes'])): ?>
<div class="detail-row">
<span class="detail-label">备注</span>
<span class="detail-label">客户备注</span>
<span class="detail-value"><?php echo htmlspecialchars($booking['notes']); ?></span>
</div>
<?php endif; ?>
@@ -430,18 +475,19 @@ try {
// 移动端优化脚本
document.addEventListener('DOMContentLoaded', function() {
// 为按钮添加触摸反馈
const buttons = document.querySelectorAll('.btn');
buttons.forEach(btn => {
var buttons = document.querySelectorAll('.btn');
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
btn.addEventListener('touchstart', function() {
this.style.transform = 'translateY(1px)';
});
btn.addEventListener('touchend', function() {
this.style.transform = 'translateY(-2px)';
});
});
}
// 检测设备类型
const isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
var isMobile = /Mobi|Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
if (isMobile) {
document.body.classList.add('mobile-device');
}
@@ -452,8 +498,8 @@ try {
}, 5 * 60 * 1000);
// 将倒计时添加到页面标题旁边
const pageHeader = document.querySelector('.page-header');
const refreshInfo = document.createElement('div');
var pageHeader = document.querySelector('.page-header');
var refreshInfo = document.createElement('div');
refreshInfo.style.cssText = 'font-size:0.9rem;color:#666;margin-top:5px;';
if (pageHeader) {
@@ -466,28 +512,29 @@ try {
// 显示北京时间及距离下次刷新倒计时
function updateBeijingTime() {
const now = new Date();
const beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); // 转为北京时间
const hours = String(beijingTime.getUTCHours()).padStart(2, '0');
const minutes = String(beijingTime.getUTCMinutes()).padStart(2, '0');
const seconds = String(beijingTime.getUTCSeconds()).padStart(2, '0');
var now = new Date();
var beijingTime = new Date(now.getTime() + 8 * 60 * 60 * 1000); // 转为北京时间
var hours = String(beijingTime.getUTCHours()).padStart(2, '0');
var minutes = String(beijingTime.getUTCMinutes()).padStart(2, '0');
var seconds = String(beijingTime.getUTCSeconds()).padStart(2, '0');
// 计算距离下次刷新的剩余时间
const refreshInterval = 5 * 60 * 1000; // 5分钟
const elapsed = now.getTime() % refreshInterval;
const remaining = refreshInterval - elapsed;
const remMinutes = Math.floor(remaining / 60000);
const remSeconds = Math.floor((remaining % 60000) / 1000);
var refreshInterval = 5 * 60 * 1000; // 5分钟
var elapsed = now.getTime() % refreshInterval;
var remaining = refreshInterval - elapsed;
var remMinutes = Math.floor(remaining / 60000);
var remSeconds = Math.floor((remaining % 60000) / 1000);
// 更新显示
if (refreshInfo) {
refreshInfo.textContent = `北京时间 ${hours}:${minutes}:${seconds} | 下次刷新 ${remMinutes}分${remSeconds}秒`;
refreshInfo.textContent = '北京时间 ' + hours + ':' + minutes + ':' + seconds + ' | 下次刷新 ' + remMinutes + '分' + remSeconds + '秒';
}
}
// 每秒更新一次
setInterval(updateBeijingTime, 1000);
updateBeijingTime(); // 初始化显示
});
</script>
</body>
+161
View File
@@ -0,0 +1,161 @@
<?php
/**
* 预约信息模板文件
* 此文件用于定义预约成功后发送给客户的信息模板
* 用户可以根据自己的需求修改此模板
*/
// 预约成功信息模板配置
$booking_templates = [
// 基本模板
'basic' => [
'title' => '预约成功!',
'message' => "预约成功!\n\n" .
"客户:{customer_name}\n" .
"手机号:{phone}\n" .
"车牌号:{car_number}\n" .
"车型:{car_model}\n" .
"\n" .
"预约时间:{start_time}\n" .
"服务项目:{package_name}\n" .
"服务时长:{duration}分钟\n" .
"总价:{total_price}元\n" .
"\n" .
"感谢您的预约!"
],
// 详细模板
'detailed' => [
'title' => '【张老师撸车工作室】预约确认',
'message' => "尊敬的 {customer_name} 先生/女士:\n\n" .
"恭喜您,您的洗车预约已成功确认!\n\n" .
"🔹 客户信息:\n" .
" - 姓名:{customer_name}\n" .
" - 手机号:{phone}\n" .
" - 车牌号:{car_number}\n" .
" - 车型:{car_model}\n" .
" - 会员类型:{member_type}\n" .
"\n" .
"🔹 预约详情:\n" .
" - 预约时间:{date} {time_range}\n" .
" - 服务项目:{package_name}\n" .
" - 自定义服务:{custom_services}\n" .
" - 服务时长:{duration}分钟\n" .
" - 总价:¥{total_price}\n" .
" - 支付状态:{payment_status}\n" .
"\n" .
"🔹 备注信息:\n" .
" {notes}\n" .
"\n" .
"🔹 温馨提示:\n" .
" 1. 请提前10分钟到达洗车地点\n" .
" 2. 如需改期或取消,请至少提前2小时联系我们\n" .
" 3. 联系电话:138-0013-8000\n" .
"\n" .
"感谢您选择张老师撸车工作室!\n" .
"祝您用车愉快!🚗✨"
],
// 简洁模板
'simple' => [
'title' => '预约确认',
'message' => "{customer_name},您的洗车预约已确认:\n" .
"时间:{start_time}\n" .
"项目:{package_name}\n" .
"总价:{total_price}元\n" .
"如需调整请联系我们。"
]
];
/**
* 生成预约信息
* @param array $booking_data 预约数据数组
* @param string $template_name 使用的模板名称
* @return string 生成的预约信息
*/
function generateBookingMessage($booking_data, $template_name = 'basic') {
global $booking_templates;
// 检查模板是否存在
if (!isset($booking_templates[$template_name])) {
$template_name = 'basic'; // 默认使用基本模板
}
$template = $booking_templates[$template_name]['message'];
// 定义替换字段
$replacements = [
'{customer_name}' => $booking_data['customer_name'] ?? '未知客户',
'{phone}' => $booking_data['phone'] ?? '未提供',
'{car_model}' => $booking_data['car_model'] ?? '未提供',
'{car_number}' => $booking_data['car_number'] ?? '未提供',
'{member_type}' => $booking_data['member_type'] ?? '普通客户',
'{package_name}' => $booking_data['package_name'] ?? '未选择套餐',
'{custom_services}' => $booking_data['custom_services'] ?? '无',
'{start_time}' => $booking_data['start_time'] ?? '未设置',
'{end_time}' => $booking_data['end_time'] ?? '未设置',
'{date}' => $booking_data['date'] ?? (isset($booking_data['start_time']) ? date('Y年m月d日', strtotime($booking_data['start_time'])) : '未设置'),
'{time_range}' => $booking_data['time_range'] ?? (isset($booking_data['start_time'], $booking_data['end_time']) ? date('H:i', strtotime($booking_data['start_time'])) . ' - ' . date('H:i', strtotime($booking_data['end_time'])) : '未设置'),
'{duration}' => $booking_data['duration'] ?? '未设置',
'{total_price}' => $booking_data['total_price'] ?? '0.00',
'{notes}' => $booking_data['notes'] ?? '无',
'{status}' => $booking_data['status'] ?? '未确认',
'{payment_status}' => $booking_data['payment_status'] ?? '未支付',
'{source}' => $booking_data['source'] ?? '未知',
'{created_at}' => $booking_data['created_at'] ?? date('Y-m-d H:i:s')
];
// 替换模板中的占位符
$message = str_replace(array_keys($replacements), array_values($replacements), $template);
return $message;
}
/**
* 获取指定模板的标题
* @param string $template_name 模板名称
* @return string 模板标题
*/
function getTemplateTitle($template_name = 'basic') {
global $booking_templates;
if (isset($booking_templates[$template_name])) {
return $booking_templates[$template_name]['title'];
}
return $booking_templates['basic']['title'];
}
/**
* 获取所有可用的模板名称
* @return array 模板名称数组
*/
function getAvailableTemplates() {
global $booking_templates;
return array_keys($booking_templates);
}
/**
* 示例用法:
* 1. 包含此文件:require_once 'booking_template.php';
* 2. 准备预约数据:
* $booking_data = [
* 'customer_name' => '张三',
* 'phone' => '138-0013-8000',
* 'car_model' => '特斯拉 Model 3',
* 'car_number' => '京A12345',
* 'member_type' => 'VIP会员',
* 'package_name' => '精致洗车套餐',
* 'start_time' => '2023-10-01 10:00:00',
* 'end_time' => '2023-10-01 11:00:00',
* 'duration' => 60,
* 'total_price' => '128.00',
* 'notes' => '车顶上有行李架,请小心清洗',
* 'status' => '已确认',
* 'payment_status' => '已付款',
* 'source' => '微信'
* ];
* 3. 生成预约信息:
* $message = generateBookingMessage($booking_data, 'detailed');
* 4. 输出或使用生成的信息:
* echo $message;
*/
?>
+209 -11
View File
@@ -25,6 +25,13 @@ if (isset($_POST['action']) && isset($_POST['booking_id'])) {
$stmt = $pdo->prepare("UPDATE bookings SET start_time = ?, end_time = ? WHERE id = ?");
$stmt->execute([$new_start_time, $new_end_time, $booking_id]);
$success_message = '预约时间更新成功!';
} elseif ($action == 'update_notes' && isset($_POST['notes_content'])) {
// 更新客服备注
$notes_content = $_POST['notes_content'];
$stmt = $pdo->prepare("UPDATE bookings SET custom_services = ? WHERE id = ?");
$stmt->execute([$notes_content, $booking_id]);
echo 'success';
exit();
}
} catch (Exception $e) {
$error_message = '更新失败:' . $e->getMessage();
@@ -34,7 +41,7 @@ if (isset($_POST['action']) && isset($_POST['booking_id'])) {
// 获取所有预约,支持状态筛选和搜索功能
try {
// 构建查询,支持筛选和搜索
$query = "SELECT b.*, p.package_name FROM bookings b LEFT JOIN packages p ON b.package_id = p.id WHERE 1=1 ";
$query = "SELECT b.*, p.package_name, p.package_reminder FROM bookings b LEFT JOIN packages p ON b.package_id = p.id WHERE 1=1 ";
$params = [];
// 状态筛选
@@ -77,6 +84,7 @@ try {
<title>张老师撸车(私家车库)工作室</title>
<link rel="stylesheet" href="style.css">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="mobile-nav.js" defer></script>
<!-- Favicon for mobile devices -->
<link rel="apple-touch-icon" sizes="180x180" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚗</text></svg>">
@@ -84,16 +92,32 @@ try {
<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">预约洗车</a>
<a href="bookings.php" class="nav-link active">预约管理</a>
<a href="pending_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">预约洗车</a>
<a href="bookings.php" class="nav-link active">预约管理</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 (isset($success_message)): ?>
<div class="success-message"><?php echo $success_message; ?></div>
@@ -105,11 +129,11 @@ try {
</div>
<?php endif; ?>
<div class="card">
<div class="card enhanced-card">
<h2>所有预约 (共 <?php echo count($bookings); ?> 条)</h2>
<!-- 筛选和搜索区域 -->
<div class="filter-search-area">
<div class="filter-search-area enhanced-card">
<form method="GET" class="filter-form">
<div class="filter-group">
<label for="status_filter">状态筛选:</label>
@@ -137,7 +161,7 @@ try {
<div class="empty-message">暂无预约记录</div>
<?php else: ?>
<?php foreach ($bookings as $booking): ?>
<div class="package-card">
<div class="package-card enhanced-card">
<div class="package-header">
<h3><?php echo htmlspecialchars($booking['customer_name']); ?> 的预约</h3>
<div class="package-status">
@@ -214,17 +238,31 @@ try {
<?php if ($booking['notes']): ?>
<div class="package-description">
<span class="detail-label">备注:</span>
<span class="detail-label">客户备注:</span>
<span><?php echo htmlspecialchars($booking['notes']); ?></span>
</div>
<?php endif; ?>
<?php if ($booking['custom_services']): ?>
<div class="package-description">
<span class="detail-label">客服备注:</span>
<span><?php echo htmlspecialchars($booking['custom_services']); ?></span>
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditNotesModal(<?php echo $booking['id']; ?>, '<?php echo htmlspecialchars($booking['custom_services']); ?>')" style="margin-left: 10px;">修改</button>
</div>
<?php else: ?>
<div class="package-description">
<span class="detail-label">客服备注:</span>
<span style="color: #999;">无</span>
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditNotesModal(<?php echo $booking['id']; ?>, '<?php echo htmlspecialchars($booking['custom_services']); ?>')" style="margin-left: 10px;">添加</button>
</div>
<?php endif; ?>
<div class="package-meta">
<span>预约时间:<?php echo $booking['created_at']; ?></span>
</div>
<?php if ($booking['status'] !== '已完成' && $booking['status'] !== '已取消'): ?>
<div class="package-actions">
<div class="package-actions">
<?php if ($booking['status'] !== '已完成' && $booking['status'] !== '已取消'): ?>
<!-- 状态更新按钮 - 使用AJAX -->
<div>
<button type="button" class="btn btn-sm btn-success" onclick="updateStatus(<?php echo $booking['id']; ?>, '已确认')">确认</button>
@@ -240,8 +278,49 @@ try {
<br>
<!-- 修改预约时间按钮 -->
<button type="button" class="btn btn-sm btn-secondary" onclick="openEditModal(<?php echo $booking['id']; ?>, '<?php echo date('Y-m-d\TH:i', strtotime($booking['start_time'])); ?>', '<?php echo date('Y-m-d\TH:i', strtotime($booking['end_time'])); ?>')">修改预约时间</button>
</div>
<?php endif; ?>
<br>
<?php endif; ?>
<!-- 转换为VIP客户按钮 -->
<?php
// 检查该客户是否已经是VIP
$stmt_check_vip = $pdo->prepare("SELECT id FROM vip_customers WHERE phone = ? AND car_number = ? AND is_active = 1");
$stmt_check_vip->execute([$booking['phone'], $booking['car_number']]);
$is_vip = $stmt_check_vip->fetch() !== false;
?>
<?php if (!$is_vip && $booking['member_type'] === '普通客户'): ?>
<form method="POST" action="vip.php" style="display: inline; margin-top: 10px;">
<input type="hidden" name="action" value="convert_from_booking">
<input type="hidden" name="booking_id" value="<?php echo $booking['id']; ?>">
<button type="submit" class="btn btn-sm" style="background-color: #ffd700; color: #333; font-weight: bold;"
onclick="return confirm('确定要将该客户转换为VIP客户吗?\n\n客户:<?php echo htmlspecialchars($booking['customer_name']); ?>\n手机号:<?php echo htmlspecialchars($booking['phone']); ?>\n车牌号:<?php echo htmlspecialchars($booking['car_number']); ?>')">
👑 转换为VIP客户
</button>
</form>
<br>
<?php endif; ?>
<!-- 复制预约信息按钮 -->
<?php
// 计算服务时长(分钟)
$start = strtotime($booking['start_time']);
$end = strtotime($booking['end_time']);
$duration = round(($end - $start) / 60);
// 获取车友备注(如果不存在则为空字符串)
$notes = $booking['notes'] ?? '';
?>
<button type="button" class="btn btn-sm btn-copy"
data-id="<?php echo $booking['id']; ?>"
data-customer-name="<?php echo htmlspecialchars($booking['customer_name']); ?>"
data-phone="<?php echo htmlspecialchars($booking['phone']); ?>"
data-car-model="<?php echo htmlspecialchars($booking['car_model']); ?>"
data-car-number="<?php echo htmlspecialchars($booking['car_number']); ?>"
data-package-name="<?php echo htmlspecialchars($booking['package_name'] ?? '未选择套餐'); ?>"
data-date="<?php echo date('Y-m-d', strtotime($booking['start_time'])); ?>"
data-time="<?php echo date('H:i', strtotime($booking['start_time'])) . ' - ' . date('H:i', strtotime($booking['end_time'])); ?>"
data-duration="<?php echo $duration; ?>"
data-notes="<?php echo htmlspecialchars($notes); ?>"
data-package-reminder="<?php echo htmlspecialchars($booking['package_reminder'] ?? ''); ?>"
onclick="copyMessage(this)">复制预约信息</button>
</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
@@ -251,6 +330,27 @@ try {
<a href="index.php" class="btn">返回预约页面</a>
</div>
</div>
<!-- 编辑客服备注模态框 -->
<div id="editNotesModal" style="display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.4);">
<div style="background-color: #fefefe; margin: 15% auto; padding: 20px; border-radius: 8px; width: 90%; max-width: 500px; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
<div class="modal-header">
<h3>编辑客服备注</h3>
<span class="close-modal" onclick="closeEditNotesModal()">&times;</span>
</div>
<form id="editNotesForm">
<input type="hidden" id="editNotesBookingId" name="booking_id">
<div class="form-group">
<label for="editNotesContent">客服备注内容:</label>
<textarea id="editNotesContent" name="notes_content" rows="5" placeholder="请输入客服备注内容..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;"></textarea>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-sm" onclick="closeEditNotesModal()">取消</button>
<button type="submit" class="btn btn-sm btn-primary">保存</button>
</div>
</form>
</div>
</div>
<!-- 修改预约时间模态框 -->
<div id="editTimeModal" class="modal">
@@ -462,15 +562,56 @@ try {
document.getElementById('editTimeModal').style.display = 'none';
}
// 编辑客服备注相关函数
function openEditNotesModal(bookingId, notesContent) {
document.getElementById('editNotesBookingId').value = bookingId;
document.getElementById('editNotesContent').value = notesContent;
document.getElementById('editNotesModal').style.display = 'block';
}
function closeEditNotesModal() {
document.getElementById('editNotesModal').style.display = 'none';
}
// 点击模态框外部关闭模态框
window.onclick = function(event) {
const modal = document.getElementById('editTimeModal');
const notesModal = document.getElementById('editNotesModal');
if (event.target == modal) {
closeEditModal();
} else if (event.target == notesModal) {
closeEditNotesModal();
}
}
// 表单提交处理 - 使用AJAX
// 编辑客服备注表单提交处理 - 使用AJAX
document.getElementById('editNotesForm').addEventListener('submit', function(event) {
event.preventDefault();
const bookingId = document.getElementById('editNotesBookingId').value;
const notesContent = document.getElementById('editNotesContent').value;
// 创建AJAX请求
const xhr = new XMLHttpRequest();
xhr.open('POST', 'bookings.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
if (xhr.status === 200) {
// 刷新页面以显示更新后的备注
window.location.reload();
} else {
alert('保存失败,请重试');
}
};
xhr.onerror = function() {
alert('网络错误,请重试');
};
// 发送请求
xhr.send('action=update_notes&booking_id=' + bookingId + '&notes_content=' + encodeURIComponent(notesContent));
});
// 修改预约时间表单提交处理 - 使用AJAX
document.getElementById('editTimeForm').addEventListener('submit', function(event) {
event.preventDefault();
@@ -601,6 +742,63 @@ try {
}
}
</script>
<!-- 单独的JavaScript块用于复制功能 -->
<!-- 复制预约信息功能 -->
<script>
function copyMessage(button) {
try {
// 从按钮的data属性获取预约信息
var id = button.dataset.id;
var customer_name = button.dataset.customerName;
var phone = button.dataset.phone;
var car_model = button.dataset.carModel;
var car_number = button.dataset.carNumber;
var package_name = button.dataset.packageName;
var date = button.dataset.date;
var time = button.dataset.time;
var duration = button.dataset.duration;
var notes = button.dataset.notes;
var package_reminder = button.dataset.packageReminder;
var msg = "【张老师撸车工作室】预约确认\n\n";
msg += "客户姓名:" + customer_name + "\n";
msg += "联系方式:" + phone + "\n";
msg += "车型:" + car_model + "\n";
msg += "车牌号:" + car_number + "\n";
msg += "预约日期:" + date + "\n";
msg += "套餐:" + package_name + "\n";
msg += "预约时间:" + time + "\n";
msg += "预计服务时长:" + duration + "分钟\n";
msg += "车友备注:" + notes + "\n";
msg += "" + (package_reminder || "") + "\n\n";
// 复制到剪贴板
var ta = document.createElement('textarea');
ta.value = msg;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
alert('预约信息已复制到剪贴板!');
} catch (err) {
alert('复制失败,请手动复制!');
}
document.body.removeChild(ta);
} catch (e) {
alert('复制过程中发生错误,请稍后重试!');
}
}
</script>
<script type="text/javascript">
// 移动端优化脚本
document.addEventListener('DOMContentLoaded', function() {
// 为按钮添加触摸反馈
+213
View File
@@ -0,0 +1,213 @@
<?php
/**
* 预约信息模板使用示例
* 此文件展示如何使用 booking_template.php 来生成预约信息
*/
// 包含模板文件
require_once 'booking_template.php';
// 示例1:使用基本模板
function example_basic_template() {
echo "=== 示例1:使用基本模板 ===\n\n";
// 准备预约数据
$booking_data = [
'customer_name' => '张三',
'phone' => '138-0013-8000',
'car_number' => '京A12345',
'car_model' => '特斯拉 Model 3',
'start_time' => '2023-10-01 10:00:00',
'package_name' => '精致洗车套餐',
'duration' => 60,
'total_price' => '128.00'
];
// 生成预约信息
$message = generateBookingMessage($booking_data, 'basic');
// 输出结果
echo "模板标题:" . getTemplateTitle('basic') . "\n";
echo "模板内容:\n" . $message . "\n\n";
}
// 示例2:使用详细模板
function example_detailed_template() {
echo "=== 示例2:使用详细模板 ===\n\n";
// 准备完整的预约数据
$booking_data = [
'customer_name' => '李四',
'phone' => '139-0013-9000',
'car_model' => '宝马 5系',
'car_number' => '沪B67890',
'member_type' => 'VIP会员',
'package_name' => '深度清洁套餐',
'custom_services' => '内饰消毒 + 轮胎养护',
'start_time' => '2023-10-02 14:30:00',
'end_time' => '2023-10-02 16:00:00',
'duration' => 90,
'total_price' => '298.00',
'notes' => '车辆右前门有轻微划痕,清洗时请注意',
'status' => '已确认',
'payment_status' => '已付款',
'source' => '抖音'
];
// 生成预约信息
$message = generateBookingMessage($booking_data, 'detailed');
// 输出结果
echo "模板标题:" . getTemplateTitle('detailed') . "\n";
echo "模板内容:\n" . $message . "\n\n";
}
// 示例3:使用简洁模板
function example_simple_template() {
echo "=== 示例3:使用简洁模板 ===\n\n";
// 准备简约的预约数据
$booking_data = [
'customer_name' => '王五',
'phone' => '137-0013-7000',
'car_number' => '粤C54321',
'car_model' => '本田 CR-V',
'start_time' => '2023-10-03 09:30:00',
'package_name' => '快速洗车',
'duration' => 30,
'total_price' => '38.00'
];
// 生成预约信息
$message = generateBookingMessage($booking_data, 'simple');
// 输出结果
echo "模板标题:" . getTemplateTitle('simple') . "\n";
echo "模板内容:\n" . $message . "\n\n";
}
// 示例4:自定义模板数据
function example_custom_data() {
echo "=== 示例4:自定义模板数据 ===\n\n";
// 从数据库获取的示例预约数据
$db_booking_data = [
'id' => 123,
'customer_name' => '赵六',
'phone' => '136-0013-6000',
'car_model' => '丰田 凯美瑞',
'car_number' => '苏D98765',
'member_type' => '普通会员',
'package_id' => 2,
'package_name' => '打蜡套餐',
'custom_services' => '',
'start_time' => '2023-10-04 16:00:00',
'end_time' => '2023-10-04 17:30:00',
'duration' => 90,
'total_price' => '198.00',
'notes' => '',
'status' => '已确认',
'payment_status' => '未付款',
'source' => '朋友介绍',
'created_at' => '2023-09-25 14:20:00',
'updated_at' => '2023-09-25 14:20:00'
];
// 生成预约信息
$message = generateBookingMessage($db_booking_data, 'detailed');
// 输出结果
echo "模板标题:" . getTemplateTitle('detailed') . "\n";
echo "模板内容:\n" . $message . "\n\n";
}
// 示例5:将模板集成到现有系统中
function example_integration() {
echo "=== 示例5:将模板集成到现有系统中 ===\n\n";
// 模拟从数据库获取预约数据
function getBookingFromDatabase($booking_id) {
// 这里应该是实际的数据库查询
return [
'id' => $booking_id,
'customer_name' => '孙七',
'phone' => '135-0013-5000',
'car_model' => '大众 帕萨特',
'car_number' => '浙E34567',
'member_type' => '普通客户',
'package_name' => '普通洗车',
'start_time' => '2023-10-05 11:00:00',
'end_time' => '2023-10-05 11:30:00',
'duration' => 30,
'total_price' => '58.00',
'notes' => '',
'status' => '已确认',
'payment_status' => '已付款',
'source' => '其他'
];
}
// 模拟发送预约确认信息
function sendBookingConfirmation($booking_id) {
// 获取预约数据
$booking = getBookingFromDatabase($booking_id);
// 生成预约信息
$template_name = $booking['member_type'] == 'VIP会员' ? 'detailed' : 'basic';
$message = generateBookingMessage($booking, $template_name);
$title = getTemplateTitle($template_name);
// 这里应该是实际的发送逻辑(短信、微信等)
echo "{$booking['phone']} 发送信息:\n";
echo "标题:{$title}\n";
echo "内容:\n{$message}\n";
return true;
}
// 使用示例
$booking_id = 456;
sendBookingConfirmation($booking_id);
}
// 运行所有示例
example_basic_template();
example_detailed_template();
example_simple_template();
example_custom_data();
example_integration();
// 显示可用模板
echo "\n=== 可用模板列表 ===\n";
echo "当前系统中可用的预约信息模板:\n";
$templates = getAvailableTemplates();
foreach ($templates as $template) {
echo "- {$template} (标题:" . getTemplateTitle($template) . ")\n";
}
// 如何自定义模板的说明
echo "\n=== 如何自定义模板 ===\n";
echo "1. 打开 booking_template.php 文件\n";
echo "2. 在 \$booking_templates 数组中添加新的模板或修改现有模板\n";
echo "3. 每个模板包含 title 和 message 两个字段\n";
echo "4. 使用 {字段名} 作为占位符,系统会自动替换为实际数据\n";
echo "5. 可使用的占位符包括:\n";
echo " - {customer_name}:客户姓名\n";
echo " - {phone}:手机号\n";
echo " - {car_model}:车型\n";
echo " - {car_number}:车牌号\n";
echo " - {member_type}:会员类型\n";
echo " - {package_name}:服务项目\n";
echo " - {custom_services}:自定义服务\n";
echo " - {start_time}:预约开始时间\n";
echo " - {end_time}:预约结束时间\n";
echo " - {date}:预约日期\n";
echo " - {time_range}:时间范围\n";
echo " - {duration}:服务时长\n";
echo " - {total_price}:总价\n";
echo " - {notes}:备注\n";
echo " - {status}:预约状态\n";
echo " - {payment_status}:支付状态\n";
echo " - {source}:来源渠道\n";
echo " - {created_at}:创建时间\n";
?>
+71 -74
View File
@@ -1,12 +1,9 @@
<?php
// 配置数据库连接参数
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "car_wash_db";
// 加载数据库配置
require_once 'db_connect.php';
// 设置响应头为JSON
header('Content-Type: application/json');
header('Content-Type: application/json; charset=utf-8');
// 获取请求参数
$phone = $_GET['phone'] ?? '';
@@ -20,82 +17,82 @@ if (empty($phone)) {
'pending' => 0,
'completed' => 0,
'bookings' => []
]);
], JSON_UNESCAPED_UNICODE);
exit;
}
// 创建数据库连接
$conn = new mysqli($servername, $username, $password, $dbname);
try {
// 准备SQL查询 - 查询用户的预约记录(使用新的数据库结构)
$sql = "SELECT b.*, p.package_name
FROM bookings b
LEFT JOIN packages p ON b.package_id = p.id
WHERE b.phone = ?
ORDER BY b.start_time DESC, b.id DESC";
// 检查连接
if ($conn->connect_error) {
$stmt = $pdo->prepare($sql);
$stmt->execute([$phone]);
$results = $stmt->fetchAll();
$bookings = [];
$pending = 0;
$completed = 0;
// 处理查询结果
foreach ($results as $row) {
// 格式化日期和时间
$booking_date = $row['start_time'] ? date('Y-m-d', strtotime($row['start_time'])) : '';
$time_slot = '';
if ($row['start_time'] && $row['end_time']) {
$start_time = date('H:i', strtotime($row['start_time']));
$end_time = date('H:i', strtotime($row['end_time']));
$time_slot = $start_time . '-' . $end_time;
}
$booking = [
'id' => $row['id'],
'booking_date' => $booking_date,
'phone' => $row['phone'],
'car_number' => $row['car_number'] ?? '',
'car_model' => $row['car_model'] ?? '',
'time_slot' => $time_slot,
'status' => $row['status'] ?? '未知',
'package_name' => $row['package_name'] ?? '',
'notes' => $row['notes'] ?? ''
];
// 统计不同状态的预约数量
if ($booking['status'] === '待确认' || $booking['status'] === '已确认' || $booking['status'] === '进行中') {
$pending++;
} elseif ($booking['status'] === '已完成') {
$completed++;
}
$bookings[] = $booking;
}
// 计算总预约数
$total = count($bookings);
// 构建响应数据
$response = [
'success' => true,
'total' => $total,
'pending' => $pending,
'completed' => $completed,
'bookings' => $bookings
];
// 返回JSON数据
echo json_encode($response, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'message' => '数据库连接失败',
'message' => '查询失败' . $e->getMessage(),
'total' => 0,
'pending' => 0,
'completed' => 0,
'bookings' => []
]);
exit;
], JSON_UNESCAPED_UNICODE);
}
// 准备SQL查询 - 查询用户的预约记录
// 假设预约表名为bookings,套餐表名为packages
$sql = "SELECT b.*, p.package_name
FROM bookings b
LEFT JOIN packages p ON b.package_id = p.id
WHERE b.phone = ?
ORDER BY b.booking_date DESC, b.id DESC";
$stmt = $conn->prepare($sql);
$stmt->bind_param("s", $phone);
$stmt->execute();
$result = $stmt->get_result();
$bookings = [];
$pending = 0;
$completed = 0;
// 处理查询结果
while ($row = $result->fetch_assoc()) {
$booking = [
'id' => $row['id'],
'booking_date' => $row['booking_date'],
'phone' => $row['phone'],
'car_number' => $row['car_number'] ?? '',
'car_model' => $row['car_model'] ?? '',
'time_slot' => $row['time_slot'] ?? '',
'status' => $row['status'] ?? '未知',
'package_name' => $row['package_name'] ?? '',
'notes' => $row['notes'] ?? ''
];
// 统计不同状态的预约数量
if ($booking['status'] === '待服务') {
$pending++;
} elseif ($booking['status'] === '已完成') {
$completed++;
}
$bookings[] = $booking;
}
// 计算总预约数
$total = count($bookings);
// 构建响应数据
$response = [
'success' => true,
'total' => $total,
'pending' => $pending,
'completed' => $completed,
'bookings' => $bookings
];
// 返回JSON数据
echo json_encode($response);
// 关闭数据库连接
$stmt->close();
$conn->close();
?>
+28
View File
@@ -0,0 +1,28 @@
<?php
// 引入数据库连接文件
require 'db_connect.php';
// 获取请求参数
$date = $_GET['date'];
// 验证日期格式
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
echo json_encode(['error' => 'Invalid date format']);
exit;
}
try {
// 查询指定日期的总预约时长,只计算有效预约
$sql = "SELECT SUM(duration) as total_duration FROM bookings WHERE DATE(start_time) = ? AND status NOT IN ('已完成', '已取消')";
$stmt = $pdo->prepare($sql);
$stmt->execute([$date]);
$row = $stmt->fetch();
// 获取总时长,如果没有预约则返回0
$total_duration = $row['total_duration'] ? $row['total_duration'] : 0;
// 返回结果
echo json_encode(['total_duration' => $total_duration]);
} catch (PDOException $e) {
echo json_encode(['error' => 'Database query failed: ' . $e->getMessage()]);
}
+28
View File
@@ -0,0 +1,28 @@
<?php
require_once 'db_connect.php';
header('Content-Type: application/json; charset=utf-8');
$id = isset($_GET['id']) ? (int)$_GET['id'] : 0;
if ($id <= 0) {
echo json_encode(['error' => '无效的套餐ID'], JSON_UNESCAPED_UNICODE);
exit;
}
try {
$stmt = $pdo->prepare("SELECT * FROM packages WHERE id = ?");
$stmt->execute([$id]);
$package = $stmt->fetch();
if (!$package) {
echo json_encode(['error' => '套餐不存在'], JSON_UNESCAPED_UNICODE);
exit;
}
echo json_encode($package, JSON_UNESCAPED_UNICODE);
} catch (Exception $e) {
echo json_encode(['error' => '获取套餐信息失败:' . $e->getMessage()], JSON_UNESCAPED_UNICODE);
}
?>
+147 -45
View File
@@ -9,7 +9,12 @@ $success_message = '';
// 处理表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$customer_type = $_POST['customer_type'];
// 验证并获取客户类型
$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表获取信息
@@ -25,25 +30,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 使用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; // 允许覆盖
// #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']);
$customer_name = trim($_POST['customer_name'] ?? '');
$phone = trim($_POST['phone'] ?? '');
$car_model = trim($_POST['car_model'] ?? '');
$car_number = trim($_POST['car_number'] ?? '');
}
$car_model = trim($_POST['car_model']);
$car_number = trim($_POST['car_number']);
$package_id = (int)$_POST['package_id'];
$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'];
$appointment_date = $_POST['appointment_date'] ?? '';
$appointment_time = $_POST['appointment_time'] ?? '';
$duration = (int)($_POST['duration'] ?? 60);
$notes = trim($_POST['notes'] ?? '');
$member_type = $_POST['member_type'];
$source = $_POST['source'];
// 验证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) ||
@@ -77,32 +91,79 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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';
$end_time = date('Y-m-d H:i:s', strtotime($start_time . " +{$duration} minutes"));
$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 > ?)
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]);
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 ($stmt->fetchColumn() > 0) {
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)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
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]);
$start_time, $end_time, $duration, $total_price, $notes, $member_type, $source, $payment_status]);
$success_message = "预约提交成功!";
@@ -161,17 +222,26 @@ $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'];
$booking_date = $booking['date']; // 使用不同的变量名避免覆盖外层$start_date
$booking_start_time = $booking['start_time'];
$booking_end_time = $booking['end_time'];
// 将预约添加到开始日期
$bookings_by_date[$start_date][] = $booking;
$bookings_by_date[$booking_date][] = $booking;
// 检查是否是跨天预约(结束时间早于开始时间)
if (strtotime($end_time) < strtotime($start_time)) {
// 检查是否是跨天预约(结束时间早于开始时间,表示跨天
// 注意:这里比较的是时间字符串(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($start_date . ' +1 day'));
$next_date = date('Y-m-d', strtotime($booking_date . ' +1 day'));
// 创建第二天的预约记录副本
$next_day_booking = $booking;
@@ -201,6 +271,7 @@ $packages_json = json_encode(array_map(function($package) {
<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搜索结果样式 */
@@ -311,16 +382,32 @@ $packages_json = json_encode(array_map(function($package) {
<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="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;">
@@ -400,7 +487,7 @@ $packages_json = json_encode(array_map(function($package) {
}
</style>
<div class="calendar" id="calendarContainer">
<div class="calendar enhanced-card" id="calendarContainer">
<!-- 日期将通过JavaScript动态生成 -->
</div>
@@ -580,7 +667,7 @@ $packages_json = json_encode(array_map(function($package) {
}
</style>
<div class="time-slots" id="timeSlots" style="display: none;">
<div class="time-slots enhanced-card" id="timeSlots" style="display: none;">
<h3>🕐 选择时间段</h3>
<div class="quick-duration">
<span>快捷时长:</span>
@@ -601,13 +688,13 @@ $packages_json = json_encode(array_map(function($package) {
</div>
<!-- 预约详情显示区域 -->
<div id="bookingDetails" class="booking-details" style="display: none;">
<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">
<div class="booking-form-section enhanced-card">
<h2>📋 预约信息</h2>
<form method="POST" class="form" id="bookingForm">
<div class="form-row">
@@ -1190,20 +1277,35 @@ $packages_json = json_encode(array_map(function($package) {
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, "\\'");
// 转义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 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>');
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">${carModel} ${carNumber}</div>` : ''}
${carModel || carNumber ? `<div class="customer-car">${escapedCarModel} ${escapedCarNumber}</div>` : ''}
</div>
`;
});
+83
View File
@@ -0,0 +1,83 @@
// 移动端导航菜单控制
function toggleMobileMenu() {
const toggle = document.querySelector('.mobile-menu-toggle');
const overlay = document.querySelector('.mobile-nav-overlay');
const nav = document.querySelector('.mobile-nav');
if (!toggle || !overlay || !nav) return;
toggle.classList.toggle('active');
overlay.classList.toggle('active');
nav.classList.toggle('active');
document.body.style.overflow = nav.classList.contains('active') ? 'hidden' : '';
}
function closeMobileMenu() {
const toggle = document.querySelector('.mobile-menu-toggle');
const overlay = document.querySelector('.mobile-nav-overlay');
const nav = document.querySelector('.mobile-nav');
if (!toggle || !overlay || !nav) return;
toggle.classList.remove('active');
overlay.classList.remove('active');
nav.classList.remove('active');
document.body.style.overflow = '';
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 点击导航链接后关闭菜单
const mobileNavLinks = document.querySelectorAll('.mobile-nav .nav-link');
mobileNavLinks.forEach(link => {
link.addEventListener('click', closeMobileMenu);
});
// 点击遮罩层关闭菜单
const overlay = document.querySelector('.mobile-nav-overlay');
if (overlay) {
overlay.addEventListener('click', closeMobileMenu);
}
// ESC键关闭菜单
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeMobileMenu();
}
});
// 防止双击缩放(iOS Safari
let lastTouchEnd = 0;
document.addEventListener('touchend', function(event) {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
event.preventDefault();
}
lastTouchEnd = now;
}, false);
// 优化触摸反馈
document.addEventListener('touchstart', function(e) {
const target = e.target;
if (target.classList.contains('btn') ||
target.classList.contains('time-slot') ||
target.classList.contains('calendar-day') ||
target.classList.contains('nav-link')) {
target.style.transition = 'transform 0.1s';
target.style.transform = 'scale(0.97)';
}
}, { passive: true });
document.addEventListener('touchend', function(e) {
const target = e.target;
if (target.classList.contains('btn') ||
target.classList.contains('time-slot') ||
target.classList.contains('calendar-day') ||
target.classList.contains('nav-link')) {
setTimeout(() => {
target.style.transform = '';
}, 100);
}
}, { passive: true });
});
+1167 -184
View File
File diff suppressed because it is too large Load Diff
+175 -66
View File
@@ -42,14 +42,17 @@ if (isset($_POST['action']) && $_POST['action'] == 'convert_to_booking' && isset
$end_time = date('Y-m-d H:i:s', strtotime($start_time) + $duration * 60);
// 将数据插入到正式预约表
// 对于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, status,
member_type, source
member_type, source, payment_status
) VALUES (
?, ?, ?, ?, ?, ?,
?, ?, ?, ?, ?, ?,
?, ?
?, ?, ?
)");
$stmt->execute([
@@ -66,7 +69,8 @@ if (isset($_POST['action']) && $_POST['action'] == 'convert_to_booking' && isset
$submission['remarks'],
'已确认', // 默认设置为已确认
'普通客户', // 默认普通客户,可根据需要调整
'其他' // 默认来源,可根据需要调整
'其他', // 默认来源,可根据需要调整
$payment_status // 根据总价自动设置付款状态
]);
$booking_id = $pdo->lastInsertId();
@@ -120,8 +124,8 @@ $booking_schedule = [];
$bookings_by_date = [];
try {
// 获取所有未来的预约
$stmt = $pdo->prepare("SELECT * FROM bookings WHERE end_time > NOW() ORDER BY start_time ASC");
// 获取所有未来的预约(排除已取消的订单)
$stmt = $pdo->prepare("SELECT * FROM bookings WHERE end_time > NOW() AND status != '已取消' ORDER BY start_time ASC");
$stmt->execute();
$all_bookings = $stmt->fetchAll();
@@ -182,13 +186,22 @@ try {
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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="待处理预约页面,处理WPS表单数据">
<meta name="keywords" content="待处理预约,预约管理,洗车预约">
<title>张老师撸车(私家车库)工作室</title>
<link rel="stylesheet" href="style.css">
<script src="mobile-nav.js" defer></script>
<!-- Favicon for mobile devices -->
<link rel="apple-touch-icon" sizes="180x180" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🚗</text></svg>">
<style>
/* 待预约页面特定样式 */
.pending-bookings-container {
max-width: 1200px;
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
@@ -201,6 +214,12 @@ try {
overflow: hidden;
}
.pending-card.selected-card {
background: #e3f2fd;
border: 2px solid #2196f3;
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
}
.pending-header {
background: #f5f5f5;
padding: 15px 20px;
@@ -288,26 +307,6 @@ try {
margin: 20px 0;
}
.copy-message {
background: #fff;
border: 1px solid #ddd;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-family: monospace;
white-space: pre-wrap;
}
.copy-btn {
background: #28a745;
color: white;
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
}
.status-badge {
padding: 5px 10px;
border-radius: 12px;
@@ -320,6 +319,50 @@ try {
color: #212529;
}
/* 预约记录选择器样式 */
.submission-selector {
margin-bottom: 20px;
}
.submission-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.submission-item {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.submission-item:hover {
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
border-color: #007bff;
}
.submission-item.selected-item {
background: #e3f2fd;
border-color: #2196f3;
box-shadow: 0 4px 15px rgba(33, 150, 243, 0.3);
}
.submission-info h4 {
margin: 0 0 10px 0;
color: #333;
}
.submission-meta {
display: flex;
flex-wrap: wrap;
gap: 15px;
color: #666;
font-size: 14px;
}
/* 日历和时间选择样式 */
.calendar-container {
margin-bottom: 20px;
@@ -400,6 +443,9 @@ try {
background: #c3e6cb;
}
/* 预约时长提示样式 */
/* 套餐信息样式 */
.package-info {
background: #f8f9fa;
@@ -517,6 +563,11 @@ try {
<body>
<div class="pending-bookings-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">预约洗车</a>
@@ -528,6 +579,17 @@ try {
</nav>
</header>
<!-- 移动端导航菜单 -->
<div class="mobile-nav-overlay" onclick="closeMobileMenu()"></div>
<nav class="mobile-nav">
<a href="index.php" class="nav-link">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="pending_bookings.php" class="nav-link active">待处理预约</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 (isset($success_message)): ?>
<div class="success-message"><?php echo $success_message; ?></div>
<?php endif; ?>
@@ -538,26 +600,39 @@ try {
</div>
<?php endif; ?>
<!-- 预约成功信息显示 -->
<?php if (isset($_SESSION['booking_success_msg'])): ?>
<div class="booking-success">
<h3>预约成功信息已生成</h3>
<div class="copy-message" id="successMessage">
<?php echo htmlspecialchars($_SESSION['booking_success_msg']); ?>
</div>
<button class="copy-btn" onclick="copyMessage()">复制信息</button>
</div>
<?php unset($_SESSION['booking_success_msg']); ?>
<?php endif; ?>
<div class="card">
<div class="card enhanced-card">
<h2>待处理预约 (共 <?php echo count($pending_submissions); ?> 条)</h2>
<?php if (empty($pending_submissions)): ?>
<div class="empty-message">暂无待处理的预约请求</div>
<?php else: ?>
<!-- 无论记录数量多少,都显示选择列表 -->
<?php if (true): ?>
<div class="submission-selector enhanced-card">
<h3>选择要处理的预约记录</h3>
<div class="submission-list">
<?php foreach ($pending_submissions as $submission): ?>
<div class="submission-item" onclick="selectSubmission(<?php echo $submission['id']; ?>)">
<div class="submission-info">
<h4><?php echo htmlspecialchars($submission['name']); ?></h4>
<div class="submission-meta">
<span>手机号:<?php echo htmlspecialchars($submission['mobile']); ?></span>
<span>车牌号:<?php echo htmlspecialchars($submission['license_plate']); ?></span>
<span>提交时间:<?php echo htmlspecialchars($submission['create_time']); ?></span>
</div>
</div>
<span class="status-badge status-pending">待处理</span>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<!-- 显示选中的预约记录的处理表单 -->
<?php foreach ($pending_submissions as $submission): ?>
<div class="pending-card">
<div class="pending-card enhanced-card" id="submission_<?php echo $submission['id']; ?>" style="display: none;">
<div class="pending-header">
<h3><?php echo htmlspecialchars($submission['name']); ?> 的预约请求</h3>
<span class="status-badge status-pending">待处理</span>
@@ -636,7 +711,7 @@ try {
</select>
</div>
<div class="package-info" id="packageInfo_<?php echo $submission['id']; ?>">
<div class="package-info enhanced-card" id="packageInfo_<?php echo $submission['id']; ?>">
<div class="package-details">
<h4 id="packageName_<?php echo $submission['id']; ?>"></h4>
<div class="package-meta">
@@ -680,6 +755,10 @@ try {
</div>
</div>
<!-- 预约时长提示区域 -->
<!-- 提示信息将通过JavaScript动态生成 -->
</div>
<div class="form-row">
<div class="form-group">
<label for="customDuration_<?php echo $submission['id']; ?>">服务时长(分钟)</label>
@@ -715,18 +794,36 @@ try {
</div>
<script>
// 复制预约成功信息到剪贴板
function copyMessage() {
const message = document.getElementById('successMessage');
const textArea = document.createElement('textarea');
textArea.value = message.textContent;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
// 选择预约记录函数(提前定义,确保在调用前可用)
function selectSubmission(submissionId) {
// 隐藏所有待处理预约卡片并移除高亮
document.querySelectorAll('.pending-card').forEach(card => {
card.style.display = 'none';
card.classList.remove('selected-card');
});
// 显示复制成功提示
alert('预约信息已复制到剪贴板!');
// 移除选择列表中所有项的高亮
document.querySelectorAll('.submission-item').forEach(item => {
item.classList.remove('selected-item');
});
// 显示选中的预约卡片并添加高亮
const selectedCard = document.getElementById(`submission_${submissionId}`);
if (selectedCard) {
selectedCard.style.display = 'block';
selectedCard.classList.add('selected-card');
}
// 为选择列表中对应的项添加高亮
const submissionItems = document.querySelectorAll('.submission-item');
submissionItems.forEach(item => {
// 查找包含当前submissionId的点击事件
if (item.onclick && item.onclick.toString().includes(`selectSubmission(${submissionId})`)) {
item.classList.add('selected-item');
}
});
}
// 工作时间设置
@@ -741,24 +838,34 @@ try {
// 页面加载时初始化所有预约的日期、时间和套餐信息
document.addEventListener('DOMContentLoaded', function() {
<?php $firstSubmissionId = null; ?>
<?php foreach ($pending_submissions as $submission): ?>
<?php if ($firstSubmissionId === null) $firstSubmissionId = $submission['id']; ?>
// 初始化日期
selectDate(<?php echo $submission['id']; ?>, '<?php echo date('Y-m-d'); ?>');
// 初始化默认套餐信息(如果有默认选择的话)
const packageSelect = document.getElementById('selected_package_<?php echo $submission['id']; ?>');
if (packageSelect.value) {
updatePackageInfo(<?php echo $submission['id']; ?>);
} else if (packageSelect.options.length > 1) {
// 默认选择第一个套餐
packageSelect.selectedIndex = 1;
updatePackageInfo(<?php echo $submission['id']; ?>);
}
(function() {
let packageSelectElem = document.getElementById('selected_package_<?php echo $submission['id']; ?>');
if (packageSelectElem.value) {
updatePackageInfo(<?php echo $submission['id']; ?>);
} else if (packageSelectElem.options.length > 1) {
// 默认选择第一个套餐
packageSelectElem.selectedIndex = 1;
updatePackageInfo(<?php echo $submission['id']; ?>);
}
})();
<?php endforeach; ?>
// 默认显示并高亮第一条记录
<?php if ($firstSubmissionId !== null): ?>
selectSubmission(<?php echo $firstSubmissionId; ?>);
<?php endif; ?>
});
// 选择日期
function selectDate(submissionId, date) {
// 选择日期
function selectDate(submissionId, date) {
const calendarGrid = document.getElementById('calendarGrid_' + submissionId);
const timeGrid = document.getElementById('timeGrid_' + submissionId);
const selectedDateInput = document.getElementById('selected_date_' + submissionId);
@@ -996,9 +1103,9 @@ try {
let form = null;
// 方式1:通过套餐选择元素获取表单
const packageSelect = document.getElementById('selected_package_' + submissionId);
if (packageSelect) {
form = packageSelect.closest('form');
const packageSelectForm = document.getElementById('selected_package_' + submissionId);
if (packageSelectForm) {
form = packageSelectForm.closest('form');
console.log('Form found via packageSelect:', form ? 'Yes' : 'No');
}
@@ -1089,6 +1196,8 @@ try {
selectDuration(submissionId, roundedDuration);
}
}
</script>
</body>
</html>
+205 -111
View File
@@ -1,111 +1,207 @@
<?php
// 配置数据库连接参数
$servername = "localhost";
$username = "root";
$password = "";
$dbname = "car_wash_db";
// 加载数据库配置
require_once 'db_connect.php';
// #region agent log
$log_data = json_encode(['location' => 'process_booking.php:3', 'message' => 'Database connection check', 'data' => ['has_pdo' => isset($pdo), 'using_db_connect' => true], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'D']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
// 检查表单提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 获取表单数据
$customer_name = $_POST['customer_name'] ?? '';
$phone = $_POST['phone'] ?? '';
$car_model = $_POST['car_model'] ?? '';
$car_number = $_POST['car_number'] ?? '';
$booking_date = $_POST['booking_date'] ?? '';
$time_slot = $_POST['time_slot'] ?? '';
$package_id = $_POST['package_id'] ?? '';
$notes = $_POST['notes'] ?? '';
$source = $_POST['source'] ?? ''; // 来源标识,用于确定返回页面
// 验证必填字段
$errors = [];
if (empty($customer_name)) {
$errors[] = '请输入客户姓名';
}
if (empty($phone)) {
$errors[] = '请输入手机号码';
} elseif (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
$errors[] = '请输入正确的手机号码';
}
if (empty($car_number)) {
$errors[] = '请输入车牌号';
}
if (empty($booking_date)) {
$errors[] = '请选择预约日期';
}
if (empty($time_slot)) {
$errors[] = '请选择预约时间';
}
if (empty($package_id)) {
$errors[] = '请选择洗车套餐';
}
// 验证日期是否为过去
$current_date = date('Y-m-d');
if ($booking_date < $current_date) {
$errors[] = '不能选择过去的日期';
}
// 如果有错误,返回错误信息
if (!empty($errors)) {
$error_message = implode('\n', $errors);
echo "<script>
alert('$error_message');
window.history.back();
</script>";
exit;
}
// 创建数据库连接
$conn = new mysqli($servername, $username, $password, $dbname);
// 检查连接
if ($conn->connect_error) {
echo "<script>
alert('数据库连接失败,请稍后重试');
window.history.back();
</script>";
exit;
}
// 检查是否已经存在相同的预约(同一天、同一时间段、同一车牌号)
$check_sql = "SELECT * FROM bookings
WHERE booking_date = ? AND time_slot = ? AND car_number = ? AND status != '已取消'";
$check_stmt = $conn->prepare($check_sql);
$check_stmt->bind_param("sss", $booking_date, $time_slot, $car_number);
$check_stmt->execute();
$check_result = $check_stmt->get_result();
if ($check_result->num_rows > 0) {
echo "<script>
alert('该时间段已存在相同车牌号的预约');
window.history.back();
</script>";
$check_stmt->close();
$conn->close();
exit;
}
$check_stmt->close();
// 插入预约记录
$status = '待服务'; // 默认为待服务状态
$create_time = date('Y-m-d H:i:s');
$insert_sql = "INSERT INTO bookings
(customer_name, phone, car_model, car_number, booking_date, time_slot, package_id, notes, status, create_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
$stmt = $conn->prepare($insert_sql);
$stmt->bind_param("ssssssssss", $customer_name, $phone, $car_model, $car_number, $booking_date, $time_slot, $package_id, $notes, $status, $create_time);
if ($stmt->execute()) {
try {
// 获取表单数据
$customer_name = $_POST['customer_name'] ?? '';
$phone = $_POST['phone'] ?? '';
$car_model = $_POST['car_model'] ?? '';
$car_number = $_POST['car_number'] ?? '';
$booking_date = $_POST['booking_date'] ?? '';
$time_slot = $_POST['time_slot'] ?? '';
$package_id = $_POST['package_id'] ?? '';
$notes = $_POST['notes'] ?? '';
$source = $_POST['source'] ?? '其他'; // 来源标识,用于确定返回页面
$duration = isset($_POST['duration']) ? (int)$_POST['duration'] : 60; // 默认60分钟
$total_price = isset($_POST['total_price']) ? (float)$_POST['total_price'] : 0;
// 验证必填字段
if (empty($customer_name)) {
throw new Exception('请输入客户姓名');
}
if (empty($phone)) {
throw new Exception('请输入手机号码');
} elseif (!preg_match('/^1[3-9]\d{9}$/', $phone)) {
throw new Exception('请输入正确的手机号码');
}
if (empty($car_number)) {
throw new Exception('请输入车牌号');
}
if (empty($booking_date)) {
throw new Exception('请选择预约日期');
}
if (empty($time_slot)) {
throw new Exception('请选择预约时间');
}
if (empty($package_id)) {
throw new Exception('请选择洗车套餐');
}
// 验证日期是否为过去
$current_date = date('Y-m-d');
if ($booking_date < $current_date) {
throw new Exception('不能选择过去的日期');
}
// 解析时间段,转换为start_time和end_time
// time_slot格式可能是 "09:00-10:00" 或 "09:00"
$start_time_str = '';
$end_time_str = '';
// 验证日期格式
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $booking_date)) {
throw new Exception('预约日期格式不正确');
}
if (strpos($time_slot, '-') !== false) {
// 格式:09:00-10:00
list($start_time_str, $end_time_str) = explode('-', $time_slot);
$start_time_str = trim($start_time_str);
$end_time_str = trim($end_time_str);
// 验证时间格式
if (!preg_match('/^\d{2}:\d{2}$/', $start_time_str) || !preg_match('/^\d{2}:\d{2}$/', $end_time_str)) {
throw new Exception('时间段格式不正确');
}
$start_time = $booking_date . ' ' . $start_time_str . ':00';
$end_time = $booking_date . ' ' . $end_time_str . ':00';
// 验证时间有效性
$start_timestamp = strtotime($start_time);
$end_timestamp = strtotime($end_time);
if ($start_timestamp === false || $end_timestamp === false) {
throw new Exception('时间段无效');
}
if ($end_timestamp <= $start_timestamp) {
throw new Exception('结束时间必须晚于开始时间');
}
} else {
// 格式:09:00,使用默认时长
$time_slot = trim($time_slot);
if (!preg_match('/^\d{2}:\d{2}$/', $time_slot)) {
throw new Exception('时间格式不正确');
}
$start_time = $booking_date . ' ' . $time_slot . ':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('计算结束时间失败');
}
}
// #region agent log
$log_data = json_encode(['location' => 'process_booking.php:70', 'message' => 'Time conversion', 'data' => ['booking_date' => $booking_date, 'time_slot' => $time_slot, 'start_time' => $start_time, 'end_time' => $end_time, 'duration' => $duration], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'E']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
// 获取套餐信息以获取价格和默认时长
$stmt = $pdo->prepare("SELECT * FROM packages WHERE id = ? AND is_active = 1");
$stmt->execute([$package_id]);
$package = $stmt->fetch();
if (!$package) {
throw new Exception('选择的套餐无效');
}
// 如果未提供价格,使用套餐价格
if ($total_price <= 0) {
$total_price = $package['price'];
}
// 如果未提供时长,使用套餐默认时长
if ($duration <= 0) {
$duration = $package['base_duration'];
// 重新计算结束时间
$end_time = date('Y-m-d H:i:s', strtotime($start_time) + $duration * 60);
}
// 检查时间冲突
// 两个时间段重叠的条件:现有预约的开始时间 < 新预约的结束时间 AND 现有预约的结束时间 > 新预约的开始时间
// #region agent log
$log_data = json_encode(['location' => 'process_booking.php:100', 'message' => 'Checking time conflict', 'data' => ['start_time' => $start_time, 'end_time' => $end_time], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'H']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
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' => 'process_booking.php:110', 'message' => 'Time conflict check result', 'data' => ['conflict_count' => $conflict_count], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'H']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
if ($conflict_count > 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, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$member_type = '普通客户'; // 默认普通客户,VIP客户应该通过index.php添加
$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,
'待确认' // status
]);
// 预约成功
$success_message = '预约添加成功!';
@@ -116,19 +212,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
alert('$success_message');
window.location.href = '$redirect_url';
</script>";
} else {
// 预约失败
} catch (Exception $e) {
$error_message = $e->getMessage();
echo "<script>
alert('预约添加失败,请稍后重试');
alert('$error_message');
window.history.back();
</script>";
}
// 关闭数据库连接
$stmt->close();
$conn->close();
} else {
// 不是POST请求,重定向到首页
header('Location: index.php');
exit;
}
?>
+696 -31
View File
@@ -61,7 +61,7 @@ body {
}
.container {
max-width: 1200px;
max-width: 1400px;
margin: 0 auto;
padding: var(--el-spacing-large);
}
@@ -132,6 +132,47 @@ body {
margin-bottom: var(--el-spacing-large);
}
/* 增强型卡片样式(参考套餐管理页面) */
.enhanced-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.enhanced-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, var(--el-primary-color), var(--el-success-color));
transform: scaleX(0);
transition: transform 0.3s ease;
}
.enhanced-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.12);
}
.enhanced-card:hover::before {
transform: scaleX(1);
}
.enhanced-card.inactive {
opacity: 0.7;
background: var(--el-bg-color-page);
}
.enhanced-card.inactive::before {
background: linear-gradient(90deg, var(--el-info-color), var(--el-border-color));
}
/* 表单样式 */
.form-group {
margin-bottom: var(--el-spacing-large);
@@ -280,6 +321,32 @@ body {
font-size: var(--el-font-size-large);
}
/* 切换状态按钮(参考套餐管理页面) */
.btn-toggle-status {
padding: 8px 16px;
border: none;
border-radius: var(--el-border-radius-base);
font-size: var(--el-font-size-small);
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-toggle-status.active {
background: var(--el-success-color);
color: white;
}
.btn-toggle-status.inactive {
background: var(--el-danger-color);
color: white;
}
.btn-toggle-status:hover {
opacity: 0.9;
transform: scale(1.05);
}
/* 消息提示样式 */
.message {
padding: var(--el-spacing-base);
@@ -345,6 +412,35 @@ body {
color: var(--el-danger-color);
}
/* 增强型状态标签(参考套餐管理页面) */
.status-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: var(--el-font-size-extra-small);
font-weight: 600;
white-space: nowrap;
}
.status-badge.active {
background: rgba(103, 194, 58, 0.1);
color: var(--el-success-color);
}
.status-badge.inactive {
background: rgba(245, 108, 108, 0.1);
color: var(--el-danger-color);
}
.status-badge.info {
background: rgba(64, 158, 255, 0.1);
color: var(--el-primary-color);
}
.status-badge.warning {
background: rgba(230, 162, 60, 0.1);
color: var(--el-warning-color);
}
/* 布局样式 */
.booking-container {
display: grid;
@@ -369,6 +465,99 @@ body {
border-bottom: 1px solid var(--el-border-lighter);
}
/* 详情项布局(参考套餐管理页面) */
.details-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: var(--el-spacing-large);
}
.detail-item {
display: flex;
flex-direction: column;
padding: 12px;
background: var(--el-bg-color-light);
border-radius: var(--el-border-radius-base);
}
.detail-label {
font-size: var(--el-font-size-extra-small);
color: var(--el-text-secondary);
margin-bottom: 4px;
}
.detail-value {
font-size: var(--el-font-size-large);
font-weight: bold;
color: var(--el-text-primary);
}
.detail-value.price {
color: var(--el-danger-color);
}
/* 标签样式(参考套餐管理页面) */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
}
.tag {
display: inline-block;
padding: 6px 12px;
background: rgba(64, 158, 255, 0.1);
color: var(--el-primary-color);
border-radius: 16px;
font-size: var(--el-font-size-extra-small);
font-weight: 500;
}
/* 提醒框样式(参考套餐管理页面) */
.reminder-box {
margin-top: var(--el-spacing-base);
padding: var(--el-spacing-base);
background: var(--el-bg-color-light);
border-radius: var(--el-border-radius-base);
border-left: 4px solid var(--el-primary-color);
}
.reminder-editor {
width: 100%;
padding: 12px;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color);
border-radius: var(--el-border-radius-small);
font-size: var(--el-font-size-base);
line-height: 1.5;
min-height: 80px;
resize: vertical;
font-family: inherit;
}
.reminder-actions {
margin-top: 12px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* 操作按钮组样式(参考套餐管理页面) */
.action-buttons {
display: flex;
gap: 8px;
margin-top: var(--el-spacing-base);
padding-top: var(--el-spacing-base);
border-top: 1px solid var(--el-border-lighter);
}
.action-buttons button,
.action-buttons form {
flex: 1;
}
/* 日历样式 */
.calendar {
display: grid;
@@ -752,6 +941,98 @@ body {
font-size: var(--el-font-size-base);
}
/* 移动端导航菜单(汉堡菜单) */
.mobile-menu-toggle {
display: none;
flex-direction: column;
gap: 5px;
padding: 8px;
background: transparent;
border: none;
cursor: pointer;
z-index: 1001;
}
.mobile-menu-toggle span {
display: block;
width: 25px;
height: 3px;
background: var(--el-text-primary);
border-radius: 2px;
transition: all 0.3s ease;
}
.mobile-menu-toggle.active span:nth-child(1) {
transform: rotate(45deg) translate(8px, 8px);
}
.mobile-menu-toggle.active span:nth-child(2) {
opacity: 0;
}
.mobile-menu-toggle.active span:nth-child(3) {
transform: rotate(-45deg) translate(7px, -7px);
}
.mobile-nav-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
transition: opacity 0.3s ease;
}
.mobile-nav-overlay.active {
display: block;
opacity: 1;
}
.mobile-nav {
display: none;
position: fixed;
top: 0;
left: -100%;
width: 280px;
max-width: 85vw;
height: 100vh;
background: white;
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
z-index: 1000;
overflow-y: auto;
transition: left 0.3s ease;
padding-top: 60px;
}
.mobile-nav.active {
left: 0;
}
.mobile-nav .nav-link {
display: block;
padding: 16px 20px;
border: none;
border-bottom: 1px solid #f0f0f0;
border-radius: 0;
text-align: left;
font-size: 16px;
min-height: 52px;
}
.mobile-nav .nav-link:first-child {
border-top: 1px solid #f0f0f0;
}
.mobile-nav .nav-link.active {
background: rgba(64, 158, 255, 0.1);
border-left: 4px solid var(--el-primary-color);
font-weight: 600;
}
/* 响应式设计 */
/* 平板设备 */
@@ -760,10 +1041,36 @@ body {
--el-spacing-base: 12px;
--el-spacing-large: 16px;
--el-spacing-extra-large: 24px;
--el-font-size-base: 15px;
}
.container {
padding: var(--el-spacing-base);
max-width: 100%;
}
.header {
position: relative;
padding: 12px 16px;
justify-content: flex-start;
gap: 16px;
}
.header h1 {
font-size: 18px;
margin: 0 auto;
}
.mobile-menu-toggle {
display: flex;
}
.nav {
display: none;
}
.mobile-nav {
display: block;
}
.booking-container {
@@ -771,16 +1078,6 @@ body {
gap: var(--el-spacing-base);
}
.header {
flex-direction: column;
align-items: stretch;
text-align: center;
}
.nav {
justify-content: center;
}
.form-row {
flex-direction: column;
gap: var(--el-spacing-base);
@@ -788,6 +1085,7 @@ body {
.form-row .form-group {
flex: none;
min-width: auto;
}
.calendar {
@@ -798,6 +1096,13 @@ body {
.quick-duration {
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
.duration-btn {
min-width: 60px;
padding: 10px 12px;
font-size: 13px;
}
.time-grid {
@@ -805,25 +1110,84 @@ body {
gap: var(--el-spacing-small);
}
.time-slot {
padding: 12px 8px;
font-size: 13px;
min-height: 44px;
}
.package-meta {
justify-content: center;
flex-wrap: wrap;
}
/* 卡片优化 */
.card {
padding: var(--el-spacing-base);
margin-bottom: var(--el-spacing-base);
}
/* 表格优化 */
.table {
font-size: 13px;
}
.table th,
.table td {
padding: 8px 6px;
}
/* VIP搜索优化 */
.vip-search-results {
max-height: 300px;
font-size: 14px;
}
.vip-search-item {
padding: 14px 16px;
min-height: 60px;
}
/* 统计卡片 */
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
padding: 16px;
}
.stat-card .stat-value {
font-size: 24px;
}
}
/* 手机设备 */
@media (max-width: 480px) {
:root {
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-large: 16px;
}
.container {
padding: var(--el-spacing-small);
padding: 8px;
}
.header {
padding: var(--el-spacing-base);
padding: 10px 12px;
border-radius: 0;
margin-bottom: var(--el-spacing-base);
margin-bottom: 12px;
position: sticky;
top: 0;
z-index: 100;
background: white;
}
.header h1 {
font-size: var(--el-font-size-large);
font-size: 16px;
line-height: 1.4;
}
.section {
@@ -831,52 +1195,277 @@ body {
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid var(--el-border-lighter);
margin-bottom: 12px;
}
.calendar {
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.calendar-day {
padding: var(--el-spacing-small);
padding: 10px 6px;
min-height: 60px;
}
.day-number {
font-size: var(--el-font-size-base);
}
.nav {
flex-direction: column;
gap: var(--el-spacing-small);
}
.nav-link {
text-align: center;
justify-content: center;
font-size: 16px;
font-weight: 600;
}
.form-actions {
flex-direction: column;
gap: var(--el-spacing-small);
gap: 10px;
position: sticky;
bottom: 0;
background: white;
padding: 12px;
margin: 0 -12px -12px;
box-shadow: 0 -2px 8px rgba(0,0,0,0.1);
}
.btn {
width: 100%;
justify-content: center;
min-height: 48px;
font-size: 16px;
padding: 14px 20px;
}
.btn-sm {
min-height: 44px;
padding: 10px 16px;
font-size: 14px;
}
.package-meta {
flex-direction: column;
align-items: stretch;
gap: 10px;
}
.modal {
padding: var(--el-spacing-small);
padding: 0;
}
.modal-content {
margin: 0;
max-height: calc(100vh - 20px);
max-height: 100vh;
border-radius: 0;
padding: 20px 16px;
overflow-y: auto;
}
/* 表单优化 */
.form-group {
margin-bottom: 16px;
}
.form-group label {
font-size: 14px;
margin-bottom: 8px;
}
.form-group input,
.form-group select,
.form-group textarea {
font-size: 16px; /* 防止iOS自动缩放 */
padding: 12px 14px;
min-height: 48px;
-webkit-appearance: none;
appearance: none;
}
/* 输入组优化 */
.input-group {
flex-direction: column;
gap: 8px;
}
.input-group .form-control {
border-radius: var(--el-border-radius-base);
width: 100%;
}
.input-group-addon {
border-radius: var(--el-border-radius-base);
text-align: center;
padding: 12px;
}
/* 卡片优化 */
.card {
padding: 16px;
margin-bottom: 12px;
border-radius: 8px;
}
/* 表格优化 - 移动端横向滚动 */
.table-wrapper {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table {
min-width: 600px;
font-size: 13px;
}
.table th,
.table td {
padding: 10px 8px;
white-space: nowrap;
}
/* VIP卡片优化 */
.vip-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.vip-card {
padding: 16px;
}
.vip-actions {
flex-direction: column;
gap: 8px;
}
.vip-actions .btn {
width: 100%;
}
/* 套餐卡片优化 */
.packages-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.package-card {
padding: 16px;
}
.package-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.package-title {
font-size: 18px;
}
.package-details {
grid-template-columns: 1fr;
gap: 10px;
}
.package-actions {
flex-direction: column;
gap: 8px;
}
.package-actions button,
.package-actions form {
width: 100%;
}
/* 搜索框优化 */
.search-box input {
font-size: 16px;
padding: 12px 16px 12px 40px;
}
/* 筛选区域优化 */
.filter-section {
padding: 16px;
}
.filter-row {
flex-direction: column;
gap: 12px;
}
.filter-group {
width: 100%;
}
.filter-group select {
width: 100%;
padding: 12px 16px;
font-size: 16px;
min-height: 48px;
}
/* 统计卡片优化 */
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 10px;
}
.stat-card {
padding: 14px;
}
.stat-card .stat-value {
font-size: 22px;
}
.stat-card .stat-label {
font-size: 12px;
}
/* 时间选择优化 */
.time-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.time-slot {
padding: 14px 8px;
font-size: 14px;
min-height: 48px;
}
/* 快速时长按钮优化 */
.quick-duration {
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
}
.duration-btn {
flex: 1;
min-width: calc(50% - 4px);
padding: 12px;
font-size: 14px;
min-height: 48px;
}
/* 消息提示优化 */
.message {
padding: 12px 16px;
font-size: 14px;
margin-bottom: 12px;
border-radius: 8px;
}
/* 空状态优化 */
.empty-state {
padding: 40px 20px;
}
.empty-icon {
font-size: 48px;
}
.empty-message {
font-size: 16px;
}
.empty-submessage {
font-size: 13px;
}
/* 套餐管理页面移动端优化 */
@@ -961,8 +1550,12 @@ body {
/* 小屏幕手机 */
@media (max-width: 360px) {
.header h1 {
font-size: 15px;
}
.calendar {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, 1fr);
}
.time-grid {
@@ -974,27 +1567,99 @@ body {
align-items: stretch;
}
.duration-btn {
width: 100%;
}
.stats-grid {
grid-template-columns: 1fr;
}
.form-group input,
.form-group select,
.form-group textarea {
font-size: 16px; /* 防止iOS缩放 */
}
.mobile-nav {
width: 260px;
}
}
/* 触摸设备优化 */
@media (hover: none) and (pointer: coarse) {
/* 增加触摸目标大小 */
.btn {
min-height: 44px;
padding: 12px 20px;
-webkit-tap-highlight-color: rgba(64, 158, 255, 0.2);
tap-highlight-color: rgba(64, 158, 255, 0.2);
}
.btn:active {
transform: scale(0.98);
opacity: 0.9;
}
.form-group input,
.form-group select,
.form-group textarea {
min-height: 44px;
-webkit-tap-highlight-color: rgba(64, 158, 255, 0.1);
tap-highlight-color: rgba(64, 158, 255, 0.1);
}
.nav-link {
min-height: 44px;
padding: 12px 16px;
-webkit-tap-highlight-color: rgba(64, 158, 255, 0.2);
tap-highlight-color: rgba(64, 158, 255, 0.2);
}
.mobile-nav .nav-link {
min-height: 52px;
}
.time-slot {
min-height: 44px;
-webkit-tap-highlight-color: rgba(64, 158, 255, 0.2);
tap-highlight-color: rgba(64, 158, 255, 0.2);
}
.time-slot:active {
transform: scale(0.95);
}
.calendar-day {
min-height: 60px;
-webkit-tap-highlight-color: rgba(64, 158, 255, 0.2);
tap-highlight-color: rgba(64, 158, 255, 0.2);
}
/* 禁用hover效果 */
.btn:hover:not(:disabled) {
transform: none;
}
.nav-link:hover {
transform: none;
}
/* 优化滚动 */
* {
-webkit-overflow-scrolling: touch;
}
/* 优化文本选择 */
body {
-webkit-user-select: none;
user-select: none;
}
input,
textarea {
-webkit-user-select: text;
user-select: text;
}
}
+20 -1
View File
@@ -62,7 +62,26 @@ try {
}
// 计算新的持续时间(分钟)
$duration = $start_dt->diff($end_dt)->i + ($start_dt->diff($end_dt)->h * 60);
// #region agent log
$log_data = json_encode(['location' => 'update_booking.php:65', 'message' => 'Calculating duration', 'data' => ['start_time' => $new_start_time, 'end_time' => $new_end_time], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'C']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
$interval = $start_dt->diff($end_dt);
$duration = ($interval->days * 24 * 60) + ($interval->h * 60) + $interval->i;
// #region agent log
$log_data = json_encode(['location' => 'update_booking.php:67', 'message' => 'Duration calculated', 'data' => ['duration' => $duration, 'days' => $interval->days, 'hours' => $interval->h, 'minutes' => $interval->i], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'C']);
// 确保日志目录存在
$log_dir = '.cursor';
if (!file_exists($log_dir)) {
mkdir($log_dir, 0777, true);
}
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
// 更新预约时间和持续时间
$stmt = $pdo->prepare("UPDATE bookings SET start_time = ?, end_time = ?, duration = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?");
+140 -9
View File
@@ -94,6 +94,56 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare("DELETE FROM vip_customers WHERE id = ?");
$stmt->execute([$id]);
$success_message = "VIP客户删除成功!";
} elseif ($action === 'convert_from_booking') {
// 从预约记录转换为VIP客户
$booking_id = isset($_POST['booking_id']) ? (int)$_POST['booking_id'] : 0;
if ($booking_id <= 0) {
throw new Exception('无效的预约ID');
}
// 获取预约信息
$stmt = $pdo->prepare("SELECT customer_name, phone, car_model, car_number FROM bookings WHERE id = ?");
$stmt->execute([$booking_id]);
$booking = $stmt->fetch();
if (!$booking) {
throw new Exception('预约记录不存在');
}
// 检查是否已经是VIP客户
$stmt = $pdo->prepare("SELECT id FROM vip_customers WHERE phone = ? AND car_number = ?");
$stmt->execute([$booking['phone'], $booking['car_number']]);
if ($stmt->fetch()) {
throw new Exception('该客户已经是VIP客户');
}
// 插入VIP客户
$stmt = $pdo->prepare("INSERT INTO vip_customers
(customer_name, phone, car_model, car_number, notes)
VALUES (?, ?, ?, ?, ?)");
try {
$notes = '从预约记录 #' . $booking_id . ' 自动转换';
$stmt->execute([
$booking['customer_name'],
$booking['phone'],
$booking['car_model'] ?? '',
$booking['car_number'] ?? '',
$notes
]);
// 更新该客户的所有预约记录的member_type为VIP会员
$stmt = $pdo->prepare("UPDATE bookings SET member_type = 'VIP会员' WHERE phone = ? AND car_number = ?");
$stmt->execute([$booking['phone'], $booking['car_number']]);
$success_message = "客户已成功转换为VIP客户!";
} catch (PDOException $e) {
if ($e->errorInfo[1] == 1062) {
throw new Exception('该客户已经是VIP客户');
}
throw $e;
}
}
}
} catch (Exception $e) {
@@ -143,20 +193,37 @@ try {
<meta name="keywords" content="VIP管理,客户管理,会员管理">
<title>张老师撸车(私家车库)工作室</title>
<link rel="stylesheet" href="style.css">
<script src="mobile-nav.js" defer></script>
</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>🚗 张老师撸车工作室 - VIP管理</h1>
<nav class="nav">
<a href="index.php" class="nav-link">预约洗车</a>
<a href="bookings.php" class="nav-link">预约管理</a>
<a href="pending_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 active">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">预约洗车</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 active">VIP管理</a>
<a href="announcement.php" class="nav-link">今日待办</a>
</nav>
<?php if (isset($success_message)): ?>
<div class="success-message"><?php echo $success_message; ?></div>
@@ -174,7 +241,7 @@ try {
<div class="vip-management">
<!-- VIP搜索表单 -->
<div class="card search-container">
<div class="card search-container enhanced-card">
<h2>🔍 VIP客户搜索</h2>
<form method="GET" class="form search-form">
<div class="form-row">
@@ -195,7 +262,7 @@ try {
</div>
<!-- VIP录入表单 -->
<div class="card">
<div class="card enhanced-card">
<h2> 录入新VIP客户</h2>
<form method="POST" class="form">
<input type="hidden" name="action" value="add_vip">
@@ -248,8 +315,66 @@ try {
</form>
</div>
<!-- 从预约转换为VIP -->
<div class="card enhanced-card">
<h2>📋 从预约记录转换为VIP客户</h2>
<p style="color: #666; margin-bottom: 15px;">选择预约记录,快速将普通客户转换为VIP客户</p>
<?php
// 获取可以转换为VIP的普通客户预约记录(最近30天,且不是VIP客户)
try {
$stmt = $pdo->prepare("SELECT DISTINCT b.id, b.customer_name, b.phone, b.car_model, b.car_number,
COUNT(*) as booking_count,
MAX(b.start_time) as last_booking_time
FROM bookings b
LEFT JOIN vip_customers v ON b.phone = v.phone AND b.car_number = v.car_number AND v.is_active = 1
WHERE b.member_type = '普通客户'
AND v.id IS NULL
AND b.start_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY b.phone, b.car_number, b.customer_name, b.car_model
ORDER BY last_booking_time DESC
LIMIT 20");
$stmt->execute();
$convertible_bookings = $stmt->fetchAll();
} catch (Exception $e) {
$convertible_bookings = [];
}
?>
<?php if (empty($convertible_bookings)): ?>
<div class="empty-message">暂无可转换的普通客户</div>
<?php else: ?>
<div style="max-height: 400px; overflow-y: auto;">
<?php foreach ($convertible_bookings as $booking): ?>
<div style="padding: 12px; border: 1px solid #e0e0e0; border-radius: 6px; margin-bottom: 10px; background: #f9f9f9;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<strong><?php echo htmlspecialchars($booking['customer_name']); ?></strong><br>
<span style="color: #666; font-size: 0.9em;">
手机号:<?php echo htmlspecialchars($booking['phone']); ?> |
车牌号:<?php echo htmlspecialchars($booking['car_number']); ?><br>
<?php if ($booking['car_model']): ?>
车型:<?php echo htmlspecialchars($booking['car_model']); ?> |
<?php endif; ?>
预约次数:<?php echo $booking['booking_count']; ?>
</span>
</div>
<form method="POST" style="margin-left: 15px;">
<input type="hidden" name="action" value="convert_from_booking">
<input type="hidden" name="booking_id" value="<?php echo $booking['id']; ?>">
<button type="submit" class="btn btn-sm" style="background-color: #ffd700; color: #333; font-weight: bold;"
onclick="return confirm('确定要将 <?php echo htmlspecialchars($booking['customer_name']); ?> 转换为VIP客户吗?')">
👑 转为VIP
</button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<!-- VIP客户列表 -->
<div class="card">
<div class="card enhanced-card">
<h2>👑 VIP客户列表 ( <?php echo count($vip_customers); ?> 位)</h2>
<?php if (empty($vip_customers)): ?>
@@ -257,7 +382,7 @@ try {
<?php else: ?>
<div class="vip-grid">
<?php foreach ($vip_customers as $vip): ?>
<div class="vip-card">
<div class="vip-card enhanced-card">
<div class="vip-header">
<h3><?php echo htmlspecialchars($vip['customer_name']); ?></h3>
<div class="vip-status">👑 VIP</div>
@@ -414,13 +539,19 @@ try {
<option value="">请选择套餐</option>
<?php
// 查询所有可用的洗车套餐
$package_result = $conn->query("SELECT id, package_name, price FROM packages ORDER BY price ASC");
if ($package_result && $package_result->num_rows > 0) {
while ($package = $package_result->fetch_assoc()) {
// #region agent log
$log_data = json_encode(['location' => 'vip.php:417', 'message' => 'Loading packages', 'data' => ['using_pdo' => isset($pdo), 'using_conn' => isset($conn)], 'timestamp' => time() * 1000, 'sessionId' => 'debug-session', 'runId' => 'run1', 'hypothesisId' => 'B']);
file_put_contents('.cursor/debug.log', $log_data . "\n", FILE_APPEND);
// #endregion
try {
$stmt = $pdo->query("SELECT id, package_name, price FROM packages WHERE is_active = 1 ORDER BY price ASC");
$packages = $stmt->fetchAll();
foreach ($packages as $package) {
echo "<option value='{$package['id']}'>{$package['package_name']} - ¥{$package['price']}</option>";
}
} catch (Exception $e) {
echo "<option value=''>加载套餐失败</option>";
}
$package_result->free_result();
?>
</select>
</div>