init
This commit is contained in:
582
frontend/public/checkin.html
Normal file
582
frontend/public/checkin.html
Normal file
@ -0,0 +1,582 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>会议签到</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group.required label::after {
|
||||
content: " *";
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.message.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-page {
|
||||
text-align: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.success-page.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.success-icon {
|
||||
font-size: 80px;
|
||||
color: #28a745;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
font-size: 28px;
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
margin-bottom: 30px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.success-info {
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.success-info h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.info-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #6c757d;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #5a6268;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1 id="mainTitle">会议签到</h1>
|
||||
<p id="headerSubtitle">请填写以下信息完成签到</p>
|
||||
</div>
|
||||
|
||||
<div id="message" class="message"></div>
|
||||
|
||||
<!-- 签到表单 -->
|
||||
<div id="checkinFormContainer">
|
||||
<form id="checkinForm">
|
||||
<div class="form-group required">
|
||||
<label for="name">姓名</label>
|
||||
<input type="text" id="name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group required">
|
||||
<label for="phone">手机号</label>
|
||||
<input type="tel" id="phone" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="company">公司</label>
|
||||
<input type="text" id="company" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="position">职位</label>
|
||||
<input type="text" id="position" class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">邮箱</label>
|
||||
<input type="email" id="email" class="form-control">
|
||||
</div>
|
||||
|
||||
<button type="submit" id="submitBtn" class="btn">签到</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 签到成功页面 -->
|
||||
<div id="successPage" class="success-page">
|
||||
<div class="success-icon">✓</div>
|
||||
<div class="success-title">签到成功!</div>
|
||||
<div class="success-message">欢迎参加本次会议,祝您会议愉快!</div>
|
||||
|
||||
<div class="success-info">
|
||||
<h3>您的签到信息</h3>
|
||||
<div class="info-item">
|
||||
<span class="info-label">姓名:</span>
|
||||
<span class="info-value" id="successName">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">手机号:</span>
|
||||
<span class="info-value" id="successPhone">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">公司:</span>
|
||||
<span class="info-value" id="successCompany">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">职位:</span>
|
||||
<span class="info-value" id="successPosition">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">邮箱:</span>
|
||||
<span class="info-value" id="successEmail">-</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">签到时间:</span>
|
||||
<span class="info-value" id="successTime">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn-secondary" onclick="resetToForm()">重新签到</button>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>© <span id="currentYear"></span> 道友签到系统</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局配置对象
|
||||
let appConfig = {
|
||||
main_title: '会议签到',
|
||||
subtitle: '请填写以下信息完成签到',
|
||||
success_title: '签到成功',
|
||||
success_message: '欢迎参加本次会议,祝您会议愉快!',
|
||||
already_checked_title: '您已签到',
|
||||
already_checked_message: '您之前已经完成签到,无需重复签到。祝您会议愉快!',
|
||||
success_main_title: '签到成功'
|
||||
};
|
||||
|
||||
// 检测当前环境并设置API基础URL
|
||||
const getApiBaseUrl = () => {
|
||||
if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
||||
return 'http://127.0.0.1:3001';
|
||||
} else {
|
||||
// 在生产环境中,您可以设置实际的API地址
|
||||
return window.location.origin;
|
||||
}
|
||||
};
|
||||
|
||||
const API_BASE_URL = getApiBaseUrl();
|
||||
|
||||
// 从API加载配置
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/config`);
|
||||
if (response.ok) {
|
||||
const configs = await response.json();
|
||||
// 更新全局配置对象
|
||||
configs.forEach(config => {
|
||||
if (appConfig.hasOwnProperty(config.config_key)) {
|
||||
appConfig[config.config_key] = config.config_value;
|
||||
}
|
||||
});
|
||||
// 更新页面显示
|
||||
updatePageContent();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('加载配置失败,使用默认配置:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新页面内容
|
||||
function updatePageContent() {
|
||||
document.getElementById('mainTitle').textContent = appConfig.main_title;
|
||||
document.getElementById('headerSubtitle').textContent = appConfig.subtitle;
|
||||
}
|
||||
|
||||
// 页面加载时初始化配置
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
loadConfig();
|
||||
});
|
||||
|
||||
document.getElementById('checkinForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// 获取表单数据
|
||||
const formData = {
|
||||
name: document.getElementById('name').value,
|
||||
phone: document.getElementById('phone').value,
|
||||
email: document.getElementById('email').value || null,
|
||||
company: document.getElementById('company').value || null,
|
||||
position: document.getElementById('position').value || null
|
||||
};
|
||||
|
||||
// 简单验证
|
||||
if (!formData.name || !formData.phone) {
|
||||
showMessage('请填写姓名和手机号', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单手机号验证
|
||||
if (!/^1[3-9]\d{9}$/.test(formData.phone)) {
|
||||
showMessage('请输入正确的手机号格式', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 简单邮箱验证
|
||||
if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||||
showMessage('请输入正确的邮箱格式', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用提交按钮
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
const originalText = submitBtn.textContent;
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = '签到中...';
|
||||
|
||||
try {
|
||||
// 首先尝试通过邮箱或手机号查找用户
|
||||
let attendeeId = null;
|
||||
let isExistingUser = false;
|
||||
|
||||
// 获取所有参会者列表来查找匹配的用户
|
||||
const listResponse = await fetch(API_BASE_URL + '/api/attendees?page_size=1000');
|
||||
if (listResponse.ok) {
|
||||
const response = await listResponse.json();
|
||||
const attendees = response.data; // 从分页响应中获取data数组
|
||||
const existingAttendee = attendees.find(a =>
|
||||
formData.phone && a.phone === formData.phone && formData.name === a.name
|
||||
);
|
||||
|
||||
if (existingAttendee) {
|
||||
attendeeId = existingAttendee.id;
|
||||
isExistingUser = true;
|
||||
|
||||
// 如果已经签到过了
|
||||
if (existingAttendee.checked_in) {
|
||||
showSuccessPage(formData, existingAttendee, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isExistingUser && attendeeId) {
|
||||
// 用户已存在,直接签到
|
||||
const checkinResponse = await fetch(API_BASE_URL + `/api/attendees/${attendeeId}/checkin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (checkinResponse.ok) {
|
||||
const result = await checkinResponse.json();
|
||||
showSuccessPage(formData, result);
|
||||
} else {
|
||||
const errorText = await checkinResponse.text();
|
||||
showMessage('签到失败:' + (errorText || '服务器返回错误状态码: ' + checkinResponse.status), 'error');
|
||||
}
|
||||
} else {
|
||||
// 用户不存在,先注册再签到
|
||||
const registerResponse = await fetch(API_BASE_URL + '/api/attendees', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
|
||||
if (registerResponse.ok) {
|
||||
const newUser = await registerResponse.json();
|
||||
|
||||
// 注册成功后立即签到
|
||||
const checkinResponse = await fetch(API_BASE_URL + `/api/attendees/${newUser.id}/checkin`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (checkinResponse.ok) {
|
||||
const result = await checkinResponse.json();
|
||||
showSuccessPage(formData, result);
|
||||
} else {
|
||||
showMessage('注册成功但签到失败,请联系管理员。', 'error');
|
||||
}
|
||||
} else {
|
||||
const errorText = await registerResponse.text();
|
||||
showMessage('注册失败:' + (errorText || '服务器返回错误状态码: ' + registerResponse.status), 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error details:', error);
|
||||
showMessage('网络错误,请稍后重试。错误详情: ' + error.message, 'error');
|
||||
} finally {
|
||||
// 恢复提交按钮
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.textContent = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
function showMessage(text, type) {
|
||||
const messageEl = document.getElementById('message');
|
||||
messageEl.textContent = text;
|
||||
messageEl.className = 'message ' + type;
|
||||
|
||||
// 3秒后自动隐藏成功消息
|
||||
if (type === 'success') {
|
||||
setTimeout(() => {
|
||||
messageEl.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function showSuccessPage(formData, result, isAlreadyCheckedIn = false) {
|
||||
// 隐藏表单和消息
|
||||
document.getElementById('checkinFormContainer').style.display = 'none';
|
||||
document.getElementById('message').style.display = 'none';
|
||||
document.getElementById('headerSubtitle').style.display = 'none';
|
||||
|
||||
// 更新主标题为配置中的成功标题
|
||||
const mainTitle = document.getElementById('mainTitle');
|
||||
mainTitle.textContent = appConfig.success_main_title;
|
||||
|
||||
// 填充成功页面信息
|
||||
document.getElementById('successName').textContent = formData.name || result.name || '-';
|
||||
document.getElementById('successPhone').textContent = formData.phone || result.phone || '-';
|
||||
document.getElementById('successCompany').textContent = formData.company || result.company || '-';
|
||||
document.getElementById('successPosition').textContent = formData.position || result.position || '-';
|
||||
document.getElementById('successEmail').textContent = formData.email || result.email || '-';
|
||||
|
||||
// 格式化签到时间
|
||||
let checkinTime = '-';
|
||||
if (result.checkin_time) {
|
||||
const date = new Date(result.checkin_time);
|
||||
// 后端已经返回北京时间,直接格式化显示
|
||||
checkinTime = date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
} else {
|
||||
// 如果没有签到时间,使用当前时间
|
||||
checkinTime = new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
}
|
||||
document.getElementById('successTime').textContent = checkinTime;
|
||||
|
||||
// 更新标题和消息
|
||||
const titleEl = document.querySelector('.success-title');
|
||||
const messageEl = document.querySelector('.success-message');
|
||||
|
||||
if (isAlreadyCheckedIn) {
|
||||
titleEl.textContent = appConfig.already_checked_title;
|
||||
messageEl.textContent = appConfig.already_checked_message;
|
||||
} else {
|
||||
titleEl.textContent = appConfig.success_title;
|
||||
messageEl.textContent = appConfig.success_message;
|
||||
}
|
||||
|
||||
// 显示成功页面
|
||||
document.getElementById('successPage').classList.add('show');
|
||||
}
|
||||
|
||||
function resetToForm() {
|
||||
// 隐藏成功页面
|
||||
document.getElementById('successPage').classList.remove('show');
|
||||
|
||||
// 恢复原始标题
|
||||
document.getElementById('mainTitle').textContent = appConfig.main_title;
|
||||
|
||||
// 显示表单和副标题
|
||||
document.getElementById('checkinFormContainer').style.display = 'block';
|
||||
document.getElementById('headerSubtitle').style.display = 'block';
|
||||
|
||||
// 重置表单
|
||||
document.getElementById('checkinForm').reset();
|
||||
|
||||
// 隐藏消息
|
||||
document.getElementById('message').style.display = 'none';
|
||||
}
|
||||
|
||||
// 设置当前年份
|
||||
document.getElementById('currentYear').textContent = new Date().getFullYear();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user