rtc_prd/preview.html
seaislee1209 066eb8f820 feat: music-creation page + MiniMax API integration + Flutter dev setup
Music Creation Page:
- Vinyl 3D flip to view lyrics, tonearm animation, glow rotation effect
- Circular SVG progress ring, speech bubble feedback, confirm dialog
- Playlist modal, free creation input, lyrics formatting optimization
- MiniMax API real music generation with SSE streaming progress

Backend:
- FastAPI proxy server.py for MiniMax API calls
- Music + lyrics file persistence to Capybara music/ directory
- GET /api/playlist endpoint for auto-building playlist from files

UI/UX Refinements:
- frontend-design skill compliance across all pages
- Glassmorphism effects, modal interactions, scroll tap prevention
- iPhone 12 Pro responsive layout (390x844)

Flutter Development Preparation:
- Installed flutter-expert skill with 6 reference docs
- Added 5 Cursor Rules: official Flutter, clean architecture, UI performance, testing, Dart standards

Assets:
- 9 Capybara music MP3 files + lyrics TXT files
- MiniMax API documentation

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-06 18:23:19 +08:00

523 lines
19 KiB
HTML
Raw Permalink 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">
<title>Airhub - 设备预览器</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: #0a0a0a;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 24px 20px;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
color: white;
}
.preview-layout {
display: flex;
gap: 32px;
align-items: flex-start;
}
/* ===== Left Panel: Controls ===== */
.control-panel {
width: 200px;
position: sticky;
top: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.panel-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 4px;
}
.device-btn {
display: block;
width: 100%;
padding: 10px 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
color: rgba(255, 255, 255, 0.6);
border-radius: 10px;
cursor: pointer;
font-size: 13px;
text-align: left;
transition: all 0.2s ease;
font-family: inherit;
}
.device-btn:hover {
background: rgba(255, 255, 255, 0.08);
color: white;
}
.device-btn.active {
background: rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.4);
color: white;
}
.device-btn .size {
font-size: 11px;
color: rgba(255, 255, 255, 0.35);
margin-top: 2px;
}
.device-btn.active .size {
color: rgba(99, 102, 241, 0.7);
}
.page-btn {
display: block;
width: 100%;
padding: 8px 12px;
border: none;
background: transparent;
color: rgba(255, 255, 255, 0.5);
border-radius: 8px;
cursor: pointer;
font-size: 13px;
text-align: left;
transition: all 0.15s ease;
font-family: inherit;
}
.page-btn:hover {
background: rgba(255, 255, 255, 0.06);
color: white;
}
.page-btn.active {
background: rgba(255, 255, 255, 0.1);
color: white;
}
.page-list {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 50vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.15) transparent;
}
/* ===== Right: Phone Frame ===== */
.phone-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.device-info {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
}
.device-info strong {
color: white;
}
.iphone-frame {
position: relative;
background: #1a1a1a;
border-radius: 55px;
padding: 15px;
box-shadow:
0 0 0 2px #333,
0 0 0 4px #1a1a1a,
0 25px 50px rgba(0, 0, 0, 0.5),
inset 0 0 0 2px rgba(255, 255, 255, 0.08);
transition: width 0.3s ease, height 0.3s ease;
}
/* Dynamic Island */
.iphone-frame::before {
content: '';
position: absolute;
top: 18px;
left: 50%;
transform: translateX(-50%);
width: 126px;
height: 37px;
background: #000;
border-radius: 20px;
z-index: 100;
}
/* Notch variant */
.iphone-frame.notch::before {
width: 150px;
height: 32px;
top: 14px;
border-radius: 0 0 20px 20px;
}
.iphone-screen {
border-radius: 42px;
overflow: hidden;
background: #fff;
position: relative;
transition: width 0.3s ease, height 0.3s ease;
}
.iphone-screen iframe {
width: 100%;
height: 100%;
border: none;
}
/* Refresh hint */
.refresh-btn {
padding: 8px 20px;
border: 1px solid rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.6);
border-radius: 20px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
font-family: inherit;
}
.refresh-btn:hover {
background: rgba(255, 255, 255, 0.12);
color: white;
}
</style>
</head>
<body>
<div class="preview-layout">
<!-- Left: Controls -->
<div class="control-panel">
<!-- Device Selector -->
<div>
<div class="panel-title">设备</div>
<div style="display:flex;flex-direction:column;gap:4px;margin-top:8px;">
<button class="device-btn active" onclick="setDevice(390, 844, 'iPhone 12 Pro', 'notch', this)">
iPhone 12 Pro
<div class="size">390 × 844</div>
</button>
<button class="device-btn" onclick="setDevice(393, 852, 'iPhone 16', 'island', this)">
iPhone 16
<div class="size">393 × 852</div>
</button>
<button class="device-btn" onclick="setDevice(402, 874, 'iPhone 16 Pro', 'island', this)">
iPhone 16 Pro
<div class="size">402 × 874</div>
</button>
<button class="device-btn" onclick="setDevice(440, 956, 'iPhone 16 Pro Max', 'island', this)">
iPhone 16 Pro Max
<div class="size">440 × 956</div>
</button>
</div>
</div>
<!-- Page Navigator -->
<div>
<div class="panel-title">页面</div>
<div class="page-list" style="margin-top:8px;">
<button class="page-btn active" onclick="goPage('index.html', this)">🏠 首页</button>
<button class="page-btn" onclick="goPage('bluetooth.html', this)">📡 蓝牙搜索</button>
<button class="page-btn" onclick="goPage('wifi-config.html', this)">📶 WiFi 配网</button>
<button class="page-btn" onclick="goPage('products.html', this)">📦 产品选择</button>
<button class="page-btn" onclick="goPage('device-control.html', this)">🎮 设备控制</button>
<button class="page-btn" onclick="goPage('music-creation.html', this)">🎵 音乐创作</button>
<button class="page-btn" onclick="goPage('story-detail.html', this)">📖 故事详情</button>
<button class="page-btn" onclick="goPage('story-loading.html', this)">⏳ 故事加载</button>
<button class="page-btn" onclick="goPage('profile.html', this)">👤 个人中心</button>
<button class="page-btn" onclick="goPage('profile-info.html', this)">📝 个人信息</button>
<button class="page-btn" onclick="goPage('settings.html', this)">⚙️ 设置</button>
<button class="page-btn" onclick="goPage('login.html', this)">🔐 登录</button>
<button class="page-btn" onclick="goPage('notifications.html', this)">🔔 通知</button>
<button class="page-btn" onclick="goPage('guide-feeding.html', this)">🍽️ 喂食指南</button>
<button class="page-btn" onclick="goPage('help.html', this)">❓ 帮助</button>
<button class="page-btn" onclick="goPage('privacy.html', this)">🔒 隐私政策</button>
<button class="page-btn" onclick="goPage('agreement.html', this)">📄 用户协议</button>
<button class="page-btn" onclick="goPage('agent-manage.html', this)">🤖 Agent 管理</button>
<button class="page-btn" onclick="goPage('collection-list.html', this)">📋 信息收集清单</button>
<button class="page-btn" onclick="goPage('sharing-list.html', this)">🔗 信息共享清单</button>
</div>
</div>
</div>
<!-- Right: Phone -->
<div class="phone-area">
<div class="device-info">
<strong id="device-name">iPhone 12 Pro</strong>
&nbsp;·&nbsp;
<span id="device-size">390 × 844</span>
&nbsp;·&nbsp;
<span id="page-name">index.html</span>
</div>
<div class="iphone-frame notch" id="phone-frame">
<div class="iphone-screen" id="phone-screen" style="width:390px;height:844px;">
<iframe src="index.html" id="app-frame"></iframe>
</div>
</div>
<button class="refresh-btn" onclick="refreshFrame()">↻ 刷新预览</button>
</div>
</div>
<script>
let currentPage = 'index.html';
function setDevice(width, height, name, type, btn) {
const screen = document.getElementById('phone-screen');
const frame = document.getElementById('phone-frame');
screen.style.width = width + 'px';
screen.style.height = height + 'px';
frame.style.width = (width + 30) + 'px';
frame.style.height = (height + 40) + 'px';
// Notch vs Dynamic Island
frame.className = 'iphone-frame ' + type;
document.getElementById('device-name').textContent = name;
document.getElementById('device-size').textContent = width + ' × ' + height;
// Update active button
document.querySelectorAll('.device-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
refreshFrame();
}
function goPage(page, btn) {
currentPage = page;
document.getElementById('app-frame').src = page;
document.getElementById('page-name').textContent = page;
document.querySelectorAll('.page-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
}
function refreshFrame() {
const iframe = document.getElementById('app-frame');
iframe.src = currentPage + '?t=' + Date.now();
}
// Keyboard shortcut: Ctrl+R to refresh frame
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'r') {
e.preventDefault();
refreshFrame();
}
});
// Set initial frame size
document.getElementById('phone-frame').style.width = '420px';
document.getElementById('phone-frame').style.height = '884px';
// =============================================
// Touch Simulation - Inject into iframe
// =============================================
const TOUCH_CSS = `
/* Hide default cursor, show nothing */
*, *::before, *::after {
cursor: none !important;
}
/* The finger dot */
#__touch-dot {
position: fixed;
width: 28px;
height: 28px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,0,0,0.22) 0%, rgba(0,0,0,0.08) 70%, transparent 100%);
box-shadow: 0 0 8px rgba(0,0,0,0.1);
pointer-events: none;
z-index: 99999;
transform: translate(-50%, -50%) scale(1);
transition: transform 0.08s ease, opacity 0.15s ease;
opacity: 0;
will-change: transform, left, top;
}
#__touch-dot.visible { opacity: 1; }
#__touch-dot.pressed {
transform: translate(-50%, -50%) scale(0.75);
background: radial-gradient(circle, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.12) 70%, transparent 100%);
}
/* Tap ripple */
.__touch-ripple {
position: fixed;
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.08);
pointer-events: none;
z-index: 99998;
transform: translate(-50%, -50%) scale(1);
animation: __ripple-expand 0.45s ease-out forwards;
}
@keyframes __ripple-expand {
0% { transform: translate(-50%, -50%) scale(1); opacity: 0.5; }
100% { transform: translate(-50%, -50%) scale(8); opacity: 0; }
}
/* Disable text selection while swiping */
body.__touch-swiping,
body.__touch-swiping * {
user-select: none !important;
-webkit-user-select: none !important;
}
`;
const TOUCH_JS = `
(function() {
if (window.__touchSimLoaded) return;
window.__touchSimLoaded = true;
// Create dot element
const dot = document.createElement('div');
dot.id = '__touch-dot';
document.body.appendChild(dot);
let isDown = false;
let startX = 0, startY = 0;
let lastX = 0, lastY = 0;
let hasMoved = false;
const SWIPE_THRESHOLD = 5;
// Find the best scrollable parent
function getScrollParent(el) {
while (el && el !== document.body) {
const style = getComputedStyle(el);
const overflowY = style.overflowY;
if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
return el;
}
el = el.parentElement;
}
// Fallback: try document.scrollingElement or body
if (document.scrollingElement && document.scrollingElement.scrollHeight > document.scrollingElement.clientHeight) {
return document.scrollingElement;
}
return document.body;
}
let scrollTarget = null;
// Move dot
document.addEventListener('mousemove', (e) => {
dot.style.left = e.clientX + 'px';
dot.style.top = e.clientY + 'px';
dot.classList.add('visible');
if (isDown) {
const dx = e.clientX - lastX;
const dy = e.clientY - lastY;
if (!hasMoved && (Math.abs(e.clientX - startX) > SWIPE_THRESHOLD || Math.abs(e.clientY - startY) > SWIPE_THRESHOLD)) {
hasMoved = true;
document.body.classList.add('__touch-swiping');
}
if (hasMoved && scrollTarget) {
// Invert: drag down = scroll up (like real phone)
scrollTarget.scrollTop -= dy;
scrollTarget.scrollLeft -= dx;
}
lastX = e.clientX;
lastY = e.clientY;
}
}, { passive: true });
// Hide dot when mouse leaves
document.addEventListener('mouseleave', () => {
dot.classList.remove('visible');
});
document.addEventListener('mouseenter', () => {
dot.classList.add('visible');
});
// Press
document.addEventListener('mousedown', (e) => {
isDown = true;
hasMoved = false;
startX = e.clientX;
startY = e.clientY;
lastX = e.clientX;
lastY = e.clientY;
dot.classList.add('pressed');
// Find scrollable parent of the target
scrollTarget = getScrollParent(e.target);
});
// Release
document.addEventListener('mouseup', (e) => {
if (isDown && !hasMoved) {
// It was a tap, not a swipe — show ripple
const ripple = document.createElement('div');
ripple.className = '__touch-ripple';
ripple.style.left = e.clientX + 'px';
ripple.style.top = e.clientY + 'px';
document.body.appendChild(ripple);
setTimeout(() => ripple.remove(), 500);
}
isDown = false;
hasMoved = false;
scrollTarget = null;
dot.classList.remove('pressed');
document.body.classList.remove('__touch-swiping');
});
// Prevent default drag behavior inside the app
document.addEventListener('dragstart', (e) => e.preventDefault());
})();
`;
function injectTouchSim(iframe) {
try {
const doc = iframe.contentDocument || iframe.contentWindow.document;
if (!doc || !doc.body) return;
// Inject CSS
const style = doc.createElement('style');
style.id = '__touch-sim-css';
style.textContent = TOUCH_CSS;
doc.head.appendChild(style);
// Inject JS
const script = doc.createElement('script');
script.id = '__touch-sim-js';
script.textContent = TOUCH_JS;
doc.body.appendChild(script);
} catch (e) {
console.warn('Touch sim injection failed (cross-origin?):', e);
}
}
// Inject on every iframe load
const appFrame = document.getElementById('app-frame');
appFrame.addEventListener('load', () => injectTouchSim(appFrame));
</script>
</body>
</html>