Files
TtxPageAdmin/lib/studio_app_shell_sidebar.dart
T
2026-05-16 01:10:06 +08:00

310 lines
9.5 KiB
Dart

part of 'studio_app.dart';
class StudioSidebar extends StatelessWidget {
const StudioSidebar({
super.key,
required this.user,
required this.themeMode,
required this.onToggleTheme,
required this.searchQuery,
required this.filteredLeaves,
required this.rootNodes,
required this.onSearchChanged,
required this.onOpenNode,
required this.onLogout,
});
final StudioUser user;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final String searchQuery;
final List<StudioNavNode> filteredLeaves;
final List<StudioNavNode> rootNodes;
final ValueChanged<String> onSearchChanged;
final ValueChanged<StudioNavNode> onOpenNode;
final VoidCallback onLogout;
@override
Widget build(BuildContext context) {
return Container(
color: _isDarkTheme(context) ? const Color(0xFF0A1320) : Colors.white,
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: _SidebarHero(
user: user,
themeMode: themeMode,
onToggleTheme: onToggleTheme,
onLogout: onLogout,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: TextField(
onChanged: onSearchChanged,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search_rounded),
hintText: '搜索菜单或功能',
),
),
),
const SizedBox(height: 16),
Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(12, 0, 12, 16),
children: [
if (searchQuery.trim().isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 6,
),
child: Text(
'搜索结果',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: _mutedColor(context),
),
),
),
...filteredLeaves.map(
(node) =>
_LeafNavTile(node: node, onTap: () => onOpenNode(node)),
),
if (filteredLeaves.isEmpty)
const Padding(
padding: EdgeInsets.all(16),
child: Text('没有匹配的菜单项'),
),
] else ...[
_QuickLaunchStrip(
nodes: _allLeaves(rootNodes).take(4).toList(),
onOpenNode: onOpenNode,
),
const SizedBox(height: 12),
...rootNodes.map(
(node) => _TreeNode(node: node, onLeafTap: onOpenNode),
),
],
],
),
),
],
),
);
}
List<StudioNavNode> _allLeaves(List<StudioNavNode> nodes) {
final result = <StudioNavNode>[];
for (final node in nodes) {
if (node.children.isEmpty) {
result.add(node);
} else {
result.addAll(_allLeaves(node.children));
}
}
return result;
}
}
class _SidebarHero extends StatelessWidget {
const _SidebarHero({
required this.user,
required this.themeMode,
required this.onToggleTheme,
required this.onLogout,
});
final StudioUser user;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
final VoidCallback onLogout;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(22),
gradient: LinearGradient(
colors: [
scheme.primary.withOpacity(0.18),
scheme.tertiary.withOpacity(0.10),
_isDarkTheme(context) ? const Color(0xFF101B2D) : Colors.white,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(color: scheme.outlineVariant.withOpacity(0.7)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 42,
height: 42,
decoration: BoxDecoration(
color: scheme.primary.withOpacity(0.16),
borderRadius: BorderRadius.circular(14),
),
child: Icon(
Icons.dashboard_customize_rounded,
color: scheme.primary,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'TTX Studio',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
Text(
'Desktop Shell',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.60)),
),
],
),
),
],
),
const SizedBox(height: 16),
Text(
user.displayName,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 4),
Text(
'${user.workspace} · ${user.role}',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_MiniBadge(text: user.languageCode.toUpperCase()),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: user.backendAppName),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: user.organizationName),
const _MiniBadge(text: 'Windows'),
const _MiniBadge(text: 'Backend'),
],
),
const SizedBox(height: 14),
FilledButton.tonalIcon(
onPressed: onLogout,
icon: const Icon(Icons.logout_rounded),
label: const Text('退出登录'),
),
const SizedBox(height: 8),
_ThemeToggleButton(themeMode: themeMode, onPressed: onToggleTheme),
],
),
);
}
}
class _QuickLaunchStrip extends StatelessWidget {
const _QuickLaunchStrip({required this.nodes, required this.onOpenNode});
final List<StudioNavNode> nodes;
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Text(
'快速入口',
style: Theme.of(context).textTheme.titleSmall
?.copyWith(color: _mutedColor(context)),
),
),
...nodes.map(
(node) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _LeafNavTile(node: node, onTap: () => onOpenNode(node)),
),
),
],
);
}
}
class _TreeNode extends StatelessWidget {
const _TreeNode({required this.node, required this.onLeafTap});
final StudioNavNode node;
final ValueChanged<StudioNavNode> onLeafTap;
@override
Widget build(BuildContext context) {
if (node.children.isEmpty) {
return _LeafNavTile(node: node, onTap: () => onLeafTap(node));
}
return ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
childrenPadding: const EdgeInsets.only(left: 14, right: 2, bottom: 6),
leading: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primaryContainer
.withOpacity(_isDarkTheme(context) ? 0.16 : 0.55),
child: Icon(node.icon, size: 18),
),
title: Text(node.title),
subtitle: Text(node.summary),
children: node.children
.map((child) => _TreeNode(node: child, onLeafTap: onLeafTap))
.toList(),
);
}
}
class _LeafNavTile extends StatelessWidget {
const _LeafNavTile({required this.node, required this.onTap});
final StudioNavNode node;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return Material(
color: Colors.transparent,
child: ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 2),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
leading: CircleAvatar(
radius: 16,
backgroundColor: Theme.of(context).colorScheme.primaryContainer
.withOpacity(_isDarkTheme(context) ? 0.16 : 0.55),
child: Icon(node.icon, size: 18),
),
title: Text(node.title),
subtitle: Text(
node.summary,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
onTap: onTap,
),
);
}
}