broker_screen.dart 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../../core/l10n/app_localizations.dart';
  5. import '../../../core/theme/app_colors.dart';
  6. import '../../../data/repositories/broker_repository.dart';
  7. // ── Providers ─────────────────────────────────────────────
  8. final _brokerInfoProvider = FutureProvider.autoDispose<Map<String, dynamic>?>((ref) {
  9. return ref.read(brokerRepositoryProvider).getBrokerInfo();
  10. });
  11. /// 今日返佣记录(对应安卓 rebatesInfo,startTime=今日零时)
  12. final _brokerTodayRewardsProvider = FutureProvider.autoDispose<List<Map<String, dynamic>>>((ref) {
  13. final now = DateTime.now();
  14. final todayStart = DateTime(now.year, now.month, now.day, 0, 0, 0);
  15. return ref.read(brokerRepositoryProvider).getRewardList(
  16. pageSize: 50,
  17. startTime: todayStart,
  18. );
  19. });
  20. // ── Screen ───────────────────────────────────────────────
  21. class BrokerScreen extends ConsumerWidget {
  22. const BrokerScreen({super.key});
  23. @override
  24. Widget build(BuildContext context, WidgetRef ref) {
  25. final cs = Theme.of(context).colorScheme;
  26. final infoAsync = ref.watch(_brokerInfoProvider);
  27. final rewardsAsync = ref.watch(_brokerTodayRewardsProvider);
  28. return Scaffold(
  29. backgroundColor: cs.surface,
  30. appBar: AppBar(
  31. backgroundColor: cs.surface,
  32. elevation: 0,
  33. leading: IconButton(
  34. icon: const Icon(Icons.arrow_back_ios, size: 18),
  35. onPressed: () => context.pop(),
  36. ),
  37. title: Text(AppLocalizations.of(context)!.broker, style: TextStyle(color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600)),
  38. centerTitle: true,
  39. ),
  40. body: infoAsync.when(
  41. loading: () => const Center(child: CircularProgressIndicator()),
  42. error: (e, _) => Center(child: Text('${AppLocalizations.of(context)!.loadFailed}: $e')),
  43. data: (info) => _BrokerBody(info: info, rewardsAsync: rewardsAsync),
  44. ),
  45. );
  46. }
  47. }
  48. class _BrokerBody extends StatelessWidget {
  49. const _BrokerBody({required this.info, required this.rewardsAsync});
  50. final Map<String, dynamic>? info;
  51. final AsyncValue<List<Map<String, dynamic>>> rewardsAsync;
  52. @override
  53. Widget build(BuildContext context) {
  54. final cs = Theme.of(context).colorScheme;
  55. final isDark = Theme.of(context).brightness == Brightness.dark;
  56. final uid = info?['id']?.toString() ?? '--';
  57. // 与 profile_provider 保持一致:优先取 email,fallback 到 username
  58. final email = (info?['email'] as String?)?.trim().isNotEmpty == true
  59. ? info!['email'] as String
  60. : (info?['username'] as String? ?? '');
  61. final maskedEmail = _maskEmail(email);
  62. // 代理商显示名:按优先级依次尝试各字段
  63. final nickName = ((info?['nickName']
  64. ?? info?['agentName']
  65. ?? info?['name']
  66. ?? info?['realName']) as String? ?? '').trim();
  67. // 统计数据(字段对应安卓 AgentInfo:uc/agent/info)
  68. final todayNew = info?['todayNewUserCount']?.toString() ?? '0';
  69. final todayTrading = info?['todayOrderUserCount']?.toString() ?? '0';
  70. final todayRebate = _truncate4(info?['todayAward']?.toString() ?? '0.00');
  71. return SingleChildScrollView(
  72. padding: const EdgeInsets.all(16),
  73. child: Column(
  74. crossAxisAlignment: CrossAxisAlignment.start,
  75. children: [
  76. // ── 用户信息卡片 ──────────────────────────────
  77. Container(
  78. width: double.infinity,
  79. padding: const EdgeInsets.all(16),
  80. decoration: BoxDecoration(
  81. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBgSecondary,
  82. borderRadius: BorderRadius.circular(12),
  83. border: Border.all(color: cs.outlineVariant.withAlpha(60)),
  84. ),
  85. child: Column(
  86. crossAxisAlignment: CrossAxisAlignment.start,
  87. children: [
  88. Text(
  89. nickName.isNotEmpty
  90. ? AppLocalizations.of(context)!.brokerWelcomeNamed(nickName)
  91. : AppLocalizations.of(context)!.brokerWelcome,
  92. style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: cs.onSurface),
  93. ),
  94. const SizedBox(height: 6),
  95. Row(children: [
  96. Flexible(
  97. child: Text(maskedEmail, maxLines: 1, overflow: TextOverflow.ellipsis,
  98. style: TextStyle(fontSize: 13, color: cs.onSurface.withAlpha(120))),
  99. ),
  100. const SizedBox(width: 8),
  101. Container(
  102. padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
  103. decoration: BoxDecoration(
  104. color: cs.onSurface.withAlpha(18),
  105. borderRadius: BorderRadius.circular(4),
  106. ),
  107. child: Text('UID: $uid',
  108. style: TextStyle(fontSize: 12, color: cs.onSurface.withAlpha(140))),
  109. ),
  110. ]),
  111. ],
  112. ),
  113. ),
  114. const SizedBox(height: 12),
  115. // ── 提示横幅 ─────────────────────────────────
  116. Container(
  117. padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
  118. decoration: BoxDecoration(
  119. color: AppColors.brand.withAlpha(25),
  120. borderRadius: BorderRadius.circular(10),
  121. border: Border.all(color: AppColors.brand.withAlpha(80)),
  122. ),
  123. child: Row(
  124. crossAxisAlignment: CrossAxisAlignment.start,
  125. children: [
  126. const Icon(Icons.info_outline, size: 16, color: AppColors.brand),
  127. const SizedBox(width: 8),
  128. Expanded(
  129. child: Text(
  130. AppLocalizations.of(context)!.brokerInviteTip,
  131. style: TextStyle(fontSize: 12, color: cs.onSurface.withAlpha(160), height: 1.5),
  132. ),
  133. ),
  134. ],
  135. ),
  136. ),
  137. const SizedBox(height: 20),
  138. // ── 统计数据(来自 agentInfo,与安卓一致)────
  139. _StatsRow(todayNew: todayNew, todayTrading: todayTrading, todayRebate: todayRebate),
  140. const SizedBox(height: 24),
  141. // ── 两个按钮 ──────────────────────────────────
  142. Row(children: [
  143. Expanded(
  144. child: ElevatedButton(
  145. style: ElevatedButton.styleFrom(
  146. backgroundColor: Colors.black,
  147. foregroundColor: Colors.white,
  148. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
  149. padding: const EdgeInsets.symmetric(vertical: 14),
  150. ),
  151. onPressed: () => context.push('/broker/my-invitations'),
  152. child: Text(AppLocalizations.of(context)!.myInvitations, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
  153. ),
  154. ),
  155. const SizedBox(width: 12),
  156. Expanded(
  157. child: ElevatedButton(
  158. style: ElevatedButton.styleFrom(
  159. backgroundColor: Colors.black,
  160. foregroundColor: Colors.white,
  161. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
  162. padding: const EdgeInsets.symmetric(vertical: 14),
  163. ),
  164. onPressed: () => context.push('/broker/team-detail'),
  165. child: Text(AppLocalizations.of(context)!.teamDetail, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600)),
  166. ),
  167. ),
  168. ]),
  169. const SizedBox(height: 24),
  170. // ── 今日返佣列表(对应安卓 awardsLiveData)────
  171. Text(AppLocalizations.of(context)!.inviteList, style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: cs.onSurface)),
  172. const SizedBox(height: 12),
  173. _RewardsTable(rewardsAsync: rewardsAsync),
  174. ],
  175. ),
  176. );
  177. }
  178. String _maskEmail(String email) {
  179. if (email.isEmpty) return '--';
  180. final atIdx = email.indexOf('@');
  181. if (atIdx <= 1) return email;
  182. return '${email[0]}**${email.substring(atIdx)}';
  183. }
  184. }
  185. /// 截断到4位小数,不四舍五入
  186. String _truncate4(String value) {
  187. final d = double.tryParse(value);
  188. if (d == null) return value;
  189. final s = d.toStringAsFixed(10);
  190. final dot = s.indexOf('.');
  191. if (dot == -1) return '$s.0000';
  192. final end = dot + 5; // dot + 4 decimals
  193. return end >= s.length ? s.padRight(end, '0') : s.substring(0, end);
  194. }
  195. class _StatsRow extends StatelessWidget {
  196. const _StatsRow({required this.todayNew, required this.todayTrading, required this.todayRebate});
  197. final String todayNew;
  198. final String todayTrading;
  199. final String todayRebate;
  200. @override
  201. Widget build(BuildContext context) {
  202. final cs = Theme.of(context).colorScheme;
  203. return Row(
  204. children: [
  205. Expanded(child: _StatCell(value: todayNew, label: AppLocalizations.of(context)!.todayNewUsers)),
  206. Container(width: 1, height: 40, color: cs.outlineVariant.withAlpha(80)),
  207. Expanded(child: _StatCell(value: todayTrading, label: AppLocalizations.of(context)!.todayTradingUsers)),
  208. Container(width: 1, height: 40, color: cs.outlineVariant.withAlpha(80)),
  209. Expanded(child: _StatCell(value: todayRebate, label: AppLocalizations.of(context)!.todayRebateLabel)),
  210. ],
  211. );
  212. }
  213. }
  214. class _StatCell extends StatelessWidget {
  215. const _StatCell({required this.value, required this.label});
  216. final String value;
  217. final String label;
  218. @override
  219. Widget build(BuildContext context) {
  220. final cs = Theme.of(context).colorScheme;
  221. return Column(
  222. mainAxisSize: MainAxisSize.min,
  223. children: [
  224. Text(value, style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: cs.onSurface)),
  225. const SizedBox(height: 4),
  226. Text(label, textAlign: TextAlign.center,
  227. style: TextStyle(fontSize: 11, color: cs.onSurface.withAlpha(120), height: 1.4)),
  228. ],
  229. );
  230. }
  231. }
  232. class _RewardsTable extends StatelessWidget {
  233. const _RewardsTable({required this.rewardsAsync});
  234. final AsyncValue<List<Map<String, dynamic>>> rewardsAsync;
  235. @override
  236. Widget build(BuildContext context) {
  237. final cs = Theme.of(context).colorScheme;
  238. return rewardsAsync.when(
  239. loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2)),
  240. error: (e, _) => Text(AppLocalizations.of(context)!.loadFailed, style: TextStyle(color: cs.error)),
  241. data: (rewards) {
  242. if (rewards.isEmpty) {
  243. return Padding(
  244. padding: const EdgeInsets.symmetric(vertical: 32),
  245. child: Center(child: Text(AppLocalizations.of(context)!.noInviteRecord, style: TextStyle(color: cs.onSurface.withAlpha(120)))),
  246. );
  247. }
  248. return Column(children: [
  249. // 表头
  250. Container(
  251. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
  252. decoration: BoxDecoration(
  253. color: cs.surfaceContainerHighest.withAlpha(60),
  254. borderRadius: BorderRadius.circular(6)),
  255. child: Row(children: [
  256. Expanded(child: Text(AppLocalizations.of(context)!.accountLabel,
  257. style: TextStyle(fontSize: 12, color: cs.onSurface.withAlpha(120)))),
  258. Expanded(child: Text('ID', textAlign: TextAlign.center,
  259. style: TextStyle(fontSize: 12, color: cs.onSurface.withAlpha(120)))),
  260. Expanded(child: Text(AppLocalizations.of(context)!.todayRebateLabel,
  261. textAlign: TextAlign.right, maxLines: 1, overflow: TextOverflow.ellipsis,
  262. style: TextStyle(fontSize: 12, color: cs.onSurface.withAlpha(120)))),
  263. ]),
  264. ),
  265. ...rewards.map((r) => _RewardRow(reward: r)),
  266. ]);
  267. },
  268. );
  269. }
  270. }
  271. class _RewardRow extends StatelessWidget {
  272. const _RewardRow({required this.reward});
  273. final Map<String, dynamic> reward;
  274. String _maskName(String? name) {
  275. if (name == null || name.isEmpty) return '--';
  276. if (name.contains('@')) {
  277. final atIdx = name.indexOf('@');
  278. if (atIdx > 1) return '${name[0]}**${name.substring(atIdx)}';
  279. return name;
  280. }
  281. if (name.length > 4) return '${name.substring(0, 3)}****${name.substring(name.length - 2)}';
  282. return name;
  283. }
  284. @override
  285. Widget build(BuildContext context) {
  286. final cs = Theme.of(context).colorScheme;
  287. final name = _maskName(reward['username'] as String?);
  288. final memberId = reward['memberId']?.toString() ?? '--';
  289. final amount = reward['num']?.toString() ?? '0.0000';
  290. final amountFormatted = double.tryParse(amount)?.toStringAsFixed(4) ?? amount;
  291. return Container(
  292. padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
  293. decoration: BoxDecoration(
  294. border: Border(bottom: BorderSide(color: cs.outlineVariant.withAlpha(40)))),
  295. child: Row(children: [
  296. Expanded(child: Text(name, maxLines: 1, overflow: TextOverflow.ellipsis,
  297. style: TextStyle(fontSize: 13, color: cs.onSurface))),
  298. Expanded(child: Text(memberId, textAlign: TextAlign.center,
  299. maxLines: 1, overflow: TextOverflow.ellipsis,
  300. style: TextStyle(fontSize: 13, color: cs.onSurface))),
  301. Expanded(child: Text(amountFormatted, textAlign: TextAlign.right,
  302. maxLines: 1, overflow: TextOverflow.ellipsis,
  303. style: TextStyle(fontSize: 13, color: cs.onSurface))),
  304. ]),
  305. );
  306. }
  307. }