From 306982207e95aae5236ef4db20a1325fa1946e7e Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sun, 9 Aug 2026 21:05:48 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=BA=BA=E7=89=A9=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/explore/person_list_page.dart | 13 +++-- lib/pages/people/person_list_page.dart | 29 +++------- lib/widgets/person_avatar.dart | 72 +++++++++++++++++++++++++ lib/widgets/person_info_sheet.dart | 27 +++------- lib/widgets/work_people_section.dart | 28 +++------- 5 files changed, 96 insertions(+), 73 deletions(-) create mode 100644 lib/widgets/person_avatar.dart diff --git a/lib/pages/explore/person_list_page.dart b/lib/pages/explore/person_list_page.dart index 0d575e8..dadd478 100644 --- a/lib/pages/explore/person_list_page.dart +++ b/lib/pages/explore/person_list_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../providers/app_provider.dart'; import '../../models/data_models.dart'; +import '../../widgets/person_avatar.dart'; import '../movies/movie_detail_page.dart'; import '../book/book_detail_page.dart'; @@ -214,13 +215,11 @@ class _PersonListPageState extends State { final totalWorks = person.movies.length + person.books.length; return ListTile( contentPadding: const EdgeInsets.symmetric(vertical: 4), - leading: CircleAvatar( - radius: 20, - backgroundColor: colors.surfaceContainerHighest, - child: Text( - person.name.isNotEmpty ? person.name[0] : '?', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.5)), - ), + leading: PersonAvatar( + photoPath: null, + name: person.name, + size: 40, + fontSize: 16, ), title: Text(person.name, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: colors.onSurface)), subtitle: Padding( diff --git a/lib/pages/people/person_list_page.dart b/lib/pages/people/person_list_page.dart index fd0502e..24a5633 100644 --- a/lib/pages/people/person_list_page.dart +++ b/lib/pages/people/person_list_page.dart @@ -4,7 +4,7 @@ import '../../models/data_models.dart'; import '../../providers/app_provider.dart'; import '../../utils/responsive.dart'; import '../../utils/toast_util.dart'; -import '../../widgets/fade_in_local_image.dart'; +import '../../widgets/person_avatar.dart'; import 'person_detail_page.dart'; import 'person_form_page.dart'; @@ -211,7 +211,6 @@ class _PersonListPageState extends State { } Widget _buildPersonItem(Person person, ColorScheme colors) { - final hasPhoto = person.photoPath != null && person.photoPath!.isNotEmpty; return InkWell( onTap: () => _navigateToDetail(person), borderRadius: BorderRadius.circular(10), @@ -224,27 +223,11 @@ class _PersonListPageState extends State { ), child: Row( children: [ - // 头像 - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - shape: BoxShape.circle, - ), - clipBehavior: Clip.antiAlias, - child: hasPhoto - ? FadeInLocalImage(path: person.photoPath, fit: BoxFit.cover) - : Center( - child: Text( - person.name.isNotEmpty ? person.name[0] : '?', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.w600, - color: colors.onSurface.withValues(alpha: 0.4), - ), - ), - ), + PersonAvatar( + photoPath: person.photoPath, + name: person.name, + size: 48, + fontSize: 20, ), const SizedBox(width: 12), // 信息 diff --git a/lib/widgets/person_avatar.dart b/lib/widgets/person_avatar.dart new file mode 100644 index 0000000..ca4d48a --- /dev/null +++ b/lib/widgets/person_avatar.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'fade_in_local_image.dart'; + +/// 人物头像。 +/// +/// 有照片时显示照片;无照片时根据名字哈希从调色板取色, +/// 显示首字占位(同一人物始终同色)。 +class PersonAvatar extends StatelessWidget { + final String? photoPath; + final String name; + final double size; + final double fontSize; + + const PersonAvatar({ + super.key, + required this.photoPath, + required this.name, + required this.size, + required this.fontSize, + }); + + // 固定调色板:背景为该色的低透明度,文字为该色本身。 + static const _palette = [ + Color(0xFF4A90D9), // 蓝 + Color(0xFF009688), // 青 + Color(0xFFE91E63), // 粉 + Color(0xFF7E57C2), // 紫 + Color(0xFFFF7043), // 橙 + Color(0xFF4CAF50), // 绿 + Color(0xFFFF9800), // 琥珀 + Color(0xFF795548), // 棕 + Color(0xFF5C6BC0), // 靛 + Color(0xFFEC407A), // 玫红 + ]; + + Color _colorFor(String name) { + var hash = 0; + for (final c in name.codeUnits) { + hash = (hash * 31 + c) & 0x7fffffff; + } + return _palette[hash % _palette.length]; + } + + @override + Widget build(BuildContext context) { + final hasPhoto = photoPath != null && photoPath!.isNotEmpty; + final color = _colorFor(name); + final initial = name.isNotEmpty ? name[0] : '?'; + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: hasPhoto ? null : color.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + clipBehavior: Clip.antiAlias, + child: hasPhoto + ? FadeInLocalImage(path: photoPath, fit: BoxFit.cover) + : Center( + child: Text( + initial, + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w600, + color: color, + ), + ), + ), + ); + } +} diff --git a/lib/widgets/person_info_sheet.dart b/lib/widgets/person_info_sheet.dart index f28cfc9..17939e4 100644 --- a/lib/widgets/person_info_sheet.dart +++ b/lib/widgets/person_info_sheet.dart @@ -4,6 +4,7 @@ import '../models/data_models.dart'; import '../pages/people/person_detail_page.dart'; import '../providers/app_provider.dart'; import 'fade_in_local_image.dart'; +import 'person_avatar.dart'; /// 人物信息浮动面板(底部 ModalBottomSheet) /// 展示人物基本信息 + 关联作品,点击「查看全部」跳转到原详情页 @@ -130,30 +131,14 @@ class _PersonInfoSheetState extends State { } Widget _buildHeader(Person person, ColorScheme colors) { - final hasPhoto = person.photoPath != null && person.photoPath!.isNotEmpty; return Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Container( - width: 64, - height: 64, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - shape: BoxShape.circle, - ), - clipBehavior: Clip.antiAlias, - child: hasPhoto - ? FadeInLocalImage(path: person.photoPath, fit: BoxFit.cover) - : Center( - child: Text( - person.name.isNotEmpty ? person.name[0] : '?', - style: TextStyle( - fontSize: 26, - fontWeight: FontWeight.w600, - color: colors.onSurface.withValues(alpha: 0.3), - ), - ), - ), + PersonAvatar( + photoPath: person.photoPath, + name: person.name, + size: 64, + fontSize: 26, ), const SizedBox(width: 14), Expanded( diff --git a/lib/widgets/work_people_section.dart b/lib/widgets/work_people_section.dart index 4f316ca..0930693 100644 --- a/lib/widgets/work_people_section.dart +++ b/lib/widgets/work_people_section.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../models/data_models.dart'; import '../providers/app_provider.dart'; -import 'fade_in_local_image.dart'; +import 'person_avatar.dart'; import 'person_info_sheet.dart'; /// 作品详情页使用的"关联人物"区块 @@ -233,33 +233,17 @@ class _WorkPeopleSectionState extends State { Widget _buildPersonChip(_PersonRole item, ColorScheme colors) { final person = item.person; - final hasPhoto = person.photoPath != null && person.photoPath!.isNotEmpty; return GestureDetector( onTap: () => PersonInfoSheet.show(context, person), child: SizedBox( width: 84, child: Column( children: [ - Container( - width: 60, - height: 60, - decoration: BoxDecoration( - color: colors.surfaceContainerHighest, - shape: BoxShape.circle, - ), - clipBehavior: Clip.antiAlias, - child: hasPhoto - ? FadeInLocalImage(path: person.photoPath, fit: BoxFit.cover) - : Center( - child: Text( - person.name.isNotEmpty ? person.name[0] : '?', - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: colors.onSurface.withValues(alpha: 0.3), - ), - ), - ), + PersonAvatar( + photoPath: person.photoPath, + name: person.name, + size: 60, + fontSize: 24, ), const SizedBox(height: 8), Text(