469 lines
16 KiB
HTML
469 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>Airhub - 搜索设备</title>
|
|
<!-- Keep base styles for header/footer consistency -->
|
|
<link rel="stylesheet" href="styles.css">
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
/* CRITICAL overrides to ensure visibility */
|
|
.bt-content {
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
justify-content: flex-start !important;
|
|
align-items: stretch !important;
|
|
padding-top: 0 !important;
|
|
overflow: hidden !important;
|
|
position: relative;
|
|
}
|
|
|
|
/* Container for the swiping cards */
|
|
#cardContainer {
|
|
flex: 1;
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 480px;
|
|
/* Increased height for safety */
|
|
/* Adjusted Mask: tighter visible area to ensure things fade out sooner */
|
|
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
|
|
mask-image: linear-gradient(to bottom, transparent 0%, black 15%, black 85%, transparent 100%);
|
|
}
|
|
|
|
/* The Card - Absolute positioning for transitions */
|
|
.device-card-item {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
width: 100%;
|
|
/* Keep smooth motion */
|
|
transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
z-index: 10;
|
|
}
|
|
|
|
/* States */
|
|
/* Center: Visible and centered */
|
|
.state-active {
|
|
transform: translate(-50%, -50%);
|
|
visibility: visible;
|
|
}
|
|
|
|
/* Above: moved UP significantly out of mask area */
|
|
.state-exit-up {
|
|
transform: translate(-50%, calc(-50% - 350px));
|
|
visibility: hidden;
|
|
/* Helper to ensure it's gone */
|
|
transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0.5s step-end;
|
|
}
|
|
|
|
/* Below: moved DOWN significantly out of mask area */
|
|
.state-exit-down {
|
|
transform: translate(-50%, calc(-50% + 350px));
|
|
visibility: hidden;
|
|
/* Helper to ensure it's gone */
|
|
transition: transform 0.5s cubic-bezier(0.2, 0.8, 0.2, 1), visibility 0.5s step-end;
|
|
}
|
|
|
|
/* Helper to hide completely if needed */
|
|
.d-none {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Icon Styling - Inline to override global styles.css */
|
|
.card-icon-wrapper {
|
|
position: relative;
|
|
width: 120px;
|
|
height: 120px;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
|
|
.card-icon-img {
|
|
width: 120px;
|
|
height: 120px;
|
|
image-rendering: pixelated;
|
|
display: block;
|
|
/* Eliminate inline spacing */
|
|
}
|
|
|
|
.card-badge {
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
background: linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%);
|
|
color: white;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
padding: 4px 8px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 10px rgba(139, 92, 246, 0.4);
|
|
}
|
|
|
|
.card-title {
|
|
margin-top: 24px;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 24px;
|
|
font-weight: 600;
|
|
color: #1F2937;
|
|
text-align: center;
|
|
}
|
|
|
|
.card-subtitle {
|
|
margin-top: 4px;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 15px;
|
|
color: #6B7280;
|
|
text-align: center;
|
|
}
|
|
|
|
/* Text at top */
|
|
.count-label {
|
|
text-align: center;
|
|
padding: 20px 0;
|
|
font-family: 'Inter', sans-serif;
|
|
font-size: 14px;
|
|
color: #9CA3AF;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.count-active {
|
|
color: #8B5CF6;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Indicators */
|
|
.dots-container {
|
|
position: absolute;
|
|
right: 20px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
z-index: 20;
|
|
}
|
|
|
|
.dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
background: rgba(139, 92, 246, 0.2);
|
|
border-radius: 50%;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.dot.active {
|
|
height: 18px;
|
|
background: #8B5CF6;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Error Box (Just in case) */
|
|
#errorLog {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
background: rgba(255, 0, 0, 0.1);
|
|
color: red;
|
|
font-size: 10px;
|
|
padding: 5px;
|
|
display: none;
|
|
z-index: 1000;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="gradient-bg">
|
|
<div class="gradient-layer layer-1"></div>
|
|
<div class="gradient-layer layer-2"></div>
|
|
<div class="gradient-layer layer-3"></div>
|
|
</div>
|
|
|
|
<div class="page bluetooth-page active">
|
|
<header class="bt-header">
|
|
<button class="back-btn" onclick="location.href='index.html'">
|
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
stroke-linejoin="round" />
|
|
</svg>
|
|
</button>
|
|
<h1 class="bt-title">搜索设备</h1>
|
|
<div class="header-spacer"></div>
|
|
</header>
|
|
|
|
<main class="bt-content">
|
|
<!-- Count Header -->
|
|
<div class="count-label" id="countLabel" style="opacity: 0;">正在搜索...</div>
|
|
|
|
<!-- Cards Container -->
|
|
<div id="cardContainer">
|
|
|
|
<!-- Searching State (Mystery Box) -->
|
|
<div id="searchState" style="text-align: center;">
|
|
<div class="mystery-box-container">
|
|
<div class="mystery-box searching">
|
|
<img src="icons/pixel-mystery-box.svg" alt="?" class="mystery-box-icon">
|
|
</div>
|
|
</div>
|
|
<div class="search-status" style="margin-top: 24px;">
|
|
<p style="color: #4B5563;">正在搜索附近设备</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card A -->
|
|
<div id="cardA" class="device-card-item d-none">
|
|
<div class="card-icon-wrapper">
|
|
<img id="imgA" class="card-icon-img" src="" alt="">
|
|
<span id="badgeA" class="card-badge">AI</span>
|
|
</div>
|
|
<div id="titleA" class="card-title">Device A</div>
|
|
<div id="subA" class="card-subtitle">Type A</div>
|
|
</div>
|
|
|
|
<!-- Card B -->
|
|
<div id="cardB" class="device-card-item d-none">
|
|
<div class="card-icon-wrapper">
|
|
<img id="imgB" class="card-icon-img" src="" alt="">
|
|
<span id="badgeB" class="card-badge">AI</span>
|
|
</div>
|
|
<div id="titleB" class="card-title">Device B</div>
|
|
<div id="subB" class="card-subtitle">Type B</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Right Dots -->
|
|
<div id="dots" class="dots-container" style="display: none;"></div>
|
|
</main>
|
|
|
|
<footer class="bt-footer">
|
|
<button class="cancel-btn" id="cancelBtn" onclick="location.href='index.html'">取消搜索</button>
|
|
<button class="connect-device-btn" id="connectBtn" style="display: none;" onclick="handleConnect()">
|
|
<span class="btn-text">连接设备</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
|
|
<div id="errorLog"></div>
|
|
|
|
<!-- No external app.js dependency for core logic to prevent breakages, only navigation helpers -->
|
|
<script>
|
|
// --- Data ---
|
|
const MOCK_DEVICES = [
|
|
{ sn: 'PLUSH_01', name: '卡皮巴拉-001', type: 'plush', hasAI: true },
|
|
{ sn: 'BADGE_01', name: 'AI电子吧唧-001', type: 'badge_ai', hasAI: true },
|
|
{ sn: 'BADGE_02', name: '电子吧唧-001', type: 'badge', hasAI: false },
|
|
{ sn: 'PLUSH_02', name: '卡皮巴拉-002', type: 'plush', hasAI: true },
|
|
];
|
|
|
|
// --- Icons Map ---
|
|
function getIcon(type) {
|
|
if (type === 'plush') return 'icons/pixel-capybara.svg';
|
|
if (type === 'badge_ai') return 'icons/pixel-badge-ai.svg';
|
|
return 'icons/pixel-badge-basic.svg';
|
|
}
|
|
function getTypeLabel(type) {
|
|
if (type === 'plush') return '毛绒机芯';
|
|
if (type === 'badge_ai') return 'AI电子吧唧';
|
|
return '普通电子吧唧';
|
|
}
|
|
|
|
// --- State ---
|
|
let devices = [];
|
|
let curIdx = 0;
|
|
let isAnimating = false;
|
|
let activeCardId = 'cardA'; // 'cardA' or 'cardB' is the visible one
|
|
|
|
// --- Init ---
|
|
window.addEventListener('load', () => {
|
|
try {
|
|
// Determine random count
|
|
setTimeout(() => {
|
|
try {
|
|
const count = Math.floor(Math.random() * 4) + 1;
|
|
devices = MOCK_DEVICES.slice(0, count);
|
|
showResults();
|
|
} catch (e) { logError(e); }
|
|
}, 2000);
|
|
} catch (e) { logError(e); }
|
|
});
|
|
|
|
function logError(e) {
|
|
const el = document.getElementById('errorLog');
|
|
el.style.display = 'block';
|
|
el.textContent = e.toString();
|
|
console.error(e);
|
|
}
|
|
|
|
function showResults() {
|
|
// Hide search, show first device
|
|
document.getElementById('searchState').style.display = 'none';
|
|
document.getElementById('connectBtn').style.display = 'flex';
|
|
document.getElementById('cancelBtn').innerText = '取消';
|
|
|
|
// Setup Count Label
|
|
const lbl = document.getElementById('countLabel');
|
|
lbl.style.opacity = 1;
|
|
updateCountLabel();
|
|
|
|
// Render first card
|
|
renderCard('cardA', devices[0]);
|
|
const cA = document.getElementById('cardA');
|
|
cA.classList.remove('d-none');
|
|
cA.classList.add('state-active');
|
|
|
|
// Setup B as invisible helper
|
|
const cB = document.getElementById('cardB');
|
|
cB.classList.remove('d-none');
|
|
cB.classList.add('state-exit-down'); // initial hidden state
|
|
|
|
// Setup Dots
|
|
if (devices.length > 1) {
|
|
document.getElementById('dots').style.display = 'flex';
|
|
renderDots();
|
|
}
|
|
|
|
// Setup Touch
|
|
setupTouch();
|
|
}
|
|
|
|
function updateCountLabel() {
|
|
const el = document.getElementById('countLabel');
|
|
if (devices.length > 1) {
|
|
el.innerHTML = `找到 <span class="count-active">${devices.length}</span> 个设备 · 滑动切换`;
|
|
} else {
|
|
el.innerHTML = `找到 <span class="count-active">1</span> 个设备`;
|
|
}
|
|
}
|
|
|
|
function renderCard(elementId, device) {
|
|
const root = document.getElementById(elementId);
|
|
// safe query selectors scoped to root
|
|
root.querySelector('.card-icon-img').src = getIcon(device.type);
|
|
root.querySelector('.card-badge').style.display = device.hasAI ? 'block' : 'none';
|
|
root.querySelector('.card-title').textContent = device.name;
|
|
root.querySelector('.card-subtitle').textContent = getTypeLabel(device.type);
|
|
}
|
|
|
|
function renderDots() {
|
|
const container = document.getElementById('dots');
|
|
let html = '';
|
|
for (let i = 0; i < devices.length; i++) {
|
|
html += `<div class="dot ${i === curIdx ? 'active' : ''}"></div>`;
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
// --- Handling Swipe ---
|
|
function onSwipe(dir) { // 'up' or 'down'
|
|
if (isAnimating || devices.length <= 1) return;
|
|
isAnimating = true;
|
|
|
|
let nextIdx;
|
|
if (dir === 'up') {
|
|
nextIdx = (curIdx + 1) % devices.length;
|
|
} else {
|
|
nextIdx = (curIdx - 1 + devices.length) % devices.length;
|
|
}
|
|
|
|
// Determine elements
|
|
const currElId = activeCardId;
|
|
const nextElId = activeCardId === 'cardA' ? 'cardB' : 'cardA';
|
|
const currEl = document.getElementById(currElId);
|
|
const nextEl = document.getElementById(nextElId);
|
|
|
|
// Prepare next element content
|
|
renderCard(nextElId, devices[nextIdx]);
|
|
|
|
// Remove transition for instant layout setup
|
|
nextEl.style.transition = 'none';
|
|
|
|
// Set start positions for animation
|
|
if (dir === 'up') {
|
|
// Next comes from bottom
|
|
nextEl.className = 'device-card-item state-exit-down';
|
|
} else {
|
|
// Next comes from top
|
|
nextEl.className = 'device-card-item state-exit-up';
|
|
}
|
|
|
|
// Force reflow
|
|
nextEl.offsetHeight;
|
|
|
|
// Restore transition
|
|
nextEl.style.transition = '';
|
|
currEl.style.transition = '';
|
|
|
|
// Animate to final state
|
|
requestAnimationFrame(() => {
|
|
if (dir === 'up') {
|
|
// Current goes up, Next goes to center
|
|
currEl.className = 'device-card-item state-exit-up';
|
|
nextEl.className = 'device-card-item state-active';
|
|
} else {
|
|
// Current goes down, Next goes to center
|
|
currEl.className = 'device-card-item state-exit-down';
|
|
nextEl.className = 'device-card-item state-active';
|
|
}
|
|
});
|
|
|
|
// Update indices
|
|
curIdx = nextIdx;
|
|
activeCardId = nextElId;
|
|
renderDots();
|
|
|
|
setTimeout(() => {
|
|
isAnimating = false;
|
|
}, 400);
|
|
}
|
|
|
|
|
|
// --- Touch Logic ---
|
|
function setupTouch() {
|
|
const zone = document.getElementById('cardContainer');
|
|
let startY = 0;
|
|
|
|
zone.addEventListener('touchstart', (e) => {
|
|
startY = e.touches[0].clientY;
|
|
}, { passive: true });
|
|
|
|
zone.addEventListener('touchend', (e) => {
|
|
const endY = e.changedTouches[0].clientY;
|
|
const diff = startY - endY;
|
|
if (Math.abs(diff) > 50) {
|
|
// diff > 0 means dragging finger UP (content moves UP)
|
|
onSwipe(diff > 0 ? 'up' : 'down');
|
|
}
|
|
}, { passive: true });
|
|
|
|
zone.addEventListener('wheel', (e) => {
|
|
if (Math.abs(e.deltaY) > 20) {
|
|
onSwipe(e.deltaY > 0 ? 'up' : 'down');
|
|
}
|
|
}, { passive: true });
|
|
}
|
|
|
|
function handleConnect() {
|
|
const d = devices[curIdx];
|
|
localStorage.setItem('lastActiveDevice', JSON.stringify(d));
|
|
if (d.type === 'badge') {
|
|
location.href = 'device-control.html';
|
|
} else {
|
|
location.href = 'wifi-config.html';
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html> |