feat(mine): 我的 Tab UI 重设计(#39~#41)
- ProfileHeroCard:72pt 渐变头像(8色 uid%8 主题)、@J{uid} handle、bio 简介、掩码手机号
- AppBar:compact,右侧 QR 图标 + 编辑铅笔
- 彩色图标行(_IconBox 36pt 圆角)+ 4 卡片组对齐 iOS SettingsView
- 账户:我的钱包 / 账户安全
- 工具:收藏 / 最近呼叫 / 链接设备 / 聊天文件夹
- 偏好设置:通知和声音 / 隐私设置 / 黑名单 / 语言 / 主题
- 关于:用户协议 / 隐私政策 / 版本号
- SettingsState 增加 bio 字段(#41),loadProfile 同步赋值
- Doc/mine_tab_architecture.md 补充 UI 重设计章节(§7)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# 我的(Mine)Tab — 架构文档
|
# 我的(Mine)Tab — 架构文档
|
||||||
|
|
||||||
> 对应 Gitea issues #5–#13
|
> 对应 Gitea issues #5–#13,#39–#41(UI 重设计)
|
||||||
> 参考实现:`im-client-ios-swift-demo` Features/Settings + Features/Profile
|
> 参考实现:`im-client-ios-swift-demo` Features/Settings + Features/Profile
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -152,7 +152,57 @@ settingsViewModelProvider
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. 待完成事项
|
## 7. UI 重设计(#39 / #40 / #41)
|
||||||
|
|
||||||
|
### 7.1 ProfileHeroCard (#39 / #41)
|
||||||
|
|
||||||
|
| 元素 | 规格 |
|
||||||
|
|------|------|
|
||||||
|
| 头像 | 72pt 圆形;无头像时 8 色渐变占位(uid%8) |
|
||||||
|
| 昵称 | fontWeight w700,titleMedium |
|
||||||
|
| Handle | `@J{uid}`,bodySmall onSurfaceVariant |
|
||||||
|
| 手机号 | 掩码 `+CC ***XXXX`,bodySmall |
|
||||||
|
| Bio | 非空显示,为空显示「添加一句话简介」(斜体,半透明) |
|
||||||
|
| AppBar | compact,右侧:QR 图标 + 编辑铅笔 |
|
||||||
|
|
||||||
|
渐变色方案(`_ProfileHeroCard._gradients[uid.abs() % 8]`):
|
||||||
|
```
|
||||||
|
0: [#4776E6, #8E54E9] 1: [#11998E, #38EF7D]
|
||||||
|
2: [#FC466B, #3F5EFB] 3: [#F7971E, #FFD200]
|
||||||
|
4: [#56CCF2, #2F80ED] 5: [#EB3349, #F45C43]
|
||||||
|
6: [#1FA2FF, #12D8FA] 7: [#9D50BB, #6E48AA]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 彩色图标行与分组卡片 (#40)
|
||||||
|
|
||||||
|
`_IconBox`:36pt 圆角正方形(8pt)白色图标,iOS Settings 风格。
|
||||||
|
|
||||||
|
| 卡片组 | 菜单项 | 颜色 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| 账户 | 我的钱包 | #FFAA5B |
|
||||||
|
| | 账户安全 | #8A5CF6 |
|
||||||
|
| 工具 | 收藏 | #FFAF45 |
|
||||||
|
| | 最近呼叫 | #4CB050 |
|
||||||
|
| | 链接设备 | #5667FF |
|
||||||
|
| | 聊天文件夹 | #F2994A |
|
||||||
|
| 偏好设置 | 通知和声音 | #FF8B5E |
|
||||||
|
| | 隐私设置 | #0BB8A9 |
|
||||||
|
| | 黑名单 | #FF4B4B |
|
||||||
|
| | 语言 | #5667FF |
|
||||||
|
| | 主题 | #8A5CF6 |
|
||||||
|
| 关于 | 用户协议 | gray |
|
||||||
|
| | 隐私政策 | gray |
|
||||||
|
| | 版本号(静态)| gray,无 chevron |
|
||||||
|
|
||||||
|
### 7.3 SettingsState bio 字段 (#41)
|
||||||
|
|
||||||
|
- `SettingsState.bio: String`(默认 `''`)
|
||||||
|
- `SettingsViewModel.loadProfile()` 赋值 `bio: profile.bio`
|
||||||
|
- 数据来源:`ProfileResponse.bio` → `GET /app/api/user/profile`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 待完成事项
|
||||||
|
|
||||||
- **#6 头像上传**:接入 CDN upload(参考 iOS CDN 流程)
|
- **#6 头像上传**:接入 CDN upload(参考 iOS CDN 流程)
|
||||||
- **#8 主题持久化**:解开 `ThemeModeNotifier.build()` 和 `setMode()` 中的 TODO
|
- **#8 主题持久化**:解开 `ThemeModeNotifier.build()` 和 `setMode()` 中的 TODO
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class SettingsState {
|
|||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
final String maskedContact;
|
final String maskedContact;
|
||||||
final int uid;
|
final int uid;
|
||||||
|
final String bio;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool isLoggingOut;
|
final bool isLoggingOut;
|
||||||
final String? error;
|
final String? error;
|
||||||
@@ -21,6 +22,7 @@ class SettingsState {
|
|||||||
this.avatarUrl,
|
this.avatarUrl,
|
||||||
this.maskedContact = '',
|
this.maskedContact = '',
|
||||||
this.uid = 0,
|
this.uid = 0,
|
||||||
|
this.bio = '',
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.isLoggingOut = false,
|
this.isLoggingOut = false,
|
||||||
this.error,
|
this.error,
|
||||||
@@ -32,6 +34,7 @@ class SettingsState {
|
|||||||
bool clearAvatarUrl = false,
|
bool clearAvatarUrl = false,
|
||||||
String? maskedContact,
|
String? maskedContact,
|
||||||
int? uid,
|
int? uid,
|
||||||
|
String? bio,
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? isLoggingOut,
|
bool? isLoggingOut,
|
||||||
String? error,
|
String? error,
|
||||||
@@ -42,6 +45,7 @@ class SettingsState {
|
|||||||
avatarUrl: clearAvatarUrl ? null : (avatarUrl ?? this.avatarUrl),
|
avatarUrl: clearAvatarUrl ? null : (avatarUrl ?? this.avatarUrl),
|
||||||
maskedContact: maskedContact ?? this.maskedContact,
|
maskedContact: maskedContact ?? this.maskedContact,
|
||||||
uid: uid ?? this.uid,
|
uid: uid ?? this.uid,
|
||||||
|
bio: bio ?? this.bio,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
isLoggingOut: isLoggingOut ?? this.isLoggingOut,
|
isLoggingOut: isLoggingOut ?? this.isLoggingOut,
|
||||||
error: clearError ? null : (error ?? this.error),
|
error: clearError ? null : (error ?? this.error),
|
||||||
@@ -84,6 +88,7 @@ class SettingsViewModel extends Notifier<SettingsState> {
|
|||||||
avatarUrl: profile.profilePic.isEmpty ? null : profile.profilePic,
|
avatarUrl: profile.profilePic.isEmpty ? null : profile.profilePic,
|
||||||
maskedContact: _maskContact(profile.contact, profile.countryCode),
|
maskedContact: _maskContact(profile.contact, profile.countryCode),
|
||||||
uid: profile.uid,
|
uid: profile.uid,
|
||||||
|
bio: profile.bio,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state = state.copyWith(isLoading: false, error: e.toString());
|
state = state.copyWith(isLoading: false, error: e.toString());
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
import 'package:im_app/core/foundation/config.dart';
|
||||||
import 'package:im_app/features/settings/presentation/settings_view_model.dart';
|
import 'package:im_app/features/settings/presentation/settings_view_model.dart';
|
||||||
|
|
||||||
/// 我的页面(原设置页)
|
/// 我的页(#39 / #40 / #41)
|
||||||
///
|
///
|
||||||
/// 结构:
|
/// ## 结构(iOS SettingsView.swift 对齐)
|
||||||
/// ┌─ 个人资料卡 ──────────────────────────────────────────┐
|
///
|
||||||
/// │ 头像 昵称 │
|
/// - ProfileHeroCard:72pt 渐变头像 + 昵称 + @J{uid} handle + 手机号 + bio
|
||||||
/// │ 手机号(掩码) UID: xxx │
|
/// - AppBar:compact,右侧 QR 图标 + 编辑铅笔
|
||||||
/// └──────────────────────────────────────────────────────┘
|
/// - 卡片组 1(账户):我的钱包 / 账户安全
|
||||||
/// 偏好设置 → 主题 / 语言 / 通知
|
/// - 卡片组 2(工具):收藏 / 最近呼叫 / 链接设备 / 聊天文件夹
|
||||||
/// 工具 → 黑名单 / 网络诊断
|
/// - 卡片组 3「偏好设置」:通知和声音 / 隐私设置 / 黑名单 / 语言 / 主题
|
||||||
/// 关于 → 关于本应用
|
/// - 卡片组 4「关于」:用户协议 / 隐私政策 / 版本号
|
||||||
/// [退出登录]
|
/// - 退出登录(全宽红色按钮)
|
||||||
class SettingsPage extends ConsumerWidget {
|
class SettingsPage extends ConsumerWidget {
|
||||||
const SettingsPage({super.key});
|
const SettingsPage({super.key});
|
||||||
|
|
||||||
|
// ── iOS 色板 ──────────────────────────────────────────────────────────────
|
||||||
|
static const Color _walletColor = Color(0xFFFFAA5B);
|
||||||
|
static const Color _securityColor = Color(0xFF8A5CF6);
|
||||||
|
static const Color _favoriteColor = Color(0xFFFFAF45);
|
||||||
|
static const Color _callColor = Color(0xFF4CB050);
|
||||||
|
static const Color _deviceColor = Color(0xFF5667FF);
|
||||||
|
static const Color _folderColor = Color(0xFFF2994A);
|
||||||
|
static const Color _notifColor = Color(0xFFFF8B5E);
|
||||||
|
static const Color _privacyColor = Color(0xFF0BB8A9);
|
||||||
|
static const Color _blockColor = Color(0xFFFF4B4B);
|
||||||
|
static const Color _langColor = Color(0xFF5667FF);
|
||||||
|
static const Color _themeColor = Color(0xFF8A5CF6);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final state = ref.watch(settingsViewModelProvider);
|
final state = ref.watch(settingsViewModelProvider);
|
||||||
@@ -26,75 +40,158 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAppBar.large(
|
// ── AppBar(compact,QR + edit) ────────────────────────────────────
|
||||||
|
SliverAppBar(
|
||||||
title: const Text('我的'),
|
title: const Text('我的'),
|
||||||
|
pinned: true,
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.qr_code_scanner_rounded),
|
||||||
|
tooltip: '我的二维码',
|
||||||
|
onPressed: () {}, // TODO: QR code page
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
tooltip: '编辑资料',
|
||||||
|
onPressed: () => vm.navigateToEditProfile(context),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
|
||||||
child: _ProfileCard(state: state, onTap: () => vm.navigateToEditProfile(context)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// ── ProfileHeroCard ───────────────────────────────────────
|
||||||
|
_ProfileHeroCard(
|
||||||
|
state: state,
|
||||||
|
onTap: () => vm.navigateToEditProfile(context),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── 卡片组 1:账户 ────────────────────────────────────────
|
||||||
|
_SettingsCard(
|
||||||
|
items: [
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.credit_card_rounded,
|
||||||
|
iconColor: _walletColor,
|
||||||
|
title: '我的钱包',
|
||||||
|
onTap: () {}, // TODO: 钱包页
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.shield_rounded,
|
||||||
|
iconColor: _securityColor,
|
||||||
|
title: '账户安全',
|
||||||
|
onTap: () {}, // TODO: 账户安全页
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── 卡片组 2:工具 ────────────────────────────────────────
|
||||||
|
_SettingsCard(
|
||||||
|
items: [
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.star_rounded,
|
||||||
|
iconColor: _favoriteColor,
|
||||||
|
title: '收藏',
|
||||||
|
onTap: () {}, // TODO: 收藏页
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.phone_rounded,
|
||||||
|
iconColor: _callColor,
|
||||||
|
title: '最近呼叫',
|
||||||
|
onTap: () {}, // TODO: 呼叫记录页
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.laptop_rounded,
|
||||||
|
iconColor: _deviceColor,
|
||||||
|
title: '链接设备',
|
||||||
|
onTap: () {}, // TODO: 设备管理页
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.folder_rounded,
|
||||||
|
iconColor: _folderColor,
|
||||||
|
title: '聊天文件夹',
|
||||||
|
onTap: () {}, // TODO: Issue #11
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── 卡片组 3:偏好设置 ────────────────────────────────────
|
||||||
_SectionLabel('偏好设置'),
|
_SectionLabel('偏好设置'),
|
||||||
_SettingsCard(
|
_SettingsCard(
|
||||||
items: [
|
items: [
|
||||||
_RowConfig(
|
_RowConfig(
|
||||||
icon: Icons.palette_outlined,
|
icon: Icons.notifications_rounded,
|
||||||
title: '主题',
|
iconColor: _notifColor,
|
||||||
onTap: () => vm.navigateToTheme(context),
|
title: '通知和声音',
|
||||||
),
|
|
||||||
_RowConfig(
|
|
||||||
icon: Icons.language,
|
|
||||||
title: '语言',
|
|
||||||
onTap: () => vm.navigateToLanguage(context),
|
|
||||||
),
|
|
||||||
_RowConfig(
|
|
||||||
icon: Icons.notifications_outlined,
|
|
||||||
title: '通知',
|
|
||||||
onTap: () {}, // TODO: 通知设置页
|
onTap: () {}, // TODO: 通知设置页
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_SectionLabel('工具'),
|
|
||||||
_SettingsCard(
|
|
||||||
items: [
|
|
||||||
_RowConfig(
|
_RowConfig(
|
||||||
icon: Icons.folder_outlined,
|
icon: Icons.lock_rounded,
|
||||||
title: '聊天文件夹',
|
iconColor: _privacyColor,
|
||||||
onTap: () {}, // TODO: Issue #11
|
title: '隐私设置',
|
||||||
|
onTap: () {}, // TODO: 隐私设置页
|
||||||
),
|
),
|
||||||
_RowConfig(
|
_RowConfig(
|
||||||
icon: Icons.block,
|
icon: Icons.block_rounded,
|
||||||
|
iconColor: _blockColor,
|
||||||
title: '黑名单',
|
title: '黑名单',
|
||||||
onTap: () => vm.navigateToBlocklist(context),
|
onTap: () => vm.navigateToBlocklist(context),
|
||||||
),
|
),
|
||||||
_RowConfig(
|
_RowConfig(
|
||||||
icon: Icons.network_check,
|
icon: Icons.language_rounded,
|
||||||
title: '网络诊断',
|
iconColor: _langColor,
|
||||||
onTap: () => vm.navigateToNetworkDiagnostics(context),
|
title: '语言',
|
||||||
|
onTap: () => vm.navigateToLanguage(context),
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.palette_rounded,
|
||||||
|
iconColor: _themeColor,
|
||||||
|
title: '主题',
|
||||||
|
onTap: () => vm.navigateToTheme(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// ── 卡片组 4:关于 ────────────────────────────────────────
|
||||||
_SectionLabel('关于'),
|
_SectionLabel('关于'),
|
||||||
_SettingsCard(
|
_SettingsCard(
|
||||||
items: [
|
items: [
|
||||||
_RowConfig(
|
_RowConfig(
|
||||||
icon: Icons.info_outline,
|
icon: Icons.description_outlined,
|
||||||
title: '关于本应用',
|
iconColor: Colors.grey,
|
||||||
|
title: '用户协议',
|
||||||
onTap: () => vm.navigateToAbout(context),
|
onTap: () => vm.navigateToAbout(context),
|
||||||
),
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.privacy_tip_outlined,
|
||||||
|
iconColor: Colors.grey,
|
||||||
|
title: '隐私政策',
|
||||||
|
onTap: () => vm.navigateToAbout(context),
|
||||||
|
),
|
||||||
|
_RowConfig(
|
||||||
|
icon: Icons.info_outline_rounded,
|
||||||
|
iconColor: Colors.grey,
|
||||||
|
title: '版本号',
|
||||||
|
subtitle: AppConfig.appVersion.isEmpty
|
||||||
|
? '1.0.0'
|
||||||
|
: AppConfig.appVersion,
|
||||||
|
onTap: () {},
|
||||||
|
showChevron: false,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// ── 退出登录 ───────────────────────────────────────────────
|
||||||
_LogoutButton(
|
_LogoutButton(
|
||||||
isLoading: state.isLoggingOut,
|
isLoading: state.isLoggingOut,
|
||||||
onTap: () => _confirmLogout(context, ref),
|
onTap: () => _confirmLogout(context, ref),
|
||||||
@@ -134,16 +231,32 @@ class SettingsPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 个人资料卡 ─────────────────────────────────────────────────────────────────
|
// ── ProfileHeroCard (#39 / #41) ───────────────────────────────────────────────
|
||||||
|
|
||||||
class _ProfileCard extends StatelessWidget {
|
/// iOS SettingsView.swift ProfileHeroCard 对齐
|
||||||
const _ProfileCard({required this.state, required this.onTap});
|
///
|
||||||
|
/// 8 色渐变占位(uid % 8)+ 昵称 + @J{uid} handle + 手机号 + bio
|
||||||
|
class _ProfileHeroCard extends StatelessWidget {
|
||||||
|
const _ProfileHeroCard({required this.state, required this.onTap});
|
||||||
|
|
||||||
final SettingsState state;
|
final SettingsState state;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
// 8 色渐变方案(iOS 端 ProfileHeroCard 同款)
|
||||||
|
static const _gradients = [
|
||||||
|
[Color(0xFF4776E6), Color(0xFF8E54E9)], // 0: 蓝紫
|
||||||
|
[Color(0xFF11998E), Color(0xFF38EF7D)], // 1: 青绿
|
||||||
|
[Color(0xFFFC466B), Color(0xFF3F5EFB)], // 2: 粉蓝
|
||||||
|
[Color(0xFFF7971E), Color(0xFFFFD200)], // 3: 橙黄
|
||||||
|
[Color(0xFF56CCF2), Color(0xFF2F80ED)], // 4: 天蓝
|
||||||
|
[Color(0xFFEB3349), Color(0xFFF45C43)], // 5: 红橙
|
||||||
|
[Color(0xFF1FA2FF), Color(0xFF12D8FA)], // 6: 蓝青
|
||||||
|
[Color(0xFF9D50BB), Color(0xFF6E48AA)], // 7: 深紫
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final cs = Theme.of(context).colorScheme;
|
||||||
return Card(
|
return Card(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -151,8 +264,13 @@ class _ProfileCard extends StatelessWidget {
|
|||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
_Avatar(avatarUrl: state.avatarUrl, isLoading: state.isLoading),
|
_HeroAvatar(
|
||||||
|
avatarUrl: state.avatarUrl,
|
||||||
|
uid: state.uid,
|
||||||
|
isLoading: state.isLoading,
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: state.isLoading
|
child: state.isLoading
|
||||||
@@ -162,26 +280,45 @@ class _ProfileCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
state.nickname.isEmpty ? '加载中…' : state.nickname,
|
state.nickname.isEmpty ? '加载中…' : state.nickname,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context)
|
||||||
fontWeight: FontWeight.w600,
|
.textTheme
|
||||||
),
|
.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 2),
|
||||||
Text(
|
if (state.uid > 0)
|
||||||
state.maskedContact.isNotEmpty
|
|
||||||
? state.maskedContact
|
|
||||||
: 'UID: ${state.uid}',
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (state.maskedContact.isNotEmpty && state.uid > 0)
|
|
||||||
Text(
|
Text(
|
||||||
'UID: ${state.uid}',
|
'@J${state.uid}',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context)
|
||||||
color: Colors.grey,
|
.textTheme
|
||||||
),
|
.bodySmall
|
||||||
|
?.copyWith(color: cs.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
|
if (state.maskedContact.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
state.maskedContact,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.copyWith(color: cs.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
state.bio.isNotEmpty ? state.bio : '添加一句话简介',
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.copyWith(
|
||||||
|
color: state.bio.isNotEmpty
|
||||||
|
? cs.onSurfaceVariant
|
||||||
|
: cs.onSurfaceVariant.withOpacity(0.5),
|
||||||
|
fontStyle: state.bio.isEmpty
|
||||||
|
? FontStyle.italic
|
||||||
|
: FontStyle.normal,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -194,33 +331,51 @@ class _ProfileCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Avatar extends StatelessWidget {
|
class _HeroAvatar extends StatelessWidget {
|
||||||
const _Avatar({required this.avatarUrl, required this.isLoading});
|
const _HeroAvatar({
|
||||||
|
required this.avatarUrl,
|
||||||
|
required this.uid,
|
||||||
|
required this.isLoading,
|
||||||
|
});
|
||||||
|
|
||||||
final String? avatarUrl;
|
final String? avatarUrl;
|
||||||
|
final int uid;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
const radius = 36.0; // 72pt diameter
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const CircleAvatar(
|
return const CircleAvatar(
|
||||||
radius: 28,
|
radius: radius,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 20,
|
width: 24,
|
||||||
height: 20,
|
height: 24,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (avatarUrl != null && avatarUrl!.isNotEmpty) {
|
if (avatarUrl != null && avatarUrl!.isNotEmpty) {
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
radius: 28,
|
radius: radius,
|
||||||
backgroundImage: NetworkImage(avatarUrl!),
|
backgroundImage: NetworkImage(avatarUrl!),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const CircleAvatar(
|
// 渐变占位(uid % 8)
|
||||||
radius: 28,
|
final colors = _ProfileHeroCard._gradients[uid.abs() % 8];
|
||||||
child: Icon(Icons.person, size: 28),
|
return Container(
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: colors,
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.person, size: 36, color: Colors.white),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -253,11 +408,10 @@ class _ProfileSkeleton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 设置分组 ───────────────────────────────────────────────────────────────────
|
// ── 设置分组 (#40) ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _SectionLabel extends StatelessWidget {
|
class _SectionLabel extends StatelessWidget {
|
||||||
const _SectionLabel(this.text);
|
const _SectionLabel(this.text);
|
||||||
|
|
||||||
final String text;
|
final String text;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -278,20 +432,23 @@ class _SectionLabel extends StatelessWidget {
|
|||||||
class _RowConfig {
|
class _RowConfig {
|
||||||
const _RowConfig({
|
const _RowConfig({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
|
required this.iconColor,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.subtitle,
|
this.subtitle,
|
||||||
required this.onTap,
|
required this.onTap,
|
||||||
|
this.showChevron = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
final Color iconColor;
|
||||||
final String title;
|
final String title;
|
||||||
final String? subtitle;
|
final String? subtitle;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
final bool showChevron;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SettingsCard extends StatelessWidget {
|
class _SettingsCard extends StatelessWidget {
|
||||||
const _SettingsCard({required this.items});
|
const _SettingsCard({required this.items});
|
||||||
|
|
||||||
final List<_RowConfig> items;
|
final List<_RowConfig> items;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -303,7 +460,7 @@ class _SettingsCard extends StatelessWidget {
|
|||||||
if (i > 0)
|
if (i > 0)
|
||||||
Divider(
|
Divider(
|
||||||
height: 1,
|
height: 1,
|
||||||
indent: 52,
|
indent: 60,
|
||||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
_SettingsRow(config: items[i]),
|
_SettingsRow(config: items[i]),
|
||||||
@@ -314,28 +471,58 @@ class _SettingsCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// iOS 风格行:36pt 彩色圆角正方形图标 + 标题 + 可选副标题 + chevron
|
||||||
class _SettingsRow extends StatelessWidget {
|
class _SettingsRow extends StatelessWidget {
|
||||||
const _SettingsRow({required this.config});
|
const _SettingsRow({required this.config});
|
||||||
|
|
||||||
final _RowConfig config;
|
final _RowConfig config;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: Icon(config.icon, size: 22),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
|
||||||
|
leading: _IconBox(icon: config.icon, color: config.iconColor),
|
||||||
title: Text(config.title),
|
title: Text(config.title),
|
||||||
subtitle: config.subtitle != null ? Text(config.subtitle!) : null,
|
subtitle: config.subtitle != null
|
||||||
trailing: const Icon(Icons.chevron_right, size: 18, color: Colors.grey),
|
? Text(
|
||||||
|
config.subtitle!,
|
||||||
|
style: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.bodySmall
|
||||||
|
?.copyWith(color: Colors.grey),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: config.showChevron
|
||||||
|
? const Icon(Icons.chevron_right, size: 18, color: Colors.grey)
|
||||||
|
: null,
|
||||||
onTap: config.onTap,
|
onTap: config.onTap,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 36pt 圆角正方形彩色图标盒(iOS Settings icon style)
|
||||||
|
class _IconBox extends StatelessWidget {
|
||||||
|
const _IconBox({required this.icon, required this.color});
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: Colors.white, size: 20),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 退出登录 ───────────────────────────────────────────────────────────────────
|
// ── 退出登录 ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class _LogoutButton extends StatelessWidget {
|
class _LogoutButton extends StatelessWidget {
|
||||||
const _LogoutButton({required this.isLoading, required this.onTap});
|
const _LogoutButton({required this.isLoading, required this.onTap});
|
||||||
|
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user