1113 lines
47 KiB
HTML
1113 lines
47 KiB
HTML
<!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> |