From 12b7df42d6c8a80ae1e50ce3d2872e34655912ae Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 25 Feb 2026 11:01:09 +0800 Subject: [PATCH] feat(home): add home screen and sheet --- apps/lib/core/theme/design_tokens.dart | 17 ++ .../contacts/ui/screens/contacts_screen.dart | 244 ++++++++++++++++++ .../features/home/ui/screens/home_screen.dart | 220 ++++++++++++++++ .../features/home/ui/screens/home_sheet.dart | 113 ++++++++ 4 files changed, 594 insertions(+) create mode 100644 apps/lib/features/contacts/ui/screens/contacts_screen.dart create mode 100644 apps/lib/features/home/ui/screens/home_screen.dart create mode 100644 apps/lib/features/home/ui/screens/home_sheet.dart diff --git a/apps/lib/core/theme/design_tokens.dart b/apps/lib/core/theme/design_tokens.dart index 373f4c1..04c937a 100644 --- a/apps/lib/core/theme/design_tokens.dart +++ b/apps/lib/core/theme/design_tokens.dart @@ -28,6 +28,23 @@ class AppColors { static const red600 = Color(0xFFDC2626); static const red500 = Color(0xFFEF4444); + static const red400 = Color(0xFFD14343); + + static const messageBg = Color(0xFFF8FAFC); + static const messageCardBg = Color(0xFFFFFFFF); + static const messageTagBg = Color(0xFFEAF3FF); + static const messageBtnWrap = Color(0xFFF8FAFF); + static const messageBtnBorder = Color(0xFFDEE7F6); + static const messageCardBorder = Color(0xFFE3EAF6); + static const messageCalendarBg = Color(0xFFEEF4FF); + static const messageArrowColor = Color(0xFF9CAFC8); + static const messageTipBg = Color(0xFFF8FAFF); + static const messageTipBorder = Color(0xFFDCE6F4); + static const messageRejectBorder = Color(0xFFF1C9CE); + static const messageAcceptBorder = Color(0xFFCFE1FB); + static const messagePlaceholder = Color(0xFF9AAAC1); + static const messageInputBorder = Color(0xFFDCE5F4); + static const messageReasonBorder = Color(0xFFE6ECF7); static const amber600 = Color(0xFFD97706); static const amber500 = Color(0xFFF59E0B); diff --git a/apps/lib/features/contacts/ui/screens/contacts_screen.dart b/apps/lib/features/contacts/ui/screens/contacts_screen.dart new file mode 100644 index 0000000..aa9fbe0 --- /dev/null +++ b/apps/lib/features/contacts/ui/screens/contacts_screen.dart @@ -0,0 +1,244 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../../../../core/theme/design_tokens.dart'; +import '../../../../shared/widgets/page_header.dart' as widgets; + +class ContactsScreen extends StatefulWidget { + const ContactsScreen({super.key}); + + @override + State createState() => _ContactsScreenState(); +} + +class _ContactsScreenState extends State { + final _searchController = TextEditingController(); + + final List _recentContacts = [ + ContactItem( + name: 'Toki', + email: 'toki@xunmee.com', + color: AppColors.blue500, + ), + ContactItem( + name: 'Mina', + email: 'mina@xunmee.com', + color: AppColors.violet600, + ), + ]; + + final List _allContacts = [ + ContactItem(name: 'Aki', email: 'aki@xunmee.com', color: AppColors.blue600), + ContactItem( + name: 'Lynn', + email: 'lynn@xunmee.com', + color: const Color(0xFF0EA5E9), + ), + ContactItem( + name: 'Nora', + email: 'nora@xunmee.com', + color: AppColors.violet500, + ), + ]; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + body: SafeArea( + child: Column( + children: [ + const PageHeader(leading: BackButton()), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSearchRow(), + const SizedBox(height: 16), + _buildSectionTitle('最近联系'), + const SizedBox(height: 8), + _buildContactCard(_recentContacts), + const SizedBox(height: 16), + _buildSectionTitle('全部联系人'), + const SizedBox(height: 8), + _buildContactCard(_allContacts), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSearchRow() { + return Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () {}, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFF), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFE4EBF7)), + ), + child: Row( + children: [ + const Icon(Icons.search, size: 16, color: AppColors.slate400), + const SizedBox(width: 8), + Text( + '搜索联系人', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: AppColors.slate400, + ), + ), + ], + ), + ), + ), + ), + const SizedBox(width: 10), + GestureDetector( + onTap: () => context.push('/contacts/add'), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: const Color(0xFFF1F7FF), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: const Color(0xFFD7E6FF)), + ), + child: const Icon( + Icons.person_add, + size: 16, + color: AppColors.blue500, + ), + ), + ), + ], + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: AppColors.slate500, + ), + ); + } + + Widget _buildContactCard(List contacts) { + return Container( + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFFE3EAF6)), + ), + child: Column( + children: [ + for (int i = 0; i < contacts.length; i++) ...[ + _buildContactItem(contacts[i]), + if (i < contacts.length - 1) + Container( + height: 1, + margin: const EdgeInsets.symmetric(horizontal: 14), + color: const Color(0xFFEEF2F7), + ), + ], + ], + ), + ); + } + + Widget _buildContactItem(ContactItem contact) { + return GestureDetector( + onTap: () => context.push('/contacts/add?id=${contact.email}'), + child: Container( + height: 70, + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Row( + children: [ + Container( + width: 42, + height: 42, + decoration: BoxDecoration( + color: _getAvatarBackground(contact.color), + borderRadius: BorderRadius.circular(21), + border: Border.all(color: _getAvatarBorder(contact.color)), + ), + child: Icon(Icons.person, size: 18, color: contact.color), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + contact.name, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: AppColors.slate900, + ), + ), + const SizedBox(height: 4), + Text( + contact.email, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + color: AppColors.slate500, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Color _getAvatarBackground(Color color) { + if (color == AppColors.blue500) return const Color(0xFFEEF4FF); + if (color == AppColors.violet600) return const Color(0xFFF3F7FF); + if (color == AppColors.blue600) return const Color(0xFFEDF5FF); + if (color == const Color(0xFF0EA5E9)) return const Color(0xFFF2F8FF); + if (color == AppColors.violet500) return const Color(0xFFF5F7FF); + return const Color(0xFFEEF4FF); + } + + Color _getAvatarBorder(Color color) { + if (color == AppColors.blue500) return const Color(0xFFDDE8FB); + if (color == AppColors.violet600) return const Color(0xFFE2EAFB); + if (color == AppColors.blue600) return const Color(0xFFDCE9FB); + if (color == const Color(0xFF0EA5E9)) return const Color(0xFFDFEAFA); + if (color == AppColors.violet500) return const Color(0xFFE4E8FA); + return const Color(0xFFDDE8FB); + } +} + +class ContactItem { + final String name; + final String email; + final Color color; + + ContactItem({required this.name, required this.email, required this.color}); +} diff --git a/apps/lib/features/home/ui/screens/home_screen.dart b/apps/lib/features/home/ui/screens/home_screen.dart new file mode 100644 index 0000000..2477db0 --- /dev/null +++ b/apps/lib/features/home/ui/screens/home_screen.dart @@ -0,0 +1,220 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../../../core/theme/design_tokens.dart'; +import 'home_sheet.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + body: SafeArea( + child: Column( + children: [ + _buildHeader(), + Expanded(child: _buildChatArea()), + _buildInputContainer(context), + ], + ), + ), + ); + } + + Widget _buildHeader() { + return SizedBox( + height: 60, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: const Icon( + LucideIcons.settings, + size: 24, + color: AppColors.slate900, + ), + onPressed: () {}, + ), + Row( + children: [ + IconButton( + icon: const Icon( + LucideIcons.calendar, + size: 24, + color: AppColors.slate900, + ), + onPressed: () {}, + ), + const SizedBox(width: 16), + IconButton( + icon: const Icon( + LucideIcons.messageSquare, + size: 24, + color: AppColors.slate900, + ), + onPressed: () {}, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildChatArea() { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _buildUserMessageRow(), + const SizedBox(height: 16), + _buildTodoCard(), + ], + ), + ); + } + + Widget _buildUserMessageRow() { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Expanded(child: SizedBox()), + Container( + padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9), + decoration: BoxDecoration( + color: const Color(0xFFEAF1FB), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), + bottomLeft: Radius.circular(12), + bottomRight: Radius.circular(0), + ), + ), + child: const Text( + '明天提醒我开会', + style: TextStyle(fontSize: 14, color: AppColors.slate900), + ), + ), + ], + ); + } + + Widget _buildTodoCard() { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + width: 4, + height: 60, + decoration: const BoxDecoration( + color: AppColors.blue500, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(4), + bottomLeft: Radius.circular(4), + ), + ), + ), + const SizedBox(width: 12), + const Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '明天 10:00', + style: TextStyle(fontSize: 12, color: AppColors.slate500), + ), + SizedBox(height: 4), + Text( + '开会', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColors.slate900, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildInputContainer(BuildContext context) { + return Container( + height: 80, + padding: const EdgeInsets.all(16), + color: const Color(0xFFF8FAFC), + child: Row( + children: [ + GestureDetector( + onTap: () => _showBottomSheet(context), + child: Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: AppColors.white, + shape: BoxShape.circle, + border: Border.all(color: const Color(0xFFE2E8F0)), + ), + child: const Icon( + LucideIcons.plus, + size: 20, + color: AppColors.slate500, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.circular(24), + ), + child: Row( + children: [ + const Expanded( + child: TextField( + decoration: InputDecoration( + hintText: '输入消息...', + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + ), + ), + const Icon( + LucideIcons.mic, + size: 20, + color: AppColors.slate500, + ), + ], + ), + ), + ), + ], + ), + ); + } + + void _showBottomSheet(BuildContext context) { + showModalBottomSheet( + context: context, + backgroundColor: Colors.transparent, + isScrollControlled: true, + builder: (context) => const HomeSheet(), + ); + } +} diff --git a/apps/lib/features/home/ui/screens/home_sheet.dart b/apps/lib/features/home/ui/screens/home_sheet.dart new file mode 100644 index 0000000..65f5b09 --- /dev/null +++ b/apps/lib/features/home/ui/screens/home_sheet.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:lucide_icons/lucide_icons.dart'; +import '../../../../core/theme/design_tokens.dart'; + +class HomeSheet extends StatelessWidget { + const HomeSheet({super.key}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.of(context).pop(), + child: Container( + color: const Color(0x4D0F172A), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + GestureDetector( + onTap: () {}, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: AppColors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(28)), + ), + child: Column( + children: [ + Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: AppColors.slate300, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(height: 16), + _buildSheetContent(context), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSheetContent(BuildContext context) { + return SizedBox( + height: 280, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildOptionCard( + context: context, + icon: LucideIcons.camera, + label: '拍照', + onTap: () => _handleCameraTap(context), + ), + const SizedBox(width: 24), + _buildOptionCard( + context: context, + icon: LucideIcons.image, + label: '相册', + onTap: () => _handlePhotoTap(context), + ), + ], + ), + ); + } + + Widget _buildOptionCard({ + required BuildContext context, + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: AppColors.blue50, + borderRadius: BorderRadius.circular(16), + ), + child: Icon(icon, size: 32, color: AppColors.blue500), + ), + const SizedBox(height: 12), + Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.slate700, + ), + ), + ], + ), + ); + } + + void _handleCameraTap(BuildContext context) { + Navigator.of(context).pop(); + } + + void _handlePhotoTap(BuildContext context) { + Navigator.of(context).pop(); + } +}