This commit is contained in:
曾志威
2026-05-16 09:26:11 +08:00
parent 9007ced788
commit 8799c6e53e
13 changed files with 1109 additions and 555 deletions
+1 -8
View File
@@ -1,13 +1,6 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.dir("../../flutter_build_android")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
+17
View File
@@ -11,12 +11,29 @@ pluginManagement {
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
maven("https://maven.aliyun.com/repository/gradle-plugin")
maven("https://maven.aliyun.com/repository/public")
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/central")
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
maven("https://maven.aliyun.com/repository/public")
maven("https://maven.aliyun.com/repository/google")
maven("https://maven.aliyun.com/repository/central")
google()
mavenCentral()
val storageUrl: String = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: "https://storage.googleapis.com"
maven("$storageUrl/download.flutter.io")
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
+7
View File
@@ -1,6 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_highlight/themes/atom-one-dark.dart';
import 'package:flutter_highlight/themes/atom-one-light.dart';
import 'package:code_text_field/code_text_field.dart';
import 'package:highlight/languages/json.dart' as json_highlight;
import 'package:highlight/languages/sql.dart' as sql_highlight;
import 'package:shared_preferences/shared_preferences.dart';
import 'backend/studio_backend_client.dart';
import 'platform/studio_download_saver.dart';
+60 -1
View File
@@ -87,10 +87,15 @@ class StudioLoginView extends StatefulWidget {
}
class _StudioLoginViewState extends State<StudioLoginView> {
static const String _prefsBackendUrlKey = 'studio_login_backend_url';
static const String _prefsWorkspaceKey = 'studio_login_workspace';
static const String _prefsUsernameKey = 'studio_login_username';
late final TextEditingController _backendUrlController;
late final TextEditingController _workspaceController;
late final TextEditingController _usernameController;
late final TextEditingController _passwordController;
Timer? _rememberTimer;
bool _loading = false;
String? _errorMessage;
Map<String, dynamic> _loginConfig = <String, dynamic>{};
@@ -104,11 +109,15 @@ class _StudioLoginViewState extends State<StudioLoginView> {
_workspaceController = TextEditingController(text: _defaultWorkspace);
_usernameController = TextEditingController();
_passwordController = TextEditingController();
_loadLoginConfig();
_backendUrlController.addListener(_queueRememberInputs);
_workspaceController.addListener(_queueRememberInputs);
_usernameController.addListener(_queueRememberInputs);
unawaited(_initializeLoginForm());
}
@override
void dispose() {
_rememberTimer?.cancel();
_backendUrlController.dispose();
_workspaceController.dispose();
_usernameController.dispose();
@@ -116,6 +125,55 @@ class _StudioLoginViewState extends State<StudioLoginView> {
super.dispose();
}
Future<void> _restoreRememberedInputs() async {
final prefs = await SharedPreferences.getInstance();
if (!mounted) {
return;
}
final backendUrl = prefs.getString(_prefsBackendUrlKey)?.trim() ?? '';
final workspace = prefs.getString(_prefsWorkspaceKey)?.trim() ?? '';
final username = prefs.getString(_prefsUsernameKey)?.trim() ?? '';
if (backendUrl.isNotEmpty) {
_backendUrlController.text = backendUrl;
}
if (workspace.isNotEmpty) {
_workspaceController.text = _normalizeWorkspace(workspace);
}
if (username.isNotEmpty) {
_usernameController.text = username;
}
}
Future<void> _initializeLoginForm() async {
await _restoreRememberedInputs();
await _loadLoginConfig();
}
void _queueRememberInputs() {
if (!mounted) {
return;
}
_rememberTimer?.cancel();
_rememberTimer = Timer(const Duration(milliseconds: 250), () {
if (!mounted) {
return;
}
unawaited(_rememberInputs());
});
}
Future<void> _rememberInputs() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsBackendUrlKey, _backendUrlController.text.trim());
await prefs.setString(
_prefsWorkspaceKey,
_normalizeWorkspace(_workspaceController.text),
);
await prefs.setString(_prefsUsernameKey, _usernameController.text.trim());
}
Future<void> _loadLoginConfig() async {
try {
final client = StudioBackendClient(
@@ -206,6 +264,7 @@ class _StudioLoginViewState extends State<StudioLoginView> {
return;
}
unawaited(_rememberInputs());
widget.onLogin(
StudioRuntime(
backendUrl: backendUrl,
+53 -84
View File
@@ -18,22 +18,20 @@ class StudioDashboardPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final quickNodes = _allLeaves(rootNodes).take(6).toList();
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.all(14),
child: Row(
children: [
Container(
width: 72,
height: 72,
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
@@ -45,35 +43,35 @@ class StudioDashboardPage extends StatelessWidget {
).colorScheme.tertiary.withOpacity(0.20),
],
),
borderRadius: BorderRadius.circular(24),
borderRadius: BorderRadius.circular(18),
),
child: const Icon(
Icons.space_dashboard_rounded,
size: 38,
size: 30,
),
),
const SizedBox(width: 18),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'工作台',
style: Theme.of(context).textTheme.headlineSmall
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(
'左侧菜单已映射自 studio.html 的核心入口,点击即可打开多标签页。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.72)),
),
],
),
),
Wrap(
spacing: 10,
runSpacing: 10,
spacing: 8,
runSpacing: 8,
children: [
_MiniBadge(text: user.workspace),
_MiniBadge(text: user.languageCode.toUpperCase()),
@@ -83,7 +81,7 @@ class StudioDashboardPage extends StatelessWidget {
),
),
),
const SizedBox(height: 14),
const SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraints) {
final cardWidth = constraints.maxWidth >= 1100
@@ -93,8 +91,8 @@ class StudioDashboardPage extends StatelessWidget {
: constraints.maxWidth;
return Wrap(
spacing: 16,
runSpacing: 16,
spacing: 12,
runSpacing: 12,
children: [
SizedBox(
width: cardWidth,
@@ -127,38 +125,7 @@ class StudioDashboardPage extends StatelessWidget {
);
},
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'快捷入口',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: quickNodes
.map(
(node) => ActionChip(
avatar: Icon(node.icon, size: 18),
label: Text(node.title),
onPressed: () => onOpenNode(node),
),
)
.toList(),
),
],
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 10),
LayoutBuilder(
builder: (context, constraints) {
final leftWidth = constraints.maxWidth >= 980
@@ -176,7 +143,7 @@ class StudioDashboardPage extends StatelessWidget {
width: leftWidth,
child: _ActivityPanel(activities: activities),
),
const SizedBox(width: 16),
const SizedBox(width: 12),
SizedBox(
width: rightWidth,
child: _FeaturePanel(onOpenNode: onOpenNode),
@@ -188,7 +155,7 @@ class StudioDashboardPage extends StatelessWidget {
return Column(
children: [
_ActivityPanel(activities: activities),
const SizedBox(height: 16),
const SizedBox(height: 10),
_FeaturePanel(onOpenNode: onOpenNode),
],
);
@@ -230,19 +197,19 @@ class _InfoPanel extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(12),
child: Row(
children: [
Container(
width: 50,
height: 50,
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.15),
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon),
child: Icon(icon, size: 20),
),
const SizedBox(width: 14),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -250,19 +217,19 @@ class _InfoPanel extends StatelessWidget {
Text(
title,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: Colors.white60),
),
const SizedBox(height: 6),
Text(
value,
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
?.copyWith(color: _mutedColor(context, 0.68)),
),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 2),
Text(
subtitle,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.72)),
),
],
),
@@ -283,38 +250,38 @@ class _ActivityPanel extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'最近活动',
style: Theme.of(context).textTheme.titleLarge
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
...activities.take(8).map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.bolt_rounded, size: 18),
const SizedBox(width: 12),
const Icon(Icons.bolt_rounded, size: 16),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.title,
style: Theme.of(context).textTheme.bodyLarge
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 4),
const SizedBox(height: 2),
Text(
item.detail,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: Colors.white70),
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.70)),
),
],
),
@@ -341,23 +308,25 @@ class _FeaturePanel extends StatelessWidget {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'功能覆盖',
style: Theme.of(context).textTheme.titleLarge
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
const Text(
const SizedBox(height: 8),
Text(
'当前 Flutter 桌面端实现了 Studio 页面的核心交互:登录、导航、标签页、JSON/SQL 编辑、日志和运维任务。',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.72)),
),
const SizedBox(height: 14),
const SizedBox(height: 10),
Wrap(
spacing: 10,
runSpacing: 10,
spacing: 8,
runSpacing: 8,
children: nodes
.take(5)
.map(
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -90,7 +90,7 @@ class _StudioHistoryPageState extends State<StudioHistoryPage> {
return SingleChildScrollView(
child: Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
@@ -102,13 +102,13 @@ class _StudioHistoryPageState extends State<StudioHistoryPage> {
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(
'从后端读取发布与修改历史。',
style: Theme.of(context).textTheme.bodyMedium
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context)),
),
],
@@ -127,7 +127,7 @@ class _StudioHistoryPageState extends State<StudioHistoryPage> {
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 8),
if (_errorMessage != null) ...[
SelectableText(
_errorMessage!,
@@ -139,7 +139,7 @@ class _StudioHistoryPageState extends State<StudioHistoryPage> {
],
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 28),
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
else
+161 -78
View File
@@ -17,7 +17,7 @@ class StudioSqlPage extends StatefulWidget {
}
class _StudioSqlPageState extends State<StudioSqlPage> {
late final TextEditingController _sqlController;
late final CodeController _sqlController;
late final TextEditingController _limitController;
late final TextEditingController _countSqlController;
late final TextEditingController _summarySqlController;
@@ -38,9 +38,10 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
@override
void initState() {
super.initState();
_sqlController = TextEditingController(
_sqlController = CodeController(
text:
'select id, code, name from ttx_demo_table order by id desc limit 20;',
'select id, code, name from item order by id desc limit 20',
language: sql_highlight.sql,
);
_limitController = TextEditingController(text: '100');
_countSqlController = TextEditingController();
@@ -347,6 +348,67 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
});
}
Map<String, TextStyle> _sqlCodeTheme(BuildContext context) {
final isDark = _isDarkTheme(context);
return isDark ? atomOneDarkTheme : atomOneLightTheme;
}
Widget _buildStatusGrid(BuildContext context) {
final rows = <DataRow>[
DataRow(
cells: [
const DataCell(Text('状态')),
DataCell(SelectableText(_status)),
],
),
DataRow(
cells: [
const DataCell(Text('详情')),
DataCell(
SelectableText(
_detail.isEmpty ? '-' : _detail,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
),
],
),
DataRow(
cells: [
const DataCell(Text('结果')),
DataCell(
SelectableText(
_resultRows.isEmpty
? '无结果'
: '${_resultRows.length} 行,${_resultColumns.length}',
),
),
],
),
];
if (_isTemplateMode) {
rows.add(
DataRow(
cells: [
const DataCell(Text('模板')),
DataCell(SelectableText('${_selectedTid()} / ${_selectedOid()}')),
],
),
);
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: const [
DataColumn(label: Text('字段')),
DataColumn(label: Text('内容')),
],
rows: rows,
),
);
}
Widget _buildTemplateBody(BuildContext context) {
final tids = _templateIndex.keys.toList();
final selectedTid = _selectedTid();
@@ -357,12 +419,11 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Wrap(
spacing: 12,
runSpacing: 12,
children: [
SizedBox(
width: 260,
LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 860;
final tidField = SizedBox(
width: wide ? 260 : double.infinity,
child: DropdownButtonFormField<String>(
value: tids.contains(selectedTid) ? selectedTid : null,
decoration: const InputDecoration(labelText: 'tid'),
@@ -381,9 +442,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
});
},
),
),
SizedBox(
width: 260,
);
final oidField = SizedBox(
width: wide ? 260 : double.infinity,
child: DropdownButtonFormField<String>(
value: oids.contains(_selectedOid()) ? _selectedOid() : null,
decoration: const InputDecoration(labelText: 'oid'),
@@ -399,18 +460,48 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
});
},
),
),
FilledButton.tonalIcon(
onPressed: _loading ? null : _loadSelectedTemplate,
icon: const Icon(Icons.folder_open_rounded),
label: const Text('加载模板'),
),
FilledButton.icon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: Text(_templateLoaded ? '更新模板' : '创建模板'),
),
],
);
final buttons = Wrap(
spacing: 12,
runSpacing: 12,
children: [
FilledButton.tonalIcon(
onPressed: _loading ? null : _loadSelectedTemplate,
icon: const Icon(Icons.folder_open_rounded),
label: const Text('加载模板'),
),
FilledButton.icon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: Text(_templateLoaded ? '更新模板' : '创建模板'),
),
],
);
if (!wide) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
tidField,
const SizedBox(height: 12),
oidField,
const SizedBox(height: 12),
buttons,
],
);
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: tidField),
const SizedBox(width: 12),
Expanded(child: oidField),
const SizedBox(width: 12),
buttons,
],
);
},
),
const SizedBox(height: 14),
_buildSqlEditors(context, includeTemplateFields: true),
@@ -429,41 +520,19 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.tab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
_isTemplateMode ? '从后端加载 SQL 模板并支持创建、更新。' : '直接执行 SQL 并查看结果。',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
],
),
),
),
const SizedBox(height: 16),
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 24),
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
else if (_isTemplateMode)
_buildTemplateBody(context)
else
_buildSqlEditors(context, includeTemplateFields: false),
const SizedBox(height: 16),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -498,15 +567,8 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
],
],
),
const SizedBox(height: 10),
SelectableText(_status),
if (_detail.isNotEmpty) ...[
const SizedBox(height: 10),
SelectableText(
_detail,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
const SizedBox(height: 8),
_buildStatusGrid(context),
],
),
),
@@ -559,17 +621,17 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
Widget _buildSqlEditors(BuildContext context, {required bool includeTemplateFields}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (includeTemplateFields) ...[
Wrap(
spacing: 12,
runSpacing: 12,
spacing: 10,
runSpacing: 10,
children: [
SizedBox(
width: 220,
width: 200,
child: TextField(
controller: _countSqlController,
decoration: const InputDecoration(labelText: 'count SQL'),
@@ -577,7 +639,7 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
),
),
SizedBox(
width: 220,
width: 200,
child: TextField(
controller: _summarySqlController,
decoration: const InputDecoration(labelText: 'summary SQL'),
@@ -607,34 +669,55 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
const SizedBox(height: 14),
],
SizedBox(
height: 280,
child: TextField(
controller: _sqlController,
expands: true,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(fontFamily: 'monospace'),
decoration: const InputDecoration(
labelText: 'SQL 内容',
alignLabelWithHint: true,
height: 240,
child: CodeTheme(
data: CodeThemeData(
styles: _sqlCodeTheme(context),
),
child: CodeField(
controller: _sqlController,
expands: true,
wrap: true,
lineNumbers: false,
isDense: true,
keyboardType: TextInputType.multiline,
textStyle: const TextStyle(
fontFamily: 'monospace',
fontSize: 13.5,
height: 1.45,
),
cursorColor: Theme.of(context).colorScheme.primary,
background: Colors.transparent,
decoration: BoxDecoration(
color: _softPanelColor(context),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
padding: const EdgeInsets.all(12),
textSelectionTheme: TextSelectionThemeData(
cursorColor: Theme.of(context).colorScheme.primary,
selectionColor: Theme.of(
context,
).colorScheme.primary.withOpacity(0.20),
),
),
),
),
if (!includeTemplateFields) ...[
const SizedBox(height: 12),
const SizedBox(height: 10),
Row(
children: [
SizedBox(
width: 160,
width: 150,
child: TextField(
controller: _limitController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'limit'),
),
),
const SizedBox(width: 12),
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _formatSql,
icon: const Icon(Icons.format_align_left_rounded),
+4 -3
View File
@@ -127,13 +127,14 @@ class _StudioShellState extends State<StudioShell> {
widget.runtime.allowedMenuIds,
);
final leaves = _allLeaves(visibleRoots);
final filteredLeaves = _searchQuery.trim().isEmpty
final query = _searchQuery.trim().toLowerCase();
final filteredLeaves = query.isEmpty
? leaves
: leaves
.where(
(node) =>
node.title.contains(_searchQuery) ||
node.summary.contains(_searchQuery),
node.title.toLowerCase().contains(query) ||
node.summary.toLowerCase().contains(query),
)
.toList();
-46
View File
@@ -77,11 +77,6 @@ class StudioSidebar extends StatelessWidget {
child: Text('没有匹配的菜单项'),
),
] else ...[
_QuickLaunchStrip(
nodes: _allLeaves(rootNodes).take(4).toList(),
onOpenNode: onOpenNode,
),
const SizedBox(height: 12),
...rootNodes.map(
(node) => _TreeNode(node: node, onLeafTap: onOpenNode),
),
@@ -94,17 +89,6 @@ class StudioSidebar extends StatelessWidget {
);
}
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 {
@@ -215,36 +199,6 @@ class _SidebarHero extends StatelessWidget {
}
}
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});
+37 -111
View File
@@ -39,29 +39,29 @@ class StudioWorkspaceView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(18),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_WorkspaceHeader(
user: user,
selectedTab: selectedTab,
onRefresh: onRefresh,
onLogout: onLogout,
themeMode: themeMode,
onToggleTheme: onToggleTheme,
),
const SizedBox(height: 14),
_TabStrip(
tabs: tabs,
selectedTabId: selectedTab.id,
onSelectTab: onSelectTab,
onCloseTab: onCloseTab,
),
const SizedBox(height: 14),
const SizedBox(height: 8),
Expanded(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 260),
layoutBuilder: (currentChild, previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
child: _buildPage(
key: ValueKey('${selectedTab.id}:$refreshToken'),
tab: selectedTab,
@@ -73,7 +73,7 @@ class StudioWorkspaceView extends StatelessWidget {
),
),
),
const SizedBox(height: 14),
const SizedBox(height: 8),
_StudioStatusBar(
user: user,
selectedTab: selectedTab,
@@ -145,92 +145,6 @@ class StudioWorkspaceView extends StatelessWidget {
}
}
class _WorkspaceHeader extends StatelessWidget {
const _WorkspaceHeader({
required this.user,
required this.selectedTab,
required this.onRefresh,
required this.onLogout,
required this.themeMode,
required this.onToggleTheme,
});
final StudioUser user;
final StudioTab selectedTab;
final VoidCallback onRefresh;
final VoidCallback onLogout;
final ThemeMode themeMode;
final VoidCallback onToggleTheme;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
selectedTab.title,
style: Theme.of(context).textTheme.headlineSmall
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 6),
Text(
selectedTab.summary,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: _mutedColor(context)),
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
_MiniBadge(text: user.workspace),
_MiniBadge(text: user.displayName),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: user.backendAppName),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: user.organizationName),
_MiniBadge(text: selectedTab.pageType.name),
],
),
],
),
),
const SizedBox(width: 16),
Wrap(
spacing: 10,
runSpacing: 10,
alignment: WrapAlignment.end,
children: [
_ThemeToggleButton(
themeMode: themeMode,
onPressed: onToggleTheme,
),
FilledButton.tonalIcon(
onPressed: onRefresh,
icon: const Icon(Icons.refresh_rounded),
label: const Text('刷新'),
),
FilledButton.tonalIcon(
onPressed: onLogout,
icon: const Icon(Icons.logout_rounded),
label: const Text('退出'),
),
],
),
],
),
),
);
}
}
class _TabStrip extends StatelessWidget {
const _TabStrip({
required this.tabs,
@@ -247,11 +161,11 @@ class _TabStrip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 58,
height: 42,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: tabs.length,
separatorBuilder: (_, __) => const SizedBox(width: 10),
separatorBuilder: (_, __) => const SizedBox(width: 4),
itemBuilder: (context, index) {
final tab = tabs[index];
final selected = tab.id == selectedTabId;
@@ -261,8 +175,8 @@ class _TabStrip extends StatelessWidget {
onTap: () => onSelectTab(tab.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 180),
width: 210,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
width: 172,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: selected
? scheme.primary.withOpacity(0.18)
@@ -275,18 +189,21 @@ class _TabStrip extends StatelessWidget {
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(tab.icon, size: 18),
const SizedBox(width: 10),
Icon(tab.icon, size: 14),
const SizedBox(width: 5),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Text(
tab.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall,
),
Text(
tab.pageType.name,
@@ -299,10 +216,19 @@ class _TabStrip extends StatelessWidget {
),
),
if (tab.id != StudioTab.dashboardId)
IconButton(
tooltip: '关闭',
onPressed: () => onCloseTab(tab.id),
icon: const Icon(Icons.close_rounded, size: 16),
SizedBox(
width: 20,
height: 20,
child: IconButton(
tooltip: '关闭',
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: 20,
height: 20,
),
onPressed: () => onCloseTab(tab.id),
icon: const Icon(Icons.close_rounded, size: 12),
),
),
],
),
@@ -331,10 +257,10 @@ class _StudioStatusBar extends StatelessWidget {
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Wrap(
spacing: 16,
runSpacing: 10,
spacing: 10,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_MiniBadge(text: '用户 ${user.displayName}'),
+23 -1
View File
@@ -55,11 +55,14 @@ ThemeData buildStudioTheme(Brightness brightness) {
surfaceTintColor: colorScheme.surfaceTint,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
drawerTheme: DrawerThemeData(
backgroundColor: isDark ? const Color(0xFF07111A) : Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
isDense: true,
fillColor: inputFill,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
@@ -73,6 +76,25 @@ ThemeData buildStudioTheme(Brightness brightness) {
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: colorScheme.primary),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIconConstraints: const BoxConstraints.tightFor(width: 34, height: 34),
suffixIconConstraints: const BoxConstraints.tightFor(width: 34, height: 34),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
minimumSize: const Size(0, 36),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
iconButtonTheme: IconButtonThemeData(
style: IconButton.styleFrom(
minimumSize: const Size(28, 28),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
);
}
@@ -86,7 +108,7 @@ class _MiniBadge extends StatelessWidget {
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: _isDarkTheme(context)
? Colors.white.withOpacity(0.06)
+2 -1
View File
@@ -37,8 +37,9 @@ dependencies:
crypto: ^3.0.6
dio: ^5.8.0
file_selector: ^1.0.3
pointycastle: ^3.9.1
pointycastle: ^4.0.0
shared_preferences: ^2.3.2
code_text_field: ^1.1.0
dev_dependencies:
flutter_test: