feat(notif): 通知占位屏 + 移动顶栏铃铛(Spec ③接口占位)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-12 18:01:07 +08:00
parent dd3323add9
commit f5669c1a67
3 changed files with 170 additions and 3 deletions
+9 -1
View File
@@ -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),
]);
+159
View File
@@ -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()),
]),
),
);
}
}
+2 -2
View File
@@ -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);
}
}