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,18 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/network/api_response.dart';
import '../../../core/providers.dart';
import '../data/earnings_api.dart';
import '../data/earnings_models.dart';
final earningsApiProvider = Provider<EarningsApi>(
(ref) => EarningsApi(ref.watch(apiClientProvider)),
);
final earningsSummaryProvider =
FutureProvider.autoDispose<EarningsSummary>((ref) async {
final res = await ref.watch(earningsApiProvider).summary();
if (!res.isOk) throw ApiException(res.message);
if (res.data == null) throw const ApiException('收益数据为空');
return res.data!;
});
@@ -0,0 +1,16 @@
import '../../../core/network/api_client.dart';
import '../../../core/network/api_response.dart';
import 'earnings_models.dart';
/// 收益接口,对应后端 `/api/worker-app/earnings/summary`。
class EarningsApi {
EarningsApi(this._client);
final ApiClient _client;
Future<ApiResponse<EarningsSummary>> summary() =>
_client.get<EarningsSummary>(
'/worker-app/earnings/summary',
parse: (d) => EarningsSummary.fromJson((d as Map).cast<String, dynamic>()),
);
}
@@ -0,0 +1,94 @@
import '../../../core/utils/id_util.dart';
/// 收益流水,对应后端 `earningsSummary().data.transactions[]`。
///
/// 说明:后端没有提现记录来源,因此实际只会返回 `type == 'earning'` 的入账流水。
class EarningTransaction {
const EarningTransaction({
required this.id,
required this.type,
required this.title,
required this.subtitle,
required this.amount,
required this.status,
required this.icon,
});
final String id;
/// earning | withdrawal
final String type;
final String title;
final String subtitle;
final double amount;
/// settled | pending | completed
final String status;
final String icon;
bool get isWithdrawal => type == 'withdrawal';
String get statusText => switch (status) {
'settled' => '已结算',
'pending' => '处理中',
'completed' => '已完成',
_ => status,
};
factory EarningTransaction.fromJson(Map<String, dynamic> json) =>
EarningTransaction(
id: json['id'] as String? ?? '',
type: json['type'] as String? ?? 'earning',
title: json['title'] as String? ?? '',
subtitle: json['subtitle'] as String? ?? '',
amount: toDoubleOr(json['amount'], 0),
status: json['status'] as String? ?? 'settled',
icon: json['icon'] as String? ?? '',
);
}
/// 收益汇总,对应 `GET /api/worker-app/earnings/summary`。
class EarningsSummary {
const EarningsSummary({
required this.totalBalance,
required this.withdrawableAmount,
required this.monthlyEarnings,
required this.weeklyTrend,
required this.transactions,
});
final double totalBalance;
final double withdrawableAmount;
final double monthlyEarnings;
/// 近 7 天每日收益(index 6 = 今天)。
final List<double> weeklyTrend;
final List<EarningTransaction> transactions;
/// 柱状图高度百分比:以 7 日峰值为 100%,全 0 时给一个最小可见高度。
List<double> get trendPercents {
final max = weeklyTrend.isEmpty
? 0.0
: weeklyTrend.reduce((a, b) => a > b ? a : b);
if (max <= 0) return List.filled(weeklyTrend.length, 8);
return weeklyTrend.map((v) => (v / max * 100).clamp(8, 100).toDouble()).toList();
}
factory EarningsSummary.fromJson(Map<String, dynamic> json) => EarningsSummary(
totalBalance: toDoubleOr(json['totalBalance'], 0),
withdrawableAmount: toDoubleOr(json['withdrawableAmount'], 0),
monthlyEarnings: toDoubleOr(json['monthlyEarnings'], 0),
weeklyTrend:
(json['weeklyTrend'] as List?)?.map((e) => toDoubleOr(e, 0)).toList() ??
const [0, 0, 0, 0, 0, 0, 0],
transactions:
(json['transactions'] as List?)
?.map(
(e) => EarningTransaction.fromJson(
(e as Map).cast<String, dynamic>(),
),
)
.toList() ??
const [],
);
}
@@ -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],
];
}