position_detail_screen.dart 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import 'package:flutter/material.dart';
  2. import '../../../core/l10n/app_localizations.dart';
  3. import '../../../core/theme/app_colors.dart';
  4. import '../../../core/utils/number_format.dart';
  5. import '../../../providers/futures_provider.dart';
  6. /// 仓位详情页
  7. class PositionDetailScreen extends StatelessWidget {
  8. const PositionDetailScreen({
  9. super.key,
  10. required this.position,
  11. required this.symbol,
  12. });
  13. final FuturesPosition position;
  14. final String symbol;
  15. String _baseCoin(String sym) {
  16. if (sym.contains('/')) return sym.split('/').first;
  17. return sym.toUpperCase().replaceFirst(RegExp(r'USDT$'), '');
  18. }
  19. String _rawNum(double v) {
  20. if (v == v.truncateToDouble()) return v.toInt().toString();
  21. return v.toString();
  22. }
  23. @override
  24. Widget build(BuildContext context) {
  25. final cs = Theme.of(context).colorScheme;
  26. final isDark = Theme.of(context).brightness == Brightness.dark;
  27. final isLong = position.side == OrderSide.long;
  28. final pnlColor = position.unrealizedPnl >= 0 ? AppColors.rise : AppColors.fall;
  29. final realizedColor = position.realizedPnl >= 0 ? AppColors.rise : AppColors.fall;
  30. final coinSymbol = _baseCoin(position.symbol);
  31. final l10n = AppLocalizations.of(context)!;
  32. final scaffoldBg = isDark ? AppColors.darkBg : AppColors.lightBgSecondary;
  33. final sideColor = isLong ? AppColors.rise : AppColors.fall;
  34. return Scaffold(
  35. backgroundColor: scaffoldBg,
  36. appBar: AppBar(
  37. elevation: 0,
  38. centerTitle: true,
  39. backgroundColor: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
  40. title: Text(
  41. l10n.positionDetail,
  42. style: TextStyle(
  43. color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w700),
  44. ),
  45. ),
  46. body: SingleChildScrollView(
  47. child: Column(
  48. crossAxisAlignment: CrossAxisAlignment.start,
  49. children: [
  50. // ── 标的行 ────────────────────────────────────────
  51. Container(
  52. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
  53. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
  54. child: Row(
  55. children: [
  56. // 多/空 chip
  57. Container(
  58. padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
  59. decoration: BoxDecoration(
  60. color: sideColor,
  61. borderRadius: BorderRadius.circular(4),
  62. ),
  63. child: Text(
  64. isLong ? l10n.openLong : l10n.openShort,
  65. style: const TextStyle(
  66. color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700),
  67. ),
  68. ),
  69. const SizedBox(width: 6),
  70. Expanded(
  71. child: Row(
  72. children: [
  73. Flexible(
  74. child: Text(
  75. position.symbol,
  76. maxLines: 1,
  77. overflow: TextOverflow.ellipsis,
  78. style: TextStyle(
  79. color: cs.onSurface,
  80. fontSize: 15,
  81. fontWeight: FontWeight.w700),
  82. ),
  83. ),
  84. const SizedBox(width: 6),
  85. _SmTag(
  86. text: position.marginMode,
  87. color: switch (position.marginMode) {
  88. '分仓' || '逐仓' => AppColors.rankPurple,
  89. _ => AppColors.tagBlue,
  90. },
  91. ),
  92. const SizedBox(width: 4),
  93. _LevTag(leverage: position.leverage.toInt()),
  94. ],
  95. ),
  96. ),
  97. ],
  98. ),
  99. ),
  100. const SizedBox(height: 8),
  101. // ── 价格 ──────────────────────────────────────────
  102. _InfoCard(rows: [
  103. _InfoRowData(l10n.openAvgPrice, _rawNum(position.entryPrice)),
  104. _InfoRowData(l10n.markPrice, _rawNum(position.markPrice), isLast: true),
  105. ]),
  106. const SizedBox(height: 8),
  107. // ── 持仓数据 ──────────────────────────────────────
  108. _InfoCard(rows: [
  109. _InfoRowData('${l10n.positionSize}($coinSymbol)', _rawNum(position.size)),
  110. _InfoRowData('${l10n.positionValue}(USDT)',
  111. formatAmount(position.size * position.markPrice)),
  112. _InfoRowData(
  113. l10n.estimatedLiqPrice,
  114. position.liquidationPrice > 0 ? _rawNum(position.liquidationPrice) : '--',
  115. valueColor: position.liquidationPrice > 0 ? AppColors.fall : null,
  116. ),
  117. _InfoRowData('${l10n.marginLabel}(USDT)', formatAmount(position.margin),
  118. isLast: true),
  119. ]),
  120. const SizedBox(height: 8),
  121. // ── 盈亏 ──────────────────────────────────────────
  122. _InfoCard(rows: [
  123. _InfoRowData(l10n.profitRateLabel, '${formatAmount(position.roe)}%',
  124. valueColor: pnlColor),
  125. _InfoRowData(l10n.unrealizedPnl, formatAmount(position.unrealizedPnl),
  126. valueColor: pnlColor),
  127. _InfoRowData(l10n.realizedPnl, formatAmount(position.realizedPnl),
  128. valueColor: realizedColor),
  129. _InfoRowData(
  130. '${l10n.fee}(USDT)',
  131. position.commissionFee > 0 ? formatAmount(position.commissionFee) : '--',
  132. isLast: true,
  133. ),
  134. ]),
  135. const SizedBox(height: 16),
  136. ],
  137. ),
  138. ),
  139. );
  140. }
  141. }
  142. // ── 共用 widgets ───────────────────────────────────────────────
  143. class _SmTag extends StatelessWidget {
  144. const _SmTag({required this.text, this.color});
  145. final String text;
  146. final Color? color;
  147. @override
  148. Widget build(BuildContext context) {
  149. final cs = Theme.of(context).colorScheme;
  150. final isDark = Theme.of(context).brightness == Brightness.dark;
  151. final c = color;
  152. if (c != null) {
  153. return Container(
  154. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  155. decoration: BoxDecoration(
  156. color: c.withAlpha(isDark ? 45 : 25),
  157. borderRadius: BorderRadius.circular(4),
  158. ),
  159. child: Text(text,
  160. style: TextStyle(
  161. color: c, fontSize: 11, fontWeight: FontWeight.w600)),
  162. );
  163. }
  164. return Container(
  165. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  166. decoration: BoxDecoration(
  167. color: cs.onSurface.withAlpha(12),
  168. border: Border.all(color: cs.outline.withAlpha(80)),
  169. borderRadius: BorderRadius.circular(4),
  170. ),
  171. child: Text(text,
  172. style: TextStyle(
  173. color: cs.onSurface.withAlpha(153),
  174. fontSize: 11,
  175. fontWeight: FontWeight.w600)),
  176. );
  177. }
  178. }
  179. class _LevTag extends StatelessWidget {
  180. const _LevTag({required this.leverage});
  181. final int leverage;
  182. @override
  183. Widget build(BuildContext context) {
  184. return Container(
  185. padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
  186. decoration: BoxDecoration(
  187. color: AppColors.leverageGoldBg,
  188. borderRadius: BorderRadius.circular(4),
  189. ),
  190. child: Text('${leverage}X',
  191. style: const TextStyle(
  192. color: AppColors.leverageGold,
  193. fontSize: 11,
  194. fontWeight: FontWeight.w700)),
  195. );
  196. }
  197. }
  198. class _InfoRowData {
  199. const _InfoRowData(this.label, this.value,
  200. {this.valueColor, this.isLast = false});
  201. final String label;
  202. final String value;
  203. final Color? valueColor;
  204. final bool isLast;
  205. }
  206. class _InfoCard extends StatelessWidget {
  207. const _InfoCard({required this.rows});
  208. final List<_InfoRowData> rows;
  209. @override
  210. Widget build(BuildContext context) {
  211. final cs = Theme.of(context).colorScheme;
  212. final isDark = Theme.of(context).brightness == Brightness.dark;
  213. return Container(
  214. color: isDark ? AppColors.darkBgSecondary : AppColors.lightBg,
  215. child: Column(
  216. children: rows.map((r) {
  217. return Column(
  218. children: [
  219. Padding(
  220. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13),
  221. child: Row(
  222. children: [
  223. Expanded(
  224. child: Text(r.label,
  225. maxLines: 1,
  226. overflow: TextOverflow.ellipsis,
  227. style: TextStyle(
  228. color: cs.onSurface.withAlpha(153), fontSize: 13)),
  229. ),
  230. const SizedBox(width: 8),
  231. Text(r.value,
  232. textAlign: TextAlign.end,
  233. style: TextStyle(
  234. color: r.valueColor ?? cs.onSurface,
  235. fontSize: 13,
  236. fontWeight: FontWeight.w500)),
  237. ],
  238. ),
  239. ),
  240. if (!r.isLast)
  241. Divider(
  242. height: 1,
  243. thickness: 0.5,
  244. indent: 16,
  245. endIndent: 16,
  246. color: cs.outline.withAlpha(60)),
  247. ],
  248. );
  249. }).toList(),
  250. ),
  251. );
  252. }
  253. }