feat: 完成智能电网电工端 Flutter 应用初始化

创建完整的 Flutter 项目结构,包含 Android 和 iOS 平台配置、核心网络层、路由、状态管理及业务模块(订单、收益、认证),适配原 uni-app 项目功能逻辑
This commit is contained in:
andyesfly committed 2026-10-06 11:54:10 +08:00
commit 56830e94ec
108 files changed
+9272

No files matched your search

@@ -0,0 +1,358 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/network/api_response.dart';
import '../../../core/providers.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_typography.dart';
import '../../../core/utils/format_util.dart';
import '../../../shared/widgets/app_feedback.dart';
import '../../../shared/widgets/x_card.dart';
import '../../../shared/widgets/x_common.dart';
import '../../../shared/widgets/x_top_bar.dart';
import '../data/notice_api.dart';
/// 消息中心:站内通知列表 + 已读标记 + 分页加载。
///
/// 列表在页面内维护(分页游标语义强于 Riverpod 缓存),未读数经
/// [noticeUnreadCountProvider] 统一刷新。
class NoticeCenterPage extends ConsumerStatefulWidget {
const NoticeCenterPage({super.key});
@override
ConsumerState<NoticeCenterPage> createState() => _NoticeCenterPageState();
}
class _NoticeCenterPageState extends ConsumerState<NoticeCenterPage> {
static const _pageSize = 10;
final _scroll = ScrollController();
final _items = <Notice>[];
int _page = 0;
bool _loading = false;
bool _loadingMore = false;
bool _hasMore = true;
String? _error;
bool get _hasUnread => _items.any((n) => !n.isRead);
@override
void initState() {
super.initState();
_scroll.addListener(_onScroll);
_load(reset: true);
}
@override
void dispose() {
_scroll
..removeListener(_onScroll)
..dispose();
super.dispose();
}
void _onScroll() {
if (!_scroll.hasClients) return;
final position = _scroll.position;
if (position.pixels >= position.maxScrollExtent - 200) _loadMore();
}
/// 加载一页。`reset` 为真时回到第 1 页(下拉刷新 / 重试)。
Future<void> _load({bool reset = false}) async {
if (reset) {
if (_loading) return;
setState(() {
_loading = true;
_error = null;
});
} else {
if (_loading || _loadingMore || !_hasMore) return;
setState(() => _loadingMore = true);
}
final nextPage = reset ? 1 : _page + 1;
try {
final res = await ref
.read(noticeApiProvider)
.list(page: nextPage, pageSize: _pageSize);
if (!mounted) return;
if (!res.isOk || res.data == null) {
setState(() {
_loading = false;
_loadingMore = false;
if (reset) _error = res.message.isEmpty ? '消息加载失败' : res.message;
});
return;
}
final data = res.data!;
setState(() {
if (reset) _items.clear();
_items.addAll(data.list);
_page = data.page;
_hasMore = data.list.isNotEmpty && _items.length < data.total;
_loading = false;
_loadingMore = false;
_error = null;
});
} on ApiException catch (e) {
if (!mounted) return;
setState(() {
_loading = false;
_loadingMore = false;
if (reset) _error = e.message;
});
}
}
Future<void> _loadMore() => _load();
Future<void> _markRead(Notice notice) async {
if (notice.isRead) return;
try {
final res = await ref.read(noticeApiProvider).markRead(notice.id);
if (!mounted) return;
if (!res.isOk) {
showAppToast(res.message.isEmpty ? '操作失败' : res.message);
return;
}
final index = _items.indexWhere((n) => n.id == notice.id);
if (index >= 0) {
setState(() => _items[index] = _items[index].copiedWithRead());
}
ref.read(noticeUnreadCountProvider.notifier).refresh();
} on ApiException catch (e) {
if (mounted) showAppToast(e.message);
}
}
Future<void> _markAllRead() async {
if (!_hasUnread) return;
try {
final res = await ref.read(noticeApiProvider).readAll();
if (!mounted) return;
if (!res.isOk) {
showAppToast(res.message.isEmpty ? '操作失败' : res.message);
return;
}
setState(() {
for (var i = 0; i < _items.length; i++) {
if (!_items[i].isRead) _items[i] = _items[i].copiedWithRead();
}
});
showAppToast('已全部标记为已读', success: true);
ref.read(noticeUnreadCountProvider.notifier).refresh();
} on ApiException catch (e) {
if (mounted) showAppToast(e.message);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.surfaceContainerLow,
body: XPageShell(
backgroundColor: AppColors.surfaceContainerLow,
topBar: XTopBar.nav(
title: '消息中心',
onBack: () => context.pop(),
trailing: _hasUnread
? GestureDetector(
onTap: _markAllRead,
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 4),
child: Text(
'全部已读',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.primary,
),
),
),
)
: const SizedBox.shrink(),
),
child: _buildBody(),
),
);
}
Widget _buildBody() {
if (_loading && _items.isEmpty) {
return const Center(
child: SizedBox(
width: 26,
height: 26,
child: CircularProgressIndicator(
strokeWidth: 2.4,
valueColor: AlwaysStoppedAnimation(AppColors.primary),
),
),
);
}
if (_items.isEmpty) {
return RefreshIndicator(
color: AppColors.primary,
onRefresh: () => _load(reset: true),
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
if (_error != null)
XPlaceholder(text: '消息加载失败\n$_error')
else
const XPlaceholder(text: '暂无消息'),
if (_error != null)
Center(
child: TextButton(
onPressed: () => _load(reset: true),
child: const Text('点击重试'),
),
),
],
),
);
}
return RefreshIndicator(
color: AppColors.primary,
onRefresh: () => _load(reset: true),
child: ListView.separated(
controller: _scroll,
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
itemCount: _items.length + 1,
separatorBuilder: (_, _) => const SizedBox(height: 12),
itemBuilder: (context, index) {
if (index == _items.length) return _buildFooter();
final notice = _items[index];
return _NoticeTile(
notice: notice,
onTap: notice.isRead ? null : () => _markRead(notice),
);
},
),
);
}
Widget _buildFooter() {
if (_loadingMore) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(AppColors.primary),
),
),
),
);
}
if (_hasMore) {
return GestureDetector(
onTap: () => _loadMore(),
behavior: HitTestBehavior.opaque,
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(
'加载更多',
style: TextStyle(fontSize: 14, color: AppColors.primary),
),
),
),
);
}
return const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(
child: Text(
'没有更多了',
style: TextStyle(fontSize: 13, color: AppColors.textMuted),
),
),
);
}
}
/// 单条消息卡片:未读有蓝底高亮 + 红点,点击标记已读。
class _NoticeTile extends StatelessWidget {
const _NoticeTile({required this.notice, this.onTap});
final Notice notice;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
final unread = !notice.isRead;
return XCard(
padding: const EdgeInsets.all(16),
background: unread
? AppColors.accentSubtle
: AppColors.surfaceContainerLowest,
onTap: onTap,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (unread)
Padding(
padding: const EdgeInsets.only(top: 7, right: 8),
child: Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppColors.error,
shape: BoxShape.circle,
),
),
),
Expanded(
child: Text(
notice.title.isEmpty ? '系统通知' : notice.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTypography.bodyLg.copyWith(
fontWeight: unread ? FontWeight.w600 : FontWeight.w500,
color: AppColors.onSurface,
),
),
),
const SizedBox(width: 12),
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
formatDateTime(notice.createdAt),
style: AppTypography.labelCaps.copyWith(
color: AppColors.textMuted,
),
),
),
],
),
if (notice.content.isNotEmpty) ...[
const SizedBox(height: 8),
Padding(
padding: EdgeInsets.only(left: unread ? 16 : 0),
child: Text(
notice.content,
style: AppTypography.bodyMd.copyWith(
color: AppColors.onSurfaceVariant,
height: 1.5,
),
),
),
],
],
),
);
}
}