From e193c294aff43b75161ba54ab6e1e468e57aab52 Mon Sep 17 00:00:00 2001 From: DelLevin-Home Date: Sat, 20 Jun 2026 17:58:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=94=B9=E5=89=8D=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/book/book_form_page.dart | 1 - lib/pages/movies/movie_form_page.dart | 34 ++- lib/pages/tag_management_page.dart | 350 +++++++++++++++----------- 3 files changed, 233 insertions(+), 152 deletions(-) diff --git a/lib/pages/book/book_form_page.dart b/lib/pages/book/book_form_page.dart index 9056037..82dd12d 100644 --- a/lib/pages/book/book_form_page.dart +++ b/lib/pages/book/book_form_page.dart @@ -1767,7 +1767,6 @@ class _MultiValueDialogState extends State<_MultiValueDialog> { Expanded( child: TextField( controller: controller, - autofocus: true, style: TextStyle(fontSize: 15, color: colors.onSurface), decoration: InputDecoration( hintText: widget.hint, diff --git a/lib/pages/movies/movie_form_page.dart b/lib/pages/movies/movie_form_page.dart index 403de5e..c1b097f 100644 --- a/lib/pages/movies/movie_form_page.dart +++ b/lib/pages/movies/movie_form_page.dart @@ -570,13 +570,32 @@ class _MovieFormPageState extends State { SizedBox( width: (MediaQuery.of(context).size.width - 52) / 2, height: 90, - child: _buildInfoCard( - label: '观看日期', - value: _watchDate != null - ? '${_watchDate!.year}.${_watchDate!.month.toString().padLeft(2, '0')}.${_watchDate!.day.toString().padLeft(2, '0')}' - : '', - icon: Icons.visibility_outlined, - onTap: () => _selectWatchDate(), + child: Stack( + children: [ + _buildInfoCard( + label: '观看日期', + value: _watchDate != null + ? '${_watchDate!.year}.${_watchDate!.month.toString().padLeft(2, '0')}.${_watchDate!.day.toString().padLeft(2, '0')}' + : '', + icon: Icons.visibility_outlined, + onTap: () => _selectWatchDate(), + ), + if (_watchDate != null) + Positioned( + top: 8, right: 8, + child: GestureDetector( + onTap: () => setState(() => _watchDate = null), + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: colors.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: Icon(Icons.close, size: 14, color: colors.onSurface.withValues(alpha: 0.4)), + ), + ), + ), + ], ), ), @@ -2180,7 +2199,6 @@ class _MultiValueDialogState extends State<_MultiValueDialog> { Expanded( child: TextField( controller: controller, - autofocus: true, style: TextStyle(fontSize: 15, color: colors.onSurface), decoration: InputDecoration( hintText: widget.hint, diff --git a/lib/pages/tag_management_page.dart b/lib/pages/tag_management_page.dart index 35f44bd..0c6f11d 100644 --- a/lib/pages/tag_management_page.dart +++ b/lib/pages/tag_management_page.dart @@ -22,6 +22,9 @@ class _TagManagementPageState extends State { final Map>> _tagCache = {}; Map _usageCounts = {}; String? _newlyAddedTagId; + String _searchQuery = ''; + final _searchController = TextEditingController(); + bool _showTypePicker = false; @override void initState() { @@ -29,6 +32,12 @@ class _TagManagementPageState extends State { _loadTags(_tabTypes[0]); } + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -86,147 +95,163 @@ class _TagManagementPageState extends State { String get _currentType => _tabTypes[_currentIndex]; - int get _activeTagCount { - return (_tagCache[_currentType] ?? []).where((t) => (_usageCounts[t['name']] ?? 0) > 0).length; - } - // ─── build ───────────────────────────────────────────────────────────── @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; + final tags = _tagCache[_currentType] ?? []; + final isSearching = _searchQuery.isNotEmpty; + final usedCount = tags.where((t) => (_usageCounts[t['name']] ?? 0) > 0 && (t['is_hidden'] as int?) != 1).length; + final unusedCount = tags.where((t) => (_usageCounts[t['name']] ?? 0) == 0 && (t['is_hidden'] as int?) != 1).length; + final hiddenCount = tags.where((t) => (t['is_hidden'] as int?) == 1).length; + return Scaffold( backgroundColor: colors.surfaceContainerHigh, - appBar: AppBar( - title: const Text('标签管理'), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(20), - child: Padding( - padding: const EdgeInsets.only(bottom: 10), - child: Text( - '共 ${(_tagCache[_currentType] ?? []).length} 个标签 · 已使用 $_activeTagCount 个', - style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3)), - ), - ), - ), - actions: [ - _isSyncing - ? Padding( - padding: const EdgeInsets.all(16), - child: SizedBox(width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)), - ) - : IconButton( - icon: const Icon(Icons.sync, size: 20), - tooltip: '从数据中同步标签', - onPressed: _syncTags, - ), - ], - ), + appBar: AppBar(title: const Text('标签管理'), actions: [ + _isSyncing + ? Padding(padding: const EdgeInsets.all(16), + child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary))) + : IconButton(icon: const Icon(Icons.sync, size: 20), tooltip: '从数据中同步标签', onPressed: _syncTags), + ]), body: Column( children: [ const SizedBox(height: 12), - _buildTabSelector(), - const SizedBox(height: 16), + // 搜索栏 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: TextField( + controller: _searchController, + style: TextStyle(fontSize: 13, color: colors.onSurface), + decoration: InputDecoration( + hintText: '搜索标签...', + hintStyle: TextStyle(fontSize: 13, color: colors.onSurface.withValues(alpha: 0.3)), + prefixIcon: Icon(Icons.search, size: 18, color: colors.onSurface.withValues(alpha: 0.3)), + suffixIcon: _searchQuery.isNotEmpty + ? GestureDetector(onTap: () { _searchController.clear(); setState(() => _searchQuery = ''); }, + child: Icon(Icons.close, size: 18, color: colors.onSurface.withValues(alpha: 0.3))) + : null, + filled: true, fillColor: colors.surface, + contentPadding: const EdgeInsets.symmetric(vertical: 10), + border: OutlineInputBorder(borderRadius: BorderRadius.circular(10), borderSide: BorderSide.none), + ), + onChanged: (v) => setState(() => _searchQuery = v.trim()), + ), + ), + const SizedBox(height: 12), + // 统计卡片 + if (!isSearching) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row(children: [ + _buildStatChip(Icons.check_circle_outline, '已使用', usedCount, colors), + const SizedBox(width: 8), + _buildStatChip(Icons.radio_button_unchecked, '未使用', unusedCount, colors), + const SizedBox(width: 8), + _buildStatChip(Icons.visibility_off_outlined, '隐藏', hiddenCount, colors), + ]), + ), + SizedBox(height: isSearching ? 0 : 12), + // 标签列表 Expanded( child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), - switchInCurve: Curves.easeOut, - switchOutCurve: Curves.easeIn, + switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn, child: _buildTagList(_currentType), ), ), ], ), - floatingActionButton: GestureDetector( - onTap: _showAddDialog, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - decoration: BoxDecoration( - color: colors.primary, - borderRadius: BorderRadius.circular(24), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)), - ], - ), - child: Row( + floatingActionButton: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 弹出的类别胶囊按钮 + if (_showTypePicker) ...[ + ...[0, 1, 2].where((i) => i != _currentIndex).map((i) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GestureDetector( + onTap: () { + setState(() => _showTypePicker = false); + _onTabChanged(i); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: colors.surface, borderRadius: BorderRadius.circular(24), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))], + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(_typeIcons[i], size: 16, color: colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 6), + Text(_typeLabels[i], style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + ]), + ), + ), + )), + const SizedBox(height: 4), + ], + // 底部按钮行 + Row( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.add, size: 18, color: colors.onPrimary), - const SizedBox(width: 6), - Text('添加${_typeLabels[_currentIndex]}', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)), + // 类别切换按钮 + GestureDetector( + onTap: () => setState(() => _showTypePicker = !_showTypePicker), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: colors.surface, borderRadius: BorderRadius.circular(24), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 8, offset: const Offset(0, 2))], + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(_typeIcons[_currentIndex], size: 16, color: colors.onSurface.withValues(alpha: 0.6)), + const SizedBox(width: 6), + Text(_typeLabels[_currentIndex], style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: colors.onSurface)), + const SizedBox(width: 4), + Icon(_showTypePicker ? Icons.arrow_drop_down : Icons.arrow_drop_up, size: 18, color: colors.onSurface.withValues(alpha: 0.4)), + ]), + ), + ), + const SizedBox(width: 10), + // 添加按钮 + GestureDetector( + onTap: _showAddDialog, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + decoration: BoxDecoration( + color: colors.primary, borderRadius: BorderRadius.circular(24), + boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4))], + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + Icon(Icons.add, size: 18, color: colors.onPrimary), + const SizedBox(width: 6), + Text('添加${_typeLabels[_currentIndex]}', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)), + ]), + ), + ), ], ), - ), + ], ), ); } - // ─── Tab 选择器 ───────────────────────────────────────────────────────── - - Widget _buildTabSelector() { - final colors = Theme.of(context).colorScheme; - return Container( - margin: const EdgeInsets.symmetric(horizontal: 24), - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - color: colors.outlineVariant, - borderRadius: BorderRadius.circular(24), - ), - child: LayoutBuilder( - builder: (context, constraints) { - final tabWidth = constraints.maxWidth / 3; - return SizedBox( - height: 42, - child: Stack( - children: [ - AnimatedPositioned( - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - left: _currentIndex * tabWidth, - top: 0, bottom: 0, width: tabWidth, - child: Padding( - padding: const EdgeInsets.all(3), - child: Container( - decoration: BoxDecoration( - color: colors.surface, - borderRadius: BorderRadius.circular(22), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.08), blurRadius: 8, offset: const Offset(0, 2)), - BoxShadow(color: Colors.black.withValues(alpha: 0.04), blurRadius: 2, offset: const Offset(0, 1)), - ], - ), - ), - ), - ), - Row( - children: List.generate(3, (i) { - final selected = _currentIndex == i; - return Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _onTabChanged(i), - child: AnimatedOpacity( - opacity: selected ? 1.0 : 0.4, - duration: const Duration(milliseconds: 200), - curve: Curves.easeInOut, - child: Container( - height: double.infinity, - alignment: Alignment.center, - child: Text(_typeLabels[i], - style: TextStyle(fontSize: 13, fontWeight: selected ? FontWeight.w600 : FontWeight.w500, - color: colors.onSurface)), - ), - ), - ), - ); - }), - ), - ], - ), - ); - }, + Widget _buildStatChip(IconData icon, String label, int count, ColorScheme colors) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(8)), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.5)), + const SizedBox(width: 4), + Text('$count', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)), + const SizedBox(width: 2), + Text(label, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))), + ], + ), ), ); } @@ -235,28 +260,73 @@ class _TagManagementPageState extends State { Widget _buildTagList(String type) { final tags = _tagCache[type] ?? []; + if (tags.isEmpty) return _buildEmptyState(type); - if (tags.isEmpty) { - return _buildEmptyState(type); + final isSearching = _searchQuery.isNotEmpty; + + // 搜索模式 + if (isSearching) { + final filtered = tags.where((t) => (t['name'] as String).toLowerCase().contains(_searchQuery.toLowerCase())).toList() + ..sort((a, b) => (_usageCounts[b['name']] ?? 0).compareTo(_usageCounts[a['name']] ?? 0)); + if (filtered.isEmpty) { + return Center(child: Text('没有找到"$_searchQuery"相关标签', style: TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.4)))); + } + return SingleChildScrollView( + key: ValueKey('search_$type'), + padding: const EdgeInsets.fromLTRB(20, 0, 20, 80), + child: Wrap(spacing: 8, runSpacing: 6, children: filtered.map(_buildTagChip).toList()), + ); } - final sorted = List>.from(tags) - ..sort((a, b) { - final ca = _usageCounts[a['name']] ?? 0; - final cb = _usageCounts[b['name']] ?? 0; - return cb.compareTo(ca); - }); + // 分组模式 + final used = >[]; + final unused = >[]; + final hidden = >[]; + for (final t in tags) { + if ((t['is_hidden'] as int?) == 1) { hidden.add(t); continue; } + if ((_usageCounts[t['name']] ?? 0) > 0) { used.add(t); continue; } + unused.add(t); + } + used.sort((a, b) => (_usageCounts[b['name']] ?? 0).compareTo(_usageCounts[a['name']] ?? 0)); - return Padding( + final colors = Theme.of(context).colorScheme; + return SingleChildScrollView( key: ValueKey(type), - padding: const EdgeInsets.symmetric(horizontal: 20), - child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 80), - child: Wrap( - spacing: 10, - runSpacing: 10, - children: sorted.map((tag) => _buildTagChip(tag)).toList(), - ), + padding: const EdgeInsets.fromLTRB(20, 0, 20, 80), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (used.isNotEmpty) ...[ + _buildGroupHeader('已使用', used.length, colors), + const SizedBox(height: 6), + Wrap(spacing: 8, runSpacing: 6, children: used.map(_buildTagChip).toList()), + const SizedBox(height: 16), + ], + if (unused.isNotEmpty) ...[ + _buildGroupHeader('未使用', unused.length, colors), + const SizedBox(height: 6), + Wrap(spacing: 8, runSpacing: 6, children: unused.map(_buildTagChip).toList()), + const SizedBox(height: 16), + ], + if (hidden.isNotEmpty) ...[ + _buildGroupHeader('隐藏', hidden.length, colors), + const SizedBox(height: 6), + Wrap(spacing: 8, runSpacing: 6, children: hidden.map(_buildTagChip).toList()), + ], + ], + ), + ); + } + + Widget _buildGroupHeader(String label, int count, ColorScheme colors) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: Row( + children: [ + Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.5))), + const SizedBox(width: 6), + Text('$count', style: TextStyle(fontSize: 12, color: colors.onSurface.withValues(alpha: 0.3))), + ], ), ); } @@ -284,28 +354,22 @@ class _TagManagementPageState extends State { Widget _tagChipContent(String name, int count, ColorScheme colors, {bool isHidden = false}) { return Container( - padding: const EdgeInsets.only(left: 14, right: 10, top: 8, bottom: 8), + margin: const EdgeInsets.only(bottom: 4), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), decoration: BoxDecoration( - color: colors.surface, + color: colors.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)), - ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text(name, style: TextStyle( - fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface, + fontSize: 12, fontWeight: FontWeight.w500, color: colors.onSurface, decoration: isHidden ? TextDecoration.lineThrough : null, )), if (count > 0) ...[ const SizedBox(width: 6), - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration(color: colors.outlineVariant, borderRadius: BorderRadius.circular(8)), - child: Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.4))), - ), + Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.35))), ], ], ),