This commit is contained in:
2025-10-23 00:22:43 +08:00
parent 0881d3fbcb
commit d67ab615ce
34 changed files with 9442 additions and 1 deletions

View 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>