feat: app 端 ui 设计完成
This commit is contained in:
325
wei_ai_app/lib/screens/library/library_screen.dart
Normal file
325
wei_ai_app/lib/screens/library/library_screen.dart
Normal file
@@ -0,0 +1,325 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../data/mock_data.dart';
|
||||
import '../../models/scenario.dart';
|
||||
import '../../widgets/tab_content_layout.dart';
|
||||
|
||||
class LibraryScreen extends StatefulWidget {
|
||||
const LibraryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LibraryScreen> createState() => _LibraryScreenState();
|
||||
}
|
||||
|
||||
class _LibraryScreenState extends State<LibraryScreen> {
|
||||
String _activeCategory = '全部';
|
||||
|
||||
final List<String> _categories = ['全部', '职场', '邻家', '科幻', 'ASMR'];
|
||||
|
||||
List<Scenario> get _filteredScenarios {
|
||||
if (_activeCategory == '全部') return mockScenarios;
|
||||
return mockScenarios.where((s) =>
|
||||
s.category == _activeCategory || s.tags.contains(_activeCategory)
|
||||
).toList();
|
||||
}
|
||||
|
||||
Color _getIntensityColor(String intensity) {
|
||||
switch (intensity) {
|
||||
case 'Low':
|
||||
return const Color(0xFF34D399); // Emerald
|
||||
case 'Medium':
|
||||
return const Color(0xFF60A5FA); // Blue
|
||||
case 'High':
|
||||
return const Color(0xFFFBBF24); // Amber
|
||||
case 'Extreme':
|
||||
return const Color(0xFFF472B6); // Pink
|
||||
default:
|
||||
return Colors.white.withOpacity(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const double bottomNavHeight = 90;
|
||||
|
||||
return TabContentLayout(
|
||||
child: Column(
|
||||
children: [
|
||||
// Filter Bar
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: _categories.length,
|
||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final category = _categories[index];
|
||||
final isActive = _activeCategory == category;
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _activeCategory = category),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: isActive
|
||||
? const LinearGradient(
|
||||
colors: [Color(0xFFC084FC), Color(0xFFF472B6)],
|
||||
)
|
||||
: null,
|
||||
color: isActive ? null : Colors.white.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isActive ? Colors.transparent : Colors.white.withOpacity(0.1),
|
||||
),
|
||||
boxShadow: isActive
|
||||
? [
|
||||
BoxShadow(
|
||||
color: const Color(0xFFC084FC).withOpacity(0.4),
|
||||
blurRadius: 15,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Text(
|
||||
category,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||
color: isActive ? Colors.white : Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Scenario List
|
||||
Expanded(
|
||||
child: _filteredScenarios.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(LucideIcons.activity, size: 32, color: Colors.white.withOpacity(0.5)),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'暂无相关剧本',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withOpacity(0.5)),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: EdgeInsets.fromLTRB(16, 8, 16, bottomNavHeight + 20),
|
||||
itemCount: _filteredScenarios.length,
|
||||
itemBuilder: (context, index) {
|
||||
final scenario = _filteredScenarios[index];
|
||||
return _ScenarioCard(
|
||||
scenario: scenario,
|
||||
intensityColor: _getIntensityColor(scenario.intensity),
|
||||
onTap: () {
|
||||
if (!scenario.isLocked) {
|
||||
context.push('/player/${scenario.id}');
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('需要积分解锁此高级内容'),
|
||||
backgroundColor: Color(0xFF4C1D95),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(duration: 400.ms, delay: (index * 80).ms)
|
||||
.slideX(begin: 0.05, end: 0);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScenarioCard extends StatelessWidget {
|
||||
final Scenario scenario;
|
||||
final Color intensityColor;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _ScenarioCard({
|
||||
required this.scenario,
|
||||
required this.intensityColor,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF4C1D95).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Left: Thumbnail
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.3),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image.network(
|
||||
scenario.cover,
|
||||
fit: BoxFit.cover,
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) return child;
|
||||
return Container(
|
||||
color: Colors.black26,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (scenario.isLocked)
|
||||
Container(
|
||||
color: const Color(0xFF2E1065).withOpacity(0.6),
|
||||
child: const Center(
|
||||
child: Icon(LucideIcons.lock, size: 16, color: Colors.white70),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
|
||||
// Center: Info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Category & Intensity
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.1)),
|
||||
),
|
||||
child: Text(
|
||||
scenario.category,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontFamily: 'monospace',
|
||||
letterSpacing: 1,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(LucideIcons.zap, size: 10, color: intensityColor),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
scenario.intensity,
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: intensityColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Title
|
||||
Text(
|
||||
scenario.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Tags
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
children: scenario.tags.take(3).map((tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: Colors.white.withOpacity(0.05)),
|
||||
),
|
||||
child: Text(
|
||||
'#$tag',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Right: Play Button
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: scenario.isLocked
|
||||
? Colors.white.withOpacity(0.05)
|
||||
: const Color(0xFFC084FC).withOpacity(0.2),
|
||||
),
|
||||
child: Center(
|
||||
child: Icon(
|
||||
scenario.isLocked ? LucideIcons.lock : LucideIcons.play,
|
||||
size: scenario.isLocked ? 16 : 18,
|
||||
color: scenario.isLocked
|
||||
? Colors.white.withOpacity(0.3)
|
||||
: const Color(0xFFC084FC),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user