profile_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import '../../core/i18n/app_localizations.dart';
  6. import '../../core/i18n/locale_provider.dart';
  7. import '../../core/theme/theme_mode_provider.dart';
  8. import '../../core/auth/role_provider.dart';
  9. import '../../shared/widgets/profile_menu_item.dart';
  10. import '../shell/nav_bar_config.dart';
  11. import '../../core/theme/app_colors.dart';
  12. import '../../core/theme/app_colors_extension.dart';
  13. class ProfilePage extends ConsumerWidget {
  14. const ProfilePage({super.key});
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. final l10n = AppLocalizations.of(context);
  18. final location = GoRouterState.of(context).uri.toString();
  19. final currentLocale = ref.watch(localeProvider);
  20. final themeMode = ref.watch(themeModeProvider);
  21. final isDark = themeMode == ThemeMode.dark;
  22. final currentRole = ref.watch(currentRoleProvider);
  23. final isAdmin = currentRole == 'admin';
  24. if (location.startsWith('/profile')) {
  25. ref
  26. .read(navBarConfigProvider.notifier)
  27. .update(
  28. NavBarConfig(
  29. title: l10n.get('tabProfile'),
  30. showBack: true,
  31. leadingIcon: Icons.close,
  32. ),
  33. );
  34. }
  35. return SingleChildScrollView(
  36. physics: const AlwaysScrollableScrollPhysics(),
  37. child: Column(
  38. children: [
  39. const SizedBox(height: 16),
  40. _buildUserInfoCard(context, l10n),
  41. const SizedBox(height: 16),
  42. // 功能列表
  43. Padding(
  44. padding: const EdgeInsets.symmetric(horizontal: 16),
  45. child: Column(
  46. children: [
  47. // ── 角色切换(测试用)──
  48. ProfileMenuItem(
  49. icon: Icons.people,
  50. label: '角色切换',
  51. trailing: _roleName(currentRole),
  52. onTap: () => _showRoleSheet(context, ref, currentRole),
  53. ),
  54. const SizedBox(height: 8),
  55. // ── 语言设置 ──
  56. ProfileMenuItem(
  57. icon: Icons.language,
  58. label: l10n.get('language'),
  59. trailing: _localeName(currentLocale),
  60. onTap: () => _showLanguageSheet(context, ref, currentLocale),
  61. ),
  62. const SizedBox(height: 8),
  63. // ── 深色模式 ──
  64. ProfileMenuItem(
  65. icon: Icons.dark_mode,
  66. label: l10n.get('darkMode'),
  67. trailingWidget: TDSwitch(
  68. isOn: isDark,
  69. onChanged: (v) {
  70. ref.read(themeModeProvider.notifier).toggle();
  71. return v;
  72. },
  73. ),
  74. ),
  75. const SizedBox(height: 8),
  76. if (isAdmin) ...[
  77. ProfileMenuItem(
  78. icon: Icons.admin_panel_settings,
  79. label: '权限管理',
  80. onTap: () => context.push('/admin/permissions'),
  81. ),
  82. const SizedBox(height: 8),
  83. ],
  84. // ── 关于 ──
  85. ProfileMenuItem(
  86. icon: Icons.info_outline,
  87. label: l10n.get('about'),
  88. onTap: () => _showAboutDialog(context, l10n),
  89. ),
  90. ],
  91. ),
  92. ),
  93. const SizedBox(height: 8),
  94. TDFooter(TDFooterType.text, text: l10n.get('version')),
  95. ],
  96. ),
  97. );
  98. }
  99. Widget _buildUserInfoCard(BuildContext context, AppLocalizations l10n) {
  100. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  101. return Container(
  102. margin: const EdgeInsets.symmetric(horizontal: 16),
  103. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
  104. decoration: BoxDecoration(
  105. color: colors.bgCard,
  106. borderRadius: BorderRadius.circular(12),
  107. ),
  108. child: Row(
  109. children: [
  110. // 头像 - 点击更换占位
  111. GestureDetector(
  112. onTap: () {
  113. TDToast.showText(
  114. '头像更换功能(占位)',
  115. context: context,
  116. duration: Duration(seconds: 2),
  117. );
  118. },
  119. child: Container(
  120. width: 64,
  121. height: 64,
  122. decoration: BoxDecoration(
  123. color: colors.primaryLight,
  124. shape: BoxShape.circle,
  125. ),
  126. child: Center(
  127. child: Text(
  128. '张',
  129. style: TextStyle(
  130. fontSize: 28,
  131. fontWeight: FontWeight.w700,
  132. color: colors.primary,
  133. ),
  134. ),
  135. ),
  136. ),
  137. ),
  138. const SizedBox(width: 16),
  139. // 用户信息
  140. Expanded(
  141. child: Column(
  142. crossAxisAlignment: CrossAxisAlignment.start,
  143. children: [
  144. Text(
  145. l10n.get('userName'),
  146. style: TextStyle(
  147. fontSize: AppFontSizes.title,
  148. fontWeight: FontWeight.w600,
  149. color: colors.textPrimary,
  150. ),
  151. ),
  152. const SizedBox(height: AppSpacing.xs),
  153. Text(
  154. l10n.get('salesDepartment'),
  155. style: TextStyle(
  156. fontSize: AppFontSizes.body,
  157. color: colors.textSecondary,
  158. ),
  159. ),
  160. const SizedBox(height: 2),
  161. Text(
  162. '销售经理',
  163. style: TextStyle(fontSize: 12, color: colors.textPlaceholder),
  164. ),
  165. ],
  166. ),
  167. ),
  168. // 箭头
  169. Icon(Icons.chevron_right, size: 16, color: colors.textPlaceholder),
  170. ],
  171. ),
  172. );
  173. }
  174. String _localeName(Locale locale) {
  175. if (locale.languageCode == 'zh' && locale.countryCode == 'TW') {
  176. return '繁體中文';
  177. }
  178. if (locale.languageCode == 'en') {
  179. return 'English';
  180. }
  181. return '简体中文';
  182. }
  183. String _roleName(String role) {
  184. for (final (value, label) in roleOptions) {
  185. if (value == role) return label;
  186. }
  187. return role;
  188. }
  189. void _showRoleSheet(
  190. BuildContext context,
  191. WidgetRef ref,
  192. String currentRole,
  193. ) {
  194. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  195. TDActionSheet.showListActionSheet(
  196. context,
  197. showCancel: false,
  198. useSafeArea: false,
  199. items: roleOptions.map((r) {
  200. final isSelected = r.$1 == currentRole;
  201. return TDActionSheetItem(
  202. label: isSelected ? '${r.$2} ✓' : r.$2,
  203. textStyle: TextStyle(
  204. color: isSelected ? colors.primary : colors.textPrimary,
  205. fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
  206. ),
  207. );
  208. }).toList(),
  209. onSelected: (item, index) {
  210. ref.read(currentRoleProvider.notifier).state = roleOptions[index].$1;
  211. },
  212. );
  213. }
  214. void _showLanguageSheet(
  215. BuildContext context,
  216. WidgetRef ref,
  217. Locale currentLocale,
  218. ) {
  219. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  220. final languages = [
  221. (label: '简体中文', locale: const Locale('zh', 'CN')),
  222. (label: 'English', locale: const Locale('en')),
  223. (label: '繁體中文', locale: const Locale('zh', 'TW')),
  224. ];
  225. TDActionSheet.showListActionSheet(
  226. context,
  227. showCancel: false,
  228. useSafeArea: false,
  229. items: languages.map((lang) {
  230. final isSelected =
  231. lang.locale.languageCode == currentLocale.languageCode &&
  232. lang.locale.countryCode == currentLocale.countryCode;
  233. return TDActionSheetItem(
  234. label: isSelected ? '${lang.label} ✓' : lang.label,
  235. textStyle: TextStyle(
  236. color: isSelected ? colors.primary : colors.textPrimary,
  237. fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
  238. ),
  239. );
  240. }).toList(),
  241. onSelected: (item, index) {
  242. ref.read(localeProvider.notifier).setLocale(languages[index].locale);
  243. },
  244. );
  245. }
  246. void _showAboutDialog(BuildContext context, AppLocalizations l10n) {
  247. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  248. showGeneralDialog(
  249. context: context,
  250. pageBuilder:
  251. (
  252. BuildContext buildContext,
  253. Animation<double> animation,
  254. Animation<double> secondaryAnimation,
  255. ) {
  256. return TDConfirmDialog(
  257. title: l10n.get('about'),
  258. titleColor: colors.textPrimary,
  259. contentWidget: Column(
  260. mainAxisSize: MainAxisSize.min,
  261. crossAxisAlignment: CrossAxisAlignment.start,
  262. children: [
  263. _aboutRow(
  264. l10n.get('version'),
  265. 'v1.0.0 (Build 20260601)',
  266. colors,
  267. ),
  268. const Divider(height: 24),
  269. GestureDetector(
  270. onTap: () {
  271. Navigator.of(buildContext).pop();
  272. TDToast.showText(
  273. '用户协议(占位)',
  274. context: context,
  275. duration: Duration(seconds: 2),
  276. );
  277. },
  278. child: Text(
  279. '用户协议',
  280. style: TextStyle(fontSize: 14, color: colors.primary),
  281. ),
  282. ),
  283. const SizedBox(height: 12),
  284. GestureDetector(
  285. onTap: () {
  286. Navigator.of(buildContext).pop();
  287. TDToast.showText(
  288. '隐私政策(占位)',
  289. context: context,
  290. duration: Duration(seconds: 2),
  291. );
  292. },
  293. child: Text(
  294. '隐私政策',
  295. style: TextStyle(fontSize: 14, color: colors.primary),
  296. ),
  297. ),
  298. ],
  299. ),
  300. buttonStyle: TDDialogButtonStyle.text,
  301. buttonText: l10n.get('close'),
  302. buttonTextColor: colors.primary,
  303. action: () => Navigator.of(buildContext).pop(),
  304. );
  305. },
  306. );
  307. }
  308. Widget _aboutRow(String label, String value, AppColorsExtension colors) {
  309. return Row(
  310. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  311. children: [
  312. Text(
  313. label,
  314. style: TextStyle(fontSize: 14, color: colors.textSecondary),
  315. ),
  316. Flexible(
  317. child: Text(
  318. value,
  319. textAlign: TextAlign.right,
  320. softWrap: true,
  321. style: TextStyle(fontSize: 14, color: colors.textPrimary),
  322. ),
  323. ),
  324. ],
  325. );
  326. }
  327. }