更改前优化

This commit is contained in:
DelLevin-Home
2026-06-20 17:58:57 +08:00
parent 58be9f1d9d
commit e193c294af
3 changed files with 233 additions and 152 deletions

View File

@@ -1767,7 +1767,6 @@ class _MultiValueDialogState extends State<_MultiValueDialog> {
Expanded( Expanded(
child: TextField( child: TextField(
controller: controller, controller: controller,
autofocus: true,
style: TextStyle(fontSize: 15, color: colors.onSurface), style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.hint, hintText: widget.hint,

View File

@@ -570,13 +570,32 @@ class _MovieFormPageState extends State<MovieFormPage> {
SizedBox( SizedBox(
width: (MediaQuery.of(context).size.width - 52) / 2, width: (MediaQuery.of(context).size.width - 52) / 2,
height: 90, height: 90,
child: _buildInfoCard( child: Stack(
label: '观看日期', children: [
value: _watchDate != null _buildInfoCard(
? '${_watchDate!.year}.${_watchDate!.month.toString().padLeft(2, '0')}.${_watchDate!.day.toString().padLeft(2, '0')}' label: '观看日期',
: '', value: _watchDate != null
icon: Icons.visibility_outlined, ? '${_watchDate!.year}.${_watchDate!.month.toString().padLeft(2, '0')}.${_watchDate!.day.toString().padLeft(2, '0')}'
onTap: () => _selectWatchDate(), : '',
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( Expanded(
child: TextField( child: TextField(
controller: controller, controller: controller,
autofocus: true,
style: TextStyle(fontSize: 15, color: colors.onSurface), style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration( decoration: InputDecoration(
hintText: widget.hint, hintText: widget.hint,

View File

@@ -22,6 +22,9 @@ class _TagManagementPageState extends State<TagManagementPage> {
final Map<String, List<Map<String, dynamic>>> _tagCache = {}; final Map<String, List<Map<String, dynamic>>> _tagCache = {};
Map<String, int> _usageCounts = {}; Map<String, int> _usageCounts = {};
String? _newlyAddedTagId; String? _newlyAddedTagId;
String _searchQuery = '';
final _searchController = TextEditingController();
bool _showTypePicker = false;
@override @override
void initState() { void initState() {
@@ -29,6 +32,12 @@ class _TagManagementPageState extends State<TagManagementPage> {
_loadTags(_tabTypes[0]); _loadTags(_tabTypes[0]);
} }
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
@@ -86,147 +95,163 @@ class _TagManagementPageState extends State<TagManagementPage> {
String get _currentType => _tabTypes[_currentIndex]; String get _currentType => _tabTypes[_currentIndex];
int get _activeTagCount {
return (_tagCache[_currentType] ?? []).where((t) => (_usageCounts[t['name']] ?? 0) > 0).length;
}
// ─── build ───────────────────────────────────────────────────────────── // ─── build ─────────────────────────────────────────────────────────────
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colors = Theme.of(context).colorScheme; 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( return Scaffold(
backgroundColor: colors.surfaceContainerHigh, backgroundColor: colors.surfaceContainerHigh,
appBar: AppBar( appBar: AppBar(title: const Text('标签管理'), actions: [
title: const Text('标签管理'), _isSyncing
bottom: PreferredSize( ? Padding(padding: const EdgeInsets.all(16),
preferredSize: const Size.fromHeight(20), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: colors.primary)))
child: Padding( : IconButton(icon: const Icon(Icons.sync, size: 20), tooltip: '从数据中同步标签', onPressed: _syncTags),
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,
),
],
),
body: Column( body: Column(
children: [ children: [
const SizedBox(height: 12), 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( Expanded(
child: AnimatedSwitcher( child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
switchInCurve: Curves.easeOut, switchInCurve: Curves.easeOut, switchOutCurve: Curves.easeIn,
switchOutCurve: Curves.easeIn,
child: _buildTagList(_currentType), child: _buildTagList(_currentType),
), ),
), ),
], ],
), ),
floatingActionButton: GestureDetector( floatingActionButton: Column(
onTap: _showAddDialog, mainAxisSize: MainAxisSize.min,
child: Container( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), children: [
decoration: BoxDecoration( // 弹出的类别胶囊按钮
color: colors.primary, if (_showTypePicker) ...[
borderRadius: BorderRadius.circular(24), ...[0, 1, 2].where((i) => i != _currentIndex).map((i) => Padding(
boxShadow: [ padding: const EdgeInsets.only(bottom: 8),
BoxShadow(color: Colors.black.withValues(alpha: 0.12), blurRadius: 12, offset: const Offset(0, 4)), child: GestureDetector(
], onTap: () {
), setState(() => _showTypePicker = false);
child: Row( _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, mainAxisSize: MainAxisSize.min,
children: [ children: [
Icon(Icons.add, size: 18, color: colors.onPrimary), // 类别切换按钮
const SizedBox(width: 6), GestureDetector(
Text('添加${_typeLabels[_currentIndex]}', onTap: () => setState(() => _showTypePicker = !_showTypePicker),
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onPrimary)), 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 _buildStatChip(IconData icon, String label, int count, ColorScheme colors) {
return Expanded(
Widget _buildTabSelector() { child: Container(
final colors = Theme.of(context).colorScheme; padding: const EdgeInsets.symmetric(vertical: 8),
return Container( decoration: BoxDecoration(color: colors.surface, borderRadius: BorderRadius.circular(8)),
margin: const EdgeInsets.symmetric(horizontal: 24), child: Row(
padding: const EdgeInsets.all(4), mainAxisAlignment: MainAxisAlignment.center,
decoration: BoxDecoration( children: [
color: colors.outlineVariant, Icon(icon, size: 14, color: colors.onSurface.withValues(alpha: 0.5)),
borderRadius: BorderRadius.circular(24), const SizedBox(width: 4),
), Text('$count', style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: colors.onSurface)),
child: LayoutBuilder( const SizedBox(width: 2),
builder: (context, constraints) { Text(label, style: TextStyle(fontSize: 10, color: colors.onSurface.withValues(alpha: 0.4))),
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)),
),
),
),
);
}),
),
],
),
);
},
), ),
); );
} }
@@ -235,28 +260,73 @@ class _TagManagementPageState extends State<TagManagementPage> {
Widget _buildTagList(String type) { Widget _buildTagList(String type) {
final tags = _tagCache[type] ?? []; final tags = _tagCache[type] ?? [];
if (tags.isEmpty) return _buildEmptyState(type);
if (tags.isEmpty) { final isSearching = _searchQuery.isNotEmpty;
return _buildEmptyState(type);
// 搜索模式
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<Map<String, dynamic>>.from(tags) // 分组模式
..sort((a, b) { final used = <Map<String, dynamic>>[];
final ca = _usageCounts[a['name']] ?? 0; final unused = <Map<String, dynamic>>[];
final cb = _usageCounts[b['name']] ?? 0; final hidden = <Map<String, dynamic>>[];
return cb.compareTo(ca); 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), key: ValueKey(type),
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.fromLTRB(20, 0, 20, 80),
child: SingleChildScrollView( child: Column(
padding: const EdgeInsets.only(bottom: 80), crossAxisAlignment: CrossAxisAlignment.start,
child: Wrap( children: [
spacing: 10, if (used.isNotEmpty) ...[
runSpacing: 10, _buildGroupHeader('已使用', used.length, colors),
children: sorted.map((tag) => _buildTagChip(tag)).toList(), 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<TagManagementPage> {
Widget _tagChipContent(String name, int count, ColorScheme colors, {bool isHidden = false}) { Widget _tagChipContent(String name, int count, ColorScheme colors, {bool isHidden = false}) {
return Container( 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( decoration: BoxDecoration(
color: colors.surface, color: colors.surfaceContainerHighest,
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 4, offset: const Offset(0, 2)),
],
), ),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(name, style: TextStyle( 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, decoration: isHidden ? TextDecoration.lineThrough : null,
)), )),
if (count > 0) ...[ if (count > 0) ...[
const SizedBox(width: 6), const SizedBox(width: 6),
Container( Text('$count', style: TextStyle(fontSize: 10, fontWeight: FontWeight.w600, color: colors.onSurface.withValues(alpha: 0.35))),
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))),
),
], ],
], ],
), ),