feat: 完成智能电网电工端 Flutter 应用初始化
创建完整的 Flutter 项目结构,包含 Android 和 iOS 平台配置、核心网络层、路由、状态管理及业务模块(订单、收益、认证),适配原 uni-app 项目功能逻辑
This commit is contained in:
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],
|
||||
];
|
||||
}
|
||||
Reference in new issue
Block a user