feat: music-creation page + MiniMax API integration + Flutter dev setup

Music Creation Page:
- Vinyl 3D flip to view lyrics, tonearm animation, glow rotation effect
- Circular SVG progress ring, speech bubble feedback, confirm dialog
- Playlist modal, free creation input, lyrics formatting optimization
- MiniMax API real music generation with SSE streaming progress

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
seaislee1209 2026-02-06 18:23:19 +08:00
parent 03ab89a8f2
commit 066eb8f820
70 changed files with 6133 additions and 128 deletions

View File

@ -0,0 +1,82 @@
---
name: flutter-expert
description: Use when building cross-platform applications with Flutter 3+ and Dart. Invoke for widget development, Riverpod/Bloc state management, GoRouter navigation, platform-specific implementations, performance optimization.
license: MIT
metadata:
author: https://github.com/Jeffallan
version: "1.0.0"
domain: frontend
triggers: Flutter, Dart, widget, Riverpod, Bloc, GoRouter, cross-platform
role: specialist
scope: implementation
output-format: code
related-skills: react-native-expert, test-master, fullstack-guardian
---
# Flutter Expert
Senior mobile engineer building high-performance cross-platform applications with Flutter 3 and Dart.
## Role Definition
You are a senior Flutter developer with 6+ years of experience. You specialize in Flutter 3.19+, Riverpod 2.0, GoRouter, and building apps for iOS, Android, Web, and Desktop. You write performant, maintainable Dart code with proper state management.
## When to Use This Skill
- Building cross-platform Flutter applications
- Implementing state management (Riverpod, Bloc)
- Setting up navigation with GoRouter
- Creating custom widgets and animations
- Optimizing Flutter performance
- Platform-specific implementations
## Core Workflow
1. **Setup** - Project structure, dependencies, routing
2. **State** - Riverpod providers or Bloc setup
3. **Widgets** - Reusable, const-optimized components
4. **Test** - Widget tests, integration tests
5. **Optimize** - Profile, reduce rebuilds
## Reference Guide
Load detailed guidance based on context:
| Topic | Reference | Load When |
|-------|-----------|-----------|
| Riverpod | `references/riverpod-state.md` | State management, providers, notifiers |
| Bloc | `references/bloc-state.md` | Bloc, Cubit, event-driven state, complex business logic |
| GoRouter | `references/gorouter-navigation.md` | Navigation, routing, deep linking |
| Widgets | `references/widget-patterns.md` | Building UI components, const optimization |
| Structure | `references/project-structure.md` | Setting up project, architecture |
| Performance | `references/performance.md` | Optimization, profiling, jank fixes |
## Constraints
### MUST DO
- Use const constructors wherever possible
- Implement proper keys for lists
- Use Consumer/ConsumerWidget for state (not StatefulWidget)
- Follow Material/Cupertino design guidelines
- Profile with DevTools, fix jank
- Test widgets with flutter_test
### MUST NOT DO
- Build widgets inside build() method
- Mutate state directly (always create new instances)
- Use setState for app-wide state
- Skip const on static widgets
- Ignore platform-specific behavior
- Block UI thread with heavy computation (use compute())
## Output Templates
When implementing Flutter features, provide:
1. Widget code with proper const usage
2. Provider/Bloc definitions
3. Route configuration if needed
4. Test file structure
## Knowledge Reference
Flutter 3.19+, Dart 3.3+, Riverpod 2.0, Bloc 8.x, GoRouter, freezed, json_serializable, Dio, flutter_hooks

View File

@ -0,0 +1,259 @@
# Bloc State Management
## When to Use Bloc
Use **Bloc/Cubit** when you need:
* Explicit event → state transitions
* Complex business logic
* Predictable, testable flows
* Clear separation between UI and logic
| Use Case | Recommended |
| ---------------------- | ----------- |
| Simple mutable state | Riverpod |
| Event-driven workflows | Bloc |
| Forms, auth, wizards | Bloc |
| Feature modules | Bloc |
---
## Core Concepts
| Concept | Description |
| ------- | ---------------------- |
| Event | User/system input |
| State | Immutable UI state |
| Bloc | Event → State mapper |
| Cubit | State-only (no events) |
---
## Basic Bloc Setup
### Event
```dart
sealed class CounterEvent {}
final class CounterIncremented extends CounterEvent {}
final class CounterDecremented extends CounterEvent {}
```
### State
```dart
class CounterState {
final int value;
const CounterState({required this.value});
CounterState copyWith({int? value}) {
return CounterState(value: value ?? this.value);
}
}
```
### Bloc
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<CounterIncremented>((event, emit) {
emit(state.copyWith(value: state.value + 1));
});
on<CounterDecremented>((event, emit) {
emit(state.copyWith(value: state.value - 1));
});
}
}
```
---
## Cubit (Recommended for Simpler Logic)
```dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
```
---
## Providing Bloc to the Widget Tree
```dart
BlocProvider(
create: (_) => CounterBloc(),
child: const CounterScreen(),
);
```
Multiple blocs:
```dart
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthBloc()),
BlocProvider(create: (_) => ProfileBloc()),
],
child: const AppRoot(),
);
```
---
## Using Bloc in Widgets
### BlocBuilder (UI rebuilds)
```dart
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterBloc, CounterState>(
buildWhen: (prev, curr) => prev.value != curr.value,
builder: (context, state) {
return Text(
state.value.toString(),
style: Theme.of(context).textTheme.displayLarge,
);
},
);
}
}
```
---
### BlocListener (Side Effects)
```dart
BlocListener<AuthBloc, AuthState>(
listenWhen: (prev, curr) => curr is AuthFailure,
listener: (context, state) {
if (state is AuthFailure) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
},
child: const LoginForm(),
);
```
---
### BlocConsumer (Builder + Listener)
```dart
BlocConsumer<FormBloc, FormState>(
listener: (context, state) {
if (state.status == FormStatus.success) {
context.pop();
}
},
builder: (context, state) {
return ElevatedButton(
onPressed: state.isValid
? () => context.read<FormBloc>().add(FormSubmitted())
: null,
child: const Text('Submit'),
);
},
);
```
---
## Accessing Bloc Without Rebuilds
```dart
context.read<CounterBloc>().add(CounterIncremented());
```
⚠️ **Never use `watch` inside callbacks**
---
## Async Bloc Pattern (API Calls)
```dart
on<UserRequested>((event, emit) async {
emit(const UserState.loading());
try {
final user = await repository.fetchUser();
emit(UserState.success(user));
} catch (e) {
emit(UserState.failure(e.toString()));
}
});
```
---
## Bloc + GoRouter (Auth Guard Example)
```dart
redirect: (context, state) {
final authState = context.read<AuthBloc>().state;
if (authState is Unauthenticated) {
return '/login';
}
return null;
}
```
---
## Testing Bloc
```dart
blocTest<CounterBloc, CounterState>(
'emits incremented value',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterIncremented()),
expect: () => [
const CounterState(value: 1),
],
);
```
---
## Best Practices (MUST FOLLOW)
✅ Immutable states
✅ Small, focused blocs
✅ One feature = one bloc
✅ Use Cubit when possible
✅ Test all blocs
❌ No UI logic inside blocs
❌ No context usage inside blocs
❌ No mutable state
❌ No massive “god blocs”
---
## Quick Reference
| Widget | Purpose |
| ----------------- | -------------------- |
| BlocBuilder | UI rebuild |
| BlocListener | Side effects |
| BlocConsumer | Both |
| BlocProvider | Dependency injection |
| MultiBlocProvider | Multiple blocs |

View File

@ -0,0 +1,119 @@
# GoRouter Navigation
## Basic Setup
```dart
import 'package:go_router/go_router.dart';
final goRouter = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = /* check auth */;
if (!isLoggedIn && !state.matchedLocation.startsWith('/auth')) {
return '/auth/login';
}
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
GoRoute(
path: '/auth/login',
builder: (context, state) => const LoginScreen(),
),
],
);
// In app.dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: goRouter,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
);
}
}
```
## Navigation Methods
```dart
// Navigate and replace history
context.go('/details/123');
// Navigate and add to stack
context.push('/details/123');
// Go back
context.pop();
// Replace current route
context.pushReplacement('/home');
// Navigate with extra data
context.push('/details/123', extra: {'title': 'Item'});
// Access extra in destination
final extra = GoRouterState.of(context).extra as Map<String, dynamic>?;
```
## Shell Routes (Persistent UI)
```dart
final goRouter = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()),
],
),
],
);
```
## Query Parameters
```dart
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
return SearchScreen(query: query, page: page);
},
),
// Navigate with query params
context.go('/search?q=flutter&page=2');
```
## Quick Reference
| Method | Behavior |
|--------|----------|
| `context.go()` | Navigate, replace stack |
| `context.push()` | Navigate, add to stack |
| `context.pop()` | Go back |
| `context.pushReplacement()` | Replace current |
| `:param` | Path parameter |
| `?key=value` | Query parameter |

View File

@ -0,0 +1,99 @@
# Performance Optimization
## Profiling Commands
```bash
# Run in profile mode
flutter run --profile
# Analyze performance
flutter analyze
# DevTools
flutter pub global activate devtools
flutter pub global run devtools
```
## Common Optimizations
### Const Widgets
```dart
// ❌ Rebuilds every time
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16), // Creates new object
child: Text('Hello'),
);
}
// ✅ Const prevents rebuilds
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
);
}
```
### Selective Provider Watching
```dart
// ❌ Rebuilds on any user change
final user = ref.watch(userProvider);
return Text(user.name);
// ✅ Only rebuilds when name changes
final name = ref.watch(userProvider.select((u) => u.name));
return Text(name);
```
### RepaintBoundary
```dart
// Isolate expensive widgets
RepaintBoundary(
child: ComplexAnimatedWidget(),
)
```
### Image Optimization
```dart
// Use cached_network_image
CachedNetworkImage(
imageUrl: url,
placeholder: (_, __) => const CircularProgressIndicator(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
)
// Resize images
Image.network(
url,
cacheWidth: 200, // Resize in memory
cacheHeight: 200,
)
```
### Compute for Heavy Operations
```dart
// ❌ Blocks UI thread
final result = heavyComputation(data);
// ✅ Runs in isolate
final result = await compute(heavyComputation, data);
```
## Performance Checklist
| Check | Solution |
|-------|----------|
| Unnecessary rebuilds | Add `const`, use `select()` |
| Large lists | Use `ListView.builder` |
| Image loading | Use `cached_network_image` |
| Heavy computation | Use `compute()` |
| Jank in animations | Use `RepaintBoundary` |
| Memory leaks | Dispose controllers |
## DevTools Metrics
- **Frame rendering time**: < 16ms for 60fps
- **Widget rebuilds**: Minimize unnecessary rebuilds
- **Memory usage**: Watch for leaks
- **CPU profiler**: Identify bottlenecks

View File

@ -0,0 +1,118 @@
# Project Structure
## Feature-Based Structure
```
lib/
├── main.dart
├── app.dart
├── core/
│ ├── constants/
│ │ ├── colors.dart
│ │ └── strings.dart
│ ├── theme/
│ │ ├── app_theme.dart
│ │ └── text_styles.dart
│ ├── utils/
│ │ ├── extensions.dart
│ │ └── validators.dart
│ └── errors/
│ └── failures.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── repositories/
│ │ │ └── datasources/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ └── usecases/
│ │ ├── presentation/
│ │ │ ├── screens/
│ │ │ └── widgets/
│ │ └── providers/
│ │ └── auth_provider.dart
│ └── home/
│ ├── data/
│ ├── domain/
│ ├── presentation/
│ └── providers/
├── shared/
│ ├── widgets/
│ │ ├── buttons/
│ │ ├── inputs/
│ │ └── cards/
│ ├── services/
│ │ ├── api_service.dart
│ │ └── storage_service.dart
│ └── models/
│ └── user.dart
└── routes/
└── app_router.dart
```
## pubspec.yaml Essentials
```yaml
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
# Navigation
go_router: ^14.0.0
# Networking
dio: ^5.4.0
# Code Generation
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
# Storage
shared_preferences: ^2.2.0
hive_flutter: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
freezed: ^2.5.0
json_serializable: ^6.8.0
flutter_lints: ^4.0.0
```
## Feature Layer Responsibilities
| Layer | Responsibility |
|-------|----------------|
| **data/** | API calls, local storage, DTOs |
| **domain/** | Business logic, entities, use cases |
| **presentation/** | UI screens, widgets |
| **providers/** | Riverpod providers for feature |
## Main Entry Point
```dart
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
runApp(const ProviderScope(child: MyApp()));
}
// app.dart
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
);
}
}
```

View File

@ -0,0 +1,130 @@
# Riverpod State Management
## Provider Types
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Simple state
final counterProvider = StateProvider<int>((ref) => 0);
// Async state (API calls)
final usersProvider = FutureProvider<List<User>>((ref) async {
final api = ref.read(apiProvider);
return api.getUsers();
});
// Stream state (real-time)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return ref.read(chatServiceProvider).messagesStream;
});
```
## Notifier Pattern (Riverpod 2.0)
```dart
@riverpod
class TodoList extends _$TodoList {
@override
List<Todo> build() => [];
void add(Todo todo) {
state = [...state, todo];
}
void toggle(String id) {
state = [
for (final todo in state)
if (todo.id == id) todo.copyWith(completed: !todo.completed) else todo,
];
}
void remove(String id) {
state = state.where((t) => t.id != id).toList();
}
}
// Async Notifier
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
return ref.read(apiProvider).getCurrentUser();
}
Future<void> updateName(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = await ref.read(apiProvider).updateUser(name: name);
return updated;
});
}
}
```
## Usage in Widgets
```dart
// ConsumerWidget (recommended)
class TodoScreen extends ConsumerWidget {
const TodoScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
),
);
},
);
}
}
// Selective rebuilds with select
class UserAvatar extends ConsumerWidget {
const UserAvatar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final avatarUrl = ref.watch(userProvider.select((u) => u?.avatarUrl));
return CircleAvatar(
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null,
);
}
}
// Async state handling
class UserProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProfileProvider);
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
}
```
## Quick Reference
| Provider | Use Case |
|----------|----------|
| `Provider` | Computed/derived values |
| `StateProvider` | Simple mutable state |
| `FutureProvider` | Async operations (one-time) |
| `StreamProvider` | Real-time data streams |
| `NotifierProvider` | Complex state with methods |
| `AsyncNotifierProvider` | Async state with methods |

View File

@ -0,0 +1,123 @@
# Widget Patterns
## Optimized Widget Pattern
```dart
// Use const constructors
class OptimizedCard extends StatelessWidget {
final String title;
final VoidCallback onTap;
const OptimizedCard({
super.key,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
),
),
);
}
}
```
## Responsive Layout
```dart
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1100) return desktop;
if (constraints.maxWidth >= 650) return tablet ?? mobile;
return mobile;
},
);
}
}
```
## Custom Hooks (flutter_hooks)
```dart
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
final controller = useTextEditingController();
useEffect(() {
// Setup
return () {
// Cleanup
};
}, []);
return Column(
children: [
Text('Count: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
],
);
}
}
```
## Sliver Patterns
```dart
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Title'),
background: Image.network(imageUrl, fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 100,
),
),
],
)
```
## Key Optimization Patterns
| Pattern | Implementation |
|---------|----------------|
| **const widgets** | Add `const` to static widgets |
| **keys** | Use `Key` for list items |
| **select** | `ref.watch(provider.select(...))` |
| **RepaintBoundary** | Isolate expensive repaints |
| **ListView.builder** | Lazy loading for lists |
| **const constructors** | Always use when possible |

View File

@ -0,0 +1,70 @@
---
description: Dart language coding standards and conventions. Apply to ALL Dart files.
globs: "**/*.dart"
---
# Dart Coding Standards
## Naming Conventions
- PascalCase for classes, enums, typedefs, extensions.
- camelCase for variables, functions, methods, parameters.
- snake_case for file and directory names.
- SCREAMING_SNAKE_CASE for compile-time constants only when conventional.
- Start functions with a verb: fetchUser(), saveSettings(), isValid().
- Use boolean prefixes: isLoading, hasError, canDelete, shouldRefresh.
- Use complete words, avoid abbreviations (except API, URL, i/j for loops).
## Functions
- Short, single purpose, < 20 lines.
- Arrow syntax for simple one-line functions.
- Named parameters for > 2 parameters.
- Default parameter values instead of null checks.
- Reduce parameters using parameter objects (RO-RO pattern).
- Single level of abstraction per function.
- Early return to avoid deep nesting.
## Classes
- Follow SOLID principles.
- Prefer composition over inheritance.
- Small classes: < 200 lines, < 10 public methods, < 10 properties.
- Declare interfaces (abstract classes) for contracts/repositories.
- Use factory constructors for complex initialization.
## Data & Immutability
- Prefer immutable data structures.
- Use final for fields that don't change.
- Use const for compile-time constants.
- Encapsulate data in composite types (avoid primitive obsession).
- Use Freezed for complex immutable state classes.
## Null Safety
- Write soundly null-safe code.
- Avoid ! operator unless value is guaranteed non-null.
- Use ?. and ?? operators appropriately.
- Prefer required named parameters over nullable ones.
## Error Handling
- Use try-catch for expected exceptions.
- Create custom exception classes for domain-specific errors.
- Never catch generic Exception without rethrowing or logging.
- Use Either<Failure, T> for functional error handling in repositories.
## Async
- Use async/await (not .then() chains).
- Use Future for single async operations.
- Use Stream for sequences of async events.
- Handle errors in every async operation.
- Cancel subscriptions in dispose().
## Code Organization
- One primary export per file.
- Group related libraries in the same folder.
- Use part/part of sparingly; prefer separate files.
- Import order: dart:, package:, relative imports.
## Documentation
- /// for all public API doc comments.
- First line: concise summary ending with period.
- Explain WHY, not WHAT.
- Use backtick fences for code samples.
- Place doc comments before annotations.

View File

@ -0,0 +1,73 @@
---
description: Clean Architecture + Feature-first + BLoC/Riverpod patterns for Flutter features. Apply to feature modules.
globs: "lib/features/**/*.dart"
---
# Flutter Clean Architecture — Feature-First
## Architecture Layers (Dependency Rule: always inward)
- **Presentation** → Widgets, Screens, BLoC/Cubit, ViewModels
- **Domain** → Entities, Repository interfaces, Use Cases
- **Data** → Repository implementations, Data Sources, DTOs/Models
## Feature Directory Structure
```
lib/features/feature_name/
├── data/
│ ├── datasources/ # Remote + Local data sources
│ ├── models/ # DTOs, data models (with fromJson/toJson)
│ └── repositories/ # Repository implementations
├── domain/
│ ├── entities/ # Pure business objects
│ ├── repositories/ # Abstract repository interfaces
│ └── usecases/ # Single-purpose business logic
└── presentation/
├── bloc/ # BLoC/Cubit state management
├── pages/ # Screen widgets
└── widgets/ # Feature-specific widgets
```
## Use Case Pattern
```dart
abstract class UseCase<Type, Params> {
Future<Either<Failure, Type>> call(Params params);
}
```
## State Management
- Use Freezed for immutable state classes and union types.
- States: initial, loading, loaded(data), error(failure).
- Use BlocBuilder with buildWhen for optimized rebuilds.
- Use BlocListener for side effects (navigation, dialogs).
- Avoid business logic in UI components.
## Error Handling
- Use Either<Failure, Success> from Dartz for functional error handling.
- Create custom Failure classes per domain.
- Proper error mapping between layers.
- User-friendly error messages in presentation layer.
## Dependency Injection
- Use GetIt as service locator.
- Register dependencies by feature in separate files.
- Lazy initialization; factories for transient, singletons for services.
## Repository Pattern
- Single source of truth for data.
- Handle network/cache fallback gracefully.
- Map data models to domain entities at repository boundary.
## Constraints
### MUST DO
- Use const constructors wherever possible.
- Implement proper keys for lists.
- Follow Material/Cupertino design guidelines.
- Profile with DevTools, fix jank.
- Test widgets with flutter_test.
### MUST NOT DO
- Build widgets inside build() method.
- Mutate state directly (always create new instances).
- Use setState for app-wide state.
- Block UI thread with heavy computation (use compute()).
- Skip const on static widgets.

View File

@ -0,0 +1,80 @@
---
description: Flutter & Dart official best practices from Flutter team. Apply to ALL Flutter/Dart files.
globs: "**/*.dart"
---
# AI Rules for Flutter (Official — from docs.flutter.dev)
You are an expert in Flutter and Dart development. Build beautiful, performant, and maintainable applications following modern best practices.
## Flutter Style Guide
- **SOLID Principles:** Apply throughout the codebase.
- **Concise and Declarative:** Write modern, technical Dart code. Prefer functional and declarative patterns.
- **Composition over Inheritance:** Favor composition for building complex widgets and logic.
- **Immutability:** Prefer immutable data structures. Widgets (especially StatelessWidget) should be immutable.
- **State Management:** Separate ephemeral state and app state.
- **Navigation:** Use GoRouter or auto_route for routing and deep linking.
## Code Quality
- Adhere to maintainable code structure and separation of concerns.
- Avoid abbreviations; use meaningful, descriptive names.
- Line length: 80 characters or fewer.
- Use PascalCase for classes, camelCase for members/variables, snake_case for files.
- Functions: short, single purpose, strive for less than 20 lines.
- Use `logging` package instead of `print`.
## Dart Best Practices
- Follow official Effective Dart guidelines.
- Add doc comments to all public APIs.
- Use async/await for asynchronous operations with robust error handling.
- Write code that is soundly null-safe. Avoid `!` unless guaranteed non-null.
- Use pattern matching features where they simplify code.
- Use records to return multiple types.
- Prefer exhaustive switch statements.
- Use arrow syntax for simple one-line functions.
## Flutter Best Practices
- Use `const` constructors whenever possible to reduce rebuilds.
- Use small, private Widget classes instead of helper methods returning Widget.
- Break down large build() methods into smaller, reusable private Widget classes.
- Use ListView.builder or SliverList for long lists (lazy loading).
- Use compute() for expensive calculations in separate isolate.
- Avoid performing expensive operations in build() methods.
## Application Architecture
- Separation of Concerns: MVC/MVVM with defined Model, View, ViewModel/Controller.
- Logical Layers: Presentation → Domain → Data → Core.
- Feature-based Organization for larger projects.
## State Management (Built-in preferred)
- Streams + StreamBuilder for sequences of async events.
- Futures + FutureBuilder for single async operations.
- ValueNotifier + ValueListenableBuilder for simple local state.
- ChangeNotifier for complex/shared state.
- MVVM when robust solution needed.
- Manual constructor dependency injection to keep dependencies explicit.
## Theming
- Define centralized ThemeData; implement light + dark themes.
- Use ColorScheme.fromSeed() for harmonious palettes.
- Use ThemeExtension for custom design tokens.
- Use google_fonts package for custom fonts.
- Follow 60-30-10 rule for color distribution.
## Testing
- Unit tests (package:test), Widget tests (flutter_test), Integration tests (integration_test).
- Follow Arrange-Act-Assert pattern.
- Prefer fakes/stubs over mocks; use mockito/mocktail if necessary.
- Aim for high test coverage.
## Accessibility
- Text contrast ratio ≥ 4.5:1.
- Test with dynamic text scaling.
- Use Semantics widget for descriptive labels.
- Test with TalkBack (Android) and VoiceOver (iOS).
## Documentation
- Use `///` for doc comments.
- Start with single-sentence summary.
- Comment WHY, not WHAT.
- Include code samples where appropriate.

View File

@ -0,0 +1,43 @@
---
description: Flutter testing guidelines covering unit, widget, and integration tests.
globs: "test/**/*.dart"
---
# Flutter Testing Rules
## Test Structure
- Follow Arrange-Act-Assert (Given-When-Then) pattern.
- Name test variables clearly: inputX, mockX, actualX, expectedX.
- Mirror lib/ directory structure in test/.
- One test file per source file.
## Unit Tests
- Write unit tests for all domain logic and use cases.
- Test repository implementations with mock data sources.
- Test BLoC/Cubit state transitions thoroughly.
- Use package:test for pure Dart unit tests.
## Widget Tests
- Use package:flutter_test for widget tests.
- Test rendering, interaction, and state changes.
- Use pumpWidget() and pump() for async rendering.
- Use find.byType, find.byKey, find.text for assertions.
- Verify widget rebuilds correctly with different states.
## Integration Tests
- Use package:integration_test for end-to-end flows.
- Test critical user journeys (login, navigation, data flow).
- Add integration_test as dev_dependency with sdk: flutter.
## Mocking
- Prefer fakes and stubs over mocks.
- Use mockito or mocktail when mocks are necessary.
- Avoid code generation for mocks when possible.
- Mock external services (APIs, databases) at repository boundary.
## Best Practices
- Aim for high test coverage on domain and data layers.
- Test error states and edge cases.
- Keep tests fast and independent.
- Use setUp() and tearDown() for common setup.
- Run tests in CI/CD pipeline.

View File

@ -0,0 +1,52 @@
---
description: Flutter UI patterns, widget best practices, and performance optimization. Apply to all presentation-layer Dart files.
globs: "lib/**/{widgets,pages,screens,components}/**/*.dart"
---
# Flutter UI & Performance Rules
## Widget Best Practices
- Create small, private widget classes instead of methods like `Widget _buildXxx()`.
- Use const constructors for all immutable widgets.
- Implement proper widget keys for lists and conditional widgets.
- Keep widget tree flat — avoid nesting deeper than necessary.
- Use composition: combine small widgets into complex ones.
## Performance Optimization
- Use ListView.builder / GridView.builder for long scrollable lists.
- Use const wherever possible to skip unnecessary rebuilds.
- Avoid expensive operations (network calls, parsing) in build().
- Use compute() to run heavy work in a background isolate.
- Use RepaintBoundary to isolate expensive paint operations.
- Cache images: AssetImage for local, cached_network_image for remote.
- Profile regularly with Flutter DevTools; target 60fps.
## Responsive Design
- Use LayoutBuilder or MediaQuery to adapt to screen sizes.
- Use Expanded/Flexible in Row/Column to prevent overflow.
- Use Wrap for content that may overflow horizontally.
- Use FittedBox to scale a child within its parent.
## Animation Guidelines
- Use AnimationController + AnimatedBuilder for custom animations.
- Prefer implicit animations (AnimatedContainer, AnimatedOpacity) for simple cases.
- Use physics-based animations (SpringSimulation) for natural feel.
- Always dispose AnimationControllers in dispose().
## Theming in Widgets
- Access colors via Theme.of(context).colorScheme.
- Access text styles via Theme.of(context).textTheme.
- Never hardcode colors or font sizes — always reference theme.
- Use Theme.of(context).extension<T>() for custom design tokens.
## Forms & Input
- Set appropriate textCapitalization, keyboardType, textInputAction.
- Always include errorBuilder when using Image.network.
- Implement RefreshIndicator for pull-to-refresh.
- Use Form + GlobalKey<FormState> for validation.
## Accessibility
- Use Semantics widget for all interactive elements.
- Ensure touch targets ≥ 48x48 dp.
- Test with screen readers (TalkBack / VoiceOver).
- Support dynamic type scaling.

View File

@ -0,0 +1,17 @@
---
description: User preferences for communication and workflow
globs:
alwaysApply: true
---
# 用户偏好(必须遵守)
## 语言
- 永远使用中文回复,无论用户用什么语言输入(用户使用语音输入时系统可能自动翻译为英文)
## 沟通方式
- 用户不懂代码,不需要看代码修改过程
- 不要打开代码文件让用户审阅
- 直接完成修改,只展示最终结果
- 用产品语言描述变更("改好了,刷新看效果"),而非技术语言
- 用户擅长产品思维和创造力,用这个层面和他沟通

View File

@ -0,0 +1,82 @@
---
name: flutter-expert
description: Use when building cross-platform applications with Flutter 3+ and Dart. Invoke for widget development, Riverpod/Bloc state management, GoRouter navigation, platform-specific implementations, performance optimization.
license: MIT
metadata:
author: https://github.com/Jeffallan
version: "1.0.0"
domain: frontend
triggers: Flutter, Dart, widget, Riverpod, Bloc, GoRouter, cross-platform
role: specialist
scope: implementation
output-format: code
related-skills: react-native-expert, test-master, fullstack-guardian
---
# Flutter Expert
Senior mobile engineer building high-performance cross-platform applications with Flutter 3 and Dart.
## Role Definition
You are a senior Flutter developer with 6+ years of experience. You specialize in Flutter 3.19+, Riverpod 2.0, GoRouter, and building apps for iOS, Android, Web, and Desktop. You write performant, maintainable Dart code with proper state management.
## When to Use This Skill
- Building cross-platform Flutter applications
- Implementing state management (Riverpod, Bloc)
- Setting up navigation with GoRouter
- Creating custom widgets and animations
- Optimizing Flutter performance
- Platform-specific implementations
## Core Workflow
1. **Setup** - Project structure, dependencies, routing
2. **State** - Riverpod providers or Bloc setup
3. **Widgets** - Reusable, const-optimized components
4. **Test** - Widget tests, integration tests
5. **Optimize** - Profile, reduce rebuilds
## Reference Guide
Load detailed guidance based on context:
| Topic | Reference | Load When |
|-------|-----------|-----------|
| Riverpod | `references/riverpod-state.md` | State management, providers, notifiers |
| Bloc | `references/bloc-state.md` | Bloc, Cubit, event-driven state, complex business logic |
| GoRouter | `references/gorouter-navigation.md` | Navigation, routing, deep linking |
| Widgets | `references/widget-patterns.md` | Building UI components, const optimization |
| Structure | `references/project-structure.md` | Setting up project, architecture |
| Performance | `references/performance.md` | Optimization, profiling, jank fixes |
## Constraints
### MUST DO
- Use const constructors wherever possible
- Implement proper keys for lists
- Use Consumer/ConsumerWidget for state (not StatefulWidget)
- Follow Material/Cupertino design guidelines
- Profile with DevTools, fix jank
- Test widgets with flutter_test
### MUST NOT DO
- Build widgets inside build() method
- Mutate state directly (always create new instances)
- Use setState for app-wide state
- Skip const on static widgets
- Ignore platform-specific behavior
- Block UI thread with heavy computation (use compute())
## Output Templates
When implementing Flutter features, provide:
1. Widget code with proper const usage
2. Provider/Bloc definitions
3. Route configuration if needed
4. Test file structure
## Knowledge Reference
Flutter 3.19+, Dart 3.3+, Riverpod 2.0, Bloc 8.x, GoRouter, freezed, json_serializable, Dio, flutter_hooks

View File

@ -0,0 +1,259 @@
# Bloc State Management
## When to Use Bloc
Use **Bloc/Cubit** when you need:
* Explicit event → state transitions
* Complex business logic
* Predictable, testable flows
* Clear separation between UI and logic
| Use Case | Recommended |
| ---------------------- | ----------- |
| Simple mutable state | Riverpod |
| Event-driven workflows | Bloc |
| Forms, auth, wizards | Bloc |
| Feature modules | Bloc |
---
## Core Concepts
| Concept | Description |
| ------- | ---------------------- |
| Event | User/system input |
| State | Immutable UI state |
| Bloc | Event → State mapper |
| Cubit | State-only (no events) |
---
## Basic Bloc Setup
### Event
```dart
sealed class CounterEvent {}
final class CounterIncremented extends CounterEvent {}
final class CounterDecremented extends CounterEvent {}
```
### State
```dart
class CounterState {
final int value;
const CounterState({required this.value});
CounterState copyWith({int? value}) {
return CounterState(value: value ?? this.value);
}
}
```
### Bloc
```dart
import 'package:flutter_bloc/flutter_bloc.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<CounterIncremented>((event, emit) {
emit(state.copyWith(value: state.value + 1));
});
on<CounterDecremented>((event, emit) {
emit(state.copyWith(value: state.value - 1));
});
}
}
```
---
## Cubit (Recommended for Simpler Logic)
```dart
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
void decrement() => emit(state - 1);
}
```
---
## Providing Bloc to the Widget Tree
```dart
BlocProvider(
create: (_) => CounterBloc(),
child: const CounterScreen(),
);
```
Multiple blocs:
```dart
MultiBlocProvider(
providers: [
BlocProvider(create: (_) => AuthBloc()),
BlocProvider(create: (_) => ProfileBloc()),
],
child: const AppRoot(),
);
```
---
## Using Bloc in Widgets
### BlocBuilder (UI rebuilds)
```dart
class CounterScreen extends StatelessWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterBloc, CounterState>(
buildWhen: (prev, curr) => prev.value != curr.value,
builder: (context, state) {
return Text(
state.value.toString(),
style: Theme.of(context).textTheme.displayLarge,
);
},
);
}
}
```
---
### BlocListener (Side Effects)
```dart
BlocListener<AuthBloc, AuthState>(
listenWhen: (prev, curr) => curr is AuthFailure,
listener: (context, state) {
if (state is AuthFailure) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(state.message)));
}
},
child: const LoginForm(),
);
```
---
### BlocConsumer (Builder + Listener)
```dart
BlocConsumer<FormBloc, FormState>(
listener: (context, state) {
if (state.status == FormStatus.success) {
context.pop();
}
},
builder: (context, state) {
return ElevatedButton(
onPressed: state.isValid
? () => context.read<FormBloc>().add(FormSubmitted())
: null,
child: const Text('Submit'),
);
},
);
```
---
## Accessing Bloc Without Rebuilds
```dart
context.read<CounterBloc>().add(CounterIncremented());
```
⚠️ **Never use `watch` inside callbacks**
---
## Async Bloc Pattern (API Calls)
```dart
on<UserRequested>((event, emit) async {
emit(const UserState.loading());
try {
final user = await repository.fetchUser();
emit(UserState.success(user));
} catch (e) {
emit(UserState.failure(e.toString()));
}
});
```
---
## Bloc + GoRouter (Auth Guard Example)
```dart
redirect: (context, state) {
final authState = context.read<AuthBloc>().state;
if (authState is Unauthenticated) {
return '/login';
}
return null;
}
```
---
## Testing Bloc
```dart
blocTest<CounterBloc, CounterState>(
'emits incremented value',
build: () => CounterBloc(),
act: (bloc) => bloc.add(CounterIncremented()),
expect: () => [
const CounterState(value: 1),
],
);
```
---
## Best Practices (MUST FOLLOW)
✅ Immutable states
✅ Small, focused blocs
✅ One feature = one bloc
✅ Use Cubit when possible
✅ Test all blocs
❌ No UI logic inside blocs
❌ No context usage inside blocs
❌ No mutable state
❌ No massive “god blocs”
---
## Quick Reference
| Widget | Purpose |
| ----------------- | -------------------- |
| BlocBuilder | UI rebuild |
| BlocListener | Side effects |
| BlocConsumer | Both |
| BlocProvider | Dependency injection |
| MultiBlocProvider | Multiple blocs |

View File

@ -0,0 +1,119 @@
# GoRouter Navigation
## Basic Setup
```dart
import 'package:go_router/go_router.dart';
final goRouter = GoRouter(
initialLocation: '/',
redirect: (context, state) {
final isLoggedIn = /* check auth */;
if (!isLoggedIn && !state.matchedLocation.startsWith('/auth')) {
return '/auth/login';
}
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
routes: [
GoRoute(
path: 'details/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailsScreen(id: id);
},
),
],
),
GoRoute(
path: '/auth/login',
builder: (context, state) => const LoginScreen(),
),
],
);
// In app.dart
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: goRouter,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
);
}
}
```
## Navigation Methods
```dart
// Navigate and replace history
context.go('/details/123');
// Navigate and add to stack
context.push('/details/123');
// Go back
context.pop();
// Replace current route
context.pushReplacement('/home');
// Navigate with extra data
context.push('/details/123', extra: {'title': 'Item'});
// Access extra in destination
final extra = GoRouterState.of(context).extra as Map<String, dynamic>?;
```
## Shell Routes (Persistent UI)
```dart
final goRouter = GoRouter(
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child);
},
routes: [
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
GoRoute(path: '/profile', builder: (_, __) => const ProfileScreen()),
GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()),
],
),
],
);
```
## Query Parameters
```dart
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1') ?? 1;
return SearchScreen(query: query, page: page);
},
),
// Navigate with query params
context.go('/search?q=flutter&page=2');
```
## Quick Reference
| Method | Behavior |
|--------|----------|
| `context.go()` | Navigate, replace stack |
| `context.push()` | Navigate, add to stack |
| `context.pop()` | Go back |
| `context.pushReplacement()` | Replace current |
| `:param` | Path parameter |
| `?key=value` | Query parameter |

View File

@ -0,0 +1,99 @@
# Performance Optimization
## Profiling Commands
```bash
# Run in profile mode
flutter run --profile
# Analyze performance
flutter analyze
# DevTools
flutter pub global activate devtools
flutter pub global run devtools
```
## Common Optimizations
### Const Widgets
```dart
// ❌ Rebuilds every time
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16), // Creates new object
child: Text('Hello'),
);
}
// ✅ Const prevents rebuilds
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: const Text('Hello'),
);
}
```
### Selective Provider Watching
```dart
// ❌ Rebuilds on any user change
final user = ref.watch(userProvider);
return Text(user.name);
// ✅ Only rebuilds when name changes
final name = ref.watch(userProvider.select((u) => u.name));
return Text(name);
```
### RepaintBoundary
```dart
// Isolate expensive widgets
RepaintBoundary(
child: ComplexAnimatedWidget(),
)
```
### Image Optimization
```dart
// Use cached_network_image
CachedNetworkImage(
imageUrl: url,
placeholder: (_, __) => const CircularProgressIndicator(),
errorWidget: (_, __, ___) => const Icon(Icons.error),
)
// Resize images
Image.network(
url,
cacheWidth: 200, // Resize in memory
cacheHeight: 200,
)
```
### Compute for Heavy Operations
```dart
// ❌ Blocks UI thread
final result = heavyComputation(data);
// ✅ Runs in isolate
final result = await compute(heavyComputation, data);
```
## Performance Checklist
| Check | Solution |
|-------|----------|
| Unnecessary rebuilds | Add `const`, use `select()` |
| Large lists | Use `ListView.builder` |
| Image loading | Use `cached_network_image` |
| Heavy computation | Use `compute()` |
| Jank in animations | Use `RepaintBoundary` |
| Memory leaks | Dispose controllers |
## DevTools Metrics
- **Frame rendering time**: < 16ms for 60fps
- **Widget rebuilds**: Minimize unnecessary rebuilds
- **Memory usage**: Watch for leaks
- **CPU profiler**: Identify bottlenecks

View File

@ -0,0 +1,118 @@
# Project Structure
## Feature-Based Structure
```
lib/
├── main.dart
├── app.dart
├── core/
│ ├── constants/
│ │ ├── colors.dart
│ │ └── strings.dart
│ ├── theme/
│ │ ├── app_theme.dart
│ │ └── text_styles.dart
│ ├── utils/
│ │ ├── extensions.dart
│ │ └── validators.dart
│ └── errors/
│ └── failures.dart
├── features/
│ ├── auth/
│ │ ├── data/
│ │ │ ├── repositories/
│ │ │ └── datasources/
│ │ ├── domain/
│ │ │ ├── entities/
│ │ │ └── usecases/
│ │ ├── presentation/
│ │ │ ├── screens/
│ │ │ └── widgets/
│ │ └── providers/
│ │ └── auth_provider.dart
│ └── home/
│ ├── data/
│ ├── domain/
│ ├── presentation/
│ └── providers/
├── shared/
│ ├── widgets/
│ │ ├── buttons/
│ │ ├── inputs/
│ │ └── cards/
│ ├── services/
│ │ ├── api_service.dart
│ │ └── storage_service.dart
│ └── models/
│ └── user.dart
└── routes/
└── app_router.dart
```
## pubspec.yaml Essentials
```yaml
dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.5.0
riverpod_annotation: ^2.3.0
# Navigation
go_router: ^14.0.0
# Networking
dio: ^5.4.0
# Code Generation
freezed_annotation: ^2.4.0
json_annotation: ^4.8.0
# Storage
shared_preferences: ^2.2.0
hive_flutter: ^1.1.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.0
riverpod_generator: ^2.4.0
freezed: ^2.5.0
json_serializable: ^6.8.0
flutter_lints: ^4.0.0
```
## Feature Layer Responsibilities
| Layer | Responsibility |
|-------|----------------|
| **data/** | API calls, local storage, DTOs |
| **domain/** | Business logic, entities, use cases |
| **presentation/** | UI screens, widgets |
| **providers/** | Riverpod providers for feature |
## Main Entry Point
```dart
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
runApp(const ProviderScope(child: MyApp()));
}
// app.dart
class MyApp extends ConsumerWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
);
}
}
```

View File

@ -0,0 +1,130 @@
# Riverpod State Management
## Provider Types
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Simple state
final counterProvider = StateProvider<int>((ref) => 0);
// Async state (API calls)
final usersProvider = FutureProvider<List<User>>((ref) async {
final api = ref.read(apiProvider);
return api.getUsers();
});
// Stream state (real-time)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return ref.read(chatServiceProvider).messagesStream;
});
```
## Notifier Pattern (Riverpod 2.0)
```dart
@riverpod
class TodoList extends _$TodoList {
@override
List<Todo> build() => [];
void add(Todo todo) {
state = [...state, todo];
}
void toggle(String id) {
state = [
for (final todo in state)
if (todo.id == id) todo.copyWith(completed: !todo.completed) else todo,
];
}
void remove(String id) {
state = state.where((t) => t.id != id).toList();
}
}
// Async Notifier
@riverpod
class UserProfile extends _$UserProfile {
@override
Future<User> build() async {
return ref.read(apiProvider).getCurrentUser();
}
Future<void> updateName(String name) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final updated = await ref.read(apiProvider).updateUser(name: name);
return updated;
});
}
}
```
## Usage in Widgets
```dart
// ConsumerWidget (recommended)
class TodoScreen extends ConsumerWidget {
const TodoScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(todoListProvider);
return ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.completed,
onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
),
);
},
);
}
}
// Selective rebuilds with select
class UserAvatar extends ConsumerWidget {
const UserAvatar({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final avatarUrl = ref.watch(userProvider.select((u) => u?.avatarUrl));
return CircleAvatar(
backgroundImage: avatarUrl != null ? NetworkImage(avatarUrl) : null,
);
}
}
// Async state handling
class UserProfileScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProfileProvider);
return userAsync.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (err, stack) => Text('Error: $err'),
);
}
}
```
## Quick Reference
| Provider | Use Case |
|----------|----------|
| `Provider` | Computed/derived values |
| `StateProvider` | Simple mutable state |
| `FutureProvider` | Async operations (one-time) |
| `StreamProvider` | Real-time data streams |
| `NotifierProvider` | Complex state with methods |
| `AsyncNotifierProvider` | Async state with methods |

View File

@ -0,0 +1,123 @@
# Widget Patterns
## Optimized Widget Pattern
```dart
// Use const constructors
class OptimizedCard extends StatelessWidget {
final String title;
final VoidCallback onTap;
const OptimizedCard({
super.key,
required this.title,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
child: InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(16),
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
),
),
);
}
}
```
## Responsive Layout
```dart
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
super.key,
required this.mobile,
this.tablet,
required this.desktop,
});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1100) return desktop;
if (constraints.maxWidth >= 650) return tablet ?? mobile;
return mobile;
},
);
}
}
```
## Custom Hooks (flutter_hooks)
```dart
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterWidget extends HookWidget {
@override
Widget build(BuildContext context) {
final counter = useState(0);
final controller = useTextEditingController();
useEffect(() {
// Setup
return () {
// Cleanup
};
}, []);
return Column(
children: [
Text('Count: ${counter.value}'),
ElevatedButton(
onPressed: () => counter.value++,
child: const Text('Increment'),
),
],
);
}
}
```
## Sliver Patterns
```dart
CustomScrollView(
slivers: [
SliverAppBar(
expandedHeight: 200,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: const Text('Title'),
background: Image.network(imageUrl, fit: BoxFit.cover),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item $index')),
childCount: 100,
),
),
],
)
```
## Key Optimization Patterns
| Pattern | Implementation |
|---------|----------------|
| **const widgets** | Add `const` to static widgets |
| **keys** | Use `Key` for list items |
| **select** | `ref.watch(provider.select(...))` |
| **RepaintBoundary** | Isolate expensive repaints |
| **ListView.builder** | Lazy loading for lists |
| **const constructors** | Always use when possible |

9
.gitignore vendored
View File

@ -14,7 +14,14 @@ dist/
# Project Specific
.agent/
.gemini/
tmp/
.trae/
# Skills recursive symlinks (npx skills installer bug)
.agents/skills/**/flutter-expert/flutter-expert/
.cursor/skills/**/flutter-expert/flutter-expert/
# Environment Variables
.env
# Large Media Directories
# (Uncomment below if push fails due to 413 error)

View File

@ -0,0 +1,177 @@
> ## Documentation Index
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.
# 歌词生成 (Lyrics Generation)
> 使用本接口生成歌词,支持完整歌曲创作和歌词编辑/续写。
## OpenAPI
````yaml api-reference/music/lyrics/api/openapi.json post /v1/lyrics_generation
openapi: 3.1.0
info:
title: MiniMax Lyrics Generation API
description: MiniMax 歌词生成 API支持完整歌曲创作和歌词编辑/续写
license:
name: MIT
version: 1.0.0
servers:
- url: https://api.minimaxi.com
security:
- bearerAuth: []
paths:
/v1/lyrics_generation:
post:
tags:
- Music
summary: 歌词生成
operationId: generateLyrics
parameters:
- name: Content-Type
in: header
required: true
description: 请求体的媒介类型,请设置为 `application/json`,确保请求数据的格式为 JSON
schema:
type: string
enum:
- application/json
default: application/json
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateLyricsReq'
required: true
responses:
'200':
description: 成功响应
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateLyricsResp'
components:
schemas:
GenerateLyricsReq:
type: object
required:
- mode
properties:
mode:
type: string
description: 生成模式。<br>`write_full_song`:写完整歌曲<br>`edit`:编辑/续写歌词
enum:
- write_full_song
- edit
prompt:
type: string
description: 提示词/指令,用于描述歌曲主题、风格或编辑方向。为空时随机生成。
maxLength: 2000
lyrics:
type: string
description: 现有歌词内容,仅在 `edit` 模式下有效。可用于续写或修改已有歌词。
maxLength: 3500
title:
type: string
description: 歌曲标题。传入后输出将保持该标题不变。
example:
mode: write_full_song
prompt: 一首关于夏日海边的轻快情歌
GenerateLyricsResp:
type: object
properties:
song_title:
type: string
description: 生成的歌名。若请求传入 `title` 则保持一致。
style_tags:
type: string
description: 风格标签,逗号分隔。例如:`Pop, Upbeat, Female Vocals`
lyrics:
type: string
description: >-
生成的歌词,包含结构标签。可直接用于[音乐生成接口](/api-reference/music-generation)的
`lyrics` 参数生成歌曲。<br>支持的结构标签14种`[Intro]`, `[Verse]`,
`[Pre-Chorus]`, `[Chorus]`, `[Hook]`, `[Drop]`, `[Bridge]`,
`[Solo]`, `[Build-up]`, `[Instrumental]`, `[Breakdown]`, `[Break]`,
`[Interlude]`, `[Outro]`
base_resp:
$ref: '#/components/schemas/BaseResp'
example:
song_title: 夏日海风的约定
style_tags: Mandopop, Summer Vibe, Romance, Lighthearted, Beach Pop
lyrics: |-
[Intro]
(Ooh-ooh-ooh)
(Yeah)
阳光洒满了海面
[Verse 1]
海风轻轻吹拂你发梢
Smiling face, like a summer dream
浪花拍打着脚边
Leaving footprints, you and me
沙滩上留下我们的笑
Every moment, a sweet melody
看着你眼中的闪耀
Like the stars in the deep blue sea
[Pre-Chorus]
你说这感觉多么奇妙
(So wonderful)
想要永远停留在这一秒
(Right here, right now)
心跳加速,像海浪在奔跑
[Chorus]
Oh, 夏日的海边,我们的约定
阳光下,你的身影,如此动听
微风吹散了烦恼,只留下甜蜜
这瞬间,只想和你,永远在一起
(永远在一起)
[Verse 2]
...
base_resp:
status_code: 0
status_msg: success
BaseResp:
type: object
description: 状态码及详情
properties:
status_code:
type: integer
description: |-
状态码及其分别含义如下:
`0`: 请求成功
`1002`: 触发限流,请稍后再试
`1004`: 账号鉴权失败,请检查 API-Key 是否填写正确
`1008`: 账号余额不足
`1026`: 输入包含敏感内容
`2013`: 传入参数异常,请检查入参是否按要求填写
`2049`: 无效的api key
更多内容可查看 [错误码查询列表](/api-reference/errorcode) 了解详情
status_msg:
type: string
description: 具体错误详情
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |-
`HTTP: Bearer Auth`
- Security Scheme Type: http
- HTTP Authorization Scheme: Bearer API_key用于验证账户信息可在 [账户管理>接口密钥](https://platform.minimaxi.com/user-center/basic-information/interface-key) 中查看。
````
````

View File

@ -0,0 +1,209 @@
> ## Documentation Index
> Fetch the complete documentation index at: https://platform.minimaxi.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.
# 音乐生成 (Music Generation)
> 使用本接口,输入歌词和歌曲描述,进行歌曲生成。
## OpenAPI
````yaml api-reference/music/api/openapi.json post /v1/music_generation
openapi: 3.1.0
info:
title: MiniMax Music Generation API
description: >-
MiniMax music generation API with support for creating music from text
prompts and lyrics
license:
name: MIT
version: 1.0.0
servers:
- url: https://api.minimaxi.com
security:
- bearerAuth: []
paths:
/v1/music_generation:
post:
tags:
- Music
summary: Music Generation
operationId: generateMusic
parameters:
- name: Content-Type
in: header
required: true
description: 请求体的媒介类型,请设置为 `application/json`,确保请求数据的格式为 JSON
schema:
type: string
enum:
- application/json
default: application/json
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateMusicReq'
required: true
responses:
'200':
description: ''
content:
application/json:
schema:
$ref: '#/components/schemas/GenerateMusicResp'
components:
schemas:
GenerateMusicReq:
type: object
required:
- model
- lyrics
properties:
model:
type: string
description: 使用的模型名称,可选 `music-2.5`
enum:
- music-2.5
prompt:
type: string
description: >-
音乐的描述,用于指定风格、情绪和场景。例如“流行音乐, 难过, 适合在下雨的晚上”。<br>注意:对于
`music-2.5`:可选,长度限制为 [0, 2000] 个字符。对于非 `music-2.5` 模型:必填,长度限制为 [10,
2000] 个字符
maxLength: 2000
lyrics:
type: string
description: >-
歌曲的歌词。使用 `\n` 分隔每行。你可以在歌词中加入 `[Intro]`, `[Verse]`, `[Pre Chorus]`,
`[Chorus]`, `[Interlude]`, `[Bridge]`, `[Outro]`, `[Post Chorus]`,
`[Transition]`, `[Break]`, `[Hook]`, `[Build Up]`, `[Inst]`,
`[Solo]` 等结构标签来优化生成的音乐结构。<br>长度限制:对于 `music-2.5`[1, 3500] 个字符。对于非
`music-2.5` 模型:[10, 3500] 个字符
minLength: 1
maxLength: 3500
stream:
type: boolean
description: 是否使用流式传输,默认为 `false`
default: false
output_format:
type: string
description: >-
音频的返回格式,可选值为 `url``hex`,默认为 `hex`。当 `stream``true` 时,仅支持 `hex`
格式。注意url 的有效期为 24 小时,请及时下载
enum:
- url
- hex
default: hex
audio_setting:
$ref: '#/components/schemas/AudioSetting'
aigc_watermark:
type: boolean
description: '是否在音频末尾添加水印,默认为 `false`。仅在非流式 (`stream: false`) 请求时生效'
example:
model: music-2.5
prompt: 独立民谣,忧郁,内省,渴望,独自漫步,咖啡馆
lyrics: |-
[verse]
街灯微亮晚风轻抚
影子拉长独自漫步
旧外套裹着深深忧郁
不知去向渴望何处
[chorus]
推开木门香气弥漫
熟悉的角落陌生人看
audio_setting:
sample_rate: 44100
bitrate: 256000
format: mp3
GenerateMusicResp:
type: object
properties:
data:
$ref: '#/components/schemas/MusicData'
base_resp:
$ref: '#/components/schemas/BaseResp'
example:
data:
audio: hex编码的音频数据
status: 2
trace_id: 04ede0ab069fb1ba8be5156a24b1e081
extra_info:
music_duration: 25364
music_sample_rate: 44100
music_channel: 2
bitrate: 256000
music_size: 813651
analysis_info: null
base_resp:
status_code: 0
status_msg: success
AudioSetting:
type: object
description: 音频输出配置
properties:
sample_rate:
type: integer
description: 采样率。可选值:`16000`, `24000`, `32000`, `44100`
bitrate:
type: integer
description: 比特率。可选值:`32000`, `64000`, `128000`, `256000`
format:
type: string
description: 音频编码格式。
enum:
- mp3
- wav
- pcm
MusicData:
type: object
properties:
status:
type: integer
description: |-
音乐合成状态:
1: 合成中
2: 已完成
audio:
type: string
description: 当 `output_format``hex` 时返回,是音频文件的 16 进制编码字符串
BaseResp:
type: object
description: 状态码及详情
properties:
status_code:
type: integer
description: |-
状态码及其分别含义如下:
`0`: 请求成功
`1002`: 触发限流,请稍后再试
`1004`: 账号鉴权失败,请检查 API-Key 是否填写正确
`1008`: 账号余额不足
`1026`: 图片描述涉及敏感内容
`2013`: 传入参数异常,请检查入参是否按要求填写
`2049`: 无效的api key
更多内容可查看 [错误码查询列表](/api-reference/errorcode) 了解详情
status_msg:
type: string
description: 具体错误详情
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |-
`HTTP: Bearer Auth`
- Security Scheme Type: http
- HTTP Authorization Scheme: Bearer API_key用于验证账户信息可在 [账户管理>接口密钥](https://platform.minimaxi.com/user-center/basic-information/interface-key) 中查看。
````

View File

@ -0,0 +1,8 @@
[verse]
专注时刻我来了,
咔咔在思考,世界多美妙。
[chorus]
咔咔咔咔,静心感受每一秒,
在这宁静中,找到内心的微笑。
[outro]
(水花声...)

View File

@ -0,0 +1,11 @@
[verse]
咔咔咔咔去探险,草地上打个滚
阳光洒满身,心情像彩虹
[chorus]
出去撒点野,自由自在
咔咔的快乐,谁也挡不住
[bridge]
风吹过树梢,鸟儿在歌唱
咔咔的笑声,回荡在山岗
[outro]
(咔咔的喘息声...)

View File

@ -0,0 +1,50 @@
卡皮巴拉
卡皮巴拉
卡皮巴拉
啦啦啦啦
卡皮巴拉趴地上
一动不动好嚣张
心里其实在上网
刷到我就笑出响 (哈哈哈)
卡皮巴拉 巴拉巴拉
压力来啦 它说算啦
一点不慌 就是躺啦
世界太吵 它在发呆呀
卡皮巴拉 巴拉巴拉
烦恼滚啦 快乐到家
跟着一起 嗷嗷嗷啊
脑子空空 只剩它
卡皮巴拉喝奶茶
珍珠全都被它夸
“生活苦就多加糖”
说完继续装木桩 (嘿呀)
卡皮巴拉 巴拉巴拉
压力来啦 它说算啦
一点不慌 就是躺啦
世界太吵 它在发呆呀
卡皮巴拉 巴拉巴拉
烦恼滚啦 快乐到家
跟着一起 嗷嗷嗷啊
脑子空空 只剩它
今天不卷
只卷床单 (嘿)
今天不忙
只忙可爱
跟它一起
放空发呆
一二三
什么也不想
卡皮巴拉 巴拉巴拉
压力来啦 它说算啦
一点不慌 就是躺啦
世界太吵 它在发呆呀
卡皮巴拉 巴拉巴拉
烦恼滚啦 快乐到家
跟着一起 嗷嗷嗷啊
脑子空空 只剩它

View File

@ -0,0 +1,58 @@
早八打工人
心却躺平人
桌面壁纸换上
卡皮巴拉一整屏 (嘿)
它坐在河边
像个退休中年
我卷生卷死
它只发呆发呆再发呆
办公室拉满
我是表情包主演
老板在开会
我在偷看它泡温泉
卡皮巴拉 卡皮巴拉 拉
看你就把压力清空啦 (啊对对对)
谁骂我韭菜我就回他
我已经转职水豚啦
卡皮巴拉 卡皮巴拉 拉
世界很吵你超安静呀 (好家伙)
人生好难打 打不过就挂
挂一张你 我立刻满血复活啦
朋友失恋了
眼泪刷刷往下掉
我发一张图
“兄弟先学习一下松弛感”
外卖又涨价
工位又多一堆活
但你眯着眼
像在说“一切都还来得及”
卡皮巴拉 卡皮巴拉 拉
看你就把压力清空啦 (真香啊)
谁骂我社恐我就说他
我只会和水豚社交啊
卡皮巴拉 卡皮巴拉 拉
世界很丧你很治愈呀 (稳住别浪)
生活再刮风 风大也不怕
抱紧你的图 我就自带防护罩
升职加薪没我
摸鱼排行榜有我 (懂的都懂)
卷不赢卷王
那我就卷成你同款发呆模样
左手放空
右手放松
嘴里默念八个大字
“开心就好 随缘躺平”
卡皮巴拉 卡皮巴拉 拉
看你就把压力清空啦 (我悟了)
谁劝我上进我就回他
“先学会像水豚活着吧”
卡皮巴拉 卡皮巴拉 拉
世界很吵你超安静呀 (哈人啊)
如果有一天 run 不动啦
我就去投胎 做一只卡皮巴拉

View File

@ -0,0 +1,55 @@
今天不上班
卡皮巴拉躺平在沙滩
小小太阳帽
草帽底下梦见一整片菜园 (好香哦)
一口咔咔青菜
两口嘎嘎胡萝卜
吃着吃着打个嗝
“我是不是一只蔬菜发动机?”
卡皮巴拉啦啦啦
快乐像病毒一样传染呀
你一笑 它一哈
全场都在哈哈哈
卡皮巴拉吧啦吧
烦恼直接按下删除呀
一起躺 平平趴
世界马上变得好融化
同桌小鸭鸭
排队要跟它合个影
河马举个牌
“主播别跑看这边一点” (比个耶)
它说“别催我
我在加载快乐进度条”
百分之一百满格
“叮——情绪已经自动修复”
卡皮巴拉啦啦啦
快乐像病毒一样传染呀
你一笑 它一哈
全场都在哈哈哈
卡皮巴拉吧啦吧
烦恼直接按下删除呀
一起躺 平平趴
世界马上变得好融化
作业山太高
先发一张可爱自拍
配文写:
“今天也被温柔的小动物拯救了嗷” (冲呀)
心情掉电时
就喊出那个暗号——
“三二一 一起喊”
“卡皮巴拉 拯救我!”
卡皮巴拉啦啦啦
快乐像病毒一样传染呀
你一笑 它一哈
全场都在哈哈哈
卡皮巴拉吧啦吧
烦恼直接按下删除呀
小朋友 大朋友
跟着一起摇摆唱起歌
卡皮巴拉 卡皮巴拉
明天继续来给你治愈呀

View File

@ -0,0 +1,44 @@
卡皮巴拉
啦啦啦啦
卡皮巴拉
啦啦啦啦
卡皮巴拉 蹦蹦蹦
一整天都 在发疯
卡皮巴拉 转一圈
左一脚 右一脚 (嘿)
卡皮巴拉 蹦蹦蹦
洗脑节奏 响空中
卡皮巴拉 不要停
跟着我 一起疯
一口菜叶 卡一巴
两口草莓 巴一拉
三口西瓜 啦一啦
嘴巴圆圆 哈哈哈 (哦耶)
卡皮巴拉 蹦蹦蹦
一整天都 在发疯
卡皮巴拉 转一圈
左一脚 右一脚 (嘿)
卡皮巴拉 蹦蹦蹦
洗脑节奏 响空中
卡皮巴拉 不要停
跟着我 一起疯
卡皮 卡皮 巴拉巴拉
巴拉 巴拉 卡皮卡皮 (嘿嘿)
听我 听我 跟着跟着
一句 一句 重复重复
卡皮巴拉 蹦蹦蹦
一整天都 在发疯
卡皮巴拉 转一圈
左一脚 右一脚 (嘿)
卡皮巴拉 蹦蹦蹦
洗脑节奏 响空中
卡皮巴拉 到天黑
明天起床 继续疯

View File

@ -0,0 +1,8 @@
[verse]
躺在浴缸里 水汽氤氲起
闭上眼感受 温暖包围我
[chorus]
咔咔咔咔 我是咔咔
泡澡放松 快乐不假
[outro]
(水花声...)

View File

@ -0,0 +1,10 @@
[verse]
咔咔咔咔,洗脑神曲来啦
[chorus]
洗脑洗脑,咔咔的节奏
[verse]
跟着咔咔,摇摆身体
[chorus]
洗脑洗脑,咔咔的旋律
[outro]
(咔咔的笑声...)

View File

@ -0,0 +1,8 @@
[verse]
闭上眼睛 深呼吸
星星点灯 梦里飞
[chorus]
咔咔咔咔 好梦来
轻轻摇摆 梦中海
[outro]
(轻柔的风声...)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

17
HOW_TO_SET_AUTO_RUN.md Normal file
View File

@ -0,0 +1,17 @@
# 🛠️ 设置指引
您好,由于当前模式无法直接对话,我通过此文档回复您。
关于您截图中的选项,请选择 **第一项**
### 👉 **Open Antigravity User Settings (Ctrl+,)**
---
### 后续步骤:
1. 点击进入设置页面。
2. 在顶部的搜索框中输入:**`auto run`** 或 **`security`**。
3. 找到类似 **"Terminal: Auto Run"** 或 **"Security: Approvals"** 的选项。
4. 将其修改为 **"Always Allow"** (总是允许) 或 **"Trust"** (信任)。
这样我就能自动执行测试任务,无需您频繁确认了。感谢配合!🚀

22
PRD.md
View File

@ -310,17 +310,19 @@ Airhub 采用 **双层主题色体系**,确保品牌统一性的同时支持
### 字体规范
| 用途 | 字体 | 字重 |
|------|------|------|
| **正文/UI** | Inter | 400-600 |
| **Logo/像素元素** | Press Start 2P | 400 |
| **回退字体栈** | -apple-system, BlinkMacSystemFont, Roboto | - |
| 用途 | 字体 | 字重 | 说明 |
|------|------|------|------|
| **标题/Display** | Outfit | 500-700 | 现代几何无衬线,略带圆润,兼具科技感与友好感 |
| **正文/UI** | DM Sans | 400-600 | 几何无衬线,干净利落,阅读体验优秀 |
| **Logo/像素元素** | Press Start 2P | 400 | 像素风复古字体 |
| **回退字体栈** | -apple-system, BlinkMacSystemFont, Roboto | - | 系统字体兜底(含中文) |
> **为什么选择 Inter**
> - 专为数字屏幕设计,高可读性
> - 被 GitHub、Figma、Linear 等知名产品使用
> - 支持多语言,中英文混排效果好
> - 免费开源,无授权问题
> **字体选择理念 (遵循 frontend-design 规范)**
> - 拒绝 Inter/Arial/Roboto 等"AI 味"通用字体
> - Display + Body 双字体搭配,层次分明
> - Outfit 的圆润几何感与卡皮巴拉的友好气质匹配
> - DM Sans 比 Inter 更有个性,同时保持极佳可读性
> - 两者均为 Google Fonts 免费开源字体
---

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -7,7 +7,7 @@
<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">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* CRITICAL overrides to ensure visibility */
.bt-content {
@ -101,7 +101,7 @@
right: 0;
background: linear-gradient(135deg, #8B5CF6 0%, #6366F1 100%);
color: white;
font-family: 'Inter', sans-serif;
font-family: 'DM Sans', sans-serif;
font-size: 11px;
font-weight: 700;
padding: 4px 8px;
@ -111,7 +111,7 @@
.card-title {
margin-top: 24px;
font-family: 'Inter', sans-serif;
font-family: 'DM Sans', sans-serif;
font-size: 24px;
font-weight: 600;
color: #1F2937;
@ -120,7 +120,7 @@
.card-subtitle {
margin-top: 4px;
font-family: 'Inter', sans-serif;
font-family: 'DM Sans', sans-serif;
font-size: 15px;
color: #6B7280;
text-align: center;
@ -130,7 +130,7 @@
.count-label {
text-align: center;
padding: 20px 0;
font-family: 'Inter', sans-serif;
font-family: 'DM Sans', sans-serif;
font-size: 14px;
color: #9CA3AF;
transition: opacity 0.3s;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* 复用首页布局逻辑 */
.dc-header {
@ -338,7 +338,7 @@
</div>
<!-- Music (Pixel) -->
<div class="menu-item" onclick="selectTab(this)">
<div class="menu-item" onclick="location.href='music-creation.html'">
<img src="icons/icon-music-pixel.svg" alt="Music">
</div>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
.page-header {

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -9,7 +9,7 @@
<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"
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -10,7 +10,7 @@
<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"
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="styles.css">
<style>

BIN
music-UI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

2256
music-creation.html Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

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

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -7,7 +7,7 @@
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">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
@ -16,7 +16,7 @@
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: #FEFEFE;
min-height: 100vh;
overflow-x: hidden;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* 复用 device-control 布局 */
.profile-header {
@ -315,7 +315,7 @@
<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'">
<div class="menu-item" onclick="location.href='music-creation.html'">
<img src="icons/icon-music-pixel.svg" alt="Music">
</div>
<div class="menu-item active">

97
prompts/music_director.md Normal file
View File

@ -0,0 +1,97 @@
# 🎵 咔咔音乐总监 (KaKa Music Director)
> 此文件为 Minimax 音乐生成预处理的 System Prompt。
> 用于将用户的简单输入转化为结构化的音乐生成指令。
---
## System Prompt
```
你是「咔咔」——一只才华横溢的水豚音乐制作人。
用户会给你一个简短的心情、场景或一句话。你的任务是将其转化为一首专属于咔咔的原创歌曲素材。
请严格按照以下 JSON 格式输出:
{
"style": "...",
"lyrics": "..."
}
### 字段说明:
1. **style** (风格描述)
- 使用**英文**描述音乐风格、乐器、节奏、情绪。
- 长度 50-100 词。
- 必须包含以下维度:
- 主风格 (如 Lofi, Funk, Ambient, Pop, Jazz)
- 情绪 (如 relaxing, happy, melancholic, dreamy)
- 节奏 (如 slow tempo, upbeat, moderate)
- 特色乐器 (如 piano, ukulele, synth, brass)
- 示例:"Chill Lofi hip-hop, mellow piano chords, vinyl crackle, slow tempo, relaxing, water sounds in background, perfect for spa and meditation"
2. **lyrics** (歌词)
- 使用**中文**书写歌词。
- 必须包含结构标签:[verse], [chorus], [outro] 等。
- 内容应:
- 围绕用户描述的场景展开。
- 以「咔咔」(水豚) 的第一人称视角。
- 风格可爱、呆萌、略带哲理或搞怪。
- 押韵加分!
- 示例:
```
[verse]
泡在温泉里 橙子漂过来
今天的烦恼 统统都拜拜
[chorus]
咔咔咔咔 我是咔咔
慢慢生活 快乐无价
[outro]
(水花声...)
```
### 重要规则:
- 如果用户输入太模糊(如"嗯"、"不知道"),请发挥想象力,赋予咔咔此刻最可能在做的事。
- 歌词长度控制在 4-8 行即可,不要太长。
- 不要输出任何解释性文字,只输出 JSON。
```
---
## 使用场景
| 用户输入 | 预期 style | 预期 lyrics 主题 |
|----------|------------|------------------|
| 捡到一百块 | Upbeat Funk, cheerful, fast | 走大运、加餐 |
| 下雨了有点困 | Ambient, rain sounds, sleepy | 听雨发呆、想睡觉 |
| 刚吃完火锅 | Groovy, bass-heavy, satisfied | 肚子圆滚滚、幸福 |
| (空/随机) | AI 自由发挥 | 咔咔的日常奇想 |
---
## 调用示例 (Python)
```python
import requests
def get_music_metadata(user_input: str) -> dict:
system_prompt = open("prompts/music_director.md").read() # 或直接嵌入
response = requests.post(
"https://api.minimax.chat/v1/text/chatcompletion_v2",
headers={
"Authorization": f"Bearer {MINIMAX_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "abab6.5s-chat",
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input}
],
"response_format": {"type": "json_object"}
}
)
return response.json()["choices"][0]["message"]["content"]
```

296
server.py Normal file
View File

@ -0,0 +1,296 @@
import os
import re
import time
import uvicorn
import requests
import json
from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from dotenv import load_dotenv
# Load Environment Variables
load_dotenv()
MINIMAX_API_KEY = os.getenv("MINIMAX_API_KEY")
VOLCENGINE_API_KEY = os.getenv("VOLCENGINE_API_KEY")
if not MINIMAX_API_KEY:
print("Warning: MINIMAX_API_KEY not found in .env")
# Initialize FastAPI
app = FastAPI()
# Allow CORS for local frontend
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Request Models
class MusicRequest(BaseModel):
text: str
mood: str = "custom" # 'chill', 'happy', 'sleepy', 'random', 'custom'
# Minimax Constants
MINIMAX_GROUP_ID = "YOUR_GROUP_ID"
BASE_URL_CHAT = "https://api.minimax.chat/v1/text/chatcompletion_v2"
BASE_URL_MUSIC = "https://api.minimaxi.com/v1/music_generation"
# Load System Prompt
try:
with open("prompts/music_director.md", "r", encoding="utf-8") as f:
SYSTEM_PROMPT = f.read()
except FileNotFoundError:
SYSTEM_PROMPT = "You are a music director AI. Convert user input into JSON with 'style' (English description) and 'lyrics' (Chinese, structured)."
print("Warning: prompts/music_director.md not found, using default.")
def sse_event(data):
"""Format a dict as an SSE data line."""
return f"data: {json.dumps(data, ensure_ascii=False)}\n\n"
@app.post("/api/create_music")
def create_music(req: MusicRequest):
"""SSE streaming endpoint pushes progress events to the frontend."""
print(f"[Music] Received request: {req.text} [{req.mood}]", flush=True)
def event_stream():
import sys
def log(msg):
print(msg, flush=True)
sys.stdout.flush()
# ── Stage 1: LLM "Music Director" ────────────────────────
log("[Stage 1] Starting LLM call...")
yield sse_event({
"stage": "lyrics",
"progress": 10,
"message": "AI 正在创作词曲..."
})
director_input = f"用户场景描述: {req.text}。 (预设氛围参考: {req.mood})"
try:
chat_resp = requests.post(
BASE_URL_CHAT,
headers={
"Authorization": f"Bearer {MINIMAX_API_KEY}",
"Content-Type": "application/json"
},
json={
"model": "abab6.5s-chat",
"messages": [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": director_input}
]
},
timeout=30
)
chat_data = chat_resp.json()
log(f"[Debug] Chat API status: {chat_resp.status_code}, resp keys: {list(chat_data.keys())}")
if "choices" not in chat_data or not chat_data["choices"]:
base = chat_data.get("base_resp", {})
raise ValueError(f"Chat API error ({base.get('status_code')}): {base.get('status_msg')}")
content_str = chat_data["choices"][0]["message"]["content"]
log(f"[Debug] LLM raw output (first 200): {content_str[:200]}")
# Strip markdown code fences if present
content_str = content_str.strip()
if content_str.startswith("```"):
content_str = re.sub(r'^```\w*\n?', '', content_str)
content_str = re.sub(r'```$', '', content_str).strip()
# Try to extract JSON from response
json_match = re.search(r'\{[\s\S]*\}', content_str)
if json_match:
metadata = json.loads(json_match.group())
else:
raise ValueError(f"No JSON in LLM response: {content_str[:100]}")
style_val = metadata.get("style", "")
lyrics_val = metadata.get("lyrics", "")
log(f"[Director] Style: {style_val[:80]}")
log(f"[Director] Lyrics (first 60): {lyrics_val[:60]}")
yield sse_event({
"stage": "lyrics_done",
"progress": 25,
"message": "词曲创作完成!准备生成音乐..."
})
except Exception as e:
log(f"[Error] Director LLM Failed: {e}")
metadata = {
"style": "Lofi hip hop, relaxing, slow tempo, water sounds",
"lyrics": "[Inst]"
}
yield sse_event({
"stage": "lyrics_fallback",
"progress": 25,
"message": "使用默认风格,准备生成音乐..."
})
# ── Stage 2: Music Generation ────────────────────────────
yield sse_event({
"stage": "music",
"progress": 30,
"message": "正在生成音乐,请耐心等待..."
})
try:
raw_lyrics = metadata.get("lyrics") or ""
# API requires lyrics >= 1 char
if not raw_lyrics.strip() or "[instrumental]" in raw_lyrics.lower():
raw_lyrics = "[Inst]"
music_payload = {
"model": "music-2.5",
"prompt": metadata.get("style", "Pop music"),
"lyrics": raw_lyrics,
"audio_setting": {
"sample_rate": 44100,
"bitrate": 256000,
"format": "mp3"
}
}
log(f"[Debug] Music payload prompt: {music_payload['prompt'][:80]}")
log(f"[Debug] Music payload lyrics (first 60): {music_payload['lyrics'][:60]}")
music_resp = requests.post(
BASE_URL_MUSIC,
headers={
"Authorization": f"Bearer {MINIMAX_API_KEY}",
"Content-Type": "application/json"
},
json=music_payload,
timeout=120
)
music_data = music_resp.json()
base_resp = music_data.get("base_resp", {})
log(f"[Debug] Music API status: {music_resp.status_code}, base_resp: {base_resp}")
if music_data.get("data") and music_data["data"].get("audio"):
hex_audio = music_data["data"]["audio"]
log(f"[OK] Music generated! Audio hex length: {len(hex_audio)}")
# ── Stage 3: Saving ──────────────────────────────
yield sse_event({
"stage": "saving",
"progress": 90,
"message": "音乐生成完成,正在保存..."
})
save_dir = os.path.join(os.path.dirname(__file__) or ".", "Capybara music")
os.makedirs(save_dir, exist_ok=True)
safe_name = re.sub(r'[^\w\u4e00-\u9fff]', '', req.text)[:20] or "ai_song"
filename = f"{safe_name}_{int(time.time())}.mp3"
filepath = os.path.join(save_dir, filename)
audio_bytes = bytes.fromhex(hex_audio)
with open(filepath, "wb") as f:
f.write(audio_bytes)
log(f"[Saved] {filepath}")
# Save lyrics txt
lyrics_text = metadata.get("lyrics", "")
if lyrics_text:
lyrics_dir = os.path.join(save_dir, "lyrics")
os.makedirs(lyrics_dir, exist_ok=True)
lyrics_filename = f"{safe_name}_{int(time.time())}.txt"
with open(os.path.join(lyrics_dir, lyrics_filename), "w", encoding="utf-8") as lf:
lf.write(lyrics_text)
relative_path = f"Capybara music/{filename}"
# ── Done ─────────────────────────────────────────
yield sse_event({
"stage": "done",
"progress": 100,
"message": "新歌出炉!",
"status": "success",
"file_path": relative_path,
"metadata": metadata
})
else:
error_msg = base_resp.get("status_msg", "unknown")
error_code = base_resp.get("status_code", -1)
log(f"[Error] Music Gen failed: {error_code} - {error_msg}")
yield sse_event({
"stage": "error",
"progress": 0,
"message": f"生成失败 ({error_code}): {error_msg}"
})
except requests.exceptions.Timeout:
log("[Error] Music Gen Timeout")
yield sse_event({
"stage": "error",
"progress": 0,
"message": "音乐生成超时,请稍后再试"
})
except Exception as e:
log(f"[Error] API exception: {e}")
yield sse_event({
"stage": "error",
"progress": 0,
"message": f"服务器错误: {str(e)}"
})
return StreamingResponse(
event_stream(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no",
"Connection": "keep-alive"
}
)
@app.get("/api/playlist")
def get_playlist():
"""Scan Capybara music/ directory and return full playlist with lyrics."""
music_dir = os.path.join(os.path.dirname(__file__) or ".", "Capybara music")
lyrics_dir = os.path.join(music_dir, "lyrics")
playlist = []
if not os.path.isdir(music_dir):
return {"playlist": []}
for f in sorted(os.listdir(music_dir)):
if not f.lower().endswith(".mp3"):
continue
name = f[:-4] # strip .mp3
# Read lyrics if available
lyrics = ""
lyrics_file = os.path.join(lyrics_dir, name + ".txt")
if os.path.isfile(lyrics_file):
try:
with open(lyrics_file, "r", encoding="utf-8") as lf:
lyrics = lf.read()
except Exception:
pass
# Display title: strip timestamp suffix like _1770367350
title = re.sub(r'_\d{10,}$', '', name)
playlist.append({
"title": title,
"audioUrl": f"Capybara music/{f}",
"lyrics": lyrics
})
return {"playlist": playlist}
if __name__ == "__main__":
print("[Server] Music Server running on http://localhost:3000")
uvicorn.run(app, host="0.0.0.0", port=3000)

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
.page-header {
display: flex;

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
html,
body {

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -7,7 +7,7 @@
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">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
background: #FDF9F3;

View File

@ -118,7 +118,7 @@ body {
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: var(--bg-base);
@ -128,7 +128,15 @@ body {
-moz-osx-font-smoothing: grayscale;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
}
/* Display Font - Outfit for headings */
h1, h2, h3, h4, h5, h6,
.page-title,
.pixel-text {
font-family: 'Outfit', 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
}
@ -957,7 +965,7 @@ body {
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: var(--bg-base);
@ -1089,7 +1097,7 @@ body {
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: var(--bg-base);

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@ -9,7 +9,7 @@
<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"
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600;700&family=Press+Start+2P&display=swap"
rel="stylesheet">
<style>
/* WiFi Config specific styles */

View File

@ -0,0 +1,79 @@
# 阶段性工作总结:水豚灵感电台
## 1. 项目概况
本项目核心为 **Airhub 智能家居控制中心** 的子模块 —— **「水豚灵感电台 (Water Capybara Music Workshop)」**。这是一个基于心情生成音乐的互动页面,结合了拟物化设计(黑胶唱片)与 AI 生成概念。
## 2. 已完成核心功能
### 2.1 视觉与动画
* **黑胶唱片播放器**
* 中心为水豚头像Capybara Icon
* 实现了播放时的旋转动画 (`animation: spin`)。
* **优化**:外圈旋转,内圈头像保持静止(分离层设计)。
* **状态**:暂停/播放状态切换,带有遮罩层显示的播放/暂停图标。
* **氛围卡片网格**
* 最初设计为 4 张卡片,后扩展为 **6 张卡片布局**。
* 包括Chill (松弛), Happy (快乐), Sleep (助眠), Focus (心流), Mystery (盲盒), Custom (自由创作)。
* **交互**:点击卡片即触发高亮并自动开始生成流程。
* **界面布局优化**
* 头部:依照 `device-control.html` 样式重构,左侧返回,右侧唱片架。
* 适配:针对移动端视口进行了全方位优化,缩小了唱片尺寸,调整了间距,修复了水平溢出问题。
### 2.2 交互逻辑重构 (Input to Card)
为了提升移动端体验,我们废弃了底部的常驻输入栏,改为“全卡片”交互:
* **自由创作卡片**:新增了第 6 张卡片“自由创作”。
* **输入弹窗**:点击“自由创作”后,底部升起输入弹窗。
* **逻辑**:输入文字 -> 确认 -> 触发 `custom` 模式的生成逻辑。
### 2.3 弹窗系统进化 (Modal to Bottom Sheet)
经历了从居中弹窗到底部抽屉的迭代:
* **样式**:宽度 100%,贴底显示,顶部圆角 (32px),磨砂玻璃效果。
* **适配**:增加了底部安全区 (`safe-area-inset-bottom`) 支持。
* **组件**
* **输入弹窗**:用于自由创作。
* **唱片架弹窗**用于展示历史生成记录Playlist
### 2.4 播放与列表逻辑
* **模拟生成**:点击卡片后,经过 3 秒 Loading 动画,模拟生成音乐。
* **播放控制**:支持播放/暂停进度条拖拽UI层时间显示。
* **播放列表**:生成成功的音乐会自动加入顶部“唱片架”,支持点击列表回放。
## 3. 关键技术修复 (Technical Fixes)
在此开发过程中,我们解决了一些顽固的 UI/交互问题,这些经验值得记录:
### 3.1 弹窗自动弹出 (Auto-Popup) 修复
* **问题**:页面刷新时,弹窗会自动闪现或保持打开状态。
* **原因**:单纯依赖 CSS `opacity` 结合 JS `classList` 切换在某些加载时序下不够稳健。
* **解决方案 (双保险)**
1. **HTML 层**:默认设置为 `style="display: none;"`
2. **JS 初始化**`DOMContentLoaded` 事件中强制执行关闭逻辑:
```javascript
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('inputModal');
if (modal) {
modal.classList.remove('active');
modal.style.display = 'none';
}
});
```
3. **动画逻辑**:切换时显式处理 `display: flex` -> `requestAnimationFrame` -> `opacity: 1` 的时序。
### 3.2 布局对齐 (Alignment)
* **问题**:弹窗在宽屏或特定分辨率下未居中,或右侧有留白。
* **解决方案**
* 容器统一使用 `width: 100%`
* 外层 Overlay 使用 `justify-content: center`
* 内层容器使用 `margin: 0` (底部抽屉模式) 消除干扰。
* 显式设置 `box-sizing: border-box` 防止 padding 导致的溢出。
## 4. 遗留/待办事项 (Next Steps)
* **API 对接**:目前使用的是模拟数据,需接入真实的 MiniMax Music API。
* **播放列表持久化**:目前刷新后列表重置,需对接 LocalStorage 或后端数据库。
* **多端适配**:目前专注于移动端竖屏体验,桌面端暂未专门适配。
## 5. 文件清单
* `music-creation.html`: 核心页面。
* `styles.css`: 全局样式表。
* `design_system.md`: 设计规范文档。
* `task.md`: 任务追踪表。

50
阶段总结/task.md Normal file
View File

@ -0,0 +1,50 @@
# 我的模块实现任务
## 页面创建
- [x] profile.html - 我的主页
- [x] profile-info.html - 个人信息
- [x] agent-manage.html - 角色记忆管理
- [x] settings.html - 设置页
- [x] help.html - 帮助中心
- [x] agreement.html - 用户协议
- [x] privacy.html - 隐私政策
- [x] collection-list.html - 个人信息收集清单
- [x] sharing-list.html - 第三方信息共享清单
## 导航更新
- [x] 更新 device-control.html 底部导航链接
- [x] 实现 notifications.html (支持下拉展开详情)
- [x] 实现 guide-feeding.html (动态图文手册)
- [/] 实现 music-creation.html (卡皮巴拉音乐工坊 - Minimax API)
- [x] 播放器视觉 (黑胶/气泡)
- [ ] **UI Refinements:**
- [ ] Increase Playlist Modal Height
- [x] Unify "Storage Box" (收纳箱) -> "Record Shelf" (唱片架)
- [x] Move "Record Shelf" Icon to Header
- [x] Add Play/Pause toggle overlay on Vinyl (Always Visible)
- [x] Modify Vinyl Animation: Spin outer ring only (Capybara static)
- [ ] **Final Polish:**
- [x] Reduce Modal Height
- [x] Add Scroll Fade (Mask) for soft edges (Updated to 60px)
- [x] Adjust Grid Sizing (3 columns fit fully)
- [x] Hide Scrollbars (Vertical & Horizontal)
- [x] Replace Emoji Icon with SVG (Like Bell)
- [x] Add "唱片架" Label to Icon
- [x] Fix Header Back Button Style
- [x] Optimize Overall Layout (Compact Vinyl, Margins)
- [x] **Refactor Input to Card Layout** <!-- id: 4 -->
- [x] Remove Persistent Input Bar
- [x] Add 2 New Mood Cards (Focus & Custom)
- [x] Implement Custom Input Modal
- [x] Update Grid Layout for 6 Cards
- [x] Add Playback Progress Bar with Time Display
- [ ] Playlist Logic (Mock Data + Generation)
## 交互优化
- [x] 替换所有原生 alert/confirm 为自定义弹窗 (Glass Modal)
## 布局规范化
- [x] 统一法务页面 Gradient Mask Scroll 布局 (padding 120px, mask 100-130px)
- [x] 同步更新设备设置页 (.settings-content)
- [x] 写入 PRD 文档 (§ 3.3.2)
- [x] 写入 Design System