rtc_prd/airhub_app/assets/www/device-control.html
2026-02-04 17:43:43 +08:00

1113 lines
47 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Airhub - 设备控制</title>
<link rel="stylesheet" href="styles.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
/* 复用首页布局逻辑 */
.dc-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 20px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
transition: opacity 0.3s ease;
}
.dc-content {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
/* 头部按钮样式 - 极简透明 */
.icon-btn {
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.4);
width: 44px;
height: 44px;
border-radius: 22px;
display: flex;
align-items: center;
justify-content: center;
color: #1F2937;
transition: all 0.2s;
}
.icon-btn:active {
transform: scale(0.92);
background: rgba(255, 255, 255, 0.4);
}
.icon-btn svg {
width: 24px;
height: 24px;
stroke-width: 1.5;
}
/* 状态胶囊 */
.status-pill {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 24px;
height: 44px;
box-sizing: border-box;
}
.status-dot-group {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #1F2937;
}
.live-dot {
width: 8px;
height: 8px;
background: #22C55E;
border-radius: 50%;
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
}
.battery-group {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 600;
color: #1F2937;
padding-left: 12px;
border-left: 1px solid rgba(0, 0, 0, 0.1);
}
/* 底部导航 - 浅色悬浮岛 (Home Style) */
.dc-footer {
position: absolute;
bottom: 40px;
bottom: calc(env(safe-area-inset-bottom, 20px) + 30px);
width: 100%;
display: flex;
justify-content: center;
padding: 0 20px;
box-sizing: border-box;
z-index: 50;
/* Ensure above story section */
}
.menu-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.6);
/* Light Glass */
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.8);
padding: 6px 8px;
border-radius: 32px;
width: 100%;
max-width: 320px;
/* Slightly narrower to match button feel */
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.15);
/* Soft colored shadow match */
}
.menu-item {
width: 56px;
height: 56px;
border-radius: 28px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
position: relative;
}
.menu-item img {
width: 28px;
height: 28px;
object-fit: contain;
opacity: 0.7;
transition: all 0.3s;
/* Inactive: Normal or slightly greyed */
filter: grayscale(100%) opacity(0.6);
}
.menu-item svg {
width: 24px;
height: 24px;
color: #6B7280;
/* Dark Grey for inactive */
transition: all 0.3s;
}
/* Active State - Matches Home Button Style */
.menu-item.active {
/* Matches Capybara 4-stop gradient */
background: linear-gradient(90deg, #E6B98D 0%, #E8C9A8 35%, #D4A373 70%, #B07D5A 100%);
box-shadow: 0 4px 15px rgba(212, 163, 115, 0.4);
/* #D4A373 shadow */
transform: translateY(-2px) scale(1.05);
}
.menu-item.active img {
opacity: 1;
transform: scale(1.1);
filter: brightness(0) invert(1);
/* White icon */
}
.menu-item.active svg {
color: #FFFFFF;
transform: scale(1.1);
}
/* Capybara Image specific tweaks override global mascot if needed */
#device-mascot {
max-width: 200px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 (复用首页) -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<!-- Custom Modal Overlay (Added for Unlock Feature) -->
<div class="modal-overlay" id="custom-modal">
<div class="glass-modal">
<div class="modal-title" id="modal-title">提示</div>
<div class="modal-desc" id="modal-desc">内容文本</div>
<div class="modal-actions">
<button class="modal-btn cancel" id="modal-cancel">取消</button>
<button class="modal-btn confirm" id="modal-confirm">确定</button>
</div>
</div>
</div>
<!-- Header -->
<header class="dc-header" id="page-header">
<button class="icon-btn" onclick="window.location.href='products.html'" title="切换设备">
<img src="icons/icon-switch.svg" style="width:20px; opacity:0.8;">
</button>
<div class="status-pill">
<div class="status-dot-group">
<div class="live-dot"></div>
<span>在线</span>
</div>
<div class="battery-group">
<img src="icons/icon-battery-full.svg" style="width:18px;">
<span id="batteryVal">--%</span>
</div>
</div>
<button class="icon-btn" title="设置" onclick="openSettings()">
<img src="icons/icon-settings-pixel.svg" style="width:20px; opacity:0.8;">
</button>
</header>
<!-- Main Content (复用首页结构) -->
<main class="dc-content" id="home-view">
<div class="mascot-container">
<!-- 使用 Capybara.png -->
<img src="Capybara.png" alt="Capybara" class="mascot" id="device-mascot">
<div class="mascot-glow"></div>
</div>
</main>
<!-- Story View Section (Hidden by default) -->
<section id="story-view" class="story-section" style="display: none;">
<div class="story-header-spacer"></div>
<!-- Bookshelf Container (Swiper) -->
<div class="bookshelf-container">
<!-- Slide 1: Current Bookshelf -->
<div class="bookshelf-slide">
<div class="story-book" id="book-1">
<div class="book-cover">
<span class="book-title">我的故事书 #1</span>
<span class="book-count">0/10</span>
</div>
<div class="story-grid" id="story-grid-1">
<!-- Slots injected by JS -->
</div>
</div>
</div>
<!-- Slide 2: Locked Bookshelf Placeholder -->
<div class="bookshelf-slide">
<!-- The Placeholder (Click to Unlock) -->
<div class="add-book-placeholder" onclick="showUnlockModal()">
<div class="placeholder-content">
<div class="add-icon">+</div>
<span class="placeholder-text">解锁<br>新书架</span>
</div>
</div>
<!-- The Real Book (Hidden initially) -->
<div class="story-book" id="book-2" style="display: none; height: 100%;">
<div class="book-cover">
<span class="book-title">我的故事书 #2</span>
<span class="book-count">0/10</span>
</div>
<div class="story-grid" id="story-grid-2">
<!-- Will be populated by JS -->
</div>
</div>
</div>
</div>
<!-- Bottom Actions Row -->
<div class="story-actions-wrapper">
<!-- Create Button -->
<button class="create-story-btn" onclick="openGenerator()">
<span class="btn-icon">+</span>
<span>创作新故事</span>
</button>
</div>
</section>
<!-- Generator Modal -->
<div id="story-generator" class="generator-modal">
<div class="modal-content">
<div class="modal-header">
<h3>创作故事</h3>
<button class="close-btn" onclick="closeGenerator()">×</button>
</div>
<!-- Tab Navigation: data-text required for CSS bold stability fix -->
<div class="generator-tabs">
<button class="gen-tab active" data-text="角色" onclick="switchGenTab('characters')">角色</button>
<button class="gen-tab" data-text="环境" onclick="switchGenTab('scenes')">环境</button>
<button class="gen-tab" data-text="道具" onclick="switchGenTab('props')">道具</button>
</div>
<!-- Scrollable Tab Content -->
<div class="generator-scroll-content">
<div class="element-grid-4col" id="active-tab-content">
<!-- Content injected by JS -->
</div>
</div>
<!-- Fixed Footer Button -->
<div class="generator-footer">
<button class="generate-btn" onclick="startGeneration()">
<span class="btn-icon"></span>
<span>开始生成</span>
</button>
</div>
</div>
</div>
<!-- Footer Menu -->
<footer class="dc-footer">
<nav class="menu-bar">
<!-- Home (Pixel Capybara) -->
<div class="menu-item active" onclick="selectTab(this)">
<img src="icons/icon-home-capybara.svg" alt="Home">
</div>
<!-- Story (Pixel) -->
<div class="menu-item" onclick="selectTab(this)">
<img src="icons/icon-story-pixel.svg" alt="Story">
</div>
<!-- Music (Pixel) -->
<div class="menu-item" onclick="selectTab(this)">
<img src="icons/icon-music-pixel.svg" alt="Music">
</div>
<!-- User (Pixel) -->
<div class="menu-item" onclick="location.href='profile.html'">
<img src="icons/icon-user-pixel.svg" alt="User">
</div>
</nav>
</footer>
</div>
<script>
// Battery Logic
document.addEventListener('DOMContentLoaded', () => {
const level = Math.floor(Math.random() * 41) + 60;
document.getElementById('batteryVal').innerText = level + '%';
// Render Bookshelf
initStorySystem();
// Check URL Params for Tab Switch
const urlParams = new URLSearchParams(window.location.search);
const tabParam = urlParams.get('tab');
if (tabParam) {
// Find element by img alt tag matching the param (lowercase to Title Case mapping needed or direct check)
const menuItems = document.querySelectorAll('.menu-item');
let targetEl = null;
menuItems.forEach(el => {
const alt = el.querySelector('img').alt.toLowerCase();
if (alt === tabParam.toLowerCase()) {
targetEl = el;
}
});
if (targetEl) {
selectTab(targetEl);
}
}
});
function selectTab(el) {
// Remove active class from all items
document.querySelectorAll('.menu-item').forEach(i => i.classList.remove('active'));
// Add active class to clicked item
el.classList.add('active');
// Determine View
const alt = el.querySelector('img').alt;
const homeView = document.getElementById('home-view');
const storyView = document.getElementById('story-view');
const pageHeader = document.getElementById('page-header');
// Toggle Button Check
const actionWrapper = document.querySelector('.story-actions-wrapper');
// Header Visibility Logic
if (alt === 'Home') {
pageHeader.style.opacity = '1';
pageHeader.style.pointerEvents = 'auto';
} else {
pageHeader.style.opacity = '0';
pageHeader.style.pointerEvents = 'none';
}
// Tab Switching Logic
if (alt === 'Story') {
homeView.style.display = 'none';
storyView.style.display = 'flex';
// Show Create Button
if (actionWrapper) { // Check existence
actionWrapper.style.display = 'flex'; // Ensure flex display
setTimeout(() => actionWrapper.classList.add('active'), 10); // Fade in
}
} else if (alt === 'Home') {
homeView.style.display = 'flex';
storyView.style.display = 'none';
stopPlayback(); // Stop audio when leaving story
// Hide Create Button
if (actionWrapper) {
actionWrapper.classList.remove('active');
setTimeout(() => actionWrapper.style.display = 'none', 300); // Wait for fade out
}
} else {
// For other tabs, just show home for now (or their own views)
homeView.style.display = 'flex';
storyView.style.display = 'none';
stopPlayback(); // Stop audio when leaving story
// Hide Create Button
if (actionWrapper) {
actionWrapper.classList.remove('active');
setTimeout(() => actionWrapper.style.display = 'none', 300);
}
}
}
// --- Story System Logic Placeholder (Will be moved to separate file later) ---
function initStorySystem() {
renderBookshelf();
// Pre-init generator to ensure no null errors if opened immediately
// But usually we render upon open.
}
function checkMagicEntrance() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('magic') === 'true') {
// Find the first empty slot (which is actually the lastfilled + 1)
// Or just pick the last filled one to animate "appearing"
const grid = document.getElementById('story-grid-1');
const lastFilled = grid.querySelector('.story-slot.filled:last-child');
// Wait, renderBookshelf renders ALL slots.
// We need to re-render but identify the "new" one.
// For MOCK, let's assume the 6th slot is new (since we have 5 mocked).
// Simpler: Just animate the last filled book?
// Or add a temporary "new" book.
// Let's add a NEW book to the mock list first?
// No, renderBookshelf uses static list 'myStories'. I should push to it if magic=true.
if (myStories.length < 10) {
myStories.push({ title: "新生成的魔法故事", cover: "story_covers/new_story.png" }); // Mock new story
renderBookshelf(); // Re-render with new book
}
// Now find the new book element
const slots = grid.querySelectorAll('.story-slot.filled');
const newSlot = slots[slots.length - 1];
if (newSlot) {
// Add Pop Animation
newSlot.classList.add('book-pop-in');
// Create Particles
createMagicParticles(newSlot);
// Clean URL
const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + '?tab=story';
window.history.replaceState({ path: newUrl }, '', newUrl);
}
}
}
function createMagicParticles(targetEl) {
const rect = targetEl.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
for (let i = 0; i < 20; i++) {
const p = document.createElement('div');
p.classList.add('magic-particle');
document.body.appendChild(p);
// Random angle and distance
const angle = Math.random() * Math.PI * 2;
const dist = 50 + Math.random() * 100;
const tx = Math.cos(angle) * dist;
const ty = Math.sin(angle) * dist;
p.style.left = centerX + 'px';
p.style.top = centerY + 'px';
p.style.setProperty('--tx', `${tx}px`);
p.style.setProperty('--ty', `${ty}px`);
p.style.animation = `sparkleFloat 0.8s ease-out forwards`;
setTimeout(() => p.remove(), 800);
}
}
// Hook into init
const originalInit = initStorySystem;
initStorySystem = function () {
originalInit();
// Delay slightly to ensure tab switch happens first
setTimeout(checkMagicEntrance, 100);
};
// Mock Data
// Categorized Mock Data (Expanded ~28 items)
const storyData = {
characters: [
{ id: 'c1', name: '宇航员', icon: '🧑‍🚀' }, { id: 'c2', name: '忍者', icon: '🥷' },
{ id: 'c3', name: '精灵', icon: '🧚' }, { id: 'c4', name: '小熊', icon: '🐻' },
{ id: 'c5', name: '机器人', icon: '🤖' }, { id: 'c6', name: '公主', icon: '👸' },
{ id: 'c7', name: '猫咪', icon: '🐱' }, { id: 'c8', name: '恐龙', icon: '🦖' },
{ id: 'c9', name: '吸血鬼', icon: '🧛' }, { id: 'c10', name: '海盗', icon: '🏴‍☠️' },
{ id: 'c11', name: '侦探', icon: '🕵️' }, { id: 'c12', name: '外星人', icon: '👽' },
{ id: 'c13', name: '幽灵', icon: '👻' }, { id: 'c14', name: '骑士', icon: '🛡️' },
{ id: 'c15', name: '超人', icon: '🦸' }, { id: 'c16', name: '僵尸', icon: '🧟' },
{ id: 'c17', name: '美人鱼', icon: '🧜‍♀️' }, { id: 'c18', name: '巫师', icon: '🧙‍♂️' },
{ id: 'c19', name: '小丑', icon: '🤡' }, { id: 'c20', name: '厨师', icon: '👨‍🍳' },
{ id: 'c21', name: '医生', icon: '👨‍⚕️' }, { id: 'c22', name: '警察', icon: '👮' },
{ id: 'c23', name: '消防员', icon: '👨‍🚒' }, { id: 'c24', name: '画家', icon: '🎨' },
{ id: 'c25', name: '国王', icon: '🤴' }, { id: 'c26', name: '王后', icon: '👸' },
{ id: 'c27', name: '兔子', icon: '🐰' }, { id: 'c28', name: '老虎', icon: '🐯' }
],
scenes: [
{ id: 's1', name: '森林', icon: '🌲' }, { id: 's2', name: '城堡', icon: '🏰' },
{ id: 's3', name: '太空', icon: '🪐' }, { id: 's4', name: '海底', icon: '🐙' },
{ id: 's5', name: '沙漠', icon: '🏜️' }, { id: 's6', name: '城市', icon: '🏙️' },
{ id: 's7', name: '雪山', icon: '🏔️' }, { id: 's8', name: '游乐园', icon: '🎡' },
{ id: 's9', name: '海滩', icon: '🏖️' }, { id: 's10', name: '学校', icon: '🏫' },
{ id: 's11', name: '农村', icon: '🚜' }, { id: 's12', name: '月球', icon: '🌕' },
{ id: 's13', name: '火星', icon: '🔴' }, { id: 's14', name: '洞穴', icon: '🦇' },
{ id: 's15', name: '鬼屋', icon: '🏚️' }, { id: 's16', name: '海盗船', icon: '🏴‍☠️' },
{ id: 's17', name: '云端', icon: '☁️' }, { id: 's18', name: '糖果屋', icon: '🍬' },
{ id: 's19', name: '动物园', icon: '🦁' }, { id: 's20', name: '博物馆', icon: '🏛️' },
{ id: 's21', name: '图书馆', icon: '📚' }, { id: 's22', name: '花园', icon: '🌷' },
{ id: 's23', name: '赛车场', icon: '🏎️' }, { id: 's24', name: '足球场', icon: '⚽' },
{ id: 's25', name: '原始森林', icon: '🌴' }, { id: 's26', name: '冰川', icon: '🧊' },
{ id: 's27', name: '火山', icon: '🌋' }, { id: 's28', name: '天空之城', icon: '🏰' }
],
props: [
{ id: 'p1', name: '魔法棒', icon: '🪄' }, { id: 'p2', name: '宝剑', icon: '🗡️' },
{ id: 'p3', name: '地图', icon: '🗺️' }, { id: 'p4', name: '宝石', icon: '💎' },
{ id: 'p5', name: '吉他', icon: '🎸' }, { id: 'p6', name: '火箭', icon: '🚀' },
{ id: 'p7', name: '汉堡', icon: '🍔' }, { id: 'p8', name: '手电筒', icon: '🔦' },
{ id: 'p9', name: '皇冠', icon: '👑' }, { id: 'p10', name: '足球', icon: '⚽' },
{ id: 'p11', name: '钥匙', icon: '🔑' }, { id: 'p12', name: '书本', icon: '📖' },
{ id: 'p13', name: '药水', icon: '🧪' }, { id: 'p14', name: '水晶球', icon: '🔮' },
{ id: 'p15', name: '望远镜', icon: '🔭' }, { id: 'p16', name: '滑板', icon: '🛹' },
{ id: 'p17', name: '单车', icon: '🚲' }, { id: 'p18', name: '蛋糕', icon: '🎂' },
{ id: 'p19', name: '披萨', icon: '🍕' }, { id: 'p20', name: '冰淇淋', icon: '🍦' },
{ id: 'p21', name: '手机', icon: '📱' }, { id: 'p22', name: '电脑', icon: '💻' },
{ id: 'p23', name: '相机', icon: '📷' }, { id: 'p24', name: '雨伞', icon: '☂️' },
{ id: 'p25', name: '背包', icon: '🎒' }, { id: 'p26', name: '眼镜', icon: '👓' },
{ id: 'p27', name: '帽子', icon: '🎩' }, { id: 'p28', name: '飞毯', icon: '🧶' }
]
};
let selectedElements = [];
const myStories = [
{ title: "卡皮巴拉的奇幻漂流", cover: "story_covers/capybara_adventure.png" },
{ title: "勇敢的小裁缝", cover: "story_covers/brave_tailor.png" },
{ title: "小红帽与大灰狼", cover: "story_covers/red_riding_hood.png" },
{ title: "杰克与魔豆", cover: "story_covers/jack_and_beanstalk.png" },
{ title: "糖果屋历险记", cover: "story_covers/hansel_and_gretel.png" }
];
function renderBookshelf() {
const grid = document.getElementById('story-grid-1');
grid.innerHTML = '';
for (let i = 0; i < 10; i++) {
const slot = document.createElement('div');
if (i < myStories.length) {
const story = myStories[i];
slot.className = 'story-slot filled clickable'; // Added clickable class
// Navigate to Read Mode with ID
slot.onclick = () => {
window.location.href = `story-detail.html?mode=read&id=${i}`;
};
slot.innerHTML = `
<img src="${story.cover}" class="story-cover-img" alt="${story.title}">
<div class="story-title-bar">${story.title}</div>
`;
} else {
slot.className = 'story-slot empty clickable';
slot.onclick = openGenerator;
slot.innerHTML = `<span class="empty-plus">+</span>`;
}
grid.appendChild(slot);
}
}
// --- Unlock Bookshelf Logic ---
function showUnlockModal() {
// Reusing the modal structure we added
const modal = document.getElementById('custom-modal');
const title = document.getElementById('modal-title');
const desc = document.getElementById('modal-desc');
const cancelBtn = document.getElementById('modal-cancel');
const confirmBtn = document.getElementById('modal-confirm');
title.innerText = '解锁新书架';
desc.innerHTML = '解锁新书架需要消耗 100 SP<br>确定要继续吗?';
modal.classList.add('active');
// Handle Cancel
cancelBtn.onclick = () => {
modal.classList.remove('active');
};
// Handle Confirm
confirmBtn.onclick = () => {
modal.classList.remove('active');
unlockBookshelf();
};
}
function unlockBookshelf() {
const placeholder = document.querySelector('.add-book-placeholder');
const container = document.querySelector('.bookshelf-container');
// 1. Fade out placeholder
placeholder.style.opacity = '0';
setTimeout(() => {
// 2. Identify the slide containing the placeholder
// The placeholder is inside a .bookshelf-slide div
const currentSlide = placeholder.parentElement;
// Construct New Book HTML
// We count how many VISIBLE books exist to number the new one correctly
// (Exclude the hidden one that we are about to replace)
const visibleBooks = Array.from(container.querySelectorAll('.story-book')).filter(el => el.style.display !== 'none');
const bookCount = visibleBooks.length + 1;
// Create Grid Slots HTML
let gridSlotsHTML = '';
for (let i = 0; i < 10; i++) {
gridSlotsHTML += `
<div class="story-slot empty clickable" onclick="openGenerator()">
<span class="empty-plus">+</span>
</div>
`;
}
const newBookHTML = `
<div class="story-book" style="display: flex; flex-direction: column; opacity: 0; transition: opacity 0.5s;">
<div class="book-cover">
<span class="book-title">我的故事书 #${bookCount}</span>
<span class="book-count">0/10</span>
</div>
<div class="story-grid">
${gridSlotsHTML}
</div>
</div>
`;
// Replace Placeholder Slide Content with New Book
currentSlide.innerHTML = newBookHTML;
const newBook = currentSlide.querySelector('.story-book');
// Trigger Fade In
// Trigger Fade In
setTimeout(() => {
newBook.style.opacity = '1';
// Auto-scroll to center the new bookshelf
newBook.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}, 50);
// 3. Append NEW Placeholder Slide (with hint-seen class since user knows the feature now)
const nextSlide = document.createElement('div');
nextSlide.className = 'bookshelf-slide';
nextSlide.innerHTML = `
<div class="add-book-placeholder hint-seen" onclick="showUnlockModal()">
<div class="placeholder-content">
<div class="add-icon">+</div>
<span class="placeholder-text">解锁<br>新书架</span>
</div>
</div>
`;
container.appendChild(nextSlide);
// Clean up any ID references if they were copied?
// Currently generated HTML doesn't use IDs, just classes. Good.
}, 200);
// 4. Show Toast
let toast = document.getElementById('custom-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'custom-toast';
toast.className = 'custom-toast';
toast.innerText = '✨ 书架解锁成功';
document.body.appendChild(toast);
}
toast.classList.add('active');
setTimeout(() => toast.classList.remove('active'), 2000);
}
// --- Generator Logic ---
function renderGeneratorUI() {
// No preview to update
switchGenTab(activeTab);
}
function switchGenTab(type) {
activeTab = type;
// Update Tab Styles
document.querySelectorAll('.gen-tab').forEach(btn => {
const isTarget = btn.getAttribute('onclick').includes(`'${type}'`);
if (isTarget) btn.classList.add('active');
else btn.classList.remove('active');
});
// Render Grid Content
const gridContainer = document.getElementById('active-tab-content');
if (!gridContainer) return;
gridContainer.innerHTML = '';
const items = storyData[type] || [];
items.forEach(el => {
const isSelected = selectedElements.some(e => e.id === el.id);
const btn = document.createElement('button');
btn.className = `element-card ${isSelected ? 'selected' : ''}`;
// Add Checkmark Badge structure
btn.innerHTML = `
<div class="card-icon">${el.icon}</div>
<div class="card-name">${el.name}</div>
<div class="check-badge"> <!-- Badge -->
<svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="4">
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
`;
btn.onclick = () => toggleElement(btn, el);
gridContainer.appendChild(btn);
});
}
function toggleElement(btn, el) {
const index = selectedElements.findIndex(e => e.id === el.id);
// Get items currently selected in THIS category
// We identify category by ID prefix: c=chars, s=scenes, p=props
// This is a heuristic, but efficient for this mock.
const prefix = el.id.charAt(0);
const currentCatCount = selectedElements.filter(e => e.id.startsWith(prefix)).length;
if (index > -1) {
// Deselect
selectedElements.splice(index, 1);
btn.classList.remove('selected');
} else {
// Check Global Limit (9 items total)
if (selectedElements.length >= 9) {
alert("总共最多选择9个元素哦~");
return;
}
// Check Category Limit (3 items per category)
if (currentCatCount >= 3) {
alert("当前分类最多选择3个元素哦~");
return;
}
// Select
selectedElements.push(el);
btn.classList.add('selected');
// Auto-Switch Tab Logic
if (currentCatCount + 1 === 3) {
// Slight delay for UX
setTimeout(() => {
autoSwitchTab();
}, 400);
}
}
// No preview update needed
}
function autoSwitchTab() {
if (activeTab === 'characters') switchGenTab('scenes');
else if (activeTab === 'scenes') switchGenTab('props');
// If props, stay here.
}
function openGenerator() {
document.getElementById('story-generator').classList.add('active');
selectedElements = []; // Reset on open
activeTab = 'characters'; // Reset tab
renderGeneratorUI();
}
function closeGenerator() {
document.getElementById('story-generator').classList.remove('active');
}
function startGeneration() {
if (selectedElements.length === 0) {
alert("请至少选择一个元素~");
return;
}
// Direct redirect to loading page
window.location.href = 'story-loading.html';
}
let isPlaying = false;
function togglePlay() {
const btn = document.getElementById('play-fab');
const icon = document.getElementById('play-icon');
if (!isPlaying) {
// Random Play Logic would go here
isPlaying = true;
icon.src = 'icons/icon-pause.svg'; // Need to ensure pause icon exists or use code
btn.classList.add('playing');
// Simulate playing a random story
} else {
// Pause
isPlaying = false;
icon.src = 'icons/icon-play.svg';
btn.classList.remove('playing');
}
}
function stopPlayback() {
if (isPlaying) {
togglePlay(); // Revert to stop state
}
}
// Bookshelf scroll listener - centers placeholder when in view
function initBookshelfScrollListener() {
const container = document.querySelector('.bookshelf-container');
if (!container) return;
container.addEventListener('scroll', () => {
const placeholder = container.querySelector('.add-book-placeholder');
if (!placeholder) return;
const containerRect = container.getBoundingClientRect();
const placeholderRect = placeholder.getBoundingClientRect();
// Check if placeholder is more than 50% visible
const visibleWidth = Math.min(placeholderRect.right, containerRect.right) -
Math.max(placeholderRect.left, containerRect.left);
const visibilityRatio = visibleWidth / placeholderRect.width;
if (visibilityRatio > 0.5) {
placeholder.classList.add('centered');
} else {
placeholder.classList.remove('centered');
}
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', initBookshelfScrollListener);
</script>
<!-- --- Settings View Overlay (Inserted at end of body for max z-index) --- -->
<div id="settings-view" class="settings-view" style="display: none;">
<div class="settings-header">
<button class="icon-btn" onclick="closeSettings()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 19L8 12L15 5" stroke="#000" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<div class="settings-title">设置</div>
<div style="width: 40px;"></div> <!-- Spacer -->
</div>
<div class="settings-content">
<!-- Group 1: Basics -->
<div class="settings-group-title">基础设置</div>
<div class="settings-group">
<div class="settings-item clickable"
onclick="event.stopPropagation(); openEditModal('device_name', '修改设备昵称')">
<span class="item-label">设备昵称</span>
<span class="item-value"><span id="val-device_name">小毛球</span> <span class="arrow"></span></span>
</div>
<!-- Divider will be auto-added by CSS ::after -->
<div class="settings-item clickable"
onclick="event.stopPropagation(); openEditModal('user_name', '修改你的称呼')">
<span class="item-label">你的称呼</span>
<span class="item-value"><span id="val-user_name">土豆</span> <span class="arrow"></span></span>
</div>
</div>
<!-- Group 2: Volume -->
<div class="settings-group-title">音量与亮度</div>
<div class="settings-group">
<div class="settings-item column"
style="flex-direction: column; align-items: flex-start; gap: 12px; padding-bottom: 20px;">
<div class="item-row" style="display:flex; justify-content:space-between; width:100%;">
<span class="item-label">音量</span>
<span class="item-value" id="vol-text">60%</span>
</div>
<div class="volume-row">
<span style="color:#8E8E93; font-size:14px;">🔈</span>
<input type="range" class="volume-slider" min="0" max="100" value="60"
oninput="document.getElementById('vol-text').innerText = this.value + '%'">
<span style="color:#8E8E93; font-size:14px;">🔊</span>
</div>
</div>
<div class="settings-item column"
style="flex-direction: column; align-items: flex-start; gap: 12px; padding-bottom: 20px;">
<div class="item-row" style="display:flex; justify-content:space-between; width:100%;">
<span class="item-label">亮度</span>
<span class="item-value" id="bri-text">85%</span>
</div>
<div class="volume-row">
<span style="color:#8E8E93; font-size:18px;"></span>
<input type="range" class="volume-slider" min="0" max="100" value="85"
oninput="document.getElementById('bri-text').innerText = this.value + '%'">
<span style="color:#8E8E93; font-size:18px;"></span>
</div>
</div>
</div>
<!-- Group 3: Network -->
<div class="settings-group-title">网络与连接</div>
<div class="settings-group">
<div class="settings-item clickable" onclick="event.stopPropagation(); reconfigureNetwork()">
<div class="item-text-col">
<div class="item-label">配置网络</div>
<div class="item-desc">为该设备添加更多 Wi-Fi、个人热点</div>
</div>
<div class="item-value">
<span class="arrow"></span>
</div>
</div>
<div class="settings-item clickable warning" onclick="event.stopPropagation(); openUnbindModal()">
<div class="item-text-col">
<div class="item-label" style="color: #EF4444;">解绑设备</div>
<div class="item-desc">解绑后的角色记忆将保存至云端</div>
</div>
</div>
</div>
<!-- Group 4: Chat -->
<div class="settings-group-title">交互体验</div>
<div class="settings-group">
<div class="settings-item">
<span class="item-label">允许打断</span>
<div class="toggle-switch active" onclick="this.classList.toggle('active')">
<div class="toggle-knob"></div>
</div>
</div>
<div class="settings-item clickable">
<span class="item-label">隐私模式</span>
<span class="item-value">已开启 <span class="arrow"></span></span>
</div>
</div>
</div>
</div>
<!-- --- Edit Modal --- -->
<div class="modal-overlay" id="edit-modal">
<div class="glass-modal">
<div class="modal-title" id="edit-title">修改名称</div>
<input type="text" class="modal-input" id="edit-input" placeholder="请输入...">
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeEditModal()">取消</button>
<button class="modal-btn confirm" onclick="saveEdit()">确定</button>
</div>
</div>
</div>
<!-- --- Network Confirm Modal --- -->
<div class="modal-overlay" id="network-modal">
<div class="glass-modal">
<div class="modal-title">添加备用网络</div>
<div class="modal-desc">需要重新连接设备蓝牙来配置新的 Wi-Fi 网络。请确保设备在附近且已开机,手机蓝牙已打开。</div>
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeNetworkModal()">取消</button>
<button class="modal-btn confirm" onclick="confirmNetwork()">开始配置</button>
</div>
</div>
</div>
<!-- --- Unbind Modal --- -->
<div class="modal-overlay" id="unbind-modal">
<div class="glass-modal unbind-style" style="padding: 24px;">
<div class="unbind-header">
<div class="warn-icon">
<!-- Warning Triangle SVG -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path
d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"
stroke="#EF4444" fill="none"></path>
<line x1="12" y1="9" x2="12" y2="13" stroke="#EF4444"></line>
<line x1="12" y1="17" x2="12.01" y2="17" stroke="#EF4444"></line>
</svg>
</div>
<div class="unbind-title">确认解绑设备?</div>
</div>
<div class="modal-desc" style="text-align: left; line-height: 1.5; color: #4B5563; margin-bottom: 24px;">
解绑后,设备 <span class="highlight-text">Airhub_5G</span> 将无法使用。您与 <span class="highlight-text">小毛球</span>
的交互数据已形成角色记忆 <span class="highlight-text">Cloud_Mem_01</span>,可注入其他设备继续养成。
</div>
<div class="modal-actions">
<button class="modal-btn danger" onclick="confirmUnbind()">解绑</button>
<button class="modal-btn secondary" onclick="closeUnbindModal()">再想想</button>
</div>
</div>
</div>
<!-- JS Logic -->
<script>
// --- Existing Settings Logic ---
function openSettings() {
const view = document.getElementById('settings-view');
view.style.display = 'flex';
view.style.animation = 'none';
view.offsetHeight;
view.style.animation = 'slideUp 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)';
}
function closeSettings() {
// Slide down animation could be added here
const view = document.getElementById('settings-view');
view.style.animation = 'slideUp 0.3s reverse forwards'; // Quick reverse hack
setTimeout(() => {
view.style.display = 'none';
view.style.animation = '';
}, 300);
}
// --- New Interaction Logic ---
let editTargetId = ''; // ID of the span to update
function openEditModal(valId, title) {
editTargetId = 'val-' + valId;
document.getElementById('edit-title').innerText = title;
const el = document.getElementById(editTargetId);
if (el) {
document.getElementById('edit-input').value = el.innerText;
}
document.getElementById('edit-modal').classList.add('active');
}
function closeEditModal() {
document.getElementById('edit-modal').classList.remove('active');
}
function saveEdit() {
const val = document.getElementById('edit-input').value;
if (val && editTargetId) {
const el = document.getElementById(editTargetId);
if (el) el.innerText = val;
}
closeEditModal();
}
function reconfigureNetwork() {
// Open Confirmation Modal
document.getElementById('network-modal').classList.add('active');
}
function closeNetworkModal() {
document.getElementById('network-modal').classList.remove('active');
}
function confirmNetwork() {
window.location.href = 'bluetooth.html';
}
function openUnbindModal() {
document.getElementById('unbind-modal').classList.add('active');
}
function closeUnbindModal() {
document.getElementById('unbind-modal').classList.remove('active');
}
function confirmUnbind() {
closeUnbindModal();
// Simulate unbind action
// In a real app, API call here.
// For demo, maybe show a toast or redirect.
// Since user wants "details", let's redirect to product list as if device is gone.
window.location.href = 'products.html';
}
</script>
</body>
</html>