feat: core code

This commit is contained in:
seaislee1209 2026-02-04 15:33:02 +08:00
commit d60f9af758
30 changed files with 12743 additions and 0 deletions

22
.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
# System Files
.DS_Store
Thumbs.db
Desktop.ini
# Python
__pycache__/
*.py[cod]
# Node
node_modules/
dist/
# Project Specific
.agent/
.gemini/
tmp/
# Large Media Directories (Uncomment if they should be ignored to fix 413 error)
动态绘本/
故事书封面图/
*.mp4

348
PRD.md Normal file
View File

@ -0,0 +1,348 @@
# Airhub App PRD (产品需求文档)
## 产品概述
Airhub 是一款用于连接和控制智能硬件设备的移动应用支持蓝牙配对、WiFi配网、NFC识别和设备控制。
---
## 支持的产品
| 产品 | 图标 | AI功能 | 连接方式 | 主题色 | 备注 |
|------|------|--------|----------|--------|------|
| **毛绒机芯** | 卡皮巴拉 + AI角标 | ✅ | 蓝牙 + WiFi | 暖棕色 | 完整流程 |
| **电子吧唧 AI版** | 圆形屏幕 + AI角标 | ✅ | 蓝牙 + WiFi | 科技蓝 | 完整流程 |
| **普通电子吧唧** | 圆形屏幕 (无角标) | ❌ | 仅蓝牙 | 浅灰蓝 | 仅蓝牙传图 |
| **AI手链** | 手绳图标 + AI角标 | ✅ | NFC扫描 | 活力橙 | RFID标签无电子元件 |
| **洛天依** | 洛天依形象 | ✅ | 外部链接 | 青绿色 | 跳转下载专属APP |
> **设备识别**蓝牙设备通过SN码识别AI手链通过NFC RFID识别
---
## 页面架构
```
首页 (立即连接)
├─ 蓝牙搜索页 ──→ 发现设备 ──→ 设备详情页
│ │
│ ├─ 毛绒机芯/电子吧唧AI → WiFi配网 → 设备控制页
│ └─ 普通电子吧唧 → 直接进入设备控制页
├─ NFC扫描 (全局) ──→ AI手链详情页
└─ 产品选择页 (卡片列表) ──→ 各设备详情页
└─ 洛天依 → 外部下载链接
```
---
## 核心页面
### 0. APP启动逻辑
```
APP启动
├─ 首次使用 (无已连接设备) → 显示欢迎首页
└─ 有已连接设备 → 显示最后操作的设备首页
```
> 使用 localStorage 记录 `lastActiveDevice`,每次操作设备时更新
### 1. 欢迎首页 ✅ 已完成
- Logo (像素风) + 吉祥物 (浮动) + 渐变背景 + "立即连接"
- **仅首次使用或无设备时显示**
### 2. 蓝牙搜索页
- 马里奥问号箱子动画 → 发现设备变换为产品图标
- SN码识别设备类型
### 3. 产品选择页
- 卡片形式展示所有产品
- 点击跳转对应设备页面
- 洛天依卡片 → 外部下载链接
### 4. 设备控制页 (Device Control)
#### 4.1 通用结构
- **头部**:沉浸式 Header包含切换设备入口、设备状态指示。
- **底部**:悬浮式 Tab 导航栏 (Icon Only),提供核心功能入口。
#### 4.2 毛绒机芯 (卡皮巴拉) 首页
- **UI布局**
- **Header**
- 左侧:🔄 切换按钮 (跳转产品列表)
- 右侧:
- 🟢 在线状态 (绿点 + "在线")
- 🔋 电量显示 (Icon + 百分比)
- ⚙️ 设置入口
- **Main (视觉核心)**
- 居中展示卡皮巴拉形象 (PNG)
- 交互:待机呼吸动画
- **Footer (功能导航)**
- 🏠 **首页 (Home)**:当前设备状态
- 📖 **故事 (Story)**AI故事生成入口
- 🎵 **歌曲 (Music)**:音乐/音效控制
- 👤 **我的 (User)**:设备专属个人中心
- **交互弹窗:故事生成器 (Story Generator)**
- **入口**:点击底部的 [故事] Tab。
- **逻辑**
- **Tab分类**:角色 / 环境 / 道具。
- **限制**每类最多选3个总数最多9个。
- **自动跳转**单类选满3个后自动跳转下一 Tab。
- **选中态**:右上角“毛绒徽章”对钩 (Apple Style),无顶部预览栏。
### 5. WiFi配网页
- 选择网络 → 输入密码 → 配网进度 → 成功/失败
---
## 特殊逻辑
### NFC全局监听
```
APP任意页面 + 扫描AI手链NFC
├─ APP未打开 → 拉起APP → 跳转AI手链页面
└─ APP已打开 → 直接跳转AI手链页面
```
### 切换设备
- 所有设备详情页头部有 🔄 图标
- 点击跳转到产品选择页
---
## 设计规范 (Visual Design System)
### 🎨 双层主题色体系
Airhub 采用 **双层主题色体系**,确保品牌统一性的同时支持产品个性化:
#### 1⃣ App 级主视觉 (Global Theme)
> 适用范围:首页 → 蓝牙搜索 → WiFi配网 → 产品列表页
| 属性 | 值 | 说明 |
|------|-----|------|
| **主渐变** | `#22D3EE → #3B82F6 → #6366F1 → #8B5CF6` | 青 → 蓝 → 靛 → 紫,水平渐变 |
| **背景** | 粉紫流动渐变 | 柔和梦幻感,多层 radial-gradient 叠加 |
| **按钮** | 3D 发光胶囊 (见下方质感规范) | 所有主要 CTA 按钮 |
#### 2⃣ 产品级主题色 (Product Theme)
> 适用范围UI主色调、卡片背景、按钮激活态。采用 **4色阶全息渐变** 以提升通透感与高级感。
| 产品 | 标识色 | 4色阶渐变定义 (Left → Right) | 风格描述 |
|------|------|----------|------|
| **毛绒机芯** | 暖杏褐 | `#E6B98D` (35%) `#E8C9A8` (30%) `#D4A373` (25%) `#B07D5A` | **醇厚**:左侧杏色起手不发白,右侧收于深褐,质感扎实。 |
| **电子吧唧AI** | 科技青 | `#22D3EE` (35%) `#60A5FA` (30%) `#818CF8` (25%) `#A78BFA` | **鲜艳**:左侧高亮青色(同首页按钮),右侧转紫,色彩跨度大。 |
| **普通吧唧** | 柔光紫 | `#C084FC` (35%) `#D8B4FE` (30%) `#C4B5FD` (25%) `#A78BFA` | **通透**:左侧亮紫,中间过渡到柔和淡紫,避免油漆感。 |
| **AI手链** | 活力橙 | `#FDBA74` (35%) `#FB923C` (30%) `#FBAF85` (25%) `#E07B54` | **轻盈**:左侧亮橙,右侧收于赤陶色,拒绝死板橘红。 |
| **洛天依** | 未来绿 | `#34D399` (35%) `#5EEAD4` (30%) `#22D3EE` (25%) `#2DD4BF` | **偏光**:翠绿 → 冰蓝 → 青色,极具未来感的冷色流动。 |
> **规则**
> 1. **左实右虚**:渐变左侧起始色必须有足够饱和度,严禁使用近白色。
> 2. **4色阶**必须包含4个色彩节点模仿光线在玉石/光盘表面的折射效果。
> 3. **通透尾部**:渐变右侧颜色稍微提亮或降低"实心感",保持呼吸感。
---
### ✨ 按钮质感规范 (Glowing Pill Button Standard)
> **无论主题色如何变化,所有主要按钮的"质感"必须统一。**
```
┌─────────────────────────────────────────────┐
│ ▓▓▓ 顶部高光层 (rgba(255,255,255,0.15)) ▓▓▓ │
│ │
│ 按钮文字 (白色) │
│ │
│ ▒▒▒ 底部暗边 (inset shadow) ▒▒▒ │
└─────────────────────────────────────────────┘
↓↓↓ 外发光 (主题色, opacity 0.15~0.4) ↓↓↓
```
| 质感属性 | CSS 实现 |
|----------|----------|
| **形状** | `border-radius: 50% of height` (胶囊形) |
| **3D 凸起** | `inset 0 1px 1px rgba(255,255,255,0.3)` 顶部高光 |
| **底部暗边** | `inset 0 -1px 2px rgba(0,0,0,0.1)` |
| **外发光** | `0 0 15px {主题色/0.35}, 0 0 30px {主题色/0.25}, 0 6px 20px {主题色/0.4}` |
| **扫光动画** | `shine` 动画3s 周期 |
### 3. Visual System & Themes (New Standard)
* **Global / Home Page (AI Tech)**:
* **Theme**: High-Tech, futuristic, crisp.
* **Colors**: Purple, Blue, Pink Gradients.
* **Buttons**: Glassmorphism or Tech Gradient.
* **Device Page: Capybara (Warm Plush)**:
* **Theme**: Soft, organic, warm, friendly.
* **Colors**: "Plush Gradient" -> Soft Tan (`#ECCFA8`) to Warm Brown (`#C99672`).
* **Goal**: Match the physical product "Plush Machine Core" feel.
* **Note**: Avoid "Vibrant Tech Orange" (Safety/Alert color) for main actions here.
### 4. Technical Architecture
* **Frontend**: Native JS, CSS Variables for theming.
| **按压反馈** | `transform: scale(0.98)` |
#### CSS 变量定义 (建议)
```css
:root {
/* App 全局主题 */
--theme-gradient: linear-gradient(90deg, #22D3EE 0%, #3B82F6 35%, #6366F1 65%, #8B5CF6 100%);
--theme-glow-1: rgba(34, 211, 238, 0.35);
--theme-glow-2: rgba(99, 102, 241, 0.25);
--theme-glow-3: rgba(99, 102, 241, 0.4);
}
/* 产品主题覆盖 (示例:毛绒机芯) */
.theme-plush-core {
--theme-gradient: linear-gradient(90deg, #D4A574 0%, #C08552 100%);
--theme-glow-1: rgba(212, 165, 116, 0.35);
--theme-glow-2: rgba(192, 133, 82, 0.25);
--theme-glow-3: rgba(192, 133, 82, 0.4);
}
```
---
### 🧩 视觉一致性规则
| 规则 | 说明 |
|------|------|
| **背景统一** | 所有页面使用相同的动态渐变背景系统 |
| **按钮质感统一** | 所有主要按钮遵循 "Glowing Pill" 规范,仅颜色变化 |
| **卡片风格** | 轻量玻璃拟态 (`bg-white/20~30 + blur`),带 `drop-shadow` 光晕 |
| **动作按钮** | **Glass Halo Capsule** (见下方详细规范) |
| **图标风格** | 像素风 SVG (Pixel Art),统一 24x24 viewBox |
| **动效时长** | 微交互 150-300ms页面过渡 300-500ms |
| **边框策略** | 玻璃态元素使用 `1px rgba(255,255,255,0.3)` 弱边框 |
---
### ✨ 毛玻璃光晕胶囊规范 (Glass Halo Capsule Standard)
> **用于卡片内部或深色渐变背景上的次要/功能性操作按钮。**
| 属性 | 实现方式 |
|------|----------|
| **背景** | `rgba(255, 255, 255, 0.2)` + `backdrop-filter: blur(8px)` |
| **边框** | `1px solid rgba(255, 255, 255, 0.3)` |
| **形状** | `border-radius: 20px` (胶囊形) |
| **文字/图标** | 针对不同操作设置颜色 (如 `#EF4444``#B07D5A`) |
| **防遮挡光晕** | **核心重点**:使用 `filter: drop-shadow(0 0 2px #FFF) drop-shadow(0 0 5px rgba(255,255,255,0.8))` 确保彩色内容在深色背景下不“吃字”。 |
---
### 📜 渐变遮罩滚动页规范 (Gradient Mask Scroll Standard)
> **用于设置页、法务文档(用户协议、隐私政策)等长文本滚动页面。**
#### 核心逻辑
- **Header**:背景**完全透明**,不使用毛玻璃,保持视觉纯净。
- **Content**:内容区域使用 CSS `mask-image` 实现顶部淡出效果,滚动时文字优雅消失,而非被 Header 硬切。
#### 参数标准
| 属性 | 值 | 说明 |
|------|-----|------|
| **内容顶部间距** | `calc(safe-area + 120px)` | 内容初始位置,避开 Header |
| **遮罩透明区** | `0px ~ 100px` | 此区域完全透明(文字不可见) |
| **遮罩过渡区** | `100px ~ 130px` | 30px 渐变过渡至完全显示 |
#### CSS 实现
```css
.content-scroll {
padding-top: calc(env(safe-area-inset-top, 20px) + 120px);
/* 淡出遮罩 */
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
/* 隐藏滚动条 */
scrollbar-width: none;
}
.content-scroll::-webkit-scrollbar { display: none; }
```
#### 适用页面
- `privacy.html` - 隐私政策
- `agreement.html` - 用户协议
- `collection-list.html` - 个人信息收集清单
- `sharing-list.html` - 第三方信息共享清单
- `settings.html` - 设置页 (已应用)
---
### 🔧 共享 UI 组件规范
> **所有页面必须使用以下共享组件**,定义在 `styles.css` 中,确保一处修改全局生效。
#### Layout Tokens (CSS 变量)
```css
:root {
--header-padding-top: calc(env(safe-area-inset-top, 20px) + 12px);
--page-padding-x: 20px;
--safe-area-bottom: env(safe-area-inset-bottom, 20px);
}
```
#### 共享组件类名
| 类名 | 用途 | 规格 |
|------|------|------|
| `.page-header` | 页面头部 | Grid 布局 (`40px 1fr 40px`) 实现完美居中 |
| `.back-btn` | 返回按钮 | 40×40px, 12px圆角, 简单玻璃磨砂 (首页/蓝牙同款) |
| `.page-title` | 页面标题 | 18px/700, 居中 (Grid Column 2) |
| `.primary-btn` | 主按钮 | Glowing Pill 质感, 高度 58px |
| `.scroll-container` | 滚动容器 | iOS 惯性滚动, 统一内边距 |
#### 使用示例
```html
<!-- Grid 让标题绝对居中,无需 Spacer -->
<header class="page-header">
<button class="back-btn"></button>
<h1 class="page-title">选择产品</h1>
</header>
<main class="scroll-container">
<!-- 页面内容 -->
</main>
```
---
### 字体规范
| 用途 | 字体 | 字重 |
|------|------|------|
| **正文/UI** | Inter | 400-600 |
| **Logo/像素元素** | Press Start 2P | 400 |
| **回退字体栈** | -apple-system, BlinkMacSystemFont, Roboto | - |
> **为什么选择 Inter**
> - 专为数字屏幕设计,高可读性
> - 被 GitHub、Figma、Linear 等知名产品使用
> - 支持多语言,中英文混排效果好
> - 免费开源,无授权问题
---
## 技术接口预留
```javascript
// 蓝牙设备发现回调
onBluetoothDeviceFound(device) { /* SN码解析 */ }
// NFC扫描回调
onNFCTagRead(tagId) { /* RFID识别 */ }
// 设备类型枚举
DEVICE_TYPES = {
PLUSH_CORE: 'plush_core', // 毛绒机芯
BADGE_AI: 'badge_ai', // 电子吧唧AI
BADGE_BASIC: 'badge_basic', // 普通电子吧唧
BRACELET_AI: 'bracelet_ai', // AI手链
LUOTIANYI: 'luotianyi' // 洛天依
}
```
---
*文档最后更新: 2026-02-04*

365
agent-manage.html Normal file
View File

@ -0,0 +1,365 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.help-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: #6B7280;
cursor: pointer;
font-size: 18px;
}
.agent-content {
padding: calc(env(safe-area-inset-top, 20px) + 100px) 20px 40px;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
}
/* 角色卡片 - 使用 Capybara 卡片样式 */
.agent-card {
/* Capybara 渐变 */
background: linear-gradient(90deg, #E6B98D 0%, #E8C9A8 35%, #D4A373 70%, #B07D5A 100%);
border-radius: 20px;
padding: 20px;
margin-bottom: 16px;
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);
position: relative;
overflow: hidden;
}
/* 卡片高光层 */
.agent-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: 20px 20px 50% 50%;
pointer-events: none;
}
.agent-header {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 12px;
}
.agent-icon {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.25);
backdrop-filter: blur(8px);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.agent-info {
flex: 1;
}
.agent-id {
font-size: 16px;
font-weight: 600;
color: #FFFFFF;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
margin-bottom: 4px;
}
.agent-date {
font-size: 12px;
color: rgba(255, 255, 255, 0.85);
position: absolute;
top: 20px;
right: 20px;
}
.agent-detail {
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
margin-bottom: 4px;
}
.agent-detail span {
color: #FFFFFF;
font-weight: 500;
}
.agent-footer {
display: flex;
justify-content: flex-end;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
/* 通用动作按钮 (毛玻璃胶囊) */
.action-btn {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255, 255, 255, 0.2);
/* 恢复低透明度 */
backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.3);
padding: 6px 14px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.2s;
}
.action-btn:active {
transform: scale(0.95);
background: rgba(255, 255, 255, 0.3);
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.4);
}
.action-btn svg {
width: 14px;
height: 14px;
}
/* 解绑 (红色警示 - 带白色光晕垫底,包含图标和文字) */
.unbind-link:not(.inject-link) {
color: #EF4444;
/* 使用 drop-shadow 滤镜可以同时给 SVG 图标和文字添加光晕 */
filter: drop-shadow(0 0 2px #FFFFFF) drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
}
/* 注入 (主题棕色 - 带白色光晕) */
.inject-link {
color: #B07D5A !important;
filter: drop-shadow(0 0 2px #FFFFFF) drop-shadow(0 0 5px rgba(255, 255, 255, 0.8));
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9CA3AF;
}
.empty-state-icon {
font-size: 64px;
margin-bottom: 16px;
opacity: 0.5;
}
.empty-state-text {
font-size: 16px;
margin-bottom: 24px;
}
.add-btn {
background: linear-gradient(135deg, #ECCFA8 0%, #C99672 100%);
border: none;
padding: 14px 32px;
border-radius: 25px;
color: white;
font-weight: 600;
font-size: 16px;
cursor: pointer;
box-shadow: 0 4px 15px rgba(201, 150, 114, 0.3);
transition: all 0.2s;
}
.add-btn:active {
transform: scale(0.95);
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">角色记忆</span>
<button class="help-btn" onclick="showHelp()">?</button>
</header>
<main class="agent-content">
<!-- 角色卡片示例 -->
<div class="agent-card">
<div class="agent-date">2025/01/15</div>
<div class="agent-header">
<div class="agent-icon">🧠</div>
<div class="agent-info">
<div class="agent-id">Airhub_Mem_01</div>
</div>
</div>
<div class="agent-detail">已绑定:<span>Airhub_5G</span></div>
<div class="agent-detail">角色昵称:<span>小毛球</span></div>
<div class="agent-footer">
<div class="unbind-link action-btn" onclick="unbindAgent('Airhub_Mem_01')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 6L6 18M6 6l12 12"></path>
</svg>
解绑
</div>
</div>
</div>
<!-- 第二个角色卡片 -->
<div class="agent-card">
<div class="agent-date">2024/08/22</div>
<div class="agent-header">
<div class="agent-icon">🐾</div>
<div class="agent-info">
<div class="agent-id">Airhub_Mem_02</div>
</div>
</div>
<div class="agent-detail">已绑定:<span>未绑定设备</span></div>
<div class="agent-detail">角色昵称:<span>豆豆</span></div>
<div class="agent-footer">
<div class="unbind-link inject-link action-btn" onclick="injectAgent('Airhub_Mem_02')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 5v14M5 12h14"></path>
</svg>
注入设备
</div>
</div>
</div>
</main>
</div>
<!-- 解绑确认弹窗 -->
<div class="modal-overlay" id="unbind-modal">
<div class="glass-modal">
<div class="modal-title">确认解绑角色记忆?</div>
<div class="modal-desc" id="unbind-desc">解绑后,该角色记忆将与当前设备断开连接,但数据会保留在云端。</div>
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeUnbindModal()">取消</button>
<button class="modal-btn confirm" style="background: #EF4444;" onclick="confirmUnbind()">确认解绑</button>
</div>
</div>
</div>
<!-- 通用提示弹窗 -->
<div class="modal-overlay" id="message-modal">
<div class="glass-modal">
<div class="modal-title" id="msg-title">提示</div>
<div class="modal-desc" id="msg-desc">内容</div>
<div class="modal-actions">
<button class="modal-btn confirm" onclick="closeMessageModal()">确定</button>
</div>
</div>
</div>
<script>
let currentAgentId = '';
// 通用 Modal
function showMessage(title, desc) {
document.getElementById('msg-title').innerText = title;
document.getElementById('msg-desc').innerText = desc;
document.getElementById('message-modal').classList.add('active');
}
function closeMessageModal() {
document.getElementById('message-modal').classList.remove('active');
}
function showHelp() {
showMessage('什么是角色记忆?', '角色记忆是您与 AI 互动产生的人格数据,它是独立的数字资产,可以在不同设备间迁移,或分享给好友。');
}
function unbindAgent(agentId) {
currentAgentId = agentId;
document.getElementById('unbind-modal').classList.add('active');
}
function closeUnbindModal() {
document.getElementById('unbind-modal').classList.remove('active');
}
function confirmUnbind() {
closeUnbindModal();
showMessage('解绑成功', `已解绑角色记忆: ${currentAgentId}\n您可以随时在其他设备上重新注入。`);
// 实际应用中这里会刷新列表
}
function injectAgent(agentId) {
// 跳转到设备选择页面
showMessage('准备注入', `正在查找附近的可用设备以注入: ${agentId}\n(模拟功能)`);
}
</script>
</body>
</html>

170
agreement.html Normal file
View File

@ -0,0 +1,170 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 24px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
color: #374151;
line-height: 1.6;
font-size: 15px;
position: relative;
z-index: 2;
scrollbar-width: none;
/* Firefox */
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
h2 {
font-size: 17px;
margin: 32px 0 12px;
color: #1F2937;
font-weight: 700;
}
h2:first-child {
margin-top: 0;
}
p {
margin-bottom: 16px;
text-align: justify;
}
ul {
margin-bottom: 16px;
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
.strong {
font-weight: 600;
color: #1F2937;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">用户协议</span>
<div style="width: 44px;"></div>
</header>
<main class="content-scroll">
<p>欢迎您使用 Airhub 产品及服务!</p>
<p><strong>特别提示:</strong> 在您开始使用 Airhub
产品(以下简称"本产品")及相关服务之前,请您务必仔细阅读本《用户协议》(以下简称"本协议")。<strong>特别是涉及免除或者限制责任的条款、法律适用和争议解决条款等,请您重点阅读。</strong>
</p>
<h2>1. 服务说明</h2>
<p>1.1 Airhub Team以下简称"我们"向用户提供包括但不限于设备连接控制、AI 语音交互、角色记忆存储、云端同步等服务(以下简称"本服务")。</p>
<p>1.2 本服务的具体内容由我们根据实际情况提供,我们有权随时变更、中断或终止部分或全部服务。</p>
<p>1.3 用户理解并同意,本服务仅供用户个人非商业性质的使用。用户不得利用本服务进行销售或其他商业用途。</p>
<h2>2. 账号注册与使用</h2>
<p>2.1 用户在使用本服务时需要注册一个 Airhub 账号。用户应保证注册信息的真实性、准确性和完整性。</p>
<p>2.2 用户有责任妥善保管注册账号信息及密码安全。因用户保管不善可能导致账号被盗及其后果,由用户自行承担。</p>
<p>2.3 如发现任何未经授权使用您账号登录、使用本服务的情况,您应立即通知我们。您理解我们对您的任何请求采取行动需要合理时间,我们对在采取行动前已经产生的后果不承担责任。</p>
<h2>3. 用户行为规范</h2>
<p>用户在使用本服务过程中,应当遵守法律法规,不得从事下列行为:</p>
<ul>
<li>发布、传送、传播、储存危害国家安全、破坏社会稳定、违反公序良俗的内容;</li>
<li>发布、传送、传播、储存侮辱、诽谤、淫秽、暴力、赌博等违法违规内容;</li>
<li>利用 AI 功能生成虚假信息、诈骗信息或用于非法用途;</li>
<li>对 AI 角色进行性骚扰、辱骂或诱导生成不当内容;</li>
<li>进行任何危害计算机网络安全的行为,包括但不限于攻击、侵入他人系统。</li>
</ul>
<h2>4. 个人信息保护</h2>
<p>4.1 保护用户个人信息是我们的基本原则。我们将按照本协议及《隐私政策》的规定收集、使用、存储和分享您的个人信息。</p>
<p>4.2 您在注册账号或使用本服务的过程中,可能需要填写一些必要的信息。若国家法律法规有特殊规定的,您需要填写真实的身份信息。若您填写的信息不完整,则无法使用本服务或在使用过程中受到限制。</p>
<h2>5. AI 服务特别说明</h2>
<p>5.1 本产品提供的 AI 交互功能基于深度学习模型。AI 生成的内容(包括语音、文本)具有随机性,不代表我们的立场或观点。</p>
<p>5.2 角色记忆功能存储的数据为您的个人数字资产,我们会采取严格的加密措施进行保护。</p>
<p>5.3 您知悉并同意由于技术的局限性AI 生成的内容可能存在错误或不准确,您应自行判断其真实性与可靠性。</p>
<h2>6. 知识产权</h2>
<p>6.1 我们在本服务中提供的内容(包括但不限于软件、技术、程序、网页、文字、图片、音频、视频、图表、版面设计、电子文档等)的知识产权属于我们所有。</p>
<p>6.2 未经我们明确书面同意,您不得对上述内容进行复制、修改、出租、出借、出售、分发或创建衍生作品。</p>
<h2>7. 免责声明</h2>
<p>7.1 鉴于网络服务的特殊性,我们不保证服务不会中断,对服务的及时性、安全性、准确性也不作保证。</p>
<p>7.2 对于因不可抗力或我们不能控制的原因造成的网络服务中断或其它缺陷,我们不承担任何责任,但将尽力减少因此而给用户造成的损失和影响。</p>
<h2>8. 协议的变更</h2>
<p>我们要根据互联网的发展和法律法规的变化,在必要时修改本协议的条款。您可以在相关服务页面查阅最新版本的协议条款。本协议条款变更后,如果您继续使用本服务,即视为您已接受修改后的协议。</p>
<p style="margin-top: 40px; color: #9CA3AF; font-size: 13px; text-align: center;">更新日期2025年1月15日</p>
</main>
</div>
</body>
</html>

65
app.js Normal file
View File

@ -0,0 +1,65 @@
/**
* Airhub App - Main JavaScript
* Premium IoT Device Control Center
*/
// ========================================
// App State
// ========================================
const AppState = {
currentPage: 'home',
isConnecting: false,
connectedDevice: null,
};
// ========================================
// Page Navigation
// ========================================
function navigateTo(pageId) {
const pages = document.querySelectorAll('.page');
pages.forEach(page => page.classList.remove('active'));
const targetPage = document.getElementById(`page-${pageId}`);
if (targetPage) {
targetPage.classList.add('active');
AppState.currentPage = pageId;
}
}
// ========================================
// Connect Button Handler
// ========================================
function handleConnect() {
if (AppState.isConnecting) return;
AppState.isConnecting = true;
const btn = document.getElementById('connect-btn');
const btnText = btn.querySelector('.btn-text');
// Add brief feedback before navigating
btn.style.pointerEvents = 'none';
btnText.textContent = '正在搜索...';
// Navigate to Bluetooth search page
setTimeout(() => {
window.location.href = 'bluetooth.html';
}, 300);
}
// ========================================
// Initialization
// ========================================
document.addEventListener('DOMContentLoaded', () => {
console.log('Airhub App initialized');
// Add touch feedback for mobile
const buttons = document.querySelectorAll('button');
buttons.forEach(btn => {
btn.addEventListener('touchstart', () => {
btn.style.transform = 'scale(0.98)';
});
btn.addEventListener('touchend', () => {
btn.style.transform = '';
});
});
});

469
bluetooth.html Normal file
View File

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

228
clean_css.py Normal file
View File

@ -0,0 +1,228 @@
import os
css_path = r"d:\Airhub\styles.css"
try:
with open(css_path, "rb") as f:
content_bytes = f.read()
content = content_bytes.decode("utf-8", errors="ignore")
# Strategy: Find the FIRST occurrence of my Settings Page injections
# Valid markers:
# "/* --- Settings Page"
# "/* --- HEADER HEIGHT CORRECTION"
markers = ["/* --- Settings Page", "/* --- HEADER HEIGHT"]
cutoff_idx = -1
for m in markers:
idx = content.find(m)
if idx != -1:
if cutoff_idx == -1 or idx < cutoff_idx:
cutoff_idx = idx
if cutoff_idx != -1:
print(f"Found marker at {cutoff_idx}. Truncating duplicate tails.")
clean_content = content[:cutoff_idx]
else:
# Fallback: Find .custom-toast and cut after
print("Markers not found. Falling back to .custom-toast.")
idx = content.find(".custom-toast.active")
if idx != -1:
end_brace = content.find("}", idx) + 1
clean_content = content[:end_brace]
else:
print("CRITICAL: No anchor found. Aborting to avoid data loss.")
exit(1)
# V5 CSS - The Definitive Version
new_css = """
/* --- Settings Page (V5 - Modals & Fixes) --- */
.settings-view {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
/* Warm Capybara Theme Background */
background: linear-gradient(135deg, #FEF5EC 0%, #FDF2F8 100%);
z-index: 2000;
display: flex; flex-direction: column;
animation: slideUp 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
/* Header - Matching Main Page Header Height */
.settings-header {
background: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border-bottom: none !important;
/* Match main page: padding-top: calc(safe-area + 48px) */
padding-top: calc(env(safe-area-inset-top, 20px) + 48px) !important;
padding-bottom: 16px !important;
position: absolute !important;
top: 0; left: 0; right: 0;
z-index: 9999;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding-left: 20px !important;
padding-right: 20px !important;
}
.settings-title {
font-size: 16px !important;
font-weight: 600 !important;
color: #1F2937 !important;
}
/* Content Area - Gradient Fading Mask */
.settings-content {
flex: 1;
overflow-y: auto;
/* Header is about safe-area + 48px + 16px bottom padding + 44px content = ~108px total */
padding-top: calc(env(safe-area-inset-top, 20px) + 100px) !important;
padding-left: 20px; padding-right: 20px; padding-bottom: 100px;
background: transparent;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 80px, black 110px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 80px, black 110px, black 100%);
pointer-events: auto !important;
position: relative;
z-index: 1;
}
.settings-content::-webkit-scrollbar { display: none; }
/* Settings Group */
.settings-group-title {
font-size: 12px; color: #8B5E3C;
margin-bottom: 8px; margin-left: 16px; margin-top: 24px;
font-weight: 500;
}
.settings-group {
background: rgba(255, 255, 255, 0.8);
border-radius: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(139, 94, 60, 0.04);
overflow: hidden;
}
.settings-item {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; min-height: 48px;
background: transparent;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.settings-item:last-child { border-bottom: none; }
.settings-item.clickable { cursor: pointer; }
.settings-item.clickable:active { background: rgba(255,255,255,0.5); }
/* Typography */
.item-label { font-size: 15px; color: #4B5563; font-weight: 400; }
.item-value { font-size: 14px; color: #9CA3AF; display: flex; align-items: center; gap: 6px; }
.arrow { font-size: 18px; color: #D1D5DB; }
.item-text-col { display: flex; flex-direction: column; justify-content: center; gap: 4px; }
.item-desc { font-size: 11px; color: #9CA3AF; line-height: 1.3; }
.settings-item.warning .item-label { color: #EF4444; }
/* Toggle */
.toggle-switch {
width: 50px; height: 30px;
background: #E5E7EB; border-radius: 15px;
position: relative; cursor: pointer; transition: background 0.3s;
}
.toggle-switch.active { background: #FFB088; }
.toggle-knob {
width: 26px; height: 26px; background: white; border-radius: 50%;
position: absolute; top: 2px; left: 2px;
transition: transform 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.toggle-switch.active .toggle-knob { transform: translateX(20px); }
/* Volume Slider */
.settings-item.column { flex-direction: column; align-items: stretch; gap: 12px; padding-bottom: 16px; }
.volume-row { display: flex; align-items: center; width: 100%; height: 32px; }
.volume-slider {
flex: 1; -webkit-appearance: none; appearance: none;
height: 24px; margin: 0 8px; background: transparent; cursor: grab;
width: 100%;
}
.volume-slider::-webkit-slider-runnable-track { width: 100%; height: 4px; background: #E5E5EA; border-radius: 2px; }
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; height: 24px; width: 24px;
background: white; border-radius: 50%; box-shadow: 0 2px 6px rgba(139,94,60,0.2);
margin-top: -10px; /* (4-24)/2 = -10 */
}
.vol-icon { font-size: 16px; color: #9CA3AF; width: 24px; text-align: center; }
/* --- MODALS (FIXED Z-INDEX) --- */
.modal-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
z-index: 2200 !important; /* Must be > settings-view (2000) */
display: none;
justify-content: center; align-items: center;
}
.modal-overlay.active {
display: flex !important;
animation: fadeIn 0.2s ease forwards;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.glass-modal {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px; padding: 24px;
width: 80%; max-width: 320px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
text-align: center;
transform: scale(1);
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
position: relative;
z-index: 2210;
pointer-events: auto;
}
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1F2937; }
.modal-desc { font-size: 14px; color: #6B7280; margin-bottom: 24px; line-height: 1.5; }
.modal-actions { display: flex; gap: 12px; justify-content: center; }
.modal-btn {
flex: 1; padding: 12px 0; border-radius: 12px; border: none; font-size: 16px; font-weight: 500; cursor: pointer;
position: relative;
z-index: 2220;
}
.modal-btn.cancel { background: #F3F4F6; color: #6B7280; }
.modal-btn.confirm { background: linear-gradient(135deg, #FFB088 0%, #FF8E53 100%); color: white; }
.modal-btn.danger { background: #EF4444; color: white; }
.modal-btn.secondary { background: #F3F4F6; color: #4B5563; }
.modal-input {
width: 100%; padding: 12px; margin-bottom: 20px; box-sizing: border-box;
border: 1px solid #E5E7EB; border-radius: 12px;
font-size: 16px; background: #F9FAFB; outline: none;
}
.modal-input:focus { border-color: #FFB088; background: white; }
/* Unbind Styles */
.unbind-header { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 12px; }
.warn-icon svg { width: 24px; color: #EF4444; }
.unbind-title { font-size: 18px; font-weight: 600; color: #1F2937; }
.highlight-text { color: #EF4444; font-weight: 600; }
"""
final_content = clean_content + "\n" + new_css
with open(css_path, "w", encoding="utf-8") as f:
f.write(final_content)
print(f"Success: Repaired CSS. Original size: {len(content)}, New size: {len(final_content)}")
except Exception as e:
print(f"Error: {e}")

28
cleanup_css.py Normal file
View File

@ -0,0 +1,28 @@
import re
# Read the file
with open('d:/Airhub/styles.css', 'r', encoding='utf-8') as f:
content = f.read()
original_size = len(content)
# 1. Replace multiple \r with single \r
content = re.sub(r'\r+', '\r', content)
# 2. Remove consecutive blank lines (keep at most 1 blank line)
content = re.sub(r'(\r?\n){3,}', '\n\n', content)
# 3. Remove trailing whitespace on each line
content = re.sub(r'[ \t]+(\r?\n)', r'\1', content)
# 4. Ensure file ends with single newline
content = content.rstrip() + '\n'
# Write back
with open('d:/Airhub/styles.css', 'w', encoding='utf-8') as f:
f.write(content)
new_size = len(content)
print(f"Original size: {original_size} bytes")
print(f"New size: {new_size} bytes")
print(f"Reduced by: {original_size - new_size} bytes ({(1 - new_size/original_size)*100:.1f}%)")

217
collection-list.html Normal file
View File

@ -0,0 +1,217 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 24px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
color: #374151;
line-height: 1.6;
font-size: 15px;
position: relative;
z-index: 2;
scrollbar-width: none;
/* Firefox */
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
.info-card {
background: rgba(255, 255, 255, 0.7);
padding: 20px;
border-radius: 16px;
margin-bottom: 16px;
border: 1px solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(10px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02);
}
.info-title {
font-weight: 700;
color: #1F2937;
margin-bottom: 12px;
font-size: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.info-title::before {
content: '';
width: 4px;
height: 16px;
background: linear-gradient(to bottom, #ECCFA8, #C99672);
border-radius: 2px;
display: block;
}
.info-row {
display: flex;
flex-direction: column;
margin-bottom: 12px;
font-size: 14px;
border-bottom: 1px dashed rgba(0, 0, 0, 0.05);
padding-bottom: 10px;
}
.info-row:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.info-label {
color: #6B7280;
font-size: 12px;
margin-bottom: 4px;
}
.info-val {
color: #374151;
font-weight: 500;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">个人信息收集清单</span>
<div style="width: 44px;"></div>
</header>
<main class="content-scroll">
<p style="margin-bottom: 20px; font-size: 13px; color: #6B7280;">为了向您提供 Airhub
的核心服务,我们需要收集以下类型的个人信息。我们将严格遵守法律法规,保护您的个人信息安全。</p>
<div class="info-card">
<div class="info-title">基础功能服务</div>
<div class="info-row">
<span class="info-label">收集信息类型</span>
<span class="info-val">手机号码、登录密码</span>
</div>
<div class="info-row">
<span class="info-label">使用目的</span>
<span class="info-val">用于账号注册、登录、找回密码及身份认证</span>
</div>
<div class="info-row">
<span class="info-label">收集场景</span>
<span class="info-val">用户注册或登录 APP 时</span>
</div>
</div>
<div class="info-card">
<div class="info-title">硬件连接与控制</div>
<div class="info-row">
<span class="info-label">收集信息类型</span>
<span class="info-val">Wi-Fi信息(SSID/BSSID)、蓝牙信息、设备序列号(SN)、MAC地址</span>
</div>
<div class="info-row">
<span class="info-label">使用目的</span>
<span class="info-val">用于发现附近设备、建立蓝牙/Wi-Fi连接、设备配网及固件升级</span>
</div>
<div class="info-row">
<span class="info-label">收集场景</span>
<span class="info-val">绑定设备、连接设备或使用设备控制功能时</span>
</div>
</div>
<div class="info-card">
<div class="info-title">AI 语音交互业务</div>
<div class="info-row">
<span class="info-label">收集信息类型</span>
<span class="info-val">语音录音、对话文本、交互时间</span>
</div>
<div class="info-row">
<span class="info-label">使用目的</span>
<span class="info-val">将语音转换为文本以理解指令、生成 AI 回复、优化语音识别模型</span>
</div>
<div class="info-row">
<span class="info-label">收集场景</span>
<span class="info-val">使用语音对话功能与 AI 角色互动时</span>
</div>
</div>
<div class="info-card">
<div class="info-title">应用安全保障</div>
<div class="info-row">
<span class="info-label">收集信息类型</span>
<span class="info-val">设备IMSI/IMEI、Android ID、IP地址、操作日志</span>
</div>
<div class="info-row">
<span class="info-label">使用目的</span>
<span class="info-val">风控验证、安全防范、故障排查与分析</span>
</div>
<div class="info-row">
<span class="info-label">收集场景</span>
<span class="info-val">APP 运行期间(包括后台运行)</span>
</div>
</div>
<p style="margin-top: 20px; color: #9CA3AF; font-size: 12px; text-align: center;">更新日期2025年1月15日</p>
</main>
</div>
</body>
</html>

99
design_system.md Normal file
View File

@ -0,0 +1,99 @@
# Airhub 设计规范 (视觉标准) 🎨
> [!IMPORTANT]
> 本文档严格区分 **App 全局规范****产品专属规范**
> 开发人员 **必须** 在应用样式前确认当前页面所属的上下文。
---
## 1. App 全局规范 (科技主题) 🌐
**适用范围**: `index.html` (首页), `bluetooth.html` (蓝牙搜索), `wifi-config.html` (配网)
**核心风格**: 未来感、洁净、科技、蓝紫色调。
### 首要行动点 (Global Primary Action)
用途:主连接按钮、配网流程的“下一步”。
- **渐变色**: `linear-gradient(90deg, #22D3EE 0%, #3B82F6 35%, #6366F1 65%, #8B5CF6 100%)`
*(青 -> 蓝 -> 靛 -> 紫)*
- **阴影**: `box-shadow: 0 0 15px rgba(34, 211, 238, 0.35)`
### 导航与头部
- **背景**: 透明或深度磨砂玻璃 (Glassmorphism)。
- **图标**: 细描边风格,冷灰色 `#6B7280`
---
## 2. 产品专属规范:毛绒机芯 (Capybara) 🐹
**适用范围**: `device-control.html` (设备控), `products.html` (卡片), 故事生成弹窗。
**核心风格**: 温暖、毛绒感、舒适、杏褐色调。
> [!NOTE]
> 所有与控制“毛绒机芯”设备相关的页面/弹窗,必须强制遵循此主题。
### 首要行动点 (Capybara Context)
用途:“创作新故事”、“开始生成”、设备页内的主要交互。
- **渐变色**: `linear-gradient(135deg, #ECCFA8 0%, #C99672 100%)`
*(浅杏色 -> 暖褐色)*
- **阴影**: `box-shadow: 0 0 15px rgba(201, 150, 114, 0.35), 0 0 30px rgba(201, 150, 114, 0.25)` (复合光晕)
- **边框**: 无 (依靠阴影和光感定义边界)
### 背景色系统
- **设备页底色**: `#FDF9F3` (暖沙色 Warm Sand) - **禁止使用纯白或渐变**
- **卡片背景**: `#FFFFFF` (奶白色) 配柔和阴影。
- **选中高亮**: `#FFF7ED` (极淡的橙色叠底)。
### 字体颜色 (Capybara Context)
- **标题**: `#4B2404` (黑巧棕)。
- **正文**: `#4B5563` (暖灰)。
### 选中交互模式 (Apple Style) 🍏
- **操作方式**: 直接点击卡片。
- **指示器**: **右上角复选徽章 (Checkmark Badge)**
- 样式:杏褐色渐变实心圆 + 白色对钩。
- **状态**: 摒弃“已选列表栏”,视觉反馈完全集中在卡片本体上。
---
## 3. 开发执行规则 📏
1. **确认上下文**: 拿到设计稿先判断:这是 App 公共页面(用科技蓝)还是某个具体产品的页面(用产品色)?
2. **按钮混用禁令**:
- 公共页 -> 必须用科技渐变 (Tech Gradient)。
- 卡皮巴拉页 -> 必须用毛绒渐变 (Plush Gradient)。
- 一致性: 严禁混搭。卡皮巴拉页面里绝不应该出现“科技蓝”的按钮,除非是系统级的报错弹窗。
---
### 2.1 设置页规范 (Settings Page - Overlay) ⚙️
**交互模式**: 全屏覆盖层 (Full-screen Overlay),从底部滑入。
#### 核心布局 (Layout & Metrics)
- **容器层级**: `z-index: 2000` (覆盖此时的主页)。
- **背景**: 暖色渐变 `linear-gradient(135deg, #FEF5EC 0%, #FDF2F8 100%)`
- **进出动画**: `slideUp 0.3s cubic-bezier(0.2, 0.8, 0.2, 1)`
#### 头部规范 (Header)
为了与主页保持视觉一致性,头部必须遵循以下严格计算:
- **定位**: `position: absolute` (悬浮于内容之上)。
- **高度**: 自然高度 (Auto),不固定。
- **上边距 (Padding-Top)**: `calc(env(safe-area-inset-top, 20px) + 48px)` —— **严格对齐首页**
- **底边距 (Padding-Bottom)**: `16px`
- **左右边距**: `20px`
#### 内容区域 (Content)
- **遮罩效果**: 顶部使用渐变遮罩 (Gradient Mask) 实现内容在头部下方平滑消失,而非简单的背景色遮挡。
- `mask-image: linear-gradient(to bottom, transparent 0, transparent 80px, black 110px, ...)`
- **上边距**: `calc(env(safe-area-inset-top) + 100px)` (确保内容不被头部遮挡)。
#### 弹窗规范 (Modals) 🚨
设置页内的二级操作(改名、解绑)必须使用以下弹窗样式:
- **层级**: `z-index: 2200` (必须高于设置页)。
- **背景**: `rgba(255, 255, 255, 0.95)` (高透明度)。
- **动画**: 必须添加 `forwards` 属性防止闪退。
- `animation: popIn 0.3s ... forwards`
- **交互**:
- 必须设置 `pointer-events: auto`
- 必须设置 `position: relative` 和独立 `z-index` 给内容元素。
- **点击阻断**: item 点击事件必须加 `event.stopPropagation()` 防止冒泡。
#### 组件样式
- **开关 (Toggle)**: 激活色 `#FFB088` (暖橙)。
- **滑动条 (Slider)**: 纯白滑块 + 投影,轨道背景 `#E5E5EA`

1113
device-control.html Normal file

File diff suppressed because it is too large Load Diff

47
find_css.py Normal file
View File

@ -0,0 +1,47 @@
import re
css_path = r"d:\Airhub\styles.css"
try:
with open(css_path, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
# regex for .dc-header { ... } (simplified)
# Matches .classname { ... } roughly
def find_block(name, text):
print(f"--- Searching for {name} ---")
idx = text.find(name)
if idx == -1:
print("Not found.")
return
# Find opening brace
open_brace = text.find("{", idx)
if open_brace == -1:
print("No opening brace.")
return
# Find closing brace (count nesting)
close_brace = -1
balance = 0
for i in range(open_brace, len(text)):
if text[i] == '{':
balance += 1
elif text[i] == '}':
balance -= 1
if balance == 0:
close_brace = i
break
if close_brace != -1:
print(text[idx:close_brace+1])
else:
print("Block not closed properly.")
find_block(".dc-header", content)
find_block(".glass-modal", content)
find_block(".status-pill", content)
except Exception as e:
print(e)

198
fix_css.py Normal file
View File

@ -0,0 +1,198 @@
import os
css_path = r"d:\Airhub\styles.css"
try:
with open(css_path, "rb") as f:
content_bytes = f.read()
content = content_bytes.decode("utf-8", errors="ignore")
# Cutoff at .custom-toast.active
idx = content.find(".custom-toast.active")
if idx == -1:
print("Could not find anchor point.")
exit(1)
cutoff_idx = content.find("}", idx)
if cutoff_idx == -1: cutoff_idx = idx + 100
else: cutoff_idx += 1
clean_content = content[:cutoff_idx]
new_css = """
/* --- Settings Page (V5 - Modals & Fixes) --- */
.settings-view {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
/* Warm Capybara Theme Background */
background: linear-gradient(135deg, #FEF5EC 0%, #FDF2F8 100%);
z-index: 2000;
display: flex; flex-direction: column;
animation: slideUp 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);
overflow: hidden;
}
@keyframes slideUp { from { transform: translateY(100%); } to { transform: translateY(0); } }
/* Header - Transparent & Gradient Mask Logic */
.settings-header {
background: transparent !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
border-bottom: none !important;
height: 44px !important;
padding-top: max(32px, env(safe-area-inset-top, 20px)) !important;
position: absolute !important;
top: 0; left: 0; right: 0;
z-index: 9999;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding-left: 16px !important;
padding-right: 16px !important;
}
.settings-title {
font-size: 16px !important;
font-weight: 600 !important;
color: #1F2937 !important;
}
/* Content Area - Gradient Fading Mask */
.settings-content {
flex: 1;
overflow-y: auto;
padding-top: calc(max(32px, env(safe-area-inset-top, 20px)) + 50px) !important;
padding-left: 20px; padding-right: 20px; padding-bottom: 100px;
background: transparent;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 60px, black 90px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 60px, black 90px, black 100%);
}
.settings-content::-webkit-scrollbar { display: none; }
/* Settings Group */
.settings-group-title {
font-size: 12px; color: #8B5E3C;
margin-bottom: 8px; margin-left: 16px; margin-top: 24px;
font-weight: 500;
}
.settings-group {
background: rgba(255, 255, 255, 0.8);
border-radius: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 16px rgba(139, 94, 60, 0.04);
overflow: hidden;
}
.settings-item {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 16px; min-height: 48px;
background: transparent;
border-bottom: 1px solid rgba(0,0,0,0.05);
}
.settings-item:last-child { border-bottom: none; }
.settings-item.clickable { cursor: pointer; }
.settings-item.clickable:active { background: rgba(255,255,255,0.5); }
/* Typography */
.item-label { font-size: 15px; color: #4B5563; font-weight: 400; }
.item-value { font-size: 14px; color: #9CA3AF; display: flex; align-items: center; gap: 6px; }
.arrow { font-size: 18px; color: #D1D5DB; }
.item-text-col { display: flex; flex-direction: column; justify-content: center; gap: 4px; }
.item-desc { font-size: 11px; color: #9CA3AF; line-height: 1.3; }
.settings-item.warning .item-label { color: #EF4444; }
/* Toggle */
.toggle-switch {
width: 50px; height: 30px;
background: #E5E7EB; border-radius: 15px;
position: relative; cursor: pointer; transition: background 0.3s;
}
.toggle-switch.active { background: #FFB088; }
.toggle-knob {
width: 26px; height: 26px; background: white; border-radius: 50%;
position: absolute; top: 2px; left: 2px;
transition: transform 0.3s; box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.toggle-switch.active .toggle-knob { transform: translateX(20px); }
/* Volume Slider */
.settings-item.column { flex-direction: column; align-items: stretch; gap: 12px; padding-bottom: 16px; }
.volume-row { display: flex; align-items: center; width: 100%; height: 32px; }
.volume-slider {
flex: 1; -webkit-appearance: none; appearance: none;
height: 24px; margin: 0 8px; background: transparent; cursor: grab;
width: 100%;
}
.volume-slider::-webkit-slider-runnable-track { width: 100%; height: 4px; background: #E5E5EA; border-radius: 2px; }
.volume-slider::-webkit-slider-thumb {
-webkit-appearance: none; height: 24px; width: 24px;
background: white; border-radius: 50%; box-shadow: 0 2px 6px rgba(139,94,60,0.2);
margin-top: -10px; /* (4-24)/2 = -10 */
}
.vol-icon { font-size: 16px; color: #9CA3AF; width: 24px; text-align: center; }
/* --- MODALS (FIXED Z-INDEX) --- */
.modal-overlay {
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
z-index: 2200 !important; /* Must be > settings-view (2000) */
display: none;
justify-content: center; align-items: center;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.glass-modal {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px; padding: 24px;
width: 80%; max-width: 320px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
text-align: center;
transform: scale(1);
animation: popIn 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes popIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.modal-title { font-size: 18px; font-weight: 600; margin-bottom: 16px; color: #1F2937; }
.modal-desc { font-size: 14px; color: #6B7280; margin-bottom: 24px; line-height: 1.5; }
.modal-actions { display: flex; gap: 12px; justify-content: center; }
.modal-btn {
flex: 1; padding: 12px 0; border-radius: 12px; border: none; font-size: 16px; font-weight: 500; cursor: pointer;
}
.modal-btn.cancel { background: #F3F4F6; color: #6B7280; }
.modal-btn.confirm { background: linear-gradient(135deg, #FFB088 0%, #FF8E53 100%); color: white; }
.modal-btn.danger { background: #EF4444; color: white; }
.modal-btn.secondary { background: #F3F4F6; color: #4B5563; }
.modal-input {
width: 100%; padding: 12px; margin-bottom: 20px; box-sizing: border-box;
border: 1px solid #E5E7EB; border-radius: 12px;
font-size: 16px; background: #F9FAFB; outline: none;
}
.modal-input:focus { border-color: #FFB088; background: white; }
/* Unbind Styles */
.unbind-header { display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 12px; }
.warn-icon svg { width: 24px; color: #EF4444; }
.unbind-title { font-size: 18px; font-weight: 600; color: #1F2937; }
.highlight-text { color: #EF4444; font-weight: 600; }
"""
final_content = clean_content + "\n" + new_css
with open(css_path, "w", encoding="utf-8") as f:
f.write(final_content)
print("Success: CSS Repaired with Modals")
except Exception as e:
print(f"Error: {e}")

188
guide-feeding.html Normal file
View File

@ -0,0 +1,188 @@
<!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">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 20px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 2;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
}
/* 像素风说明书卡片 */
.manual-card {
background: #FFF;
border-radius: 24px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
border: 2px solid #F3F4F6;
}
.manual-hero {
display: flex;
justify-content: center;
margin-bottom: 24px;
}
.pixel-art-img {
width: 128px;
height: 128px;
image-rendering: pixelated;
filter: drop-shadow(0 8px 16px rgba(139, 94, 60, 0.15));
}
.manual-section {
margin-bottom: 32px;
}
.manual-h2 {
font-family: 'Press Start 2P', monospace;
font-size: 14px;
color: #8B5E3C;
margin-bottom: 16px;
line-height: 1.6;
display: flex;
align-items: center;
gap: 10px;
}
.manual-h2::before {
content: '';
display: block;
width: 8px;
height: 8px;
background: #8B5E3C;
box-shadow: -2px 2px 0 #E6B98D;
}
.manual-p {
font-size: 15px;
color: #4B5563;
line-height: 1.7;
margin-bottom: 12px;
}
.highlight-box {
background: #FFF7ED;
border-left: 4px solid #F97316;
padding: 12px 16px;
border-radius: 0 8px 8px 0;
margin: 16px 0;
font-size: 14px;
color: #9A3412;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
<div class="gradient-layer layer-2"></div>
</div>
<!-- Header -->
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<span class="page-title">喂养指南</span>
<div style="width: 44px;"></div>
</header>
<!-- Main Content -->
<main class="content-scroll">
<div class="manual-card">
<div class="manual-hero">
<!-- 使用之前生成的像素画 -->
<img src="pixel_capybara_eating_guide_1770187625762.png" class="pixel-art-img"
alt="Eating Capybara">
</div>
<div class="manual-section">
<div class="manual-h2">如何喂食你的电子宠物?</div>
<div class="manual-p">当你的毛绒机芯显示“饿了”的图标时,它需要补充能量!</div>
<div class="manual-p">1. 打开 APP 首页,点击右下角的 <strong>[能量]</strong> 按钮。</div>
<div class="manual-p">2. 从列表中选择它喜欢的食物(胡萝卜、西瓜或干草饼干)。</div>
<div class="manual-p">3. 点击“投喂”,观察它的反应!</div>
<div class="highlight-box">
💡 <strong>小贴士:</strong> 不同的食物会增加不同的心情值哦!西瓜会让它超级开心。
</div>
</div>
<div class="manual-section">
<div class="manual-h2">心情与成长</div>
<div class="manual-p">保持饱腹感可以提升心情值。心情值越高,它的互动反应就越丰富。</div>
<div class="manual-p">如果你连续 3 天忘记喂食,它可能会变得懒洋洋的,不愿理人哦... 💤</div>
</div>
<div class="manual-section">
<div class="manual-h2">特殊互动</div>
<div class="manual-p">在喂食的时候,试着抚摸它的头(在屏幕上滑动),它会发出满意的咕噜声!</div>
</div>
</div>
</main>
</div>
</body>
</html>

371
help.html Normal file
View File

@ -0,0 +1,371 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.help-content {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 20px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 2;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.help-content::-webkit-scrollbar {
display: none;
}
/* 标题区 */
.help-header {
text-align: center;
margin-bottom: 24px;
}
.help-title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
margin-bottom: 8px;
}
.help-date {
font-size: 13px;
color: #9CA3AF;
}
/* 指南卡片 */
.guide-card {
display: flex;
align-items: center;
gap: 16px;
background: linear-gradient(135deg, #FEF9E7 0%, #FDF2E9 100%);
border-radius: 16px;
padding: 16px 20px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.1);
}
.guide-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #ECCFA8 0%, #C99672 100%);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.guide-info {
flex: 1;
}
.guide-title {
font-size: 16px;
font-weight: 600;
color: #1F2937;
margin-bottom: 4px;
}
.guide-desc {
font-size: 13px;
color: #6B7280;
}
.guide-btn {
background: linear-gradient(135deg, #ECCFA8 0%, #C99672 100%);
border: none;
padding: 10px 20px;
border-radius: 20px;
color: white;
font-weight: 600;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.guide-btn:active {
transform: scale(0.95);
}
/* FAQ 分类 */
.faq-section {
margin-bottom: 20px;
}
.faq-category {
font-size: 14px;
font-weight: 500;
color: #9CA3AF;
padding: 0 4px 8px;
margin-bottom: 0;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: none;
}
.faq-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.08);
}
.faq-item {
background: transparent;
backdrop-filter: none;
border-radius: 0;
margin-bottom: 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
box-shadow: none;
}
.faq-item:last-child {
border-bottom: none;
}
.faq-question {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
cursor: pointer;
transition: background 0.2s;
}
.faq-question:active {
background: rgba(255, 255, 255, 0.5);
}
.faq-q-text {
font-size: 15px;
color: #1F2937;
flex: 1;
}
.faq-arrow {
color: #D1D5DB;
font-size: 18px;
transition: transform 0.3s;
}
.faq-item.open .faq-arrow {
transform: rotate(90deg);
}
.faq-answer {
padding: 0 20px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.faq-item.open .faq-answer {
max-height: 200px;
padding: 0 20px 16px;
}
.faq-a-text {
font-size: 14px;
color: #6B7280;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">帮助中心</span>
<div style="width: 44px;"></div>
</header>
<main class="help-content">
<div class="help-header">
<div class="help-title">帮助 Q&A</div>
<div class="help-date">更新日期2025年1月15日</div>
</div>
<!-- 喂养指南 -->
<div class="guide-card">
<div class="guide-icon">📖</div>
<div class="guide-info">
<div class="guide-title">喂养指南</div>
<div class="guide-desc">详细的角色养成方法和日常照顾指南</div>
</div>
<button class="guide-btn" onclick="openGuide()">
查看 →
</button>
</div>
<!-- FAQ: 设备连接 -->
<div class="faq-section">
<div class="faq-category">设备连接与管理</div>
<div class="faq-card">
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">手机连接设备时"未扫描到设备"</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">请检查设备是否在配网模式下双击设备电源键按钮直至呈现Wi-Fi图标请确保设备和手机距离在10m内点击【重新扫描】。</p>
</div>
</div>
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">手机连接设备时"连接设备失败"</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">可能为服务超时造成的异常,请保持设备处于配网模式下,点击【再试一次】。</p>
</div>
</div>
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">如何添加多个 Wi-Fi 网络?</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">进入设备控制页 → 设置 → 配置网络,按提示添加备用网络。设备会自动切换到信号最强的网络。</p>
</div>
</div>
</div>
</div>
<!-- FAQ: 角色养成 -->
<div class="faq-section">
<div class="faq-category">角色养成</div>
<div class="faq-card">
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">什么是角色记忆?</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">角色记忆是您与 AI 互动过程中产生的人格数据,包含对话风格、喜好偏好等信息。角色记忆可以在不同设备间迁移,让您的 AI 伙伴始终如一。
</p>
</div>
</div>
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">如何将角色记忆迁移到新设备?</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">进入「我的」→「角色记忆」,找到需要迁移的记忆,点击「注入设备」,选择目标设备即可完成迁移。</p>
</div>
</div>
</div>
</div>
<!-- FAQ: 常见问题 -->
<div class="faq-section">
<div class="faq-category">常见问题</div>
<div class="faq-card">
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">设备离线怎么办?</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">请检查设备电源和网络连接。如果问题持续,尝试重启设备或重新配网。</p>
</div>
</div>
<div class="faq-item" onclick="toggleFaq(this)">
<div class="faq-question">
<span class="faq-q-text">如何联系客服?</span>
<span class="faq-arrow"></span>
</div>
<div class="faq-answer">
<p class="faq-a-text">您可以通过「我的」→「意见反馈」联系我们,或发送邮件至 support@airhub.com。</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function openGuide() {
window.location.href = 'guide-feeding.html';
}
function toggleFaq(item) {
// 关闭其他打开的项
document.querySelectorAll('.faq-item.open').forEach(el => {
if (el !== item) el.classList.remove('open');
});
// 切换当前项
item.classList.toggle('open');
}
</script>
</body>
</html>

57
index.html Normal file
View File

@ -0,0 +1,57 @@
<!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">
<title>Airhub</title>
<meta name="description" content="Airhub - 智能硬件连接中心">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
<div class="gradient-layer layer-2"></div>
<div class="gradient-layer layer-3"></div>
</div>
<!-- 首页 -->
<div class="page home-page active" id="page-home">
<!-- 顶部 Logo - 像素风格 -->
<header class="home-header">
<div class="pixel-logo" id="airhub-logo">
<span class="pixel-text">Airhub</span>
<div class="pixel-glow"></div>
</div>
</header>
<!-- 中心吉祥物 -->
<main class="home-content">
<div class="mascot-container">
<img src="首页吉祥物.png" alt="Airhub Spirit" class="mascot" id="mascot">
<div class="mascot-glow"></div>
</div>
</main>
<!-- 底部按钮 -->
<footer class="home-footer">
<button class="connect-btn" id="connect-btn" onclick="handleConnect()">
<span class="btn-text">立即连接</span>
<span class="btn-shine"></span>
</button>
</footer>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

754
login.html Normal file
View File

@ -0,0 +1,754 @@
<!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>
<meta name="description" content="Airhub - 登录">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<style>
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
/* 登录页容器 */
.login-container {
min-height: 100vh;
display: flex;
flex-direction: column;
position: relative;
z-index: 1;
}
/* 顶部区域 - Logo */
.login-top {
padding-top: calc(env(safe-area-inset-top, 20px) + 60px);
text-align: center;
}
.login-logo {
font-family: 'Press Start 2P', cursive;
font-size: 26px;
color: #4B2E83;
text-shadow:
0 2px 10px rgba(139, 92, 246, 0.3),
0 0 40px rgba(139, 92, 246, 0.15);
letter-spacing: 2px;
}
/* 中间区域 - 吉祥物 */
.login-middle {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-mascot {
width: 220px;
height: 220px;
animation: gentleFloat 5s ease-in-out infinite;
}
.login-mascot img {
width: 100%;
height: 100%;
object-fit: contain;
filter: drop-shadow(0 20px 40px rgba(139, 92, 246, 0.25));
}
@keyframes gentleFloat {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
}
/* 底部区域 - 表单 */
.login-bottom {
padding: 0 32px;
padding-bottom: calc(env(safe-area-inset-bottom, 20px) + 40px);
}
/* 主按钮 */
.login-btn-primary {
width: 100%;
height: 56px;
border: none;
border-radius: 28px;
font-size: 17px;
font-weight: 600;
color: white;
cursor: pointer;
position: relative;
overflow: hidden;
background: linear-gradient(90deg, #22D3EE 0%, #3B82F6 35%, #6366F1 65%, #8B5CF6 100%);
box-shadow:
0 4px 20px rgba(99, 102, 241, 0.4),
0 0 40px rgba(139, 92, 246, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: transform 0.2s, box-shadow 0.2s;
}
.login-btn-primary:active {
transform: scale(0.98);
box-shadow:
0 2px 15px rgba(99, 102, 241, 0.3),
0 0 30px rgba(139, 92, 246, 0.15);
}
.login-btn-primary::after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
animation: btnShine 3s ease-in-out infinite;
}
@keyframes btnShine {
0% {
left: -100%;
}
50%,
100% {
left: 100%;
}
}
/* 次要链接 */
.login-link {
display: block;
text-align: center;
margin-top: 20px;
font-size: 14px;
color: rgba(75, 46, 131, 0.7);
text-decoration: none;
transition: color 0.2s;
}
.login-link:active {
color: rgba(75, 46, 131, 1);
}
/* 协议区域 */
.login-agreement {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 10px;
margin-top: 28px;
}
.agreement-check {
width: 18px;
height: 18px;
border: 1.5px solid rgba(75, 46, 131, 0.3);
border-radius: 5px;
background: rgba(255, 255, 255, 0.6);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
transition: all 0.2s;
}
.agreement-check.checked {
background: linear-gradient(135deg, #6366F1, #8B5CF6);
border-color: transparent;
}
.agreement-check .checkmark {
display: none;
color: white;
font-size: 11px;
font-weight: 700;
}
.agreement-check.checked .checkmark {
display: block;
}
.agreement-text {
font-size: 12px;
color: rgba(75, 46, 131, 0.6);
line-height: 1.6;
}
.agreement-text a {
color: #6366F1;
text-decoration: none;
}
.agreement-text a:active {
text-decoration: underline;
}
/* ========== 验证码登录视图 (重构版 - 更优雅) ========== */
.sms-view {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 100;
display: none;
flex-direction: column;
}
.sms-view.active {
display: flex;
}
/* 极简头部 */
.sms-header {
position: relative;
z-index: 10;
padding: 16px 24px;
/* 固定顶部留白 60px确保与蓝牙页对齐 */
padding-top: 60px !important;
}
/* 圆形返回按钮 - 磨砂质感 */
.sms-back {
width: 40px;
height: 40px;
border-radius: 50%;
border: 1px solid rgba(255, 255, 255, 0.6);
background: rgba(255, 255, 255, 0.4);
backdrop-filter: blur(12px);
color: #4B2E83;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
}
.sms-back:hover {
transform: translateY(-1px);
background: rgba(255, 255, 255, 0.6);
}
.sms-back:active {
transform: scale(0.95);
}
.sms-back svg {
width: 22px;
height: 22px;
stroke-width: 2;
}
/* 内容区 */
.sms-body {
position: relative;
z-index: 1;
flex: 1;
padding: 0 32px;
/* 调整内容对齐,稍微偏下一点 */
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-top: 60px;
padding-bottom: calc(env(safe-area-inset-bottom, 20px) + 40px);
}
.sms-heading {
font-size: 32px;
font-weight: 700;
color: #4B2E83;
margin-bottom: 12px;
letter-spacing: -0.5px;
}
.sms-subheading {
font-size: 15px;
color: rgba(75, 46, 131, 0.6);
margin-bottom: 48px;
font-weight: 400;
}
/* 优雅的输入框 */
.input-group {
margin-bottom: 24px;
}
.input-box {
position: relative;
display: flex;
align-items: center;
height: 64px;
/* 更高更舒适 */
background: rgba(255, 255, 255, 0.55);
border: 1px solid rgba(255, 255, 255, 0.8);
border-radius: 20px;
/* 大圆角 */
padding: 0 24px;
transition: all 0.3s ease;
box-shadow: 0 2px 10px rgba(139, 92, 246, 0.03);
}
.input-box:focus-within {
background: rgba(255, 255, 255, 0.9);
border-color: #8B5CF6;
box-shadow: 0 8px 25px rgba(139, 92, 246, 0.12);
transform: translateY(-2px);
}
.input-prefix {
font-size: 16px;
font-weight: 600;
color: #4B2E83;
margin-right: 16px;
padding-right: 16px;
border-right: 1px solid rgba(75, 46, 131, 0.1);
}
.input-text {
flex: 1;
border: none;
background: transparent;
outline: none;
font-size: 17px;
color: #1F2937;
caret-color: #8B5CF6;
font-weight: 500;
min-width: 0;
/* 防止溢出关键 */
}
.input-text::placeholder {
color: rgba(75, 46, 131, 0.35);
font-weight: 400;
}
/* 内嵌式验证码按钮 */
.code-send-link {
border: none;
background: none;
font-size: 14px;
/* 稍微缩小一点 */
font-weight: 600;
color: #6366F1;
padding-left: 14px;
margin-left: 10px;
border-left: 1px solid rgba(75, 46, 131, 0.1);
cursor: pointer;
white-space: nowrap;
transition: color 0.2s;
flex-shrink: 0;
/* 防止被挤压 */
}
.code-send-link:active {
opacity: 0.7;
}
.code-send-link:disabled {
color: #9CA3AF;
cursor: default;
}
/* 登录按钮 */
.sms-submit {
width: 100%;
height: 60px;
margin-top: 48px;
border: none;
border-radius: 30px;
font-size: 18px;
font-weight: 600;
color: white;
/* 使用全局统一的 Primary Gradient */
background: linear-gradient(90deg, #22D3EE 0%, #3B82F6 35%, #6366F1 65%, #8B5CF6 100%);
background-size: 200% auto;
cursor: pointer;
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.3);
transition: all 0.3s ease;
}
.sms-submit:hover {
background-position: right center;
transform: translateY(-1px);
}
.sms-submit:active {
transform: scale(0.98);
}
.sms-submit:disabled {
opacity: 0.6;
pointer-events: none;
box-shadow: none;
}
/* 协议确认弹窗 */
.agree-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
z-index: 1000;
display: none;
justify-content: center;
align-items: center;
padding: 24px;
}
.agree-modal.visible {
display: flex;
}
.agree-modal-box {
background: white;
border-radius: 20px;
padding: 28px 24px;
width: 100%;
max-width: 320px;
text-align: center;
animation: modalPop 0.25s ease;
}
@keyframes modalPop {
from {
transform: scale(0.9);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
.agree-modal-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
margin-bottom: 12px;
}
.agree-modal-text {
font-size: 14px;
color: #6B7280;
line-height: 1.6;
margin-bottom: 24px;
}
.agree-modal-text a {
color: #6366F1;
text-decoration: none;
}
.agree-modal-btns {
display: flex;
gap: 12px;
}
.agree-modal-btn {
flex: 1;
height: 48px;
border-radius: 24px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
border: none;
transition: transform 0.15s;
}
.agree-modal-btn:active {
transform: scale(0.97);
}
.agree-modal-btn.cancel {
background: #F3F4F6;
color: #6B7280;
}
.agree-modal-btn.confirm {
background: linear-gradient(90deg, #6366F1, #8B5CF6);
color: white;
}
</style>
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
<div class="gradient-layer layer-2"></div>
<div class="gradient-layer layer-3"></div>
</div>
<!-- 登录页 -->
<div class="login-container">
<!-- Logo -->
<div class="login-top">
<div class="login-logo">Airhub</div>
</div>
<!-- 吉祥物 -->
<div class="login-middle">
<div class="login-mascot">
<img src="首页吉祥物.png" alt="Airhub Spirit">
</div>
</div>
<!-- 登录表单 -->
<div class="login-bottom">
<button class="login-btn-primary" id="btnOneClick">
本机号码一键登录
</button>
<a href="#" class="login-link" id="smsLink">使用验证码登录</a>
<div class="login-agreement">
<div class="agreement-check" id="agreeCheck">
<span class="checkmark"></span>
</div>
<span class="agreement-text">
我已阅读并同意
<a href="#">《用户协议》</a><a href="#">《隐私政策》</a>
</span>
</div>
</div>
</div>
<!-- 验证码登录 -->
<div class="sms-view" id="smsView">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
<div class="gradient-layer layer-2"></div>
<!-- Layer 3 removed for cleaner look in SMS view -->
</div>
<div class="sms-header">
<button class="sms-back" id="smsBack">
<!-- SVG Arrow -->
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round"
stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
</button>
</div>
<div class="sms-body">
<h1 class="sms-heading">欢迎使用 Airhub</h1>
<p class="sms-subheading">请输入您的手机号验证登录</p>
<div class="input-group">
<div class="input-box">
<span class="input-prefix">+86</span>
<input type="tel" class="input-text" id="phoneInput" placeholder="请输入手机号" maxlength="11">
</div>
</div>
<div class="input-group">
<div class="input-box">
<input type="text" class="input-text" id="codeInput" placeholder="输入验证码" maxlength="6">
<button class="code-send-link" id="codeSend">获取验证码</button>
</div>
</div>
<button class="sms-submit" id="smsSubmit" disabled>立即登录</button>
</div>
</div>
</div>
<!-- Toast -->
<div class="toast-msg" id="toast"></div>
<!-- 协议确认弹窗 -->
<div class="agree-modal" id="agreeModal">
<div class="agree-modal-box">
<div class="agree-modal-title">服务协议</div>
<div class="agree-modal-text">
请先阅读并同意<a href="#">《用户协议》</a><a href="#">《隐私政策》</a>,以便为您提供更好的服务。
</div>
<div class="agree-modal-btns">
<button class="agree-modal-btn cancel" id="agreeCancel">再想想</button>
<button class="agree-modal-btn confirm" id="agreeConfirm">同意并继续</button>
</div>
</div>
</div>
<script>
// State
let agreed = false;
let countdown = 0;
// Elements
const btnOneClick = document.getElementById('btnOneClick');
const smsLink = document.getElementById('smsLink');
const agreeCheck = document.getElementById('agreeCheck');
const smsView = document.getElementById('smsView');
const smsBack = document.getElementById('smsBack');
const phoneInput = document.getElementById('phoneInput');
const codeInput = document.getElementById('codeInput');
const codeSend = document.getElementById('codeSend');
const smsSubmit = document.getElementById('smsSubmit');
const toast = document.getElementById('toast');
// Toast
function showToast(msg) {
toast.textContent = msg;
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 2000);
}
// Validation
function isValidPhone(p) {
return /^1[3-9]\d{9}$/.test(p);
}
function updateSubmitState() {
const valid = isValidPhone(phoneInput.value) && codeInput.value.length === 6;
smsSubmit.disabled = !valid;
}
// Agreement toggle
agreeCheck.onclick = () => {
agreed = !agreed;
agreeCheck.classList.toggle('checked', agreed);
};
// 弹窗相关
const agreeModal = document.getElementById('agreeModal');
const agreeCancel = document.getElementById('agreeCancel');
const agreeConfirm = document.getElementById('agreeConfirm');
let pendingAction = null; // 待执行的操作
function showAgreeModal(action) {
pendingAction = action;
agreeModal.classList.add('visible');
}
function hideAgreeModal() {
agreeModal.classList.remove('visible');
pendingAction = null;
}
agreeCancel.onclick = hideAgreeModal;
agreeConfirm.onclick = () => {
// 保存待执行操作
const action = pendingAction;
// 同意协议
agreed = true;
agreeCheck.classList.add('checked');
hideAgreeModal();
// 继续执行待定操作
if (action === 'oneclick') {
doOneClickLogin();
} else if (action === 'sms') {
smsView.classList.add('active');
}
};
// One-click login
function doOneClickLogin() {
showToast('正在获取本机号码...');
setTimeout(() => {
showToast('登录成功');
setTimeout(() => {
localStorage.setItem('user_token', 'token_' + Date.now());
window.location.href = 'index.html';
}, 1000);
}, 1500);
}
btnOneClick.onclick = () => {
if (!agreed) {
showAgreeModal('oneclick');
return;
}
doOneClickLogin();
};
// Switch to SMS
smsLink.onclick = (e) => {
e.preventDefault();
if (!agreed) {
showAgreeModal('sms');
return;
}
smsView.classList.add('active');
};
// Back from SMS
smsBack.onclick = () => smsView.classList.remove('active');
// Send code
codeSend.onclick = () => {
if (!isValidPhone(phoneInput.value)) {
showToast('请输入正确的手机号');
return;
}
countdown = 60;
codeSend.disabled = true;
codeSend.textContent = countdown + 's';
const t = setInterval(() => {
countdown--;
if (countdown <= 0) {
clearInterval(t);
codeSend.disabled = false;
codeSend.textContent = '获取验证码';
} else {
codeSend.textContent = countdown + 's';
}
}, 1000);
showToast('验证码已发送');
};
// Input listeners
phoneInput.oninput = updateSubmitState;
codeInput.oninput = updateSubmitState;
// SMS Submit
smsSubmit.onclick = () => {
if (smsSubmit.disabled) return;
smsSubmit.textContent = '登录中...';
smsSubmit.disabled = true;
setTimeout(() => {
showToast('登录成功');
localStorage.setItem('user_token', 'token_' + Date.now());
setTimeout(() => window.location.href = 'index.html', 1000);
}, 1500);
};
// Auto redirect if logged in (开发阶段已注释,上线时取消注释)
// if (localStorage.getItem('user_token')) {
// window.location.href = 'index.html';
// }
</script>
</body>
</html>

399
notifications.html Normal file
View File

@ -0,0 +1,399 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
/* Gradient Mask Scroll Standard */
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 20px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
position: relative;
z-index: 2;
scrollbar-width: none;
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
}
/* Notification List Styles */
.notification-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-bottom: 40px;
}
/* 通知卡片容器 */
.notification-item {
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.4);
overflow: hidden;
transition: all 0.3s ease;
}
.notification-item.open {
background: rgba(255, 255, 255, 0.85);
}
/* 通知卡片头部(可点击) */
.notification-header {
display: flex;
gap: 14px;
padding: 16px;
cursor: pointer;
transition: background 0.2s;
}
.notification-header:active {
background: rgba(255, 255, 255, 0.9);
}
.notif-icon-box {
width: 40px;
height: 40px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
}
.notif-icon-box.system {
background: #EFF6FF;
color: #3B82F6;
}
.notif-icon-box.activity {
background: #FFF7ED;
color: #F97316;
}
.notif-content {
flex: 1;
}
.notif-header-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.notif-title {
font-size: 15px;
font-weight: 600;
color: #1F2937;
}
.notif-time {
font-size: 12px;
color: #9CA3AF;
}
.notif-desc {
font-size: 13px;
color: #6B7280;
line-height: 1.5;
}
.notif-arrow {
color: #D1D5DB;
font-size: 18px;
transition: transform 0.3s ease;
display: flex;
align-items: center;
}
.notification-item.open .notif-arrow {
transform: rotate(90deg);
}
.notif-dot {
width: 8px;
height: 8px;
background: #EF4444;
border-radius: 50%;
margin-top: 6px;
display: none;
}
.notification-item.unread .notif-dot {
display: block;
}
/* 展开的详情区域 */
.notification-detail {
max-height: 0;
overflow: hidden;
transition: max-height 0.35s ease, padding 0.35s ease;
background: rgba(249, 250, 251, 0.5);
border-top: 0 solid rgba(0, 0, 0, 0.05);
}
.notification-item.open .notification-detail {
max-height: 600px;
border-top-width: 1px;
}
.detail-content {
padding: 0 20px;
font-size: 14px;
color: #374151;
line-height: 1.7;
}
.notification-item.open .detail-content {
padding: 20px;
}
.detail-content p {
margin-bottom: 14px;
}
.detail-content strong {
color: #111827;
font-weight: 600;
}
.detail-content ul {
margin-bottom: 14px;
padding-left: 20px;
}
.detail-content li {
margin-bottom: 6px;
list-style-type: none;
position: relative;
}
.detail-content li::before {
content: "•";
color: #FFB088;
font-weight: bold;
display: inline-block;
width: 1em;
margin-left: -1em;
}
.detail-content img {
max-width: 100%;
border-radius: 12px;
margin: 8px 0 16px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08);
}
</style>
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
<div class="gradient-layer layer-2"></div>
</div>
<!-- Header -->
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<span class="page-title">消息通知</span>
<div style="width: 44px;"></div>
</header>
<!-- Main Content -->
<main class="content-scroll">
<div class="notification-list">
<!-- 消息 1 (未读) -->
<div class="notification-item unread" id="notif-1">
<div class="notification-header" onclick="toggleNotif(1)">
<div class="notif-icon-box system">
<svg width="20" height="20" 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">
</path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<div class="notif-content">
<div class="notif-header-row">
<span class="notif-title">系统更新</span>
<span class="notif-time">10:30</span>
</div>
<div class="notif-desc">Airhub V1.2.0 版本更新已准备就绪</div>
</div>
<span class="notif-arrow"></span>
<div class="notif-dot"></div>
</div>
<div class="notification-detail">
<div class="detail-content">
<p><strong>Airhub V1.2.0 版本更新说明:</strong></p>
<ul>
<li>新增<strong>"喂养指南"</strong>功能,现在您可以查看详细的电子宠物养成手册了。</li>
<li>优化了设备连接的稳定性,修复了部分机型搜索不到设备的问题。</li>
<li>提升了整体界面的流畅度,增加了更多微交互动画。</li>
</ul>
<p>建议您连接 Wi-Fi 后进行更新,以获得最佳体验。</p>
</div>
</div>
</div>
<!-- 消息 2 -->
<div class="notification-item" id="notif-2">
<div class="notification-header" onclick="toggleNotif(2)">
<div class="notif-icon-box activity">
<span style="font-size: 18px;">🎁</span>
</div>
<div class="notif-content">
<div class="notif-header-row">
<span class="notif-title">新春活动</span>
<span class="notif-time">昨天</span>
</div>
<div class="notif-desc">领取您的新春限定水豚皮肤"招财进宝"</div>
</div>
<span class="notif-arrow"></span>
</div>
<div class="notification-detail">
<div class="detail-content">
<p>🎉 <strong>新春限定皮肤上线啦!</strong></p>
<p>为了庆祝即将到来的春节,我们特别推出了水豚的"招财进宝"限定皮肤。</p>
<img src="https://images.unsplash.com/photo-1549608276-5786777e6587?q=80&w=600&auto=format&fit=crop"
alt="Event Image">
<p><strong>活动亮点:</strong></p>
<ul>
<li>限定版红色唐装外观</li>
<li>专属的春节互动音效</li>
<li>限时免费领取的节庆道具</li>
</ul>
<p><strong>活动截止日期:</strong> 2月15日</p>
</div>
</div>
</div>
<!-- 消息 3 -->
<div class="notification-item" id="notif-3">
<div class="notification-header" onclick="toggleNotif(3)">
<div class="notif-icon-box system">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
</div>
<div class="notif-content">
<div class="notif-header-row">
<span class="notif-title">新设备绑定</span>
<span class="notif-time">1月20日</span>
</div>
<div class="notif-desc">您的新设备"Airhub_5G"已成功绑定</div>
</div>
<span class="notif-arrow"></span>
</div>
<div class="notification-detail">
<div class="detail-content">
<p>恭喜!您已成功绑定新设备 <strong>Airhub_5G</strong></p>
<p>接下来的几步可以帮助您快速上手:</p>
<ul>
<li>前往<strong>角色记忆</strong>页面,注入您喜欢的角色人格。</li>
<li>进入设置页面配置您的偏好设置。</li>
<li>查看帮助中心的入门指南,解锁更多互动玩法。</li>
</ul>
<p>祝您开启一段奇妙的 AI 陪伴旅程!</p>
</div>
</div>
</div>
</div>
</main>
</div>
<script>
function toggleNotif(id) {
const item = document.getElementById('notif-' + id);
const wasOpen = item.classList.contains('open');
// 关闭其他打开的项 (可选:保持手风琴效果)
document.querySelectorAll('.notification-item.open').forEach(el => {
if (el !== item) el.classList.remove('open');
});
// 切换当前项
item.classList.toggle('open');
// 标记已读
if (item.classList.contains('unread')) {
item.classList.remove('unread');
}
// 如果是展开操作,平滑滚动到该卡片顶部
if (!wasOpen) {
setTimeout(() => {
item.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 50);
}
}
</script>
</body>
</html>

162
preview.html Normal file
View File

@ -0,0 +1,162 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Airhub - iPhone 16 预览</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
min-height: 100vh;
background: #000000;
display: flex;
justify-content: center;
align-items: center;
padding: 40px 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.device-label {
color: white;
font-size: 18px;
font-weight: 500;
opacity: 0.9;
}
/* iPhone 16 Frame */
.iphone-frame {
position: relative;
width: 423px;
/* 393 + 30 for frame padding */
height: 892px;
/* 852 + 40 for frame padding */
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.4),
inset 0 0 0 2px rgba(255, 255, 255, 0.1);
}
.iphone-frame::before {
/* Dynamic Island */
content: '';
position: absolute;
top: 18px;
left: 50%;
transform: translateX(-50%);
width: 126px;
height: 37px;
background: #000;
border-radius: 20px;
z-index: 100;
}
.iphone-screen {
width: 393px;
height: 852px;
border-radius: 42px;
overflow: hidden;
background: #fff;
position: relative;
}
.iphone-screen iframe {
width: 100%;
height: 100%;
border: none;
}
/* Device selector */
.device-selector {
display: flex;
gap: 12px;
}
.device-btn {
padding: 10px 20px;
border: 2px solid rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
color: white;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
}
.device-btn:hover,
.device-btn.active {
background: rgba(255, 255, 255, 0.25);
border-color: white;
}
.instructions {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
text-align: center;
max-width: 400px;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="preview-container">
<div class="device-label">📱 iPhone 16 (393 × 852)</div>
<div class="device-selector">
<button class="device-btn active" onclick="setDevice(393, 852, 'iPhone 16')">iPhone 16</button>
<button class="device-btn" onclick="setDevice(402, 874, 'iPhone 16 Pro')">iPhone 16 Pro</button>
<button class="device-btn" onclick="setDevice(440, 956, 'iPhone 16 Pro Max')">Pro Max</button>
</div>
<div class="iphone-frame" id="phone-frame">
<div class="iphone-screen" id="phone-screen">
<iframe src="index.html" id="app-frame"></iframe>
</div>
</div>
<p class="instructions">
💡 这是 iPhone 真实尺寸预览。<br>
修改代码后刷新此页面即可看到更新效果。
</p>
</div>
<script>
function setDevice(width, height, name) {
const screen = document.getElementById('phone-screen');
const frame = document.getElementById('phone-frame');
const label = document.querySelector('.device-label');
screen.style.width = width + 'px';
screen.style.height = height + 'px';
frame.style.width = (width + 30) + 'px';
frame.style.height = (height + 40) + 'px';
label.textContent = '📱 ' + name + ' (' + width + ' × ' + height + ')';
// Update active button
document.querySelectorAll('.device-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Reload iframe
document.getElementById('app-frame').src = 'index.html';
}
</script>
</body>
</html>

167
privacy.html Normal file
View File

@ -0,0 +1,167 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 24px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
color: #374151;
line-height: 1.6;
font-size: 15px;
position: relative;
z-index: 2;
scrollbar-width: none;
/* Firefox */
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
h2 {
font-size: 17px;
margin: 32px 0 12px;
color: #1F2937;
font-weight: 700;
}
h2:first-child {
margin-top: 0;
}
p {
margin-bottom: 16px;
text-align: justify;
}
ul {
margin-bottom: 16px;
padding-left: 20px;
}
li {
margin-bottom: 8px;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">隐私政策</span>
<div style="width: 44px;"></div>
</header>
<main class="content-scroll">
<p>Airhub (以下简称"我们")非常重视您的隐私。本隐私政策(以下简称"本政策")旨在向您说明我们在您使用 Airhub 产品及服务时如何收集、使用、保存、共享和转让您的个人信息,以及您所享有的相关权利。
</p>
<p><strong>请您在使用我们的服务前,仔细阅读并了解本政策。</strong></p>
<h2>1. 我们如何收集您的个人信息</h2>
<p>为了向您提供优质的服务,我们会按照合法、正当、必要的原则收集您的信息:</p>
<ul>
<li><strong>账号注册信息:</strong> 当您注册 Airhub 账号时,我们会收集您的手机号码或电子邮箱地址,用于验证身份及为您提供服务。</li>
<li><strong>设备连接信息:</strong> 当您使用 Airhub 硬件设备时,我们会收集设备的 MAC 地址、SN 序列号、固件版本、IP 地址、Wi-Fi
信号强度等信息,以便实现设备连接、控制及固件升级功能。</li>
<li><strong>语音交互数据:</strong> 当您使用语音功能与 AI 角色互动时,我们会收集您的语音指令及对话内容。这些数据将用于生成 AI 回复并优化模型效果。您可以选择不保留历史对话记录。
</li>
<li><strong>角色记忆数据:</strong> 您的 AI 角色养成数据(如亲密度、性格标签、记忆库)存储于云端,以便支持跨设备无缝迁移体验。</li>
<li><strong>日志信息:</strong> 为保障服务安全及运行稳定,我们会收集您的操作日志、错误日志等。</li>
</ul>
<h2>2. 我们如何使用您的个人信息</h2>
<p>我们将收集的信息用于以下用途:</p>
<ul>
<li><strong>提供各项服务:</strong> 包括设备配网、远程控制、AI 语音对话等核心功能。</li>
<li><strong>产品优化:</strong> 分析用户使用习惯,改善产品功能和用户体验。</li>
<li><strong>安全保障:</strong> 监测账号异常状态,防范欺诈风险,保障系统安全。</li>
<li><strong>个性化推荐:</strong> 基于您的角色记忆,为您提供更符合您偏好的 AI 个性化回复。</li>
</ul>
<h2>3. 信息的共享、转让与公开披露</h2>
<p>3.1 <strong>共享:</strong> 我们不会向任何第三方共享您的个人信息,但以下情况除外:</p>
<ul>
<li>获得您的明确同意;</li>
<li>为了实现核心功能需要与合作伙伴(如云服务提供商、语音识别技术提供商)共享必要信息;</li>
<li>法律法规规定的情形。</li>
</ul>
<p>3.2 <strong>转让:</strong> 我们不会将您的个人信息转让给任何第三方,除非发生合并、收购或破产清算,我们将要求受让方继续受本政策约束。</p>
<h2>4. 信息的存储与保护</h2>
<p>4.1 <strong>存储地点:</strong> 我们依照法律法规的规定,将收集的个人信息存储于中华人民共和国境内。</p>
<p>4.2 <strong>存储期限:</strong> 我们仅在实现服务目的所必需的时间内保留您的个人信息。账号注销后,我们将对您的个人信息进行删除或匿名化处理。</p>
<p>4.3 <strong>安全措施:</strong> 我们采用 SSL 加密传输、AES 数据加密存储、严格的访问权限控制等技术措施保护您的信息安全。</p>
<h2>5. 您的权利</h2>
<p>5.1 <strong>访问与更正:</strong> 您有权登录 APP 查阅或修改您的个人信息。</p>
<p>5.2 <strong>删除:</strong> 您可以通过【我的-设置-账号安全】申请注销账号。注销后,我们将删除您的所有数据且不可恢复。</p>
<p>5.3 <strong>撤回同意:</strong> 您可以通过设备系统设置关闭相关权限(如麦克风权限),撤回您的授权。</p>
<h2>6. 联系我们</h2>
<p>如您对本隐私政策有任何疑问或投诉,请发送邮件至 privacy@airhub.com 联系我们。</p>
<p style="margin-top: 40px; color: #9CA3AF; font-size: 13px; text-align: center;">更新日期2025年1月15日</p>
</main>
</div>
</body>
</html>

405
products.html Normal file
View File

@ -0,0 +1,405 @@
<!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;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -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>

338
profile-info.html Normal file
View File

@ -0,0 +1,338 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.save-btn {
background: linear-gradient(135deg, #ECCFA8 0%, #C99672 100%);
border: none;
padding: 10px 20px;
border-radius: 20px;
color: white;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.save-btn:active {
transform: scale(0.95);
}
.save-btn:disabled {
opacity: 0.5;
}
.profile-content {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 20px 40px;
display: flex;
flex-direction: column;
align-items: center;
}
/* 头像区域 */
.avatar-section {
margin-bottom: 32px;
position: relative;
}
.avatar-large {
width: 100px;
height: 100px;
border-radius: 50px;
background: linear-gradient(135deg, #FFECD2, #FCB69F);
overflow: hidden;
box-shadow: 0 8px 24px rgba(139, 94, 60, 0.2);
}
.avatar-large img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-edit-badge {
position: absolute;
bottom: 0;
right: 0;
width: 32px;
height: 32px;
background: linear-gradient(135deg, #ECCFA8 0%, #C99672 100%);
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
cursor: pointer;
}
.avatar-edit-badge svg {
width: 14px;
height: 14px;
color: white;
}
/* 表单 */
.form-card {
width: 100%;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.08);
}
.form-item {
display: flex;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.form-item:last-child {
border-bottom: none;
}
.form-label {
width: 80px;
font-size: 15px;
color: #6B7280;
}
.form-value {
flex: 1;
text-align: right;
font-size: 15px;
color: #1F2937;
}
.form-input {
flex: 1;
text-align: right;
font-size: 15px;
color: #1F2937;
border: none;
background: transparent;
outline: none;
}
.form-arrow {
color: #D1D5DB;
font-size: 18px;
margin-left: 8px;
}
.form-item.clickable {
cursor: pointer;
transition: background 0.2s;
}
.form-item.clickable:active {
background: rgba(255, 255, 255, 0.5);
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">个人信息</span>
<button class="save-btn" onclick="saveProfile()">保存</button>
</header>
<main class="profile-content">
<div class="avatar-section">
<div class="avatar-large">
<img src="Capybara.png" alt="Avatar" id="avatar-img">
</div>
<div class="avatar-edit-badge" onclick="changeAvatar()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z">
</path>
<circle cx="12" cy="13" r="4"></circle>
</svg>
</div>
</div>
<div class="form-card">
<div class="form-item">
<span class="form-label">昵称</span>
<input type="text" class="form-input" id="nickname" value="土豆" placeholder="请输入昵称">
</div>
<div class="form-item clickable" onclick="selectGender()">
<span class="form-label">性别</span>
<span class="form-value" id="gender"></span>
<span class="form-arrow"></span>
</div>
<div class="form-item clickable" onclick="selectBirthday()">
<span class="form-label">生日</span>
<span class="form-value" id="birthday">1994-12-09</span>
<span class="form-arrow"></span>
</div>
</div>
</main>
</div>
<!-- 性别选择弹窗 -->
<div class="modal-overlay" id="gender-modal">
<div class="glass-modal">
<div class="modal-title">选择性别</div>
<div class="modal-actions" style="flex-direction: column; gap: 8px;">
<button class="modal-btn confirm" style="flex: none;" onclick="setGender('男')"></button>
<button class="modal-btn confirm" style="flex: none;" onclick="setGender('女')"></button>
<button class="modal-btn cancel" style="flex: none; margin-top: 8px;"
onclick="closeGenderModal()">取消</button>
</div>
</div>
</div>
<!-- 通用提示弹窗 -->
<div class="modal-overlay" id="message-modal">
<div class="glass-modal">
<div class="modal-title" id="msg-title">提示</div>
<div class="modal-desc" id="msg-desc">内容</div>
<div class="modal-actions">
<button class="modal-btn confirm" onclick="closeMessageModal()">确定</button>
</div>
</div>
</div>
<!-- 输入弹窗 (用于生日) -->
<div class="modal-overlay" id="input-modal">
<div class="glass-modal">
<div class="modal-title" id="input-title">输入信息</div>
<input type="text" class="modal-input" id="input-field"
style="width: 100%; padding: 12px; border-radius: 12px; border: 1px solid rgba(0,0,0,0.1); background: rgba(255,255,255,0.8); margin: 16px 0; font-size: 16px; box-sizing: border-box;"
placeholder="">
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeInputModal()">取消</button>
<button class="modal-btn confirm" onclick="confirmInput()">确定</button>
</div>
</div>
</div>
<script>
// 通用 Modal
function showMessage(title, desc) {
document.getElementById('msg-title').innerText = title;
document.getElementById('msg-desc').innerText = desc;
document.getElementById('message-modal').classList.add('active');
}
function closeMessageModal() {
document.getElementById('message-modal').classList.remove('active');
}
function changeAvatar() {
showMessage('头像设置', '头像更换功能开发中...');
}
function selectGender() {
document.getElementById('gender-modal').classList.add('active');
}
function closeGenderModal() {
document.getElementById('gender-modal').classList.remove('active');
}
function setGender(gender) {
document.getElementById('gender').innerText = gender;
closeGenderModal();
}
// 生日输入相关
let inputCallback = null;
function selectBirthday() {
const currentVal = document.getElementById('birthday').innerText;
showInput('请输入生日 (YYYY-MM-DD)', currentVal, (val) => {
if (val) document.getElementById('birthday').innerText = val;
});
}
function showInput(title, defaultVal, callback) {
document.getElementById('input-title').innerText = title;
document.getElementById('input-field').value = defaultVal;
inputCallback = callback;
document.getElementById('input-modal').classList.add('active');
}
function closeInputModal() {
document.getElementById('input-modal').classList.remove('active');
inputCallback = null;
}
function confirmInput() {
const val = document.getElementById('input-field').value;
if (inputCallback) inputCallback(val);
closeInputModal();
}
function saveProfile() {
const nickname = document.getElementById('nickname').value;
const gender = document.getElementById('gender').innerText;
const birthday = document.getElementById('birthday').innerText;
showMessage('保存成功', `昵称: ${nickname}\n性别: ${gender}\n生日: ${birthday}`);
setTimeout(() => {
/* Optional: Auto close or navigate back */
}, 2000);
}
</script>
</body>
</html>

361
profile.html Normal file
View File

@ -0,0 +1,361 @@
<!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>
/* 复用 device-control 布局 */
.profile-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.profile-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
/* 头部按钮 - 通知铃铛 */
.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;
position: relative;
}
.icon-btn:active {
transform: scale(0.92);
background: rgba(255, 255, 255, 0.4);
}
.notification-dot {
position: absolute;
top: 10px;
right: 10px;
width: 8px;
height: 8px;
background: #EF4444;
border-radius: 50%;
border: 2px solid white;
}
/* 主内容区 */
.profile-content {
padding: calc(env(safe-area-inset-top, 20px) + 100px) 20px 140px;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
}
/* 用户信息卡片 */
.user-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
border-radius: 20px;
margin-bottom: 20px;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.08);
cursor: pointer;
transition: all 0.2s;
}
.user-card:active {
transform: scale(0.98);
background: rgba(255, 255, 255, 0.95);
}
.user-avatar {
width: 64px;
height: 64px;
border-radius: 32px;
background: linear-gradient(135deg, #ECCFA8, #C99672);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.user-info {
flex: 1;
}
.user-name {
font-size: 20px;
font-weight: 600;
color: #1F2937;
margin-bottom: 4px;
}
.user-id {
font-size: 13px;
color: #9CA3AF;
}
.arrow-icon {
color: #D1D5DB;
font-size: 20px;
}
/* 菜单列表 */
.menu-list {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.08);
}
.menu-list-item {
display: flex;
align-items: center;
padding: 18px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: background 0.2s;
}
.menu-list-item:last-child {
border-bottom: none;
}
.menu-list-item:active {
background: rgba(255, 255, 255, 0.5);
}
.menu-icon {
width: 24px;
height: 24px;
margin-right: 16px;
font-size: 20px;
}
.menu-text {
flex: 1;
font-size: 16px;
color: #1F2937;
}
.menu-arrow {
color: #D1D5DB;
font-size: 18px;
}
.menu-badge {
background: #EF4444;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
margin-right: 8px;
}
/* 底部导航 - 复用 device-control 样式 */
.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;
}
.menu-bar {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.6);
backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.8);
padding: 6px 8px;
border-radius: 32px;
width: 100%;
max-width: 320px;
box-shadow: 0 10px 30px rgba(139, 92, 246, 0.15);
}
.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;
filter: grayscale(100%) opacity(0.6);
}
.menu-item.active {
background: linear-gradient(90deg, #E6B98D 0%, #E8C9A8 35%, #D4A373 70%, #B07D5A 100%);
box-shadow: 0 4px 15px rgba(212, 163, 115, 0.4);
transform: translateY(-2px) scale(1.05);
}
.menu-item.active img {
opacity: 1;
transform: scale(1.1);
filter: brightness(0) invert(1);
}
</style>
</head>
<body>
<div class="app-container">
<!-- 动态渐变背景 -->
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<!-- Header -->
<header class="profile-header">
<div style="width: 44px;"></div>
<span class="profile-title">我的</span>
<button class="icon-btn" title="通知" onclick="location.href='notifications.html'">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
<span class="notification-dot"></span>
</button>
</header>
<!-- Main Content -->
<main class="profile-content">
<!-- 用户信息卡片 -->
<div class="user-card" onclick="location.href='profile-info.html'">
<div class="user-avatar">
<img src="Capybara.png" alt="Avatar">
</div>
<div class="user-info">
<div class="user-name">土豆</div>
<div class="user-id">ID: 138****3069</div>
</div>
<span class="arrow-icon"></span>
</div>
<!-- 功能菜单 -->
<div class="menu-list">
<div class="menu-list-item" onclick="location.href='agent-manage.html'">
<span class="menu-icon">🧠</span>
<span class="menu-text">角色记忆</span>
<span class="menu-arrow"></span>
</div>
<div class="menu-list-item" onclick="location.href='products.html'">
<span class="menu-icon">📦</span>
<span class="menu-text">我的设备</span>
<span class="menu-arrow"></span>
</div>
<div class="menu-list-item" onclick="location.href='settings.html'">
<span class="menu-icon">⚙️</span>
<span class="menu-text">设置</span>
<span class="menu-badge">NEW</span>
<span class="menu-arrow"></span>
</div>
<div class="menu-list-item" onclick="openFeedback()">
<span class="menu-icon">💬</span>
<span class="menu-text">意见反馈</span>
<span class="menu-arrow"></span>
</div>
<div class="menu-list-item" onclick="location.href='help.html'">
<span class="menu-icon"></span>
<span class="menu-text">帮助中心</span>
<span class="menu-arrow"></span>
</div>
</div>
</main>
<!-- 底部导航 -->
<footer class="dc-footer">
<nav class="menu-bar">
<div class="menu-item" onclick="location.href='device-control.html'">
<img src="icons/icon-home-capybara.svg" alt="Home">
</div>
<div class="menu-item" onclick="location.href='device-control.html?tab=story'">
<img src="icons/icon-story-pixel.svg" alt="Story">
</div>
<div class="menu-item" onclick="location.href='device-control.html?tab=music'">
<img src="icons/icon-music-pixel.svg" alt="Music">
</div>
<div class="menu-item active">
<img src="icons/icon-user-pixel.svg" alt="User">
</div>
</nav>
</footer>
</div>
<!-- 反馈弹窗 -->
<div class="modal-overlay" id="feedback-modal">
<div class="glass-modal">
<div class="modal-title">意见反馈</div>
<textarea class="modal-input" id="feedback-input" placeholder="请输入您的意见或建议..."
style="min-height: 100px; resize: none;"></textarea>
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeFeedback()">取消</button>
<button class="modal-btn confirm" onclick="submitFeedback()">提交</button>
</div>
</div>
</div>
<script>
function openFeedback() {
document.getElementById('feedback-modal').classList.add('active');
}
function closeFeedback() {
document.getElementById('feedback-modal').classList.remove('active');
}
function submitFeedback() {
const feedback = document.getElementById('feedback-input').value;
if (feedback.trim()) {
alert('感谢您的反馈!');
closeFeedback();
document.getElementById('feedback-input').value = '';
}
}
</script>
</body>
</html>

357
settings.html Normal file
View File

@ -0,0 +1,357 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.settings-content {
padding: calc(env(safe-area-inset-top, 20px) + 100px) 20px 40px;
overflow-y: auto;
height: 100%;
box-sizing: border-box;
}
.settings-section {
margin-bottom: 24px;
}
.section-title {
font-size: 13px;
color: #9CA3AF;
padding: 0 4px 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.settings-card {
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(20px);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 20px rgba(139, 94, 60, 0.08);
}
.settings-item {
display: flex;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: background 0.2s;
}
.settings-item:last-child {
border-bottom: none;
}
.settings-item:active {
background: rgba(255, 255, 255, 0.5);
}
.settings-item-icon {
width: 24px;
margin-right: 12px;
font-size: 18px;
}
.settings-item-text {
flex: 1;
font-size: 16px;
color: #1F2937;
}
.settings-item-value {
font-size: 14px;
color: #9CA3AF;
margin-right: 8px;
}
.settings-item-arrow {
color: #D1D5DB;
font-size: 18px;
}
/* 危险操作 */
.settings-item.danger .settings-item-text {
color: #EF4444;
}
.settings-item.danger .settings-item-icon {
color: #EF4444;
}
/* 版本信息 */
.version-info {
text-align: center;
padding: 32px 20px;
color: #9CA3AF;
font-size: 13px;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">设置</span>
<div style="width: 44px;"></div>
</header>
<main class="settings-content">
<!-- 账号安全 -->
<div class="settings-section">
<div class="section-title">账号安全</div>
<div class="settings-card">
<div class="settings-item" onclick="showPhone()">
<span class="settings-item-icon">📱</span>
<span class="settings-item-text">绑定手机</span>
<span class="settings-item-value">138****3069</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="changePassword()">
<span class="settings-item-icon">🔐</span>
<span class="settings-item-text">账号密码</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="location.href='products.html'">
<span class="settings-item-icon">📦</span>
<span class="settings-item-text">设备管理</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="toggleNotification()">
<span class="settings-item-icon">🔔</span>
<span class="settings-item-text">推送通知权限</span>
<span class="settings-item-value" id="notif-status">已开启</span>
<span class="settings-item-arrow"></span>
</div>
</div>
</div>
<!-- 关于 -->
<div class="settings-section">
<div class="section-title">关于</div>
<div class="settings-card">
<div class="settings-item" onclick="checkUpdate()">
<span class="settings-item-icon">🔄</span>
<span class="settings-item-text">检查更新</span>
<span class="settings-item-value">当前最新 1.0.0</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="showHardwareInfo()">
<span class="settings-item-icon">💻</span>
<span class="settings-item-text">硬件信息</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="showAgreement()">
<span class="settings-item-icon">📄</span>
<span class="settings-item-text">用户协议</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="showPrivacy()">
<span class="settings-item-icon">🔒</span>
<span class="settings-item-text">隐私政策</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="showDataCollection()">
<span class="settings-item-icon">📋</span>
<span class="settings-item-text">个人信息收集清单</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item" onclick="showThirdParty()">
<span class="settings-item-icon">🔗</span>
<span class="settings-item-text">第三方信息共享清单</span>
<span class="settings-item-arrow"></span>
</div>
</div>
</div>
<!-- 账号操作 -->
<div class="settings-section">
<div class="settings-card">
<div class="settings-item danger" onclick="logout()">
<span class="settings-item-icon">🚪</span>
<span class="settings-item-text">退出登录</span>
<span class="settings-item-arrow"></span>
</div>
<div class="settings-item danger" onclick="deleteAccount()">
<span class="settings-item-icon">⚠️</span>
<span class="settings-item-text">账号注销</span>
<span class="settings-item-arrow"></span>
</div>
</div>
</div>
<div class="version-info">
Airhub v1.0.0<br>
© 2025 Airhub Team
</div>
</main>
</div>
<!-- 退出登录确认 -->
<div class="modal-overlay" id="logout-modal">
<div class="glass-modal">
<div class="modal-title">确认退出登录?</div>
<div class="modal-desc">退出后需要重新登录才能使用。</div>
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeLogoutModal()">取消</button>
<button class="modal-btn confirm" onclick="confirmLogout()">退出</button>
</div>
</div>
</div>
<!-- 通用提示弹窗 -->
<div class="modal-overlay" id="message-modal">
<div class="glass-modal">
<div class="modal-title" id="msg-title">提示</div>
<div class="modal-desc" id="msg-desc">内容</div>
<div class="modal-actions">
<button class="modal-btn confirm" onclick="closeMessageModal()">确定</button>
</div>
</div>
</div>
<!-- 账号注销确认 -->
<div class="modal-overlay" id="delete-modal">
<div class="glass-modal">
<div class="modal-title">确认注销账号?</div>
<div class="modal-desc" style="color: #EF4444;">账号注销后所有数据将被永久删除,且无法恢复。</div>
<div class="modal-actions">
<button class="modal-btn cancel" onclick="closeDeleteModal()">取消</button>
<button class="modal-btn confirm" style="background: #EF4444;" onclick="confirmDelete()">确认注销</button>
</div>
</div>
</div>
<script>
// Modal Helpers
function showMessage(title, desc) {
document.getElementById('msg-title').innerText = title;
document.getElementById('msg-desc').innerText = desc;
document.getElementById('message-modal').classList.add('active');
}
function closeMessageModal() {
document.getElementById('message-modal').classList.remove('active');
}
function showPhone() {
showMessage('绑定手机', '138****3069');
}
function changePassword() {
showMessage('提示', '密码修改功能开发中...');
}
function toggleNotification() {
const el = document.getElementById('notif-status');
const status = el.innerText === '已开启' ? '已关闭' : '已开启';
el.innerText = status;
/* showMessage('推送通知', `推送通知已${status}`); */
}
function checkUpdate() {
showMessage('检查更新', '当前已是最新版本 v1.0.0');
}
function showHardwareInfo() {
showMessage('硬件信息', '设备型号: Airhub_5G\n固件版本: 2.1.3\nMAC地址: AA:BB:CC:DD:EE:FF');
}
function showAgreement() {
location.href = 'agreement.html';
}
function showPrivacy() {
location.href = 'privacy.html';
}
function showDataCollection() {
location.href = 'collection-list.html';
}
function showThirdParty() {
location.href = 'sharing-list.html';
}
function logout() {
document.getElementById('logout-modal').classList.add('active');
}
function closeLogoutModal() {
document.getElementById('logout-modal').classList.remove('active');
}
function confirmLogout() {
closeLogoutModal();
location.href = 'login.html';
}
function deleteAccount() {
document.getElementById('delete-modal').classList.add('active');
}
function closeDeleteModal() {
document.getElementById('delete-modal').classList.remove('active');
}
function confirmDelete() {
closeDeleteModal();
showMessage('已提交', '账号注销申请已提交将在7个工作日内处理。');
}
</script>
</body>
</html>

192
sharing-list.html Normal file
View File

@ -0,0 +1,192 @@
<!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>
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
padding-top: calc(env(safe-area-inset-top, 20px) + 48px);
width: 100%;
box-sizing: border-box;
position: absolute;
top: 0;
z-index: 10;
}
.back-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;
cursor: pointer;
transition: all 0.2s;
}
.back-btn:active {
transform: scale(0.92);
}
.page-title {
font-size: 18px;
font-weight: 600;
color: #1F2937;
}
.content-scroll {
padding: calc(env(safe-area-inset-top, 20px) + 120px) 24px 40px;
overflow-y: auto;
flex: 1;
width: 100%;
height: 100%;
box-sizing: border-box;
color: #374151;
line-height: 1.6;
font-size: 15px;
position: relative;
z-index: 2;
scrollbar-width: none;
/* Firefox */
-webkit-mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
mask-image: linear-gradient(to bottom, transparent 0px, transparent 100px, black 130px, black 100%);
}
.content-scroll::-webkit-scrollbar {
display: none;
/* Chrome/Safari */
}
.share-item {
background: rgba(255, 255, 255, 0.7);
border-radius: 16px;
padding: 20px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.4);
}
.share-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
padding-bottom: 12px;
margin-bottom: 12px;
}
.share-company {
font-weight: 700;
font-size: 16px;
color: #1F2937;
margin-bottom: 2px;
}
.share-type {
font-size: 12px;
color: #B07D5A;
background: rgba(176, 125, 90, 0.1);
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
}
.share-row {
display: flex;
margin-bottom: 8px;
font-size: 13px;
line-height: 1.5;
}
.share-label {
color: #6B7280;
width: 70px;
flex-shrink: 0;
}
.share-val {
color: #374151;
flex: 1;
}
</style>
</head>
<body>
<div class="app-container">
<div class="gradient-bg">
<div class="gradient-layer layer-1"></div>
</div>
<header class="page-header">
<button class="back-btn" onclick="history.back()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M15 18l-6-6 6-6"></path>
</svg>
</button>
<span class="page-title">第三方共享清单</span>
<div style="width: 44px;"></div>
</header>
<main class="content-scroll">
<p style="margin-bottom: 20px; font-size: 13px; color: #6B7280;">为保障 Airhub
的相关功能实现与应用安全稳定运行我们可能会接入第三方提供的软件开发包SDK或服务。我们将审慎评估合作方的安全保障能力并要求其遵守严格的保密协议。</p>
<div class="share-item">
<div class="share-header">
<div class="share-company">阿里云计算有限公司</div>
<div class="share-type">阿里云对象存储/API网关 SDK</div>
</div>
<div class="share-row"><span class="share-label">使用目的</span><span
class="share-val">用于存储角色记忆数据、用户头像及语音文件提供云端API服务稳定性。</span></div>
<div class="share-row"><span class="share-label">收集数据</span><span
class="share-val">设备信息(IP地址)、网络连接状态。</span></div>
<div class="share-row"><span class="share-label">共享方式</span><span class="share-val">SDK 本机采集</span>
</div>
<div class="share-row"><span class="share-label">隐私政策</span><span
class="share-val">https://terms.aliyun.com/legal-agreement/terms</span></div>
</div>
<div class="share-item">
<div class="share-header">
<div class="share-company">深圳市腾讯计算机系统有限公司</div>
<div class="share-type">微信开放平台 SDK</div>
</div>
<div class="share-row"><span class="share-label">使用目的</span><span
class="share-val">用于支持微信账号一键登录、分享内容至微信朋友圈或会话。</span></div>
<div class="share-row"><span class="share-label">收集数据</span><span
class="share-val">设备标识信息、微信个人授权信息(昵称、头像)。</span></div>
<div class="share-row"><span class="share-label">共享方式</span><span class="share-val">SDK 本机采集</span>
</div>
<div class="share-row"><span class="share-label">隐私政策</span><span
class="share-val">https://weixin.qq.com/cgi-bin/readtemplate?t=weixin_agreement</span></div>
</div>
<div class="share-item">
<div class="share-header">
<div class="share-company">深圳市和讯华谷信息技术有限公司</div>
<div class="share-type">极光推送 (JPush) SDK</div>
</div>
<div class="share-row"><span class="share-label">使用目的</span><span
class="share-val">用于实现消息推送功能,向您发送设备状态更新通知或系统公告。</span></div>
<div class="share-row"><span class="share-label">收集数据</span><span
class="share-val">设备标识符(IMEI/MAC/Android ID/IDFA)、网络信息、应用安装列表。</span></div>
<div class="share-row"><span class="share-label">共享方式</span><span class="share-val">SDK 本机采集</span>
</div>
<div class="share-row"><span class="share-label">隐私政策</span><span
class="share-val">https://www.jiguang.cn/license/privacy</span></div>
</div>
<p style="margin-top: 20px; color: #9CA3AF; font-size: 12px; text-align: center;">更新日期2025年1月15日</p>
</main>
</div>
</body>
</html>

639
story-detail.html Normal file
View File

@ -0,0 +1,639 @@
<!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?v=6">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
/* Prevent body scroll */
background: #FDF9F3;
}
.result-container {
height: 100%;
display: flex;
flex-direction: column;
padding: var(--header-padding-top) 24px 110px;
/* Space for footer */
max-width: 600px;
margin: 0 auto;
position: relative;
}
.dc-header {
flex-shrink: 0;
/* Header stays fixed */
position: relative;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
min-height: 44px;
}
.back-btn-custom {
position: absolute;
left: 0;
background: none;
border: none;
padding: 8px;
cursor: pointer;
color: #4B5563;
}
/* Story Card becomes the Scroll Container */
.story-paper {
flex: 1;
/* Fill remaining space */
overflow-y: auto;
/* Internal Scroll */
margin-bottom: 10px;
/* Hide Scrollbar for App Feel */
scrollbar-width: none;
/* Firefox */
-ms-overflow-style: none;
/* IE/Edge */
}
/* Video Mode - Maximize Content */
.story-paper.video-mode {
padding: 0 !important;
background: transparent !important;
box-shadow: none !important;
display: flex;
align-items: center;
justify-content: center;
}
/* Webkit Scrollbar styling (Chrome/Safari) */
.story-paper::-webkit-scrollbar {
display: none;
}
/* Typography Polish */
.story-content {
font-size: 16px;
line-height: 2.0;
color: #374151;
text-align: justify;
}
.story-content p {
margin-bottom: 20px;
text-indent: 2em;
}
/* Fullscreen Button */
.fullscreen-btn {
position: absolute;
bottom: 16px;
right: 16px;
background: rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
color: white;
cursor: pointer;
z-index: 20;
/* Above Click Overlay */
backdrop-filter: blur(4px);
transition: all 0.2s;
}
.fullscreen-btn:active {
transform: scale(0.9);
background: rgba(0, 0, 0, 0.7);
}
/* In-Card Loading State */
.video-loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
color: #4B5563;
}
</style>
</head>
<body>
<!-- Remove separate loading mask, moved inside card -->
<!-- Custom Modal Overlay -->
<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>
<!-- Custom Toast -->
<div class="custom-toast" id="custom-toast">
✨ 操作成功
</div>
<!-- Main Scroll Container -->
<div class="result-container" id="main-container">
<!-- Header -->
<header class="dc-header" id="page-header">
<button class="back-btn-custom" onclick="window.location.href='device-control.html?tab=story'">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<path d="M15 18l-6-6 6-6" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<div class="header-title" style="font-weight:700; color:#4B2404; font-size: 18px;">星际忍者的茶话会</div>
</header>
<!-- Tab Switcher (Hidden by default) -->
<div class="tab-switch-container" id="content-tabs" style="display:none;">
<div class="tab-switch">
<button class="switch-btn active" id="tab-text" onclick="switchContentTab('text')">📄 故事</button>
<button class="switch-btn" id="tab-video" onclick="switchContentTab('video')">🎬 绘本</button>
</div>
</div>
<!-- Story Content Card -->
<div class="story-paper" id="story-card">
<!-- Text Content -->
<div class="story-content" id="view-text">
<p>
在遥远的银河系边缘,有一个被星云包裹的神秘茶馆。今天,这里迎来了两位特殊的客人:刚执行完火星探测任务的宇航员波波,和正在追捕暗影怪兽的忍者小次郎。
</p>
<p>
“这儿的重力好像有点不对劲?”波波飘在半空中,试图抓住飞来飞去的茶杯。小次郎则冷静地倒挂在天花板上,手里紧握着一枚手里剑——其实那是用来切月饼的。
</p>
<p>
突然,桌上的魔法茶壶“噗”地一声喷出了七彩烟雾,一只会说话的卡皮巴拉钻了出来:“别打架,别打架,喝了这杯‘银河气泡茶’,我们都是好朋友!”
</p>
<p>
于是,宇宙中最奇怪的组合诞生了。他们决定,下一站,去黑洞边缘钓星星。
</p>
</div>
<!-- Video Content (Hidden by default) -->
<div class="video-view-container" id="view-video">
<!-- Loading Wrapper -->
<div class="video-loading-state" id="video-loading-state" style="display:none;">
<div class="loader-spinner"
style="border-width: 3px; width: 40px; height: 40px; border-top-color: #F43F5E;"></div>
<div style="font-weight:600; margin-top:16px;">AI 正在绘制动态绘本...</div>
<div style="font-size:12px; color:#9CA3AF; margin-top:8px;">消耗 10 SP</div>
</div>
<!-- Video Wrapper -->
<div id="video-content-wrapper">
<video class="story-video" id="video-player" playsinline loop>
<source src="动态绘本/失控的魔法扫帚.mp4" type="video/mp4">
您的浏览器不支持视频标签。
</video>
<!-- Click Overlay -->
<div class="video-overlay" id="video-overlay">
<div class="play-btn-circle" id="overlay-icon">
<!-- Play Icon -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
</div>
</div>
<!-- Fullscreen Button -->
<button class="fullscreen-btn" id="fs-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2.5">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Generator Mode Footer (New Story) -->
<div class="generator-footer" id="footer-gen">
<div class="btn-group-2col">
<button class="btn-secondary" onclick="window.location.href='story-loading.html'">
<span>↻ 重写</span>
</button>
<button class="btn-capybara-primary" id="save-btn-magic">
<span>保存故事</span>
</button>
</div>
</div>
<!-- Read Mode Footer: Text Tab -->
<div class="generator-footer" id="footer-text-mode" style="display: none;">
<div class="btn-group-2col">
<button class="btn-secondary" id="tts-btn"
style="height: 50px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 6px;">
<span id="tts-icon" style="display: flex;">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 18v-6a9 9 0 0 1 18 0v6"></path>
<path
d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z">
</path>
</svg>
</span>
<span id="tts-text">朗读</span>
</button>
<button class="btn-capybara-primary" id="make-book-btn">
<span id="make-book-text">变绘本</span>
</button>
</div>
</div>
<!-- Read Mode Footer: Video Tab -->
<div class="generator-footer" id="footer-video-mode" style="display: none;">
<!-- Single Full Width Button for Regen -->
<button class="btn-capybara-primary" id="regen-video-btn" style="width:100%; justify-content:center;">
<span>↻ 重新生成</span>
</button>
</div>
<script>
// Mock Stories for Testing
const mockStories = [
{
title: "星际忍者的茶话会",
content: `
<p>在遥远的银河系边缘,有一个被星云包裹的神秘茶馆。今天,这里迎来了两位特殊的客人:刚执行完火星探测任务的宇航员波波,和正在追捕暗影怪兽的忍者小次郎。</p>
<p>“这儿的重力好像有点不对劲?”波波飘在半空中,试图抓住飞来飞去的茶杯。小次郎则冷静地倒挂在天花板上,手里紧握着一枚手里剑——其实那是用来切月饼的。</p>
<p>突然,桌上的魔法茶壶“噗”地一声喷出了七彩烟雾,一只会说话的卡皮巴拉钻了出来:“别打架,别打架,喝了这杯‘银河气泡茶’,我们都是好朋友!”</p>
<p>于是,宇宙中最奇怪的组合诞生了。他们决定,下一站,去黑洞边缘钓星星。</p>
`
},
{
title: "卡皮巴拉的午睡奇遇",
content: `
<p>午后的阳光洒在亚马逊河畔,一只名叫卡卡的水豚正躺在巨大的睡莲叶上打盹。它的鼻子上停着一只蓝色的蜻蜓,而它的肚子上,竟然正在举行一场蚂蚁足球赛。</p>
<p>“嘿!那是犯规!”一只穿着草裙的松鼠裁判吹响了哨子。卡卡迷迷糊糊地睁开眼,发现周围聚集了一圈森林里的小动物,连平时最害羞的树懒都慢慢爬下来围观。</p>
<p>原来,卡卡睡觉时发出的呼噜声,正好是一首完美的桑巴舞曲。小动物们忍不住跟着节奏扭动起来。卡卡打了个哈欠,心想:“既然大家都这么开心,那我再睡五分钟吧。”</p>
<p>就这样,森林里最盛大的午后舞会,在一只水豚的梦境中拉开了帷幕。</p>
`
},
{
title: "失控的魔法扫帚",
content: `
<p>魔法学院的期末考试正在进行中小女巫艾米紧张地握着她的新扫帚“光轮2026”。考试题目是平稳飞越学校的钟楼并且不撞到任何一只鸽子。</p>
<p>“起飞!”艾米念出咒语。可是,扫帚似乎有了自己的想法,它没有飞向钟楼,而是像火箭一样冲向了食堂的窗户!</p>
<p>“糟糕!那是校长的草莓蛋糕!”艾米惊呼。就在千钧一发之际,扫帚突然一个急转弯,稳稳地停在了蛋糕前——原来它只是饿了。</p>
<p>虽然考试不及格,但艾米发明了全校最快的“外卖配送术”。从此以后,魔法学院的学生们再也不用担心吃不到热乎乎的披萨了。</p>
`
}
];
let hasGeneratedVideo = false;
// --- Custom Modal Helpers ---
function showCustomConfirm(message, onConfirm) {
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 = message; // Using innerHTML for <br> support
modal.classList.add('active');
// Clean up old listeners to avoid multiple triggers
const newConfirm = confirmBtn.cloneNode(true);
const newCancel = cancelBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirm, confirmBtn);
cancelBtn.parentNode.replaceChild(newCancel, cancelBtn);
newConfirm.addEventListener('click', () => {
modal.classList.remove('active');
if (onConfirm) onConfirm();
});
newCancel.addEventListener('click', () => {
modal.classList.remove('active');
});
}
function showCustomToast(message) {
const toast = document.getElementById('custom-toast');
toast.innerText = message;
toast.classList.add('active');
setTimeout(() => {
toast.classList.remove('active');
}, 2000);
}
// Global function for tab switching
window.switchContentTab = function (type) {
const textView = document.getElementById('view-text');
const videoView = document.getElementById('view-video');
const btnText = document.getElementById('tab-text');
const btnVideo = document.getElementById('tab-video');
const video = document.getElementById('video-player');
const card = document.getElementById('story-card');
// Footer Elements
const footerText = document.getElementById('footer-text-mode');
const footerVideo = document.getElementById('footer-video-mode');
if (type === 'text') {
textView.style.display = 'block';
videoView.style.display = 'none';
btnText.classList.add('active');
btnVideo.classList.remove('active');
video.pause();
updateOverlayIcon(false);
// RESTORE TEXT STYLE: Remove video-mode class
card.classList.remove('video-mode');
// Update Button Text if video exists
if (hasGeneratedVideo) {
const makeBookText = document.getElementById('make-book-text');
makeBookText.innerText = "重新生成";
}
// Show Text Footer Logic
footerText.style.display = 'flex';
footerVideo.style.display = 'none';
} else {
textView.style.display = 'none';
videoView.style.display = 'block';
btnText.classList.remove('active');
btnVideo.classList.add('active');
// FIX: Do NOT auto-play on simple switch. Only update icon state.
updateOverlayIcon(!video.paused);
// Show Video Footer & Toggle Maximize Mode
card.classList.add('video-mode'); // Removes padding/shadow/background
footerText.style.display = 'none';
footerVideo.style.display = 'flex';
}
};
function updateOverlayIcon(isPlaying) {
const overlay = document.getElementById('video-overlay');
const icon = document.getElementById('overlay-icon');
if (isPlaying) {
overlay.classList.add('hidden');
} else {
overlay.classList.remove('hidden');
icon.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z" />
</svg>
`;
}
}
document.addEventListener('DOMContentLoaded', () => {
// Check Current Index from LocalStorage
let index = parseInt(localStorage.getItem('story_test_index') || '0');
// --- Check ID Param Override ---
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('id')) {
index = parseInt(urlParams.get('id'));
} else if (document.referrer.includes('story-loading.html')) {
// Only increment if coming from new generation and NO ID param
index = (index + 1) % mockStories.length;
localStorage.setItem('story_test_index', index);
}
// Safety check
if (index >= mockStories.length) index = 0;
const story = mockStories[index];
// Render
document.querySelector('.header-title').innerText = story.title;
const contentEl = document.querySelector('.story-content');
contentEl.innerHTML = story.content;
// --- Video Logic ---
const video = document.getElementById('video-player');
const overlay = document.getElementById('video-overlay');
// Video Click Handler
overlay.addEventListener('click', (e) => {
const videoEl = document.getElementById('video-player');
if (videoEl.paused) {
videoEl.play();
updateOverlayIcon(true);
} else {
videoEl.pause();
updateOverlayIcon(false);
}
});
video.addEventListener('click', () => {
const videoEl = document.getElementById('video-player');
if (!videoEl.paused) {
videoEl.pause();
updateOverlayIcon(false);
}
});
// Fullscreen Handler
const fsBtn = document.getElementById('fs-btn');
fsBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent toggling play/pause
const videoEl = document.getElementById('video-player');
if (videoEl.requestFullscreen) {
videoEl.requestFullscreen();
} else if (videoEl.webkitEnterFullscreen) {
videoEl.webkitEnterFullscreen(); // iOS Safari
} else if (videoEl.mozRequestFullScreen) {
videoEl.mozRequestFullScreen();
} else if (videoEl.msRequestFullscreen) {
videoEl.msRequestFullscreen();
}
});
// --- Mode Handling ---
const mode = urlParams.get('mode');
if (mode === 'read') {
// Hide Generator Footer
document.getElementById('footer-gen').style.display = 'none';
// Show Initial Text Footer
document.getElementById('footer-text-mode').style.display = 'flex';
// --- TTS Logic ---
const ttsBtn = document.getElementById('tts-btn');
const ttsIcon = document.getElementById('tts-icon');
const ttsText = document.getElementById('tts-text');
let utterance = null;
// Stop any previous speech
window.speechSynthesis.cancel();
function setPauseUI() {
// Pause Icon
ttsIcon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg>`;
ttsText.innerText = '暂停';
ttsBtn.classList.add('playing');
}
function setPlayUI() {
// Play Icon
ttsIcon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`;
ttsText.innerText = '继续';
ttsBtn.classList.remove('playing');
}
function resetTTSUI() {
// Headset Icon
ttsIcon.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>`;
ttsText.innerText = '朗读';
ttsBtn.classList.remove('playing');
}
ttsBtn.onclick = () => {
const synth = window.speechSynthesis;
if (!utterance) {
const pureText = contentEl.innerText;
utterance = new SpeechSynthesisUtterance(pureText);
utterance.lang = 'zh-CN';
utterance.rate = 1.0;
utterance.onend = () => {
utterance = null;
resetTTSUI();
};
synth.speak(utterance);
setPauseUI();
} else {
if (synth.paused) {
synth.resume();
setPauseUI();
} else {
if (synth.speaking) {
synth.pause();
setPlayUI();
} else {
utterance = null;
ttsBtn.click(); // Restart
}
}
}
};
// --- Helper: Simulate Generation ---
function runGenerationProcess() {
const loader = document.getElementById('video-loading-state');
const videoWrapper = document.getElementById('video-content-wrapper');
// Show Tabs & Switch to Video Mode
document.getElementById('content-tabs').style.display = 'flex';
hasGeneratedVideo = true;
switchContentTab('video');
// 1. Show Loading State, Hide Content
loader.style.display = 'flex';
videoWrapper.style.display = 'none';
// 2. Mock Wait
setTimeout(() => {
// 3. Swap to Content
loader.style.display = 'none';
videoWrapper.style.display = 'block';
// Auto-play ONLY on fresh generation
const video = document.getElementById('video-player');
video.play().catch(e => console.log("Autoplay failed", e));
updateOverlayIcon(true);
}, 2500);
}
// --- Make Picture Book Logic ---
const makeBookBtn = document.getElementById('make-book-btn');
makeBookBtn.onclick = () => {
// STOP TTS IF PLAYING
if (window.speechSynthesis.speaking) {
window.speechSynthesis.cancel();
resetTTSUI();
}
const actionText = hasGeneratedVideo
? "重新生成将消耗 10 SP<br>确定要继续吗?"
: "生成动态绘本将消耗 10 SP<br>确定要继续吗?";
showCustomConfirm(actionText, () => {
runGenerationProcess();
});
};
// --- Regenerate Video Logic ---
const regenBtn = document.getElementById('regen-video-btn');
regenBtn.onclick = () => {
// Force Video Pause immediately
const video = document.getElementById('video-player');
video.pause();
updateOverlayIcon(false);
showCustomConfirm("重新生成将消耗 10 SP<br>确定要继续吗?", () => {
runGenerationProcess();
});
};
} else {
// Default: Generator Mode (Magic Save)
const saveBtn = document.getElementById('save-btn-magic');
if (saveBtn) {
saveBtn.addEventListener('click', (e) => {
const card = document.querySelector('.story-paper');
if (!card) return;
const btnRect = saveBtn.getBoundingClientRect();
const cardRect = card.getBoundingClientRect();
const cardCenterX = cardRect.left + cardRect.width / 2;
const cardCenterY = cardRect.top + cardRect.height / 2;
const btnCenterX = btnRect.left + btnRect.width / 2;
const btnCenterY = btnRect.top + btnRect.height / 2;
const tx = btnCenterX - cardCenterX;
const ty = btnCenterY - cardCenterY;
card.style.setProperty('--tx', `${tx}px`);
card.style.setProperty('--ty', `${ty}px`);
card.classList.add('genie-effect');
setTimeout(() => {
window.location.href = 'device-control.html?tab=story&magic=true';
}, 800);
});
}
}
});
</script>
</body>
</html>

73
story-loading.html Normal file
View File

@ -0,0 +1,73 @@
<!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;600;700&display=swap" rel="stylesheet">
<style>
body {
background: #FDF9F3;
}
/* Force Warm Sand */
</style>
</head>
<body>
<div class="loading-container">
<!-- Center Image -->
<img src="kapi写作中.png" alt="Writing Capybara" class="kapi-writing-img">
<!-- Dynamic Text -->
<div class="loading-text" id="loading-text">构思故事中...</div>
<!-- Progress Bar -->
<div class="progress-track">
<div class="progress-fill" id="progress-fill"></div>
</div>
</div>
<script>
// Progress Logic
const texts = [
{ pct: 20, text: "正在收集灵感碎片..." },
{ pct: 50, text: "正在往故事里撒魔法粉..." },
{ pct: 80, text: "正在编制最后的魔法..." },
{ pct: 98, text: "大功告成!" }
];
let progress = 0;
const fill = document.getElementById('progress-fill');
const textEl = document.getElementById('loading-text');
const interval = setInterval(() => {
progress += 1; // 0 to 100
// Visual Updates
fill.style.width = `${progress}%`;
// Text Updates
const target = texts.find(t => t.pct === progress);
if (target) {
textEl.innerText = target.text;
textEl.style.opacity = 0;
setTimeout(() => textEl.style.opacity = 1, 100); // Fade effect
}
if (progress >= 100) {
clearInterval(interval);
// Redirect
setTimeout(() => {
window.location.href = 'story-detail.html';
}, 500);
}
}, 35); // Total time ~3.5s
</script>
</body>
</html>

4408
styles.css Normal file

File diff suppressed because it is too large Load Diff

502
wifi-config.html Normal file
View File

@ -0,0 +1,502 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Airhub - WiFi配网</title>
<link rel="stylesheet" href="styles.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<style>
/* WiFi Config specific styles */
.wifi-steps {
display: flex;
justify-content: center;
gap: 8px;
margin-bottom: 32px;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(167, 139, 250, 0.3);
transition: all 0.3s ease;
}
.step-dot.active {
background: #8B5CF6;
width: 24px;
border-radius: 4px;
}
.step-dot.completed {
background: #22C55E;
}
.wifi-icon {
width: 80px;
height: 80px;
margin-bottom: 24px;
}
.wifi-list {
width: 100%;
max-width: 320px;
display: flex;
flex-direction: column;
gap: 12px;
}
.wifi-item {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
}
.wifi-item:hover {
background: rgba(255, 255, 255, 0.95);
transform: translateY(-2px);
}
.wifi-item.selected {
border-color: #8B5CF6;
box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2);
}
.wifi-name {
flex: 1;
font-weight: 500;
color: #1F2937;
}
.wifi-signal {
color: #6B7280;
font-size: 20px;
}
.password-input {
width: 100%;
max-width: 320px;
padding: 16px 20px;
border: 1px solid rgba(255, 255, 255, 0.5);
background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px);
border-radius: 16px;
font-size: 16px;
font-family: inherit;
outline: none;
transition: all 0.2s ease;
}
.password-input:focus {
border-color: #8B5CF6;
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.15);
}
.progress-container {
width: 100%;
max-width: 280px;
text-align: center;
}
.progress-bar {
height: 6px;
background: rgba(139, 92, 246, 0.2);
border-radius: 3px;
overflow: hidden;
margin-bottom: 16px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #22D3EE, #8B5CF6);
border-radius: 3px;
width: 0%;
transition: width 0.3s ease;
}
.progress-text {
color: #6B7280;
font-size: 14px;
}
.result-icon {
font-size: 64px;
margin-bottom: 16px;
}
.result-title {
font-size: 24px;
font-weight: 600;
color: #1F2937;
margin-bottom: 8px;
}
.result-desc {
color: #6B7280;
font-size: 14px;
}
.step-content {
display: none;
flex-direction: column;
align-items: center;
width: 100%;
}
.step-content.active {
display: flex;
}
</style>
</head>
<body>
<!-- Animated 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>
<div class="page bluetooth-page active">
<header class="bt-header">
<button class="back-btn" onclick="goBack()">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M15 18L9 12L15 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
</button>
<h1 class="bt-title">WiFi配网</h1>
<div class="header-spacer"></div>
</header>
<main class="bt-content">
<!-- Step indicators -->
<div class="wifi-steps">
<div class="step-dot active" id="dot1"></div>
<div class="step-dot" id="dot2"></div>
<div class="step-dot" id="dot3"></div>
<div class="step-dot" id="dot4"></div>
</div>
<!-- Step 1: Select Network -->
<div class="step-content active" id="step1">
<svg class="wifi-icon" viewBox="0 0 24 24" fill="none">
<path d="M5 12.55a11 11 0 0 1 14.08 0" stroke="#8B5CF6" stroke-width="2" stroke-linecap="round" />
<path d="M1.42 9a16 16 0 0 1 21.16 0" stroke="#8B5CF6" stroke-width="2" stroke-linecap="round" />
<path d="M8.53 16.11a6 6 0 0 1 6.95 0" stroke="#8B5CF6" stroke-width="2" stroke-linecap="round" />
<circle cx="12" cy="20" r="1" fill="#8B5CF6" />
</svg>
<h2 style="margin-bottom: 8px; color: #1F2937;">选择WiFi网络</h2>
<p style="color: #6B7280; margin-bottom: 24px;">设备需要连接WiFi以使用AI功能</p>
<div class="wifi-list" id="wifiList">
<div class="wifi-item" onclick="selectWifi(this, 'Home_5G')">
<span class="wifi-name">Home_5G</span>
<img src="icons/wifi-4.svg" class="wifi-level-icon" alt="Strong">
</div>
<div class="wifi-item" onclick="selectWifi(this, 'Office_WiFi')">
<span class="wifi-name">Office_WiFi</span>
<img src="icons/wifi-3.svg" class="wifi-level-icon" alt="Good">
</div>
<div class="wifi-item" onclick="selectWifi(this, 'Guest_Network')">
<span class="wifi-name">Guest_Network</span>
<img src="icons/wifi-2.svg" class="wifi-level-icon" alt="Weak">
</div>
</div>
</div>
<!-- Step 2: Enter Password -->
<div class="step-content" id="step2">
<svg class="wifi-icon" viewBox="0 0 24 24" fill="none">
<rect x="3" y="11" width="18" height="11" rx="2" stroke="#8B5CF6" stroke-width="2" />
<circle cx="12" cy="16" r="1" fill="#8B5CF6" />
<path d="M7 11V7a5 5 0 0110 0v4" stroke="#8B5CF6" stroke-width="2" />
</svg>
<h2 style="margin-bottom: 8px; color: #1F2937;" id="selectedWifiName">网络名称</h2>
<p style="color: #6B7280; margin-bottom: 24px;">请输入WiFi密码</p>
<input type="password" class="password-input" id="wifiPassword" placeholder="输入密码">
</div>
<!-- Step 3: Connecting -->
<div class="step-content" id="step3">
<svg class="wifi-icon connecting-anim" viewBox="0 0 24 24" fill="none">
<path class="wifi-wave wave-3" d="M5 12.55a11 11 0 0 1 14.08 0" stroke="#8B5CF6" stroke-width="2"
stroke-linecap="round" />
<path class="wifi-wave wave-2" d="M1.42 9a16 16 0 0 1 21.16 0" stroke="#8B5CF6" stroke-width="2"
stroke-linecap="round" />
<path class="wifi-wave wave-1" d="M8.53 16.11a6 6 0 0 1 6.95 0" stroke="#8B5CF6" stroke-width="2"
stroke-linecap="round" />
<circle class="wifi-dot" cx="12" cy="20" r="1" fill="#8B5CF6" />
</svg>
<h2 style="margin-bottom: 24px; color: #1F2937;">正在配网...</h2>
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<p class="progress-text" id="progressText">正在连接WiFi...</p>
</div>
</div>
<!-- Step 4: Result -->
<div class="step-content" id="step4">
<!-- Dynamic Device Icon Container -->
<div class="success-icon-wrapper">
<img src="" alt="Device" id="successDeviceImg" class="success-device-img">
<div class="success-badge"></div>
</div>
<h2 class="result-title" id="resultTitle">配网成功!</h2>
<p class="result-desc" id="resultDesc">设备已成功连接到网络</p>
</div>
</main>
<footer class="bt-footer">
<button class="cancel-btn" id="cancelBtn" onclick="goBack()">取消</button>
<button class="connect-device-btn" id="nextBtn" onclick="nextStep()" style="display: none;">
<span class="btn-text" id="nextBtnText">下一步</span>
</button>
</footer>
</div>
<style>
.wifi-level-icon {
width: 24px;
height: 24px;
opacity: 0.8;
}
/* Connecting Animation */
.connecting-anim .wifi-wave {
stroke-opacity: 0.3;
animation: wifiPulse 1.5s infinite ease-in-out;
}
.connecting-anim .wave-1 {
animation-delay: 0.0s;
}
.connecting-anim .wave-2 {
animation-delay: 0.5s;
}
.connecting-anim .wave-3 {
animation-delay: 1.0s;
}
/* Outer wave */
@keyframes wifiPulse {
0%,
100% {
stroke-opacity: 0.3;
stroke: #8B5CF6;
}
50% {
stroke-opacity: 1;
stroke: #8B5CF6;
}
}
/* Success Icon Styles */
.success-icon-wrapper {
position: relative;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
}
.success-device-img {
width: 120px;
height: 120px;
image-rendering: pixelated;
filter: drop-shadow(0 10px 20px rgba(139, 92, 246, 0.2));
animation: floatSuccess 3s ease-in-out infinite;
}
.success-badge {
position: absolute;
bottom: -5px;
right: -5px;
width: 32px;
height: 32px;
background: #22C55E;
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 3px solid white;
box-shadow: 0 4px 10px rgba(34, 197, 94, 0.3);
animation: popIn 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}
@keyframes floatSuccess {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
@keyframes popIn {
from {
transform: scale(0);
}
to {
transform: scale(1);
}
}
</style>
<script>
let currentStep = 1;
let selectedWifi = '';
// Load device info immediately
let currentDevice = {};
try {
currentDevice = JSON.parse(localStorage.getItem('lastActiveDevice') || '{}');
console.log("Loaded device:", currentDevice);
} catch (e) { console.error(e); }
function getDeviceIcon(type) {
// Normalize type keys to match bluetooth.html
// Mappings: 'plush' -> capybara, 'badge_ai' -> badge-ai, 'badge' -> badge-basic
if (type === 'plush_core' || type === 'plush') return 'icons/pixel-capybara.svg';
if (type === 'badge_ai') return 'icons/pixel-badge-ai.svg';
if (type === 'badge_basic' || type === 'badge') return 'icons/pixel-badge-basic.svg';
return 'icons/pixel-badge-basic.svg'; // Fallback
}
function selectWifi(element, name) {
document.querySelectorAll('.wifi-item').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
selectedWifi = name;
document.getElementById('nextBtn').style.display = 'flex';
}
function nextStep() {
if (currentStep === 1 && !selectedWifi) return;
if (currentStep === 2 && !document.getElementById('wifiPassword').value) return;
currentStep++;
updateStepUI();
if (currentStep === 2) {
document.getElementById('selectedWifiName').textContent = selectedWifi;
}
if (currentStep === 3) {
startConnecting();
}
if (currentStep === 4) {
// Show device icon
const img = document.getElementById('successDeviceImg');
img.src = getDeviceIcon(currentDevice.type || 'badge_basic');
document.getElementById('nextBtnText').textContent = '进入设备';
document.getElementById('cancelBtn').style.display = 'none';
}
}
function updateStepUI() {
// Update step dots
for (let i = 1; i <= 4; i++) {
const dot = document.getElementById('dot' + i);
dot.classList.remove('active', 'completed');
if (i < currentStep) dot.classList.add('completed');
if (i === currentStep) dot.classList.add('active');
}
// Update step content
for (let i = 1; i <= 4; i++) {
const step = document.getElementById('step' + i);
step.classList.remove('active');
if (i === currentStep) step.classList.add('active');
}
// Update button text
if (currentStep === 2) {
document.getElementById('nextBtnText').textContent = '连接';
}
}
function startConnecting() {
document.getElementById('nextBtn').style.display = 'none';
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const steps = [
{ progress: 30, text: '正在连接WiFi...' },
{ progress: 60, text: '正在验证密码...' },
{ progress: 90, text: '正在同步设备...' },
{ progress: 100, text: '完成!' }
];
let stepIndex = 0;
const interval = setInterval(() => {
if (stepIndex < steps.length) {
progressFill.style.width = steps[stepIndex].progress + '%';
progressText.textContent = steps[stepIndex].text;
stepIndex++;
} else {
clearInterval(interval);
setTimeout(() => {
currentStep = 4;
// Use button-triggered transition or auto
updateStepUI();
// Setup final state manually here as well to be safe
const img = document.getElementById('successDeviceImg');
if (img) img.src = getDeviceIcon(currentDevice.type || 'badge_basic');
document.getElementById('nextBtn').style.display = 'flex';
document.getElementById('nextBtnText').textContent = '进入设备';
document.getElementById('cancelBtn').style.display = 'none';
document.getElementById('nextBtn').onclick = () => {
window.location.href = 'device-control.html?t=' + new Date().getTime();
};
}, 500);
}
}, 800);
}
function goBack() {
if (currentStep > 1) {
currentStep--;
updateStepUI();
if (currentStep === 1) {
document.getElementById('nextBtn').style.display = selectedWifi ? 'flex' : 'none';
document.getElementById('nextBtnText').textContent = '下一步';
}
} else {
window.location.href = 'bluetooth.html';
}
}
</script>
</body>
</html>

1
这里是说明.md Normal file
View File

@ -0,0 +1 @@
之后的APP文件和这个项目放在这里