更改前优化

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(
child: TextField(
controller: controller,
autofocus: true,
style: TextStyle(fontSize: 15, color: colors.onSurface),
decoration: InputDecoration(
hintText: widget.hint,

View File

@@ -570,13 +570,32 @@ class _MovieFormPageState extends State<MovieFormPage> {
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,

View File

@@ -22,6 +22,9 @@ class _TagManagementPageState extends State<TagManagementPage> {
final Map<String, List<Map<String, dynamic>>> _tagCache = {};
Map<String, int> _usageCounts = {};
String? _newlyAddedTagId;
String _searchQuery = '';
final _searchController = TextEditingController();
bool _showTypePicker = false;
@override
void initState() {
@@ -29,6 +32,12 @@ class _TagManagementPageState extends State<TagManagementPage> {
_loadTags(_tabTypes[0]);
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
@@ -86,147 +95,163 @@ class _TagManagementPageState extends State<TagManagementPage> {
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<TagManagementPage> {
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<Map<String, dynamic>>.from(tags)
..sort((a, b) {
final ca = _usageCounts[a['name']] ?? 0;
final cb = _usageCounts[b['name']] ?? 0;
return cb.compareTo(ca);
});
// 分组模式
final used = <Map<String, dynamic>>[];
final unused = <Map<String, dynamic>>[];
final hidden = <Map<String, dynamic>>[];
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<TagManagementPage> {
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))),
],
],
),