rtc_prd/products.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

405 lines
14 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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #FEFEFE;
min-height: 100vh;
overflow-x: hidden;
}
/* Product List uses shared .scroll-container + custom styling */
.product-list {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Product Card - Banner Style */
.product-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
min-height: 140px;
border-radius: 28px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
position: relative;
overflow: hidden;
/* Glowing Pill Quality Standard (Adapted for Cards) */
background: linear-gradient(90deg, #22D3EE 0%, #3B82F6 35%, #6366F1 65%, #8B5CF6 100%);
box-shadow:
0 0 15px rgba(34, 211, 238, 0.25),
0 0 30px rgba(99, 102, 241, 0.15),
0 8px 24px rgba(99, 102, 241, 0.25),
inset 0 1px 1px rgba(255, 255, 255, 0.25);
}
/* Top highlight layer */
.product-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, transparent 100%);
border-radius: 28px 28px 50% 50%;
pointer-events: none;
}
.product-card:active {
transform: scale(0.98);
}
/* Per-product Theme Colors - Wide Hue Range, Lower Saturation */
.product-card.capybara {
/* Warm family: Dark Apricot → Peach → Sand → Dark Muted Orange (Rich & Complex) */
background: linear-gradient(90deg, #E6B98D 0%, #E8C9A8 35%, #D4A373 70%, #B07D5A 100%);
box-shadow:
0 0 20px rgba(201, 160, 122, 0.25),
0 8px 24px rgba(166, 124, 82, 0.2),
inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
.product-card.badge-ai {
/* Cool family: Vibrant Cyan → Sky Blue → Periwinkle → Soft Violet (Complex & Translucent) */
background: linear-gradient(90deg, #22D3EE 0%, #60A5FA 35%, #818CF8 70%, #A78BFA 100%);
box-shadow:
0 0 20px rgba(96, 165, 250, 0.25),
0 8px 24px rgba(129, 140, 248, 0.2),
inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
.product-card.badge-basic {
/* Lavender family: Bright Lilac → Orchid → Soft Purple → Muted Violet (Not heavy) */
background: linear-gradient(90deg, #C084FC 0%, #D8B4FE 35%, #C4B5FD 70%, #A78BFA 100%);
box-shadow:
0 0 15px rgba(200, 165, 232, 0.25),
0 8px 24px rgba(167, 139, 205, 0.2),
inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
.product-card.bracelet {
/* Warm-Orange family: Bright Orange → Peach → Coral → Soft Terracotta (Light end) */
background: linear-gradient(90deg, #FDBA74 0%, #FB923C 35%, #FBAF85 70%, #E07B54 100%);
box-shadow:
0 0 20px rgba(244, 177, 131, 0.25),
0 8px 24px rgba(224, 123, 84, 0.2),
inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
.product-card.vsinger {
/* Teal family: Emerald → Mint → Aqua → Soft Teal (Translucent) */
background: linear-gradient(90deg, #34D399 0%, #5EEAD4 35%, #22D3EE 70%, #2DD4BF 100%);
box-shadow:
0 0 20px rgba(94, 187, 172, 0.25),
0 8px 24px rgba(61, 154, 139, 0.2),
inset 0 1px 1px rgba(255, 255, 255, 0.35);
}
/* Icon Box */
.p-icon {
width: 72px;
height: 72px;
flex-shrink: 0;
border-radius: 20px;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.p-icon img {
width: 48px;
height: 48px;
object-fit: contain;
filter: brightness(0) invert(1);
/* White icons */
}
/* Capybara uses colored image */
.product-card.capybara .p-icon img {
filter: none;
width: 56px;
height: 56px;
}
/* AI Badge on Icon */
.p-icon .ai-tag {
position: absolute;
top: -6px;
right: -6px;
background: rgba(255, 255, 255, 0.95);
color: #6366F1;
font-size: 9px;
font-weight: 700;
padding: 3px 6px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
/* Text Info */
.p-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.p-name {
font-size: 20px;
font-weight: 700;
color: #FFFFFF;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.p-status {
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
display: flex;
align-items: center;
gap: 6px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
}
.status-dot.online {
background: #34D399;
box-shadow: 0 0 0 3px rgba(52, 211, 153, 0.3);
}
/* Arrow */
.p-arrow {
width: 28px;
height: 28px;
color: rgba(255, 255, 255, 0.7);
flex-shrink: 0;
}
/* Fade in animation */
.product-card {
animation: fadeSlideUp 0.4s ease-out backwards;
}
.product-card:nth-child(1) {
animation-delay: 0.05s;
}
.product-card:nth-child(2) {
animation-delay: 0.1s;
}
.product-card:nth-child(3) {
animation-delay: 0.15s;
}
.product-card:nth-child(4) {
animation-delay: 0.2s;
}
.product-card:nth-child(5) {
animation-delay: 0.25s;
}
@keyframes fadeSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</head>
<body>
<!-- Reusing gradient background -->
<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>
<header class="page-header">
<button class="back-btn" onclick="window.history.back()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<h1 class="page-title">选择产品</h1>
<div class="header-spacer"></div>
</header>
<main class="scroll-container product-list">
<!-- 1. Plush Core (Capybara) -->
<div class="product-card capybara" onclick="selectDevice('device-control.html')">
<div class="p-icon">
<span class="ai-tag">AI</span>
<img src="Capybara.png" alt="毛绒机芯">
</div>
<div class="p-info">
<div class="p-name">毛绒机芯</div>
<div class="p-status">
<span class="status-dot online"></span>
已连接
</div>
</div>
<svg class="p-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6" />
</svg>
</div>
<!-- 2. Badge AI -->
<div class="product-card badge-ai" onclick="selectDevice('#')">
<div class="p-icon">
<span class="ai-tag">AI</span>
<img src="icons/icon-product-badge.svg" alt="电子吧唧">
</div>
<div class="p-info">
<div class="p-name">电子吧唧 AI</div>
<div class="p-status">
<span class="status-dot"></span>
离线
</div>
</div>
<svg class="p-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6" />
</svg>
</div>
<!-- 3. Badge Basic -->
<div class="product-card badge-basic" onclick="selectDevice('#')">
<div class="p-icon">
<img src="icons/icon-product-badge.svg" alt="普通吧唧">
</div>
<div class="p-info">
<div class="p-name">普通吧唧</div>
<div class="p-status">
<span class="status-dot"></span>
未配对
</div>
</div>
<svg class="p-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6" />
</svg>
</div>
<!-- 4. AI Bracelet -->
<div class="product-card bracelet" onclick="selectDevice('#')">
<div class="p-icon">
<span class="ai-tag">AI</span>
<img src="icons/icon-product-bracelet.svg" alt="AI手链">
</div>
<div class="p-info">
<div class="p-name">AI 手链</div>
<div class="p-status">
<span class="status-dot"></span>
点击扫描
</div>
</div>
<svg class="p-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6" />
</svg>
</div>
<!-- 5. VSinger (Luo Tianyi) -->
<div class="product-card vsinger" onclick="window.open('https://example.com', '_blank')">
<div class="p-icon">
<img src="icons/icon-product-luo.svg" alt="洛天依">
</div>
<div class="p-info">
<div class="p-name">洛天依</div>
<div class="p-status">去下载专属 APP →</div>
</div>
<svg class="p-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<path d="M9 18l6-6-6-6" />
</svg>
</div>
</main>
<script>
function selectDevice(url) {
if (url === '#') {
// Haptic feedback simulation
if (navigator.vibrate) navigator.vibrate(10);
return;
}
window.location.href = url;
}
// Scroll Fade Effect (JavaScript Fallback)
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('.scroll-container');
const cards = document.querySelectorAll('.product-card');
// If elements missing, abort
if (!container || cards.length === 0) return;
function handleScroll() {
cards.forEach(card => {
const rect = card.getBoundingClientRect();
// Fade zone logic (Updated for smoother "Half-Fade"):
// Resting (~130px) -> Opaque.
// Start fading at 120px (just as it starts moving up).
// End fading at 0px (top of screen).
// At header height (~60px), opacity will be 0.5 (Half Visible).
const startFade = 120;
const endFade = 0;
if (rect.top < startFade) {
let progress = (rect.top - endFade) / (startFade - endFade);
// Clamp value between 0 and 1
progress = Math.min(Math.max(progress, 0), 1);
// Apply opacity and scale
card.style.opacity = progress;
card.style.transform = `scale(${0.95 + (0.05 * progress)})`;
} else {
// Reset properties if outside fade zone
card.style.opacity = 1;
card.style.transform = 'scale(1)';
}
});
}
// Passive listener for performance
container.addEventListener('scroll', handleScroll, { passive: true });
// Initial render check
handleScroll();
});
</script>
</body>
</html>