SHA256
275 lines
8.4 KiB
Dart
275 lines
8.4 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 ...[
|
|
...rootNodes.map(
|
|
(node) => _TreeNode(node: node, onLeafTap: onOpenNode),
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
}
|
|
|
|
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),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: FilledButton.tonalIcon(
|
|
onPressed: onLogout,
|
|
icon: const Icon(Icons.logout_rounded),
|
|
label: const Text('退出登录'),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
Expanded(
|
|
child: _ThemeToggleButton(
|
|
themeMode: themeMode,
|
|
onPressed: onToggleTheme,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|