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,482 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../../core/providers.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart';
import '../../../core/theme/app_typography.dart';
import '../../../shared/widgets/app_feedback.dart';
import '../../../shared/widgets/x_card.dart';
import '../../../shared/widgets/x_common.dart';
import '../../../shared/widgets/x_icon.dart';
import '../../../shared/widgets/x_top_bar.dart';
import '../application/earnings_providers.dart';
import '../data/earnings_models.dart';
/// rgba(255,255,255,0.8) —— 余额卡内的次级文字。
const _white80 = Color(0xCCFFFFFF);
/// 柱状图绘图区高度(趋势卡总高 160,余下留给 8px 间距与周标签)。
const _barPlotHeight = 130.0;
/// 「收益」Tab,逐像素复刻源 `src/pages/earnings/index.vue`。
///
/// 顶栏由本页渲染;底部 Tab 栏由 `TabsPage` 统一提供,故此处不渲染导航。
class EarningsTab extends ConsumerWidget {
const EarningsTab({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final summary = ref.watch(earningsSummaryProvider);
final unread = ref.watch(noticeUnreadCountProvider);
return XPageShell(
topBar: XTopBar.brand(
height: 56,
logoSize: 28,
titleSize: 18,
titleWeight: FontWeight.w600,
trailingBadge: unread,
onTrailingTap: () => context.push('/notice'),
),
child: RefreshIndicator(
color: AppColors.primary,
onRefresh: () async {
ref.invalidate(earningsSummaryProvider);
await ref.read(earningsSummaryProvider.future);
},
child: summary.when(
loading: () => _placeholder('加载中...'),
error: (error, _) => _placeholder(error.toString()),
data: (data) => ListView(
physics: const AlwaysScrollableScrollPhysics(),
padding: const EdgeInsets.only(bottom: 96),
children: [
const Padding(
padding: EdgeInsets.fromLTRB(16, 24, 16, 24),
child: XPageTitle(
title: '收益中心',
subtitle: '管理您的职业收入与提现详情',
),
),
_balanceCard(data),
_trendSection(data),
_transactionsSection(data),
_referralCard(),
],
),
),
),
);
}
/// 加载 / 错误占位,保持可下拉(`AlwaysScrollableScrollPhysics`)。
Widget _placeholder(String text) => ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [XPlaceholder(text: text)],
);
/// 余额卡:渐变底 + 总余额 + 两列指标 + 白色提现按钮。
Widget _balanceCard(EarningsSummary data) {
return Container(
margin: const EdgeInsets.fromLTRB(16, 0, 16, 24),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primaryDeep, AppColors.primary],
),
borderRadius: BorderRadius.circular(AppRadius.xl),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'总余额',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: _white80,
letterSpacing: 0.24,
),
),
const SizedBox(height: 8),
Text(
'¥ ${_formatAmount(data.totalBalance)}',
style: AppTypography.displayLg.copyWith(
color: AppColors.onPrimary,
letterSpacing: -0.8,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: _balanceItem(
'可提现金额',
_formatAmount(data.withdrawableAmount),
),
),
const SizedBox(width: 16),
Expanded(
child: _balanceItem(
'本月累计收益',
_formatAmount(data.monthlyEarnings),
),
),
],
),
const SizedBox(height: 24),
SizedBox(
height: 44,
width: double.infinity,
child: Material(
color: AppColors.surfaceContainerLowest,
borderRadius: BorderRadius.circular(10),
child: InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () => showAppToast('提现功能'),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
XIcon('earnings', size: 20, color: AppColors.primary),
SizedBox(width: 8),
Text(
'立即提现',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
],
),
),
),
),
],
),
);
}
Widget _balanceItem(String label, String value) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 12, color: _white80)),
const SizedBox(height: 4),
Text(
'¥ $value',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.onPrimary,
),
),
],
);
/// 最近 7 天趋势:柱高取 `trendPercents`,高亮今天(末位)。
Widget _trendSection(EarningsSummary data) {
final percents = data.trendPercents;
final labels = _weekdayLabels();
final activeIndex = percents.isEmpty ? -1 : percents.length - 1;
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'最近7天趋势',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.onSurface,
),
),
GestureDetector(
onTap: () => showAppToast('近一周'),
behavior: HitTestBehavior.opaque,
child: const Row(
children: [
Text(
'近一周',
style: TextStyle(
fontSize: 14,
color: AppColors.primary,
fontWeight: FontWeight.w500,
),
),
SizedBox(width: 4),
XIcon('arrow-down', size: 14, color: AppColors.primary),
],
),
),
],
),
const SizedBox(height: 12),
XCard(
radius: AppRadius.lg,
padding: const EdgeInsets.all(24),
child: SizedBox(
height: 160,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
for (var i = 0; i < percents.length; i++)
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FractionallySizedBox(
widthFactor: 0.8,
child: Container(
height: percents[i] / 100 * _barPlotHeight,
decoration: BoxDecoration(
color: i == activeIndex
? AppColors.primary
: AppColors.primaryFixedDim,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(4),
),
),
),
),
const SizedBox(height: 8),
Text(
labels[i % labels.length],
style: TextStyle(
fontSize: 10,
fontWeight: i == activeIndex
? FontWeight.w600
: FontWeight.w400,
color: i == activeIndex
? AppColors.primary
: AppColors.textMuted,
),
),
],
),
),
],
),
),
),
],
),
);
}
/// 收益明细:入账 / 提现两种配色 + 金额与状态。
Widget _transactionsSection(EarningsSummary data) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'收益明细',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.onSurface,
),
),
GestureDetector(
onTap: () => showAppToast('查看全部'),
behavior: HitTestBehavior.opaque,
child: const Text(
'查看全部',
style: TextStyle(
fontSize: 15,
color: AppColors.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 12),
for (var i = 0; i < data.transactions.length; i++) ...[
if (i > 0) const SizedBox(height: 8),
_transactionItem(data.transactions[i]),
],
],
),
);
}
Widget _transactionItem(EarningTransaction tx) {
final withdrawal = tx.isWithdrawal;
final settled = tx.status == 'settled' || tx.status == 'completed';
final statusColor = settled ? AppColors.primary : AppColors.textMuted;
return XCard(
radius: AppRadius.lg,
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
width: 40,
height: 40,
alignment: Alignment.center,
decoration: BoxDecoration(
color: withdrawal
? AppColors.surfaceVariant
: AppColors.secondaryFixed,
shape: BoxShape.circle,
),
child: XIcon(
'lightning',
size: 20,
color: withdrawal
? AppColors.onSurfaceVariant
: AppColors.onSecondaryFixed,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
tx.title,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: AppColors.onSurface,
),
),
const SizedBox(height: 2),
Text(
tx.subtitle,
style: const TextStyle(
fontSize: 13,
color: AppColors.textMuted,
),
),
],
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${withdrawal ? '-' : '+'}¥${_formatAmount(tx.amount.abs())}',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: withdrawal
? AppColors.onSurface
: AppColors.tertiaryContainer,
),
),
const SizedBox(height: 4),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: BoxDecoration(
color: statusColor,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
tx.statusText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: statusColor,
),
),
],
),
],
),
],
),
);
}
/// 邀请好友卡(源里无实际跳转,点击仅提示)。
Widget _referralCard() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: GestureDetector(
onTap: () => showAppToast('邀请好友加入'),
behavior: HitTestBehavior.opaque,
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: AppColors.secondaryFixed,
borderRadius: BorderRadius.circular(AppRadius.xl),
border: Border.all(color: AppColors.borderSoft),
boxShadow: AppShadows.card,
),
child: const Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'邀请好友加入',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: AppColors.onSecondaryFixed,
),
),
SizedBox(height: 4),
Text(
'每成功邀请一位专业电工,您将获得 ¥100 额外奖励。',
style: TextStyle(
fontSize: 15,
color: Color(0xB30C006A),
),
),
],
),
),
SizedBox(width: 16),
XIcon('star', size: 48, color: AppColors.primary),
],
),
),
),
);
}
}
/// `formatNumber`:千分位 + 两位小数,空值 / 0 回退 `0.00`(与源实现一致)。
String _formatAmount(double? value) {
if (value == null || value == 0) return '0.00';
final parts = value.toStringAsFixed(2).split('.');
final grouped = parts[0].replaceAllMapped(
RegExp(r'\B(?=(\d{3})+(?!\d))'),
(_) => ',',
);
return '$grouped.${parts[1]}';
}
/// 周标签:index 6 为今天,向前回溯 6 天,保证标签与真实星期一致。
List<String> _weekdayLabels() {
const names = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
final today = DateTime.now();
return [
for (var i = 6; i >= 0; i--)
names[today.subtract(Duration(days: i)).weekday - 1],
];
}