diff --git a/client/lib/screens/account_page.dart b/client/lib/screens/account_page.dart index f91f661..d4cbf23 100644 --- a/client/lib/screens/account_page.dart +++ b/client/lib/screens/account_page.dart @@ -11,8 +11,10 @@ import '../state/auth_provider.dart'; import '../state/navigation_provider.dart'; import '../widgets/account_screens.dart'; import '../widgets/app_top_bar.dart'; +import '../widgets/notification_bell.dart'; import '../widgets/pangolin_icons.dart'; import 'invite_page.dart'; +import 'notifications_page.dart'; import 'orders_page.dart'; import 'payment_page.dart'; import 'purchase_page.dart'; @@ -129,7 +131,13 @@ class AccountPage extends ConsumerWidget { if (isWide) return body; return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - AppTopBar(brand: t.brand), + AppTopBar( + brand: t.brand, + trailing: NotificationBell( + hasUnread: true, + onTap: () => open(NavView.notifications, NotificationsScreen(t: t)), + ), + ), PageTitle(title: t.meTitle), Expanded(child: body), ]); diff --git a/client/lib/screens/notifications_page.dart b/client/lib/screens/notifications_page.dart new file mode 100644 index 0000000..7b3e07b --- /dev/null +++ b/client/lib/screens/notifications_page.dart @@ -0,0 +1,159 @@ +// notifications_page.dart — 通知(占位屏) +// +// TODO(spec-3): 列表来自 GET /v1/notices,已读态本地持久化。当前为 3 条静态示例, +// 仅供 IA 走查——接后端前不接任何真实业务逻辑。 +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../l10n/app_text.dart'; +import '../pangolin_theme.dart'; +import '../widgets/pangolin_icons.dart'; +import '../widgets/status_pill.dart'; + +typedef _Notice = ({ + IconData icon, + PangolinStatus status, + String typeLabel, + String title, + String sub, + bool read, +}); + +class NotificationsScreen extends ConsumerWidget { + const NotificationsScreen({super.key, required this.t, this.onBack, this.embedded = false}); + final AppText t; + final VoidCallback? onBack; + final bool embedded; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final c = context.pangolin; + + // 静态示例数据(占位):News / Feature / Important 各一条,类型 pill 文案走 t.notifType*。 + final notices = <_Notice>[ + ( + icon: PangolinIcons.bell, + status: PangolinStatus.neutral, + typeLabel: t.notifTypeNews, + title: '产品动态标题占位', + sub: '2 天前', + read: false, + ), + ( + icon: PangolinIcons.zap, + status: PangolinStatus.connected, + typeLabel: t.notifTypeFeature, + title: '新功能上线标题占位', + sub: '5 天前', + read: false, + ), + ( + icon: PangolinIcons.alertTriangle, + status: PangolinStatus.error, + typeLabel: t.notifTypeImportant, + title: '重要通知标题占位', + sub: '1 周前', + read: true, + ), + ]; + + Widget empty() => Padding( + padding: const EdgeInsets.only(top: 60), + child: Column(children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration(color: c.bgSubtle, shape: BoxShape.circle), + child: Icon(PangolinIcons.bell, size: 26, color: c.fg3), + ), + const SizedBox(height: 14), + Text(t.notifEmpty, style: PangolinText.sm.copyWith(color: c.fg3)), + ]), + ); + + Widget row(_Notice n, bool divider) { + return Container( + decoration: BoxDecoration(border: divider ? Border(bottom: BorderSide(color: c.border)) : null), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13), + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Stack(clipBehavior: Clip.none, children: [ + Container( + width: 38, + height: 38, + decoration: BoxDecoration(color: c.accentSubtle, borderRadius: BorderRadius.circular(PangolinRadius.md)), + child: Icon(n.icon, size: 18, color: c.accent), + ), + if (!n.read) + Positioned( + top: -2, + right: -2, + child: Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: c.danger, shape: BoxShape.circle), + ), + ), + ]), + const SizedBox(width: 12), + Expanded( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Row(children: [ + Expanded( + child: Text(n.title, + overflow: TextOverflow.ellipsis, + style: PangolinText.sm.copyWith( + color: c.fg1, fontWeight: n.read ? FontWeight.w500 : FontWeight.w700, fontSize: 14.5)), + ), + const SizedBox(width: 8), + StatusPill(label: n.typeLabel, status: n.status), + ]), + const SizedBox(height: 4), + Text(n.sub, style: PangolinText.caption.copyWith(color: c.fg3, fontWeight: FontWeight.w400)), + ]), + ), + ]), + ); + } + + Widget content() => ListView( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 24), + children: [ + if (notices.isEmpty) + empty() + else + Container( + decoration: BoxDecoration( + color: c.surface, + borderRadius: BorderRadius.circular(PangolinRadius.lg), + border: Border.all(color: c.border), + boxShadow: PangolinShadow.sm, + ), + clipBehavior: Clip.antiAlias, + child: Column(children: [ + for (var i = 0; i < notices.length; i++) row(notices[i], i < notices.length - 1), + ]), + ), + ], + ); + + if (embedded) return content(); + return Scaffold( + backgroundColor: c.bg, + body: SafeArea( + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.fromLTRB(8, 6, 16, 10), + child: Row(children: [ + IconButton( + onPressed: onBack ?? () => Navigator.of(context).maybePop(), + icon: Icon(PangolinIcons.arrowLeft, size: 22, color: c.fg1), + ), + Text(t.notifTitle, style: PangolinText.h3.copyWith(color: c.fg1, fontWeight: FontWeight.w700)), + ]), + ), + Expanded(child: content()), + ]), + ), + ); + } +} diff --git a/client/lib/shell/desktop_shell.dart b/client/lib/shell/desktop_shell.dart index b846edd..94b031a 100644 --- a/client/lib/shell/desktop_shell.dart +++ b/client/lib/shell/desktop_shell.dart @@ -10,6 +10,7 @@ import '../screens/connect_page.dart'; import '../screens/contact_page.dart'; import '../screens/invite_page.dart'; import '../screens/nodes_page.dart'; +import '../screens/notifications_page.dart'; import '../screens/orders_page.dart'; import '../screens/payment_page.dart'; import '../screens/purchase_page.dart'; @@ -91,8 +92,7 @@ class DesktopShell extends ConsumerWidget { case NavView.invite: return InviteScreen(t: t, embedded: true); case NavView.notifications: - // TODO(task-6): 通知屏尚未接线,占位回落账户页,避免非穷尽 switch 编译失败。 - return const AccountPage(isWide: true); + return NotificationsScreen(t: t, embedded: true); } }