167 lines
4.9 KiB
Dart
167 lines
4.9 KiB
Dart
import 'package:autosos_flutter/model/misc.dart';
|
||
import 'package:autosos_flutter/provider/providers.dart';
|
||
import 'package:autosos_flutter/router/app_router.dart';
|
||
import 'package:autosos_flutter/widget/multi_state_view.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:intl/intl.dart';
|
||
|
||
/// 消息中心(对应 Android MessengerActivity)
|
||
class MessengerPage extends ConsumerStatefulWidget {
|
||
const MessengerPage({super.key});
|
||
|
||
@override
|
||
ConsumerState<MessengerPage> createState() => _MessengerPageState();
|
||
}
|
||
|
||
class _MessengerPageState extends ConsumerState<MessengerPage> {
|
||
MultiState _state = MultiState.loading;
|
||
List<MessengerItem> _items = [];
|
||
int _page = 1;
|
||
bool _hasMore = true;
|
||
bool _loadingMore = false;
|
||
final _scroll = ScrollController();
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_load(reset: true);
|
||
_scroll.addListener(() {
|
||
if (_scroll.position.pixels >=
|
||
_scroll.position.maxScrollExtent - 200) {
|
||
_load();
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_scroll.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _load({bool reset = false}) async {
|
||
if (reset) {
|
||
_page = 1;
|
||
_hasMore = true;
|
||
_items = [];
|
||
setState(() => _state = MultiState.loading);
|
||
}
|
||
if (!_hasMore || _loadingMore) return;
|
||
_loadingMore = true;
|
||
try {
|
||
final resp = await ref.read(userApiProvider).messages(
|
||
page: _page,
|
||
size: 20,
|
||
);
|
||
if (!mounted) return;
|
||
if (!resp.isSuccess) {
|
||
if (reset) setState(() => _state = MultiState.error);
|
||
return;
|
||
}
|
||
final list = resp.data?.list ?? [];
|
||
_items.addAll(list);
|
||
_hasMore = (resp.data?.hasMore ?? false) && list.length >= 20;
|
||
_page++;
|
||
setState(() {
|
||
_state = _items.isEmpty ? MultiState.empty : MultiState.success;
|
||
});
|
||
} catch (e) {
|
||
if (mounted && reset) setState(() => _state = MultiState.error);
|
||
} finally {
|
||
_loadingMore = false;
|
||
}
|
||
}
|
||
|
||
Future<void> _onTap(MessengerItem item) async {
|
||
if (!item.read) {
|
||
try {
|
||
await ref.read(userApiProvider).readMessage(item.id);
|
||
} catch (_) {}
|
||
}
|
||
if (!mounted) return;
|
||
setState(() {
|
||
final i = _items.indexWhere((e) => e.id == item.id);
|
||
if (i >= 0) {
|
||
_items[i] = MessengerItem(
|
||
id: item.id,
|
||
title: item.title,
|
||
content: item.content,
|
||
createdAt: item.createdAt,
|
||
read: true,
|
||
link: item.link,
|
||
);
|
||
}
|
||
});
|
||
if (item.link != null && item.link!.isNotEmpty) {
|
||
context.push(
|
||
'${Routes.webview}?url=${Uri.encodeComponent(item.link!)}&title=${Uri.encodeComponent(item.title)}',
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('消息中心')),
|
||
body: RefreshIndicator(
|
||
onRefresh: () => _load(reset: true),
|
||
child: MultiStateView(
|
||
state: _state,
|
||
emptyText: '暂无消息',
|
||
onRetry: () => _load(reset: true),
|
||
child: ListView.separated(
|
||
controller: _scroll,
|
||
itemCount: _items.length + (_hasMore ? 1 : 0),
|
||
separatorBuilder: (_, __) => Divider(
|
||
height: 1,
|
||
color: Colors.grey.shade200,
|
||
indent: 16,
|
||
),
|
||
itemBuilder: (ctx, i) {
|
||
if (i >= _items.length) {
|
||
return const Padding(
|
||
padding: EdgeInsets.all(16),
|
||
child: Center(
|
||
child: CircularProgressIndicator(strokeWidth: 2),
|
||
),
|
||
);
|
||
}
|
||
final m = _items[i];
|
||
return ListTile(
|
||
leading: CircleAvatar(
|
||
backgroundColor: m.read
|
||
? Colors.grey.shade300
|
||
: Theme.of(context).primaryColor,
|
||
child: const Icon(Icons.notifications,
|
||
color: Colors.white, size: 20),
|
||
),
|
||
title: Text(
|
||
m.title,
|
||
style: TextStyle(
|
||
fontWeight: m.read
|
||
? FontWeight.normal
|
||
: FontWeight.bold,
|
||
),
|
||
),
|
||
subtitle: Text(
|
||
m.content,
|
||
maxLines: 2,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(fontSize: 13),
|
||
),
|
||
trailing: Text(
|
||
DateFormat('MM-dd HH:mm').format(m.createdAt),
|
||
style: const TextStyle(fontSize: 11, color: Colors.grey),
|
||
),
|
||
onTap: () => _onTap(m),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|