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, responseType: ResponseType.bytes,
followRedirects: true, followRedirects: true,
receiveTimeout: const Duration(seconds: 60), receiveTimeout: const Duration(seconds: 60),
extra: const <String, dynamic>{'withCredentials': true},
), ),
); );
_captureCookies(response); _captureCookies(response);
@@ -594,6 +595,8 @@ class StudioBackendClient {
session: session, session: session,
includeSessionCookies: includeSessionCookies, includeSessionCookies: includeSessionCookies,
), ),
// Web 端需要浏览器带上 cookie,避免登录态和会话丢失。
extra: const <String, dynamic>{'withCredentials': true},
), ),
); );
_captureCookies(response); _captureCookies(response);
@@ -623,6 +626,7 @@ class StudioBackendClient {
? (request, options) => utf8.encode(request) ? (request, options) => utf8.encode(request)
: null, : null,
followRedirects: followRedirects, followRedirects: followRedirects,
extra: const <String, dynamic>{'withCredentials': true},
), ),
); );
_captureCookies(response); _captureCookies(response);
@@ -668,6 +672,7 @@ class StudioBackendClient {
includeSessionCookies: true, includeSessionCookies: true,
), ),
followRedirects: true, followRedirects: true,
extra: const <String, dynamic>{'withCredentials': true},
), ),
); );
_captureCookies(response); _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-dark.dart';
import 'package:flutter_highlight/themes/atom-one-light.dart'; import 'package:flutter_highlight/themes/atom-one-light.dart';
import 'package:code_text_field/code_text_field.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:highlight/languages/sql.dart' as sql_highlight;
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
+1
View File
@@ -194,6 +194,7 @@ class _StudioLoginViewState extends State<StudioLoginView> {
return; return;
} }
setState(() { setState(() {
// 登录页的配置读取只做“增强体验”;就算后端暂时不可达,用户仍然可以手动输入地址继续登录。
_errorMessage = '后端配置暂时不可用,仍可继续尝试登录'; _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( StudioNavNode(
id: 'history', id: 'history',
title: '模型日志', 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( StudioNavNode(
id: 'ModelSync', id: 'ModelSync',
title: '配置下载', title: '配置下载',
+44 -145
View File
@@ -6,15 +6,11 @@ class StudioDashboardPage extends StatelessWidget {
required this.tab, required this.tab,
required this.user, required this.user,
required this.activities, required this.activities,
required this.rootNodes,
required this.onOpenNode,
}); });
final StudioTab tab; final StudioTab tab;
final StudioUser user; final StudioUser user;
final List<StudioActivity> activities; final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final ValueChanged<StudioNavNode> onOpenNode;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -126,58 +122,12 @@ class StudioDashboardPage extends StatelessWidget {
}, },
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
LayoutBuilder( _ActivityPanel(activities: activities),
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),
],
);
},
),
], ],
), ),
), ),
); );
} }
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 { class _InfoPanel extends StatelessWidget {
@@ -216,20 +166,23 @@ class _InfoPanel extends StatelessWidget {
children: [ children: [
Text( Text(
title, title,
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall?.copyWith(
?.copyWith(color: _mutedColor(context, 0.68)), color: _mutedColor(context, 0.68),
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
value, value,
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium?.copyWith(
?.copyWith(fontWeight: FontWeight.w700), fontWeight: FontWeight.w700,
),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
subtitle, subtitle,
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall?.copyWith(
?.copyWith(color: _mutedColor(context, 0.72)), color: _mutedColor(context, 0.72),
),
), ),
], ],
), ),
@@ -256,102 +209,48 @@ class _ActivityPanel extends StatelessWidget {
children: [ children: [
Text( Text(
'最近活动', '最近活动',
style: Theme.of(context).textTheme.titleMedium style: Theme.of(
?.copyWith(fontWeight: FontWeight.w700), context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
...activities.take(8).map( ...activities
(item) => Padding( .take(8)
padding: const EdgeInsets.only(bottom: 8), .map(
child: Row( (item) => Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(bottom: 8),
children: [ child: Row(
const Icon(Icons.bolt_rounded, size: 16), crossAxisAlignment: CrossAxisAlignment.start,
const SizedBox(width: 8), children: [
Expanded( const Icon(Icons.bolt_rounded, size: 16),
child: Column( const SizedBox(width: 8),
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
children: [ child: Column(
Text( crossAxisAlignment: CrossAxisAlignment.start,
item.title, children: [
style: Theme.of(context).textTheme.bodyMedium Text(
?.copyWith(fontWeight: FontWeight.w600), 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.tab,
required this.runtime, required this.runtime,
required this.onAction, required this.onAction,
required this.onOpenMenu,
}); });
final StudioTab tab; final StudioTab tab;
final StudioRuntime runtime; final StudioRuntime runtime;
final void Function(String title, String detail) onAction; final void Function(String title, String detail) onAction;
final VoidCallback onOpenMenu;
@override @override
State<StudioPortalMaintainPage> createState() => _StudioPortalMaintainPageState(); State<StudioPortalMaintainPage> createState() => _StudioPortalMaintainPageState();
@@ -108,6 +110,12 @@ class _StudioPortalMaintainPageState extends State<StudioPortalMaintainPage> {
icon: const Icon(Icons.refresh_rounded), icon: const Icon(Icons.refresh_rounded),
label: const Text('刷新'), 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), const SizedBox(height: 12),
+267 -154
View File
@@ -39,8 +39,7 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
void initState() { void initState() {
super.initState(); super.initState();
_sqlController = CodeController( _sqlController = CodeController(
text: text: 'select id, code, name from item order by id desc limit 20',
'select id, code, name from item order by id desc limit 20',
language: sql_highlight.sql, language: sql_highlight.sql,
); );
_limitController = TextEditingController(text: '100'); _limitController = TextEditingController(text: '100');
@@ -157,10 +156,20 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
setState(() { setState(() {
_templateLoaded = true; _templateLoaded = true;
_sqlController.text = _readValue(template, const ['value', 'sql']); _sqlController.text = _readValue(template, const ['value', 'sql']);
_countSqlController.text = _readValue(template, const ['countSql', 'count']); _countSqlController.text = _readValue(template, const [
_summarySqlController.text = _readValue(template, const ['summarySql', 'summary']); 'countSql',
_criteriaController.text = _readValue(template, const ['criteria'], '{}'); 'count',
_extConfigController.text = _readValue(template, const ['extConfig'], '{}'); ]);
_summarySqlController.text = _readValue(template, const [
'summarySql',
'summary',
]);
_criteriaController.text = _readValue(template, const [
'criteria',
], '{}');
_extConfigController.text = _readValue(template, const [
'extConfig',
], '{}');
_status = '模板已加载'; _status = '模板已加载';
_detail = '$tid / $oid'; _detail = '$tid / $oid';
}); });
@@ -319,7 +328,11 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
return text; 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) { for (final key in keys) {
final value = map[key]; final value = map[key];
if (value != null && value.toString().trim().isNotEmpty) { if (value != null && value.toString().trim().isNotEmpty) {
@@ -354,58 +367,40 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
} }
Widget _buildStatusGrid(BuildContext context) { Widget _buildStatusGrid(BuildContext context) {
final rows = <DataRow>[ final items = <_StatusItem>[
DataRow( _StatusItem('状态', _status),
cells: [ _StatusItem('详情', _detail.isEmpty ? '-' : _detail),
const DataCell(Text('状态')), _StatusItem(
DataCell(SelectableText(_status)), '结果',
], _resultRows.isEmpty
), ? '无结果'
DataRow( : '${_resultRows.length} 行,${_resultColumns.length}',
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)
_StatusItem('模板', '${_selectedTid()} / ${_selectedOid()}'),
]; ];
if (_isTemplateMode) { return LayoutBuilder(
rows.add( builder: (context, constraints) {
DataRow( final cardWidth = constraints.maxWidth >= 920
cells: [ ? (constraints.maxWidth - 12) / 2
const DataCell(Text('模板')), : constraints.maxWidth;
DataCell(SelectableText('${_selectedTid()} / ${_selectedOid()}')), return Wrap(
], spacing: 12,
), runSpacing: 12,
); children: items
} .map(
(item) => SizedBox(
return SingleChildScrollView( width: cardWidth,
scrollDirection: Axis.horizontal, child: _StatusTile(
child: DataTable( title: item.title,
columns: const [ content: item.content,
DataColumn(label: Text('字段')), ),
DataColumn(label: Text('内容')), ),
], )
rows: rows, .toList(),
), );
},
); );
} }
@@ -428,7 +423,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
value: tids.contains(selectedTid) ? selectedTid : null, value: tids.contains(selectedTid) ? selectedTid : null,
decoration: const InputDecoration(labelText: 'tid'), decoration: const InputDecoration(labelText: 'tid'),
items: tids items: tids
.map((tid) => DropdownMenuItem(value: tid, child: Text(tid))) .map(
(tid) => DropdownMenuItem(value: tid, child: Text(tid)),
)
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
@@ -436,7 +433,8 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
} }
setState(() { setState(() {
_selectedTidValue = value; _selectedTidValue = value;
_selectedOidValue = (_templateIndex[value] ?? const <String>[]).isNotEmpty _selectedOidValue =
(_templateIndex[value] ?? const <String>[]).isNotEmpty
? _templateIndex[value]!.first ? _templateIndex[value]!.first
: ''; : '';
}); });
@@ -449,7 +447,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
value: oids.contains(_selectedOid()) ? _selectedOid() : null, value: oids.contains(_selectedOid()) ? _selectedOid() : null,
decoration: const InputDecoration(labelText: 'oid'), decoration: const InputDecoration(labelText: 'oid'),
items: oids items: oids
.map((oid) => DropdownMenuItem(value: oid, child: Text(oid))) .map(
(oid) => DropdownMenuItem(value: oid, child: Text(oid)),
)
.toList(), .toList(),
onChanged: (value) { onChanged: (value) {
if (value == null) { if (value == null) {
@@ -511,114 +511,188 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (_isTemplateMode && _selectedTidValue.isEmpty && _templateIndex.isNotEmpty) { if (_isTemplateMode &&
_selectedTidValue.isEmpty &&
_templateIndex.isNotEmpty) {
_selectedTidValue = _templateIndex.keys.first; _selectedTidValue = _templateIndex.keys.first;
_selectedOidValue = _templateIndex[_selectedTidValue]!.first; _selectedOidValue = _templateIndex[_selectedTidValue]!.first;
} }
return SingleChildScrollView( return LayoutBuilder(
child: Column( builder: (context, constraints) {
crossAxisAlignment: CrossAxisAlignment.stretch, final wide = constraints.maxWidth >= 1120;
children: [ final content = _loading
if (_loading) ? const Padding(
const Padding( padding: EdgeInsets.symmetric(vertical: 16),
padding: EdgeInsets.symmetric(vertical: 16), child: Center(child: CircularProgressIndicator()),
child: Center(child: CircularProgressIndicator()), )
) : _isTemplateMode
else if (_isTemplateMode) ? _buildTemplateBody(context)
_buildTemplateBody(context) : _buildSqlEditors(context, includeTemplateFields: false);
else
_buildSqlEditors(context, includeTemplateFields: false), if (!wide) {
const SizedBox(height: 12), return SingleChildScrollView(
Card( child: Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.stretch,
padding: const EdgeInsets.all(14), children: [
child: Column( _buildSqlHeader(context),
crossAxisAlignment: CrossAxisAlignment.start, 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: [ children: [
Row( Expanded(
children: [ flex: 3,
Expanded( child: SingleChildScrollView(child: content),
child: Text( ),
'执行状态', const SizedBox(width: 12),
style: Theme.of(context).textTheme.titleLarge SizedBox(
?.copyWith(fontWeight: FontWeight.w700), 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( Widget _buildSqlHeader(BuildContext context) {
crossAxisAlignment: CrossAxisAlignment.start, return Card(
children: [ child: Padding(
Text( padding: const EdgeInsets.all(14),
'执行结果', child: Row(
style: Theme.of(context).textTheme.titleLarge children: [
?.copyWith(fontWeight: FontWeight.w700), Expanded(
), child: Text(
const SizedBox(height: 12), _isTemplateMode ? 'SQL 模板执行' : 'SQL 执行',
SingleChildScrollView( style: Theme.of(
scrollDirection: Axis.horizontal, context,
child: DataTable( ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
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(),
),
),
],
),
), ),
), ),
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( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
@@ -642,7 +716,9 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
width: 200, width: 200,
child: TextField( child: TextField(
controller: _summarySqlController, controller: _summarySqlController,
decoration: const InputDecoration(labelText: 'summary SQL'), decoration: const InputDecoration(
labelText: 'summary SQL',
),
maxLines: 3, maxLines: 3,
), ),
), ),
@@ -671,9 +747,7 @@ class _StudioSqlPageState extends State<StudioSqlPage> {
SizedBox( SizedBox(
height: 240, height: 240,
child: CodeTheme( child: CodeTheme(
data: CodeThemeData( data: CodeThemeData(styles: _sqlCodeTheme(context)),
styles: _sqlCodeTheme(context),
),
child: CodeField( child: CodeField(
controller: _sqlController, controller: _sqlController,
expands: true, 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: List<StudioActivity>.unmodifiable(
_activities, _activities,
), ),
rootNodes: visibleRoots,
refreshToken: _refreshRevision, refreshToken: _refreshRevision,
onSelectTab: _selectTab, onSelectTab: _selectTab,
onCloseTab: _closeTab, onCloseTab: _closeTab,
@@ -250,7 +249,6 @@ class _StudioShellState extends State<StudioShell> {
selectedTab: _selectedTab, selectedTab: _selectedTab,
tabs: List<StudioTab>.unmodifiable(_tabs), tabs: List<StudioTab>.unmodifiable(_tabs),
activities: List<StudioActivity>.unmodifiable(_activities), activities: List<StudioActivity>.unmodifiable(_activities),
rootNodes: visibleRoots,
refreshToken: _refreshRevision, refreshToken: _refreshRevision,
onSelectTab: _selectTab, onSelectTab: _selectTab,
onCloseTab: _closeTab, 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), const SizedBox(height: 14),
FilledButton.tonalIcon( Row(
onPressed: onLogout, children: [
icon: const Icon(Icons.logout_rounded), Expanded(
label: const Text('退出登录'), 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.selectedTab,
required this.tabs, required this.tabs,
required this.activities, required this.activities,
required this.rootNodes,
required this.refreshToken, required this.refreshToken,
required this.onSelectTab, required this.onSelectTab,
required this.onCloseTab, required this.onCloseTab,
@@ -25,7 +24,6 @@ class StudioWorkspaceView extends StatelessWidget {
final StudioTab selectedTab; final StudioTab selectedTab;
final List<StudioTab> tabs; final List<StudioTab> tabs;
final List<StudioActivity> activities; final List<StudioActivity> activities;
final List<StudioNavNode> rootNodes;
final int refreshToken; final int refreshToken;
final ValueChanged<String> onSelectTab; final ValueChanged<String> onSelectTab;
final ValueChanged<String> onCloseTab; final ValueChanged<String> onCloseTab;
@@ -66,20 +64,12 @@ class StudioWorkspaceView extends StatelessWidget {
key: ValueKey('${selectedTab.id}:$refreshToken'), key: ValueKey('${selectedTab.id}:$refreshToken'),
tab: selectedTab, tab: selectedTab,
activities: activities, activities: activities,
rootNodes: rootNodes,
runtime: runtime, runtime: runtime,
onPageAction: onPageAction, onPageAction: onPageAction,
onOpenNode: onOpenNode, 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 Key key,
required StudioTab tab, required StudioTab tab,
required List<StudioActivity> activities, required List<StudioActivity> activities,
required List<StudioNavNode> rootNodes,
required StudioRuntime runtime, required StudioRuntime runtime,
required void Function(String title, String detail) onPageAction, required void Function(String title, String detail) onPageAction,
required ValueChanged<StudioNavNode> onOpenNode, required ValueChanged<StudioNavNode> onOpenNode,
@@ -101,8 +90,6 @@ class StudioWorkspaceView extends StatelessWidget {
tab: tab, tab: tab,
user: user, user: user,
activities: activities, activities: activities,
rootNodes: rootNodes,
onOpenNode: onOpenNode,
); );
case StudioPageType.history: case StudioPageType.history:
return StudioHistoryPage( return StudioHistoryPage(
@@ -133,6 +120,11 @@ class StudioWorkspaceView extends StatelessWidget {
tab: tab, tab: tab,
runtime: runtime, runtime: runtime,
onAction: onPageAction, onAction: onPageAction,
onOpenMenu: () => onOpenNode(
studioNavTree
.expand((node) => [node, ...node.children])
.firstWhere((node) => node.id == 'NavigationEditor'),
),
); );
default: default:
return StudioFormEditorPage( 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) { ThemeData buildStudioTheme(Brightness brightness) {
final colorScheme = ColorScheme.fromSeed( final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1AA6A6), seedColor: brightness == Brightness.light
? const Color(0xFF1275DA)
: const Color(0xFF1AA6A6),
brightness: brightness, brightness: brightness,
); );
final isDark = brightness == Brightness.dark; final isDark = brightness == Brightness.dark;
@@ -45,7 +47,9 @@ ThemeData buildStudioTheme(Brightness brightness) {
: const Color(0xFFF6F8FB), : const Color(0xFFF6F8FB),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
centerTitle: false, centerTitle: false,
backgroundColor: isDark ? const Color(0xFF07111A) : const Color(0xFFF8FBFF), backgroundColor: isDark
? const Color(0xFF07111A)
: const Color(0xFFF8FBFF),
foregroundColor: colorScheme.onSurface, foregroundColor: colorScheme.onSurface,
elevation: 0, elevation: 0,
), ),
@@ -77,8 +81,14 @@ ThemeData buildStudioTheme(Brightness brightness) {
borderSide: BorderSide(color: colorScheme.primary), borderSide: BorderSide(color: colorScheme.primary),
), ),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
prefixIconConstraints: const BoxConstraints.tightFor(width: 34, height: 34), prefixIconConstraints: const BoxConstraints.tightFor(
suffixIconConstraints: const BoxConstraints.tightFor(width: 34, height: 34), width: 34,
height: 34,
),
suffixIconConstraints: const BoxConstraints.tightFor(
width: 34,
height: 34,
),
), ),
filledButtonTheme: FilledButtonThemeData( filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom( style: FilledButton.styleFrom(
@@ -117,8 +127,9 @@ class _MiniBadge extends StatelessWidget {
), ),
child: Text( child: Text(
text, text,
style: Theme.of(context).textTheme.bodySmall style: Theme.of(
?.copyWith(color: _mutedColor(context, 0.78)), context,
).textTheme.bodySmall?.copyWith(color: _mutedColor(context, 0.78)),
), ),
); );
} }
+4
View File
@@ -37,9 +37,13 @@ dependencies:
crypto: ^3.0.6 crypto: ^3.0.6
dio: ^5.8.0 dio: ^5.8.0
file_selector: ^1.0.3 file_selector: ^1.0.3
flutter_highlight: ^0.7.0
pointycastle: ^4.0.0 pointycastle: ^4.0.0
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
code_text_field: ^1.1.0 code_text_field: ^1.1.0
highlight: ^0.7.0
json_field_editor: ^1.2.1
linked_scroll_controller: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: