866 lines
29 KiB
HTML
866 lines
29 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ config.event_title }} - 签到大屏</title>
|
|
<style>
|
|
:root {
|
|
--primary-color: {{ config.primary_color }};
|
|
--secondary-color: {{ config.secondary_color }};
|
|
--bg-color: {{ config.bg_color }};
|
|
--card-bg: rgba(12, 24, 50, 0.6);
|
|
--text-color: #ffffff;
|
|
--header-img: url('{{ config.header_image }}');
|
|
--bg-opacity: {{ config.wall_config.bg_opacity if config.wall_config and config.wall_config.bg_opacity is not none else 0.3 }};
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
|
background-color: var(--bg-color);
|
|
color: var(--text-color);
|
|
overflow: hidden;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
position: relative;
|
|
}
|
|
|
|
/* Simplified Background */
|
|
.bg-layer {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background-image: var(--header-img);
|
|
background-size: cover;
|
|
background-position: center;
|
|
opacity: var(--bg-opacity);
|
|
z-index: -3;
|
|
/* Dynamic blur: no blur if opacity is 1 */
|
|
filter: {{ 'none' if config.wall_config and config.wall_config.bg_opacity == 1 else 'blur(10px)' }};
|
|
}
|
|
|
|
/* Subtle overlay to ensure text contrast */
|
|
.clean-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.2);
|
|
z-index: -2;
|
|
}
|
|
|
|
/* Header */
|
|
header {
|
|
position: absolute;
|
|
top: 30px;
|
|
left: 0;
|
|
width: 100%;
|
|
text-align: center;
|
|
z-index: 20; /* Above bubbles */
|
|
pointer-events: none; /* Let clicks pass through */
|
|
}
|
|
|
|
h1 {
|
|
font-size: 3rem;
|
|
color: #ffffff;
|
|
font-weight: 900;
|
|
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
|
margin: 0;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.subtitle {
|
|
font-size: 1.2rem;
|
|
color: var(--primary-color);
|
|
letter-spacing: 4px;
|
|
text-transform: uppercase;
|
|
margin-bottom: 10px;
|
|
opacity: 0.9;
|
|
text-shadow: 0 0 5px rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
/* Container for User Bubbles */
|
|
#bubble-container {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.user-bubble {
|
|
position: absolute;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 60px;
|
|
height: 60px; /* Reduced from 80px */
|
|
transition: transform 0.8s cubic-bezier(0.2, 0.8, 0.2, 1), opacity 0.5s ease;
|
|
will-change: transform, left, top;
|
|
}
|
|
|
|
.bubble-avatar {
|
|
width: 100%; /* Fill container */
|
|
height: 100%; /* Fill container */
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
|
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 0.9rem; /* Reduced font size */
|
|
color: #fff;
|
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
|
margin-bottom: 0; /* Removed margin */
|
|
overflow: hidden;
|
|
text-align: center;
|
|
padding: 2px;
|
|
line-height: 1.1;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.bubble-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.bubble-name {
|
|
font-size: 0.9rem;
|
|
color: #fff;
|
|
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 100px;
|
|
text-align: center;
|
|
background: rgba(0,0,0,0.3);
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* Central Spotlight Card */
|
|
.spotlight-container {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -45%);
|
|
width: 80%;
|
|
max-width: 900px;
|
|
z-index: 10;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.spotlight-card {
|
|
background: rgba(10, 20, 40, 0.85);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
border-radius: 20px;
|
|
padding: 50px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
|
text-align: center;
|
|
opacity: 0;
|
|
transform: scale(0.9) translateY(20px);
|
|
transition: all 0.8s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
}
|
|
|
|
.spotlight-card.active {
|
|
opacity: 1;
|
|
transform: scale(1) translateY(0);
|
|
}
|
|
|
|
.user-info {
|
|
font-size: 1.8rem;
|
|
color: #b0c4de;
|
|
margin-bottom: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.user-avatar-large {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--secondary-color), var(--primary-color));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 2rem;
|
|
color: #fff;
|
|
border: 3px solid rgba(255,255,255,0.5);
|
|
}
|
|
|
|
.company-name {
|
|
font-size: 3rem;
|
|
font-weight: 800;
|
|
color: #fff;
|
|
margin-bottom: 20px;
|
|
background: linear-gradient(90deg, #fff, #b3d9ff);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.card-position {
|
|
font-size: 1.2rem;
|
|
color: var(--primary-color);
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.card-vision {
|
|
font-size: 1.4rem;
|
|
color: #dbe4ff;
|
|
font-style: italic;
|
|
margin-top: 15px;
|
|
}
|
|
|
|
.card-scope {
|
|
font-size: 1.2rem;
|
|
color: #dbe4ff;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.card-custom-field {
|
|
font-size: 1.2rem;
|
|
color: #dbe4ff;
|
|
margin-top: 10px;
|
|
padding: 8px 15px;
|
|
background: rgba(255,255,255,0.05);
|
|
border-radius: 8px;
|
|
display: inline-block;
|
|
}
|
|
|
|
/* Stats Counter */
|
|
.stats-counter {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
right: 40px;
|
|
text-align: right;
|
|
z-index: 20;
|
|
background: rgba(0,0,0,0.3);
|
|
padding: 10px 20px;
|
|
border-radius: 10px;
|
|
border: 1px solid rgba(255,255,255,0.1);
|
|
}
|
|
|
|
.stats-number {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: var(--primary-color);
|
|
}
|
|
|
|
.stats-label {
|
|
font-size: 0.8rem;
|
|
color: #aaa;
|
|
}
|
|
|
|
/* 二维码容器 */
|
|
.qrcode-container {
|
|
position: fixed;
|
|
top: 50%;
|
|
right: 40px;
|
|
transform: translateY(-50%);
|
|
z-index: 25;
|
|
animation: fadeInUp 0.6s ease-out;
|
|
}
|
|
|
|
.qrcode-box {
|
|
background: rgba(10, 20, 40, 0.9);
|
|
backdrop-filter: blur(20px);
|
|
-webkit-backdrop-filter: blur(20px);
|
|
border: 2px solid rgba(0, 242, 255, 0.5);
|
|
border-radius: 16px;
|
|
padding: 20px;
|
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5), 0 0 20px rgba(0, 242, 255, 0.2);
|
|
}
|
|
|
|
.qrcode-title {
|
|
font-size: 1rem;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
text-align: center;
|
|
margin-bottom: 12px;
|
|
text-shadow: 0 0 10px rgba(0, 242, 255, 0.5);
|
|
}
|
|
|
|
.qrcode-image {
|
|
width: 150px;
|
|
height: 150px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-50%) translateX(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
}
|
|
|
|
/* Mobile Learn More Button */
|
|
.mobile-btn-container {
|
|
display: none;
|
|
position: absolute;
|
|
bottom: 80px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 30;
|
|
width: 80%;
|
|
text-align: center;
|
|
}
|
|
|
|
.learn-more-btn {
|
|
background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
|
|
color: white;
|
|
padding: 12px 30px;
|
|
border-radius: 50px;
|
|
text-decoration: none;
|
|
font-weight: bold;
|
|
box-shadow: 0 5px 20px rgba(0, 242, 255, 0.4);
|
|
display: inline-block;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.learn-more-btn:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
/* Mobile Optimization */
|
|
@media (max-width: 768px) {
|
|
.mobile-btn-container {
|
|
display: block;
|
|
}
|
|
h1 {
|
|
font-size: 1.8rem;
|
|
}
|
|
.subtitle {
|
|
font-size: 0.8rem;
|
|
letter-spacing: 2px;
|
|
}
|
|
.spotlight-card {
|
|
padding: 20px;
|
|
width: 90%;
|
|
}
|
|
.company-name {
|
|
font-size: 1.5rem;
|
|
}
|
|
.user-info {
|
|
font-size: 1.2rem;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
.user-avatar-large {
|
|
width: 60px;
|
|
height: 60px;
|
|
font-size: 1.5rem;
|
|
}
|
|
.stats-counter {
|
|
bottom: 10px;
|
|
right: 10px;
|
|
padding: 5px 10px;
|
|
}
|
|
.stats-number {
|
|
font-size: 1.5rem;
|
|
}
|
|
.qrcode-container {
|
|
position: fixed;
|
|
top: auto;
|
|
bottom: 80px;
|
|
right: 50%;
|
|
transform: translateX(50%);
|
|
}
|
|
.qrcode-box {
|
|
padding: 12px;
|
|
}
|
|
.qrcode-title {
|
|
font-size: 0.9rem;
|
|
}
|
|
.qrcode-image {
|
|
width: 100px;
|
|
height: 100px;
|
|
}
|
|
}
|
|
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{% set wc = config.wall_config if config.wall_config else {} %}
|
|
{% if wc.show_title != false %}
|
|
<header>
|
|
<div class="subtitle">{{ config.event_sub_title }}</div>
|
|
<h1>{{ config.event_title }}</h1>
|
|
</header>
|
|
{% endif %}
|
|
|
|
<div class="bg-layer"></div>
|
|
<div class="clean-overlay"></div>
|
|
|
|
<div id="bubble-container"></div>
|
|
|
|
{% set wc = config.wall_config if config.wall_config else {} %}
|
|
{% set show = wc.show_fields if wc.show_fields else {} %}
|
|
|
|
<div class="spotlight-container">
|
|
<div class="spotlight-card" id="card">
|
|
<div class="user-info" id="card-header">
|
|
<div class="user-avatar-large" id="card-avatar">?</div>
|
|
<div>
|
|
<span id="card-name" style="font-weight: bold; color: white;"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="card-fields" style="margin-top: 20px;">
|
|
<!-- 动态字段将在这里渲染 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 扫码签到二维码 -->
|
|
<div class="qrcode-container" id="qrcode-container" style="display: none;">
|
|
<div class="qrcode-box">
|
|
<div class="qrcode-title">扫码签到</div>
|
|
<img src="/static/qrcode.png" alt="签到二维码" class="qrcode-image" id="wall-qrcode">
|
|
</div>
|
|
</div>
|
|
|
|
{% if wc.learn_more_url %}
|
|
<div class="mobile-btn-container">
|
|
<a href="{{ wc.learn_more_url }}" class="learn-more-btn" target="_blank">了解我们</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="stats-counter">
|
|
<div class="stats-number" id="checkin-count">0</div>
|
|
<div class="stats-label">已签到嘉宾</div>
|
|
</div>
|
|
|
|
<script>
|
|
// --- Configuration ---
|
|
const CONFIG = {
|
|
gridThreshold: 20, // Switch to grid when users > 20
|
|
maxDisplay: 30, // Maximum bubbles to display at once
|
|
rotationInterval: 10000, // Rotation interval in ms (10s)
|
|
bubbleSize: 60, // Width of bubble
|
|
bubbleHeight: 60, // Height of bubble
|
|
speed: 1.5, // Floating speed
|
|
};
|
|
|
|
const isMobile = () => window.innerWidth <= 768;
|
|
|
|
// 大屏显示字段配置(从后端获取)
|
|
const WALL_SHOW_FIELDS = {{ config.wall_config.show_fields | tojson if config.wall_config and config.wall_config.show_fields else '{}' }};
|
|
const CHECKIN_FIELD_CONFIG = {{ config.checkin_field_config | tojson if config.checkin_field_config else '{}' }};
|
|
const WALL_CONFIG = {{ config.wall_config | tojson if config.wall_config else '{}' }};
|
|
|
|
// --- State ---
|
|
let allUsers = []; // All fetched users
|
|
let displayedUsers = []; // Currently displayed users
|
|
let bubbles = []; // Array of UserBubble instances
|
|
let currentIndex = 0; // For spotlight
|
|
let rotationIndex = 0; // For batch rotation
|
|
let rotationTimer = null;
|
|
let isGridMode = false;
|
|
|
|
const container = document.getElementById('bubble-container');
|
|
let width = window.innerWidth;
|
|
let height = window.innerHeight;
|
|
|
|
window.addEventListener('resize', () => {
|
|
width = window.innerWidth;
|
|
height = window.innerHeight;
|
|
if (isGridMode) arrangeGrid();
|
|
});
|
|
|
|
// 控制二维码显示/隐藏
|
|
const qrcodeContainer = document.getElementById('qrcode-container');
|
|
if (qrcodeContainer) {
|
|
// 如果 show_qrcode 为 true 则显示,默认为 false
|
|
if (WALL_CONFIG.show_qrcode === true) {
|
|
qrcodeContainer.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// 随机图标列表
|
|
const RANDOM_ICONS = ['🎯', '⭐', '🌟', '💫', '✨', '🎊', '🎉', '🎈', '🚀', '💎', '🏆', '🎖️', '🌈', '🦄', '🎪', '🎨', '🎭', '🎪', '🎯', '🎲'];
|
|
|
|
// --- Class: UserBubble ---
|
|
class UserBubble {
|
|
constructor(user) {
|
|
this.user = user;
|
|
this.id = user.gsdh_id || user.name + Math.random(); // Fallback ID
|
|
this.element = document.createElement('div');
|
|
this.element.className = 'user-bubble';
|
|
|
|
const showName = WALL_SHOW_FIELDS.name !== false;
|
|
let bubbleContent;
|
|
let fontSize = '0.9rem';
|
|
|
|
if (showName) {
|
|
const name = user.name || '嘉宾';
|
|
if (name.length > 3) fontSize = '0.7rem';
|
|
if (name.length > 4) fontSize = '0.6rem';
|
|
bubbleContent = name;
|
|
} else {
|
|
const randomIndex = Math.floor(Math.random() * RANDOM_ICONS.length);
|
|
bubbleContent = RANDOM_ICONS[randomIndex];
|
|
fontSize = '1.5rem';
|
|
}
|
|
|
|
this.element.innerHTML = `
|
|
<div class="bubble-avatar" style="font-size: ${fontSize}">${bubbleContent}</div>
|
|
`;
|
|
|
|
// Interaction: Click to show in spotlight
|
|
this.element.style.cursor = 'pointer';
|
|
this.element.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
showUserManual(this.user);
|
|
});
|
|
|
|
// Initial Fade In
|
|
this.element.style.opacity = '0';
|
|
container.appendChild(this.element);
|
|
|
|
// Trigger reflow for transition
|
|
requestAnimationFrame(() => {
|
|
this.element.style.opacity = '1';
|
|
});
|
|
|
|
// Initial Position (Random)
|
|
this.x = Math.random() * (width - CONFIG.bubbleSize);
|
|
this.y = Math.random() * (height - CONFIG.bubbleHeight);
|
|
|
|
// Velocity for floating
|
|
this.vx = (Math.random() - 0.5) * CONFIG.speed;
|
|
this.vy = (Math.random() - 0.5) * CONFIG.speed;
|
|
|
|
// Set initial DOM position
|
|
this.updatePosition();
|
|
}
|
|
|
|
updatePosition() {
|
|
this.element.style.transform = `translate(${this.x}px, ${this.y}px)`;
|
|
}
|
|
|
|
float() {
|
|
if (isGridMode) return; // Do not float in grid mode
|
|
|
|
this.x += this.vx;
|
|
this.y += this.vy;
|
|
|
|
// Bounce off edges
|
|
if (this.x <= 0 || this.x >= width - CONFIG.bubbleSize) this.vx *= -1;
|
|
if (this.y <= 0 || this.y >= height - CONFIG.bubbleHeight) this.vy *= -1;
|
|
|
|
this.updatePosition();
|
|
}
|
|
|
|
moveTo(targetX, targetY) {
|
|
// Use CSS transition for smooth movement to grid
|
|
this.x = targetX;
|
|
this.y = targetY;
|
|
this.updatePosition();
|
|
}
|
|
|
|
remove() {
|
|
this.element.style.opacity = '0';
|
|
this.element.style.transform = `translate(${this.x}px, ${this.y}px) scale(0.5)`;
|
|
setTimeout(() => {
|
|
if (this.element.parentNode) {
|
|
this.element.parentNode.removeChild(this.element);
|
|
}
|
|
}, 500); // Wait for transition
|
|
}
|
|
}
|
|
|
|
// --- Logic: Layout & Display Manager ---
|
|
|
|
function updateDisplay() {
|
|
// Determine which users to show
|
|
let targetUsers = [];
|
|
|
|
if (allUsers.length <= CONFIG.maxDisplay) {
|
|
targetUsers = [...allUsers];
|
|
// Reset rotation index if we fit in one page
|
|
rotationIndex = 0;
|
|
} else {
|
|
// Rotation Logic
|
|
// If rotationIndex is out of bounds, reset
|
|
if (rotationIndex >= allUsers.length) rotationIndex = 0;
|
|
|
|
// Get slice
|
|
targetUsers = allUsers.slice(rotationIndex, rotationIndex + CONFIG.maxDisplay);
|
|
|
|
// Wrap around if needed (though user asked for "batch", maybe wrapping to fill page is better?)
|
|
// If we want to maintain constant count:
|
|
if (targetUsers.length < CONFIG.maxDisplay) {
|
|
const remaining = CONFIG.maxDisplay - targetUsers.length;
|
|
targetUsers = targetUsers.concat(allUsers.slice(0, remaining));
|
|
}
|
|
}
|
|
|
|
displayedUsers = targetUsers;
|
|
syncBubbles(targetUsers);
|
|
|
|
// Update Grid/Float mode based on DISPLAYED count (usually maxDisplay if full)
|
|
// But config says switch to grid when users > 20.
|
|
// If maxDisplay is 30, then 20-30 will be grid.
|
|
const count = displayedUsers.length;
|
|
const wasGrid = isGridMode;
|
|
isGridMode = count > CONFIG.gridThreshold;
|
|
|
|
if (isGridMode) {
|
|
arrangeGrid();
|
|
} else if (wasGrid && !isGridMode) {
|
|
// Switching back to float, ensure they have random velocities if needed
|
|
// But typically we don't go back from grid to float unless people leave.
|
|
}
|
|
}
|
|
|
|
function syncBubbles(targetUsers) {
|
|
const targetIds = new Set(targetUsers.map(u => u.gsdh_id || u.name));
|
|
|
|
// 1. Remove bubbles not in target
|
|
// Filter bubbles in place
|
|
const keptBubbles = [];
|
|
for (const b of bubbles) {
|
|
if (!targetIds.has(b.id)) {
|
|
b.remove();
|
|
} else {
|
|
keptBubbles.push(b);
|
|
}
|
|
}
|
|
bubbles = keptBubbles;
|
|
|
|
// 2. Add new bubbles
|
|
const currentIds = new Set(bubbles.map(b => b.id));
|
|
for (const u of targetUsers) {
|
|
const uid = u.gsdh_id || u.name;
|
|
if (!currentIds.has(uid)) {
|
|
bubbles.push(new UserBubble(u));
|
|
}
|
|
}
|
|
|
|
// Re-arrange if grid
|
|
if (isGridMode) {
|
|
// Delay slightly to let new bubbles exist in DOM?
|
|
// arrangeGrid uses 'bubbles' array, so it's fine.
|
|
// We might want to call arrangeGrid immediately.
|
|
setTimeout(arrangeGrid, 50);
|
|
}
|
|
}
|
|
|
|
function arrangeGrid() {
|
|
// Simple Grid Algorithm
|
|
const count = bubbles.length;
|
|
if (count === 0) return;
|
|
|
|
const cols = Math.ceil(Math.sqrt(count * (width / height)));
|
|
const rows = Math.ceil(count / cols);
|
|
|
|
const cellW = width / cols;
|
|
const cellH = height / rows;
|
|
|
|
bubbles.forEach((bubble, index) => {
|
|
const col = index % cols;
|
|
const row = Math.floor(index / cols);
|
|
|
|
// Center in cell
|
|
const targetX = col * cellW + (cellW - CONFIG.bubbleSize) / 2;
|
|
const targetY = row * cellH + (cellH - CONFIG.bubbleHeight) / 2;
|
|
|
|
bubble.moveTo(targetX, targetY);
|
|
});
|
|
}
|
|
|
|
function startRotationTimer() {
|
|
if (rotationTimer) clearInterval(rotationTimer);
|
|
rotationTimer = setInterval(() => {
|
|
if (allUsers.length > CONFIG.maxDisplay) {
|
|
rotationIndex += CONFIG.maxDisplay;
|
|
if (rotationIndex >= allUsers.length) rotationIndex = 0;
|
|
updateDisplay();
|
|
}
|
|
}, CONFIG.rotationInterval);
|
|
}
|
|
|
|
// --- Animation Loop ---
|
|
function animate() {
|
|
if (!isGridMode) {
|
|
bubbles.forEach(b => b.float());
|
|
}
|
|
requestAnimationFrame(animate);
|
|
}
|
|
animate(); // Start loop
|
|
|
|
// --- Data Fetching ---
|
|
async function fetchData() {
|
|
try {
|
|
const res = await fetch('/api/wall/data');
|
|
const json = await res.json();
|
|
if (json.success) {
|
|
const fetchedUsers = json.data;
|
|
document.getElementById('checkin-count').textContent = fetchedUsers.length;
|
|
|
|
// Check if new data arrived
|
|
if (fetchedUsers.length !== allUsers.length) {
|
|
const oldCount = allUsers.length;
|
|
allUsers = fetchedUsers;
|
|
|
|
// If we have more users than before
|
|
if (allUsers.length > oldCount) {
|
|
// Strategy: If we are rotating, jump to the NEWEST users immediately
|
|
// The API returns users sorted by created_at DESC (Newest first).
|
|
// So new users are at index 0.
|
|
// Wait, API says: ORDER BY created_at DESC
|
|
// So new users are at the TOP (0).
|
|
|
|
// If we want to show new users immediately:
|
|
rotationIndex = 0; // Show the first batch (newest)
|
|
updateDisplay();
|
|
|
|
// Reset timer to keep this view for a full interval
|
|
startRotationTimer();
|
|
|
|
// Trigger spotlight for newest
|
|
if (currentIndex === 0 && allUsers.length > 0 && !isMobile()) {
|
|
// Maybe spotlight the very first one
|
|
updateCard(allUsers[0]);
|
|
// Restart spotlight loop
|
|
startSpotlight();
|
|
}
|
|
} else {
|
|
// Users removed? Just update
|
|
updateDisplay();
|
|
}
|
|
} else {
|
|
// Data didn't change count, but maybe content changed?
|
|
// For simplicity, assume count change triggers update.
|
|
// Or if it's the first load
|
|
if (bubbles.length === 0 && allUsers.length > 0) {
|
|
updateDisplay();
|
|
startRotationTimer();
|
|
if (!isMobile()) startSpotlight();
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Fetch error", e);
|
|
}
|
|
}
|
|
|
|
// --- Spotlight Logic ---
|
|
let spotlightInterval = null;
|
|
|
|
function updateCard(user) {
|
|
const card = document.getElementById('card');
|
|
card.classList.remove('active');
|
|
|
|
setTimeout(() => {
|
|
const showName = WALL_SHOW_FIELDS.name !== false;
|
|
const cardHeader = document.getElementById('card-header');
|
|
|
|
if (showName) {
|
|
cardHeader.style.display = 'flex';
|
|
if(document.getElementById('card-name')) document.getElementById('card-name').textContent = user.name || '';
|
|
if(document.getElementById('card-avatar')) document.getElementById('card-avatar').textContent = user.name ? user.name.charAt(0) : '?';
|
|
} else {
|
|
cardHeader.style.display = 'none';
|
|
}
|
|
|
|
renderCardFields(user);
|
|
card.classList.add('active');
|
|
}, 800);
|
|
}
|
|
|
|
/**
|
|
* 根据配置动态渲染卡片字段
|
|
*/
|
|
function renderCardFields(user) {
|
|
const container = document.getElementById('card-fields');
|
|
container.innerHTML = '';
|
|
|
|
let html = '';
|
|
|
|
Object.keys(WALL_SHOW_FIELDS).forEach(key => {
|
|
if (!WALL_SHOW_FIELDS[key]) return;
|
|
|
|
const fieldConfig = CHECKIN_FIELD_CONFIG[key];
|
|
if (!fieldConfig) return;
|
|
|
|
const fieldValue = user[key] || '';
|
|
|
|
if (!fieldValue) return;
|
|
|
|
if (key === 'company_name') {
|
|
html += `<div class="company-name">${fieldValue}</div>`;
|
|
} else if (key === 'vision_2026') {
|
|
html += `<div class="card-vision">🎯 ${fieldValue}</div>`;
|
|
} else if (key === 'business_scope') {
|
|
html += `<div class="card-scope">${fieldValue}</div>`;
|
|
} else if (key === 'position') {
|
|
html += `<div class="card-position">${fieldValue}</div>`;
|
|
} else {
|
|
html += `<div class="card-custom-field">${fieldValue}</div>`;
|
|
}
|
|
});
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function startSpotlight() {
|
|
if (isMobile()) return;
|
|
if (spotlightInterval) clearInterval(spotlightInterval);
|
|
showNextUser();
|
|
spotlightInterval = setInterval(showNextUser, 8000);
|
|
}
|
|
|
|
function showNextUser() {
|
|
if (displayedUsers.length === 0) return;
|
|
const user = displayedUsers[currentIndex];
|
|
currentIndex = (currentIndex + 1) % displayedUsers.length;
|
|
updateCard(user);
|
|
}
|
|
|
|
function showUserManual(user) {
|
|
// Pause auto-rotation
|
|
if (spotlightInterval) clearInterval(spotlightInterval);
|
|
|
|
// Show clicked user immediately
|
|
updateCard(user);
|
|
|
|
// Resume auto-rotation after 15 seconds (only on desktop)
|
|
if (!isMobile()) {
|
|
spotlightInterval = setTimeout(() => {
|
|
startSpotlight();
|
|
}, 15000);
|
|
}
|
|
}
|
|
|
|
// Initial Load
|
|
fetchData();
|
|
setInterval(fetchData, 5000); // Check every 5s
|
|
|
|
</script>
|
|
</body>
|
|
</html> |