This commit is contained in:
曾志威
2026-05-16 19:03:01 +08:00
parent 67d719cd46
commit 57ccd99bd5
13 changed files with 1475 additions and 811 deletions
+5
View File
@@ -535,6 +535,7 @@ class StudioBackendClient {
responseType: ResponseType.bytes,
followRedirects: true,
receiveTimeout: const Duration(seconds: 60),
extra: const <String, dynamic>{'withCredentials': true},
),
);
_captureCookies(response);
@@ -594,6 +595,8 @@ class StudioBackendClient {
session: session,
includeSessionCookies: includeSessionCookies,
),
// Web 端需要浏览器带上 cookie,避免登录态和会话丢失。
extra: const <String, dynamic>{'withCredentials': true},
),
);
_captureCookies(response);
@@ -623,6 +626,7 @@ class StudioBackendClient {
? (request, options) => utf8.encode(request)
: null,
followRedirects: followRedirects,
extra: const <String, dynamic>{'withCredentials': true},
),
);
_captureCookies(response);
@@ -668,6 +672,7 @@ class StudioBackendClient {
includeSessionCookies: true,
),
followRedirects: true,
extra: const <String, dynamic>{'withCredentials': true},
),
);
_captureCookies(response);
+2 -1
View File
@@ -5,7 +5,8 @@ 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:json_field_editor/json_field_editor.dart';
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
import 'package:highlight/languages/sql.dart' as sql_highlight;
import 'package:shared_preferences/shared_preferences.dart';
+1
View File
@@ -194,6 +194,7 @@ class _StudioLoginViewState extends State<StudioLoginView> {
return;
}
setState(() {
// 登录页的配置读取只做“增强体验”;就算后端暂时不可达,用户仍然可以手动输入地址继续登录。
_errorMessage = '后端配置暂时不可用,仍可继续尝试登录';
});
}
-67
View File
@@ -161,43 +161,6 @@ final List<StudioNavNode> studioNavTree = [
),
],
),
StudioNavNode(
id: 'json-center',
title: '模型 JSON',
summary: '直写 JSON 与脚本化编辑',
icon: Icons.data_object_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'TableJsonEditor',
title: '数据模型 Json',
summary: 'JSON 结构和字段定义',
icon: Icons.data_object_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'BillJsonEditor',
title: '单据模型 Json',
summary: '单据配置的 JSON 编辑器',
icon: Icons.code_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'ViewJsonEditor',
title: '界面模型 Json',
summary: '界面配置的 JSON 编辑器',
icon: Icons.article_rounded,
pageType: StudioPageType.jsonEditor,
),
StudioNavNode(
id: 'NavigatorEditor',
title: '菜单维护 Json',
summary: '导航树的 JSON 配置',
icon: Icons.list_alt_rounded,
pageType: StudioPageType.navigationEditor,
),
],
),
StudioNavNode(
id: 'history',
title: '模型日志',
@@ -235,36 +198,6 @@ final List<StudioNavNode> studioNavTree = [
),
],
),
StudioNavNode(
id: 'manage',
title: '模型管理',
summary: '在线维护与发布',
icon: Icons.tune_rounded,
pageType: StudioPageType.dashboard,
children: [
StudioNavNode(
id: 'WsoDataManage',
title: '界面模型管理',
summary: '界面模型维护',
icon: Icons.preview_rounded,
pageType: StudioPageType.manage,
),
StudioNavNode(
id: 'BillModelManage',
title: '单据模型管理',
summary: '单据模型维护',
icon: Icons.inventory_2_rounded,
pageType: StudioPageType.manage,
),
StudioNavNode(
id: 'TableModelManage',
title: '数据模型管理',
summary: '数据模型维护',
icon: Icons.storage_rounded,
pageType: StudioPageType.manage,
),
],
),
StudioNavNode(
id: 'ModelSync',
title: '配置下载',
+44 -145
View File
@@ -6,15 +6,11 @@ class StudioDashboardPage extends StatelessWidget {
required this.tab,
required this.user,
required this.activities,
required this.rootNodes,
required this.onOpenNode,
});
final StudioTab tab;
final StudioUser user;
final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
@@ -126,58 +122,12 @@ class StudioDashboardPage extends StatelessWidget {
},
),
const SizedBox(height: 10),
LayoutBuilder(
builder: (context, constraints) {
final leftWidth = constraints.maxWidth >= 980
? (constraints.maxWidth - 16) * 0.58
: constraints.maxWidth;
final rightWidth = constraints.maxWidth >= 980
? (constraints.maxWidth - 16) * 0.42
: constraints.maxWidth;
if (constraints.maxWidth >= 980) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: leftWidth,
child: _ActivityPanel(activities: activities),
),
const SizedBox(width: 12),
SizedBox(
width: rightWidth,
child: _FeaturePanel(onOpenNode: onOpenNode),
),
],
);
}
return Column(
children: [
_ActivityPanel(activities: activities),
const SizedBox(height: 10),
_FeaturePanel(onOpenNode: onOpenNode),
],
);
},
),
_ActivityPanel(activities: activities),
],
),
),
);
}
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 _InfoPanel extends StatelessWidget {
@@ -216,20 +166,23 @@ class _InfoPanel extends StatelessWidget {
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.68)),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _mutedColor(context, 0.68),
),
),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.72)),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _mutedColor(context, 0.72),
),
),
],
),
@@ -256,102 +209,48 @@ class _ActivityPanel extends StatelessWidget {
children: [
Text(
'最近活动',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
...activities.take(8).map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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.bodyMedium
?.copyWith(fontWeight: FontWeight.w600),
...activities
.take(8)
.map(
(item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
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.bodyMedium
?.copyWith(fontWeight: FontWeight.w600),
),
const SizedBox(height: 2),
Text(
item.detail,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(
color: _mutedColor(context, 0.70),
),
),
],
),
const SizedBox(height: 2),
Text(
item.detail,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.70)),
),
],
),
),
],
),
],
),
),
),
),
],
),
),
);
}
}
class _FeaturePanel extends StatelessWidget {
const _FeaturePanel({required this.onOpenNode});
final ValueChanged<StudioNavNode> onOpenNode;
@override
Widget build(BuildContext context) {
final nodes = _allLeaves(studioNavTree);
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'功能覆盖',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
Text(
'当前 Flutter 桌面端实现了 Studio 页面的核心交互:登录、导航、标签页、JSON/SQL 编辑、日志和运维任务。',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.72)),
),
const SizedBox(height: 10),
Wrap(
spacing: 8,
runSpacing: 8,
children: nodes
.take(5)
.map(
(node) => FilledButton.tonal(
onPressed: () => onOpenNode(node),
child: Text(node.title),
),
)
.toList(),
),
],
),
),
);
}
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;
}
}
File diff suppressed because it is too large Load Diff
+8
View File
@@ -6,11 +6,13 @@ class StudioPortalMaintainPage extends StatefulWidget {
required this.tab,
required this.runtime,
required this.onAction,
required this.onOpenMenu,
});
final StudioTab tab;
final StudioRuntime runtime;
final void Function(String title, String detail) onAction;
final VoidCallback onOpenMenu;
@override
State<StudioPortalMaintainPage> createState() => _StudioPortalMaintainPageState();
@@ -108,6 +110,12 @@ class _StudioPortalMaintainPageState extends State<StudioPortalMaintainPage> {
icon: const Icon(Icons.refresh_rounded),
label: const Text('刷新'),
),
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: widget.onOpenMenu,
icon: const Icon(Icons.menu_rounded),
label: const Text('设置菜单'),
),
],
),
const SizedBox(height: 12),
+267 -154
View File
@@ -39,8 +39,7 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
void initState() {
super.initState();
_sqlController = CodeController(
text:
'select id, code, name from item order by id desc limit 20',
text: 'select id, code, name from item order by id desc limit 20',
language: sql_highlight.sql,
);
_limitController = TextEditingController(text: '100');
@@ -157,10 +156,20 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
setState(() {
_templateLoaded = true;
_sqlController.text = _readValue(template, const ['value', 'sql']);
_countSqlController.text = _readValue(template, const ['countSql', 'count']);
_summarySqlController.text = _readValue(template, const ['summarySql', 'summary']);
_criteriaController.text = _readValue(template, const ['criteria'], '{}');
_extConfigController.text = _readValue(template, const ['extConfig'], '{}');
_countSqlController.text = _readValue(template, const [
'countSql',
'count',
]);
_summarySqlController.text = _readValue(template, const [
'summarySql',
'summary',
]);
_criteriaController.text = _readValue(template, const [
'criteria',
], '{}');
_extConfigController.text = _readValue(template, const [
'extConfig',
], '{}');
_status = '模板已加载';
_detail = '$tid / $oid';
});
@@ -319,7 +328,11 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
return text;
}
String _readValue(Map<String, dynamic> map, List<String> keys, [String fallback = '']) {
String _readValue(
Map<String, dynamic> map,
List<String> keys, [
String fallback = '',
]) {
for (final key in keys) {
final value = map[key];
if (value != null && value.toString().trim().isNotEmpty) {
@@ -354,58 +367,40 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
}
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}',
),
),
],
final items = <_StatusItem>[
_StatusItem('状态', _status),
_StatusItem('详情', _detail.isEmpty ? '-' : _detail),
_StatusItem(
'结果',
_resultRows.isEmpty
? '无结果'
: '${_resultRows.length} 行,${_resultColumns.length}',
),
if (_isTemplateMode)
_StatusItem('模板', '${_selectedTid()} / ${_selectedOid()}'),
];
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,
),
return LayoutBuilder(
builder: (context, constraints) {
final cardWidth = constraints.maxWidth >= 920
? (constraints.maxWidth - 12) / 2
: constraints.maxWidth;
return Wrap(
spacing: 12,
runSpacing: 12,
children: items
.map(
(item) => SizedBox(
width: cardWidth,
child: _StatusTile(
title: item.title,
content: item.content,
),
),
)
.toList(),
);
},
);
}
@@ -428,7 +423,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
value: tids.contains(selectedTid) ? selectedTid : null,
decoration: const InputDecoration(labelText: 'tid'),
items: tids
.map((tid) => DropdownMenuItem(value: tid, child: Text(tid)))
.map(
(tid) => DropdownMenuItem(value: tid, child: Text(tid)),
)
.toList(),
onChanged: (value) {
if (value == null) {
@@ -436,7 +433,8 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
}
setState(() {
_selectedTidValue = value;
_selectedOidValue = (_templateIndex[value] ?? const <String>[]).isNotEmpty
_selectedOidValue =
(_templateIndex[value] ?? const <String>[]).isNotEmpty
? _templateIndex[value]!.first
: '';
});
@@ -449,7 +447,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
value: oids.contains(_selectedOid()) ? _selectedOid() : null,
decoration: const InputDecoration(labelText: 'oid'),
items: oids
.map((oid) => DropdownMenuItem(value: oid, child: Text(oid)))
.map(
(oid) => DropdownMenuItem(value: oid, child: Text(oid)),
)
.toList(),
onChanged: (value) {
if (value == null) {
@@ -511,114 +511,188 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
@override
Widget build(BuildContext context) {
if (_isTemplateMode && _selectedTidValue.isEmpty && _templateIndex.isNotEmpty) {
if (_isTemplateMode &&
_selectedTidValue.isEmpty &&
_templateIndex.isNotEmpty) {
_selectedTidValue = _templateIndex.keys.first;
_selectedOidValue = _templateIndex[_selectedTidValue]!.first;
}
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_loading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
else if (_isTemplateMode)
_buildTemplateBody(context)
else
_buildSqlEditors(context, includeTemplateFields: false),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
return LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 1120;
final content = _loading
? const Padding(
padding: EdgeInsets.symmetric(vertical: 16),
child: Center(child: CircularProgressIndicator()),
)
: _isTemplateMode
? _buildTemplateBody(context)
: _buildSqlEditors(context, includeTemplateFields: false);
if (!wide) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSqlHeader(context),
const SizedBox(height: 12),
content,
const SizedBox(height: 12),
_buildExecutionStatusCard(context),
if (_resultRows.isNotEmpty) ...[
const SizedBox(height: 16),
_buildExecutionResultCard(context),
],
],
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildSqlHeader(context),
const SizedBox(height: 12),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
Expanded(
child: Text(
'执行状态',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
Expanded(
flex: 3,
child: SingleChildScrollView(child: content),
),
const SizedBox(width: 12),
SizedBox(
width: 400,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildExecutionStatusCard(context),
if (_resultRows.isNotEmpty) ...[
const SizedBox(height: 16),
_buildExecutionResultCard(context),
],
],
),
if (!_isTemplateMode)
FilledButton.tonalIcon(
onPressed: _loading ? null : _runSql,
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('执行 SQL'),
),
if (!_isTemplateMode) const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _formatSql,
icon: const Icon(Icons.format_align_left_rounded),
label: const Text('格式化'),
),
if (_isTemplateMode) ...[
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: const Text('保存模板'),
),
],
],
),
),
const SizedBox(height: 8),
_buildStatusGrid(context),
],
),
),
),
if (_resultRows.isNotEmpty) ...[
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),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: _resultColumns
.map((column) => DataColumn(label: Text(column)))
.toList(),
rows: _resultRows
.map(
(row) => DataRow(
cells: _resultColumns
.map(
(column) => DataCell(
SelectableText(_formatCellValue(row[column])),
),
)
.toList(),
),
)
.toList(),
),
),
],
),
],
);
},
);
}
Widget _buildSqlHeader(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
children: [
Expanded(
child: Text(
_isTemplateMode ? 'SQL 模板执行' : 'SQL 执行',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
),
if (!_isTemplateMode)
FilledButton.tonalIcon(
onPressed: _loading ? null : _runSql,
icon: const Icon(Icons.play_arrow_rounded),
label: const Text('执行 SQL'),
),
if (!_isTemplateMode) const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _formatSql,
icon: const Icon(Icons.format_align_left_rounded),
label: const Text('格式化'),
),
if (_isTemplateMode) ...[
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading ? null : _saveTemplate,
icon: const Icon(Icons.save_rounded),
label: const Text('保存模板'),
),
],
],
],
),
),
);
}
Widget _buildSqlEditors(BuildContext context, {required bool includeTemplateFields}) {
Widget _buildExecutionStatusCard(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'执行状态',
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
_buildStatusGrid(context),
],
),
),
);
}
Widget _buildExecutionResultCard(BuildContext context) {
return 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),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: DataTable(
columns: _resultColumns
.map((column) => DataColumn(label: Text(column)))
.toList(),
rows: _resultRows
.map(
(row) => DataRow(
cells: _resultColumns
.map(
(column) => DataCell(
SelectableText(_formatCellValue(row[column])),
),
)
.toList(),
),
)
.toList(),
),
),
],
),
),
);
}
Widget _buildSqlEditors(
BuildContext context, {
required bool includeTemplateFields,
}) {
return Card(
child: Padding(
padding: const EdgeInsets.all(14),
@@ -642,7 +716,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
width: 200,
child: TextField(
controller: _summarySqlController,
decoration: const InputDecoration(labelText: 'summary SQL'),
decoration: const InputDecoration(
labelText: 'summary SQL',
),
maxLines: 3,
),
),
@@ -671,9 +747,7 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
SizedBox(
height: 240,
child: CodeTheme(
data: CodeThemeData(
styles: _sqlCodeTheme(context),
),
data: CodeThemeData(styles: _sqlCodeTheme(context)),
child: CodeField(
controller: _sqlController,
expands: true,
@@ -743,3 +817,42 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
}
}
class _StatusItem {
const _StatusItem(this.title, this.content);
final String title;
final String content;
}
class _StatusTile extends StatelessWidget {
const _StatusTile({required this.title, required this.content});
final String title;
final String content;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: _mutedColor(context, 0.68),
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 8),
SelectableText(
content,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
),
),
);
}
}
-3
View File
@@ -230,7 +230,6 @@ class _StudioShellState extends State<StudioShell> {
activities: List<StudioActivity>.unmodifiable(
_activities,
),
rootNodes: visibleRoots,
refreshToken: _refreshRevision,
onSelectTab: _selectTab,
onCloseTab: _closeTab,
@@ -250,7 +249,6 @@ class _StudioShellState extends State<StudioShell> {
selectedTab: _selectedTab,
tabs: List<StudioTab>.unmodifiable(_tabs),
activities: List<StudioActivity>.unmodifiable(_activities),
rootNodes: visibleRoots,
refreshToken: _refreshRevision,
onSelectTab: _selectTab,
onCloseTab: _closeTab,
@@ -266,4 +264,3 @@ class _StudioShellState extends State<StudioShell> {
);
}
}
+17 -6
View File
@@ -186,13 +186,24 @@ class _SidebarHero extends StatelessWidget {
],
),
const SizedBox(height: 14),
FilledButton.tonalIcon(
onPressed: onLogout,
icon: const Icon(Icons.logout_rounded),
label: const Text('退出登录'),
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,
),
),
],
),
const SizedBox(height: 8),
_ThemeToggleButton(themeMode: themeMode, onPressed: onToggleTheme),
],
),
);
+5 -52
View File
@@ -8,7 +8,6 @@ class StudioWorkspaceView extends StatelessWidget {
required this.selectedTab,
required this.tabs,
required this.activities,
required this.rootNodes,
required this.refreshToken,
required this.onSelectTab,
required this.onCloseTab,
@@ -25,7 +24,6 @@ class StudioWorkspaceView extends StatelessWidget {
final StudioTab selectedTab;
final List<StudioTab> tabs;
final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final int refreshToken;
final ValueChanged<String> onSelectTab;
final ValueChanged<String> onCloseTab;
@@ -66,20 +64,12 @@ class StudioWorkspaceView extends StatelessWidget {
key: ValueKey('${selectedTab.id}:$refreshToken'),
tab: selectedTab,
activities: activities,
rootNodes: rootNodes,
runtime: runtime,
onPageAction: onPageAction,
onOpenNode: onOpenNode,
),
),
),
const SizedBox(height: 8),
_StudioStatusBar(
user: user,
selectedTab: selectedTab,
tabsCount: tabs.length,
activitiesCount: activities.length,
),
],
),
);
@@ -89,7 +79,6 @@ class StudioWorkspaceView extends StatelessWidget {
required Key key,
required StudioTab tab,
required List<StudioActivity> activities,
required List<StudioNavNode> rootNodes,
required StudioRuntime runtime,
required void Function(String title, String detail) onPageAction,
required ValueChanged<StudioNavNode> onOpenNode,
@@ -101,8 +90,6 @@ class StudioWorkspaceView extends StatelessWidget {
tab: tab,
user: user,
activities: activities,
rootNodes: rootNodes,
onOpenNode: onOpenNode,
);
case StudioPageType.history:
return StudioHistoryPage(
@@ -133,6 +120,11 @@ class StudioWorkspaceView extends StatelessWidget {
tab: tab,
runtime: runtime,
onAction: onPageAction,
onOpenMenu: () => onOpenNode(
studioNavTree
.expand((node) => [node, ...node.children])
.firstWhere((node) => node.id == 'NavigationEditor'),
),
);
default:
return StudioFormEditorPage(
@@ -239,42 +231,3 @@ class _TabStrip extends StatelessWidget {
);
}
}
class _StudioStatusBar extends StatelessWidget {
const _StudioStatusBar({
required this.user,
required this.selectedTab,
required this.tabsCount,
required this.activitiesCount,
});
final StudioUser user;
final StudioTab selectedTab;
final int tabsCount;
final int activitiesCount;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Wrap(
spacing: 10,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
_MiniBadge(text: '用户 ${user.displayName}'),
_MiniBadge(text: '工作区 ${user.workspace}'),
if (user.backendAppName.isNotEmpty)
_MiniBadge(text: '应用 ${user.backendAppName}'),
if (user.organizationName.isNotEmpty)
_MiniBadge(text: '机构 ${user.organizationName}'),
_MiniBadge(text: '当前页 ${selectedTab.title}'),
_MiniBadge(text: '标签 ${tabsCount}'),
_MiniBadge(text: '活动 ${activitiesCount}'),
],
),
),
);
}
}
+17 -6
View File
@@ -28,7 +28,9 @@ List<Color> _shellGradientColors(BuildContext context) {
ThemeData buildStudioTheme(Brightness brightness) {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1AA6A6),
seedColor: brightness == Brightness.light
? const Color(0xFF1275DA)
: const Color(0xFF1AA6A6),
brightness: brightness,
);
final isDark = brightness == Brightness.dark;
@@ -45,7 +47,9 @@ ThemeData buildStudioTheme(Brightness brightness) {
: const Color(0xFFF6F8FB),
appBarTheme: AppBarTheme(
centerTitle: false,
backgroundColor: isDark ? const Color(0xFF07111A) : const Color(0xFFF8FBFF),
backgroundColor: isDark
? const Color(0xFF07111A)
: const Color(0xFFF8FBFF),
foregroundColor: colorScheme.onSurface,
elevation: 0,
),
@@ -77,8 +81,14 @@ ThemeData buildStudioTheme(Brightness brightness) {
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),
prefixIconConstraints: const BoxConstraints.tightFor(
width: 34,
height: 34,
),
suffixIconConstraints: const BoxConstraints.tightFor(
width: 34,
height: 34,
),
),
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
@@ -117,8 +127,9 @@ class _MiniBadge extends StatelessWidget {
),
child: Text(
text,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: _mutedColor(context, 0.78)),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: _mutedColor(context, 0.78)),
),
);
}
+4
View File
@@ -37,9 +37,13 @@ dependencies:
crypto: ^3.0.6
dio: ^5.8.0
file_selector: ^1.0.3
flutter_highlight: ^0.7.0
pointycastle: ^4.0.0
shared_preferences: ^2.3.2
code_text_field: ^1.1.0
highlight: ^0.7.0
json_field_editor: ^1.2.1
linked_scroll_controller: ^0.2.0
dev_dependencies:
flutter_test: