SHA256
859 lines
26 KiB
Dart
859 lines
26 KiB
Dart
part of 'studio_app.dart';
|
|
|
|
class StudioSqlPage extends StatefulWidget {
|
|
const StudioSqlPage({
|
|
super.key,
|
|
required this.tab,
|
|
required this.runtime,
|
|
required this.onAction,
|
|
});
|
|
|
|
final StudioTab tab;
|
|
final StudioRuntime runtime;
|
|
final void Function(String title, String detail) onAction;
|
|
|
|
@override
|
|
State<StudioSqlPage> createState() => _StudioSqlPageState();
|
|
}
|
|
|
|
class _StudioSqlPageState extends State<StudioSqlPage> {
|
|
late final CodeController _sqlController;
|
|
late final TextEditingController _limitController;
|
|
late final TextEditingController _countSqlController;
|
|
late final TextEditingController _summarySqlController;
|
|
late final TextEditingController _criteriaController;
|
|
late final TextEditingController _extConfigController;
|
|
bool _loading = true;
|
|
bool _templateLoaded = false;
|
|
String _status = '等待加载';
|
|
String _detail = '';
|
|
Map<String, List<String>> _templateIndex = <String, List<String>>{};
|
|
List<Map<String, dynamic>> _resultRows = <Map<String, dynamic>>[];
|
|
List<String> _resultColumns = <String>[];
|
|
String _selectedTidValue = '';
|
|
String _selectedOidValue = '';
|
|
|
|
bool get _isTemplateMode => widget.tab.pageType == StudioPageType.sqlTemplate;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_sqlController = CodeController(
|
|
text: 'select id, code, name from item order by id desc limit 20',
|
|
language: sql_highlight.sql,
|
|
);
|
|
_limitController = TextEditingController(text: '100');
|
|
_countSqlController = TextEditingController();
|
|
_summarySqlController = TextEditingController();
|
|
_criteriaController = TextEditingController(text: '{}');
|
|
_extConfigController = TextEditingController(text: '{}');
|
|
if (_isTemplateMode) {
|
|
_loadTemplateIndex();
|
|
} else {
|
|
_loading = false;
|
|
_status = '可直接执行 SQL';
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_sqlController.dispose();
|
|
_limitController.dispose();
|
|
_countSqlController.dispose();
|
|
_summarySqlController.dispose();
|
|
_criteriaController.dispose();
|
|
_extConfigController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _loadTemplateIndex() async {
|
|
try {
|
|
final items = await widget.runtime.client.fetchSqlTemplateIndex(
|
|
session: widget.runtime.session,
|
|
);
|
|
final index = <String, List<String>>{};
|
|
for (final item in items) {
|
|
final tid = _readValue(item, const ['tid']);
|
|
final oid = _readValue(item, const ['oid']);
|
|
if (tid.isEmpty || oid.isEmpty) {
|
|
continue;
|
|
}
|
|
index.putIfAbsent(tid, () => <String>[]).add(oid);
|
|
}
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_templateIndex = index;
|
|
_status = index.isEmpty ? '没有找到 SQL 模板' : '已加载模板索引';
|
|
_loading = false;
|
|
});
|
|
if (index.isNotEmpty) {
|
|
await _loadSelectedTemplate();
|
|
}
|
|
} catch (error, stackTrace) {
|
|
_printError('加载 SQL 模板索引失败', error, stackTrace);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_status = '加载模板失败';
|
|
_detail = _normalizeError(error);
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
String _selectedTid() {
|
|
if (_selectedTidValue.isNotEmpty) {
|
|
return _selectedTidValue;
|
|
}
|
|
if (_templateIndex.isEmpty) {
|
|
return '';
|
|
}
|
|
return _templateIndex.keys.first;
|
|
}
|
|
|
|
String _selectedOid([String? tid]) {
|
|
final selectedTid = tid ?? _selectedTid();
|
|
final oids = _templateIndex[selectedTid];
|
|
if (oids == null || oids.isEmpty) {
|
|
return '';
|
|
}
|
|
if (_selectedOidValue.isNotEmpty && oids.contains(_selectedOidValue)) {
|
|
return _selectedOidValue;
|
|
}
|
|
return oids.first;
|
|
}
|
|
|
|
Future<void> _loadSelectedTemplate() async {
|
|
final tid = _selectedTid();
|
|
final oid = _selectedOid(tid);
|
|
if (tid.isEmpty || oid.isEmpty) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_loading = true;
|
|
_status = '正在加载模板...';
|
|
});
|
|
try {
|
|
final template = await widget.runtime.client.fetchSqlTemplate(
|
|
tid: tid,
|
|
oid: oid,
|
|
session: widget.runtime.session,
|
|
);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
if (template == null) {
|
|
setState(() {
|
|
_templateLoaded = false;
|
|
_status = '模板不存在';
|
|
_detail = '$tid / $oid';
|
|
});
|
|
return;
|
|
}
|
|
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',
|
|
], '{}');
|
|
_status = '模板已加载';
|
|
_detail = '$tid / $oid';
|
|
});
|
|
widget.onAction('加载模板', '$tid / $oid');
|
|
} catch (error, stackTrace) {
|
|
_printError('加载 SQL 模板失败', error, stackTrace);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_templateLoaded = false;
|
|
_status = '加载模板失败';
|
|
_detail = _normalizeError(error);
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _runSql() async {
|
|
setState(() {
|
|
_loading = true;
|
|
_status = '正在执行 SQL...';
|
|
_detail = '';
|
|
_resultRows = <Map<String, dynamic>>[];
|
|
_resultColumns = <String>[];
|
|
});
|
|
try {
|
|
final limit = int.tryParse(_limitController.text.trim()) ?? 100;
|
|
final result = await widget.runtime.client.runSql(
|
|
sql: _sqlController.text,
|
|
limit: limit,
|
|
session: widget.runtime.session,
|
|
);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
final data = result.data is Map
|
|
? Map<String, dynamic>.from(result.data as Map)
|
|
: <String, dynamic>{};
|
|
final rows = _extractRows(data['list'] ?? data['rows'] ?? data['data']);
|
|
setState(() {
|
|
_resultRows = rows;
|
|
_resultColumns = rows.isEmpty ? <String>[] : rows.first.keys.toList();
|
|
_status = result.success ? 'SQL 执行完成' : 'SQL 执行失败';
|
|
_detail = _formatActionResult(result);
|
|
});
|
|
widget.onAction('执行 SQL', widget.tab.title);
|
|
} catch (error, stackTrace) {
|
|
_printError('执行 SQL 失败', error, stackTrace);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_status = 'SQL 执行失败';
|
|
_detail = _normalizeError(error);
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _saveTemplate() async {
|
|
final tid = _selectedTid();
|
|
final oid = _selectedOid(tid);
|
|
if (tid.isEmpty || oid.isEmpty) {
|
|
setState(() {
|
|
_status = '请选择 tid 和 oid';
|
|
});
|
|
return;
|
|
}
|
|
setState(() {
|
|
_loading = true;
|
|
_status = _templateLoaded ? '正在更新模板...' : '正在创建模板...';
|
|
_detail = '';
|
|
});
|
|
try {
|
|
final payload = <String, dynamic>{
|
|
'value': _sqlController.text,
|
|
'count': _countSqlController.text,
|
|
'criteria': _criteriaController.text,
|
|
'summary': _summarySqlController.text,
|
|
'extConfig': _extConfigController.text,
|
|
};
|
|
final wasLoaded = _templateLoaded;
|
|
final result = _templateLoaded
|
|
? await widget.runtime.client.updateSqlTemplate(
|
|
tid: tid,
|
|
oid: oid,
|
|
payload: payload,
|
|
session: widget.runtime.session,
|
|
)
|
|
: await widget.runtime.client.createSqlTemplate(
|
|
tid: tid,
|
|
oid: oid,
|
|
payload: payload,
|
|
session: widget.runtime.session,
|
|
);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_templateLoaded = true;
|
|
_status = result.success ? '模板已保存' : '保存失败';
|
|
_detail = _formatActionResult(result);
|
|
});
|
|
widget.onAction(wasLoaded ? '更新模板' : '创建模板', '$tid / $oid');
|
|
} catch (error, stackTrace) {
|
|
_printError('保存 SQL 模板失败', error, stackTrace);
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_status = '保存失败';
|
|
_detail = _normalizeError(error);
|
|
});
|
|
} finally {
|
|
if (mounted) {
|
|
setState(() {
|
|
_loading = false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _formatSql() {
|
|
setState(() {
|
|
_sqlController.text = _sqlController.text
|
|
.replaceAll(RegExp(r'\s+'), ' ')
|
|
.trim();
|
|
});
|
|
widget.onAction('格式化 SQL', widget.tab.title);
|
|
}
|
|
|
|
void _printError(String title, Object error, StackTrace stackTrace) {
|
|
debugPrint('========== $title ==========');
|
|
debugPrint('error: $error');
|
|
debugPrint('stackTrace:\n$stackTrace');
|
|
debugPrint('========== $title end ==========');
|
|
}
|
|
|
|
String _normalizeError(Object error) {
|
|
final text = error.toString();
|
|
const marker = 'Bad state: ';
|
|
if (text.startsWith(marker)) {
|
|
return text.substring(marker.length);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
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) {
|
|
return value.toString();
|
|
}
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
List<Map<String, dynamic>> _extractRows(dynamic value) {
|
|
if (value is List) {
|
|
return value
|
|
.whereType<Map>()
|
|
.map((item) => Map<String, dynamic>.from(item))
|
|
.toList();
|
|
}
|
|
return <Map<String, dynamic>>[];
|
|
}
|
|
|
|
String _formatActionResult(StudioBackendActionResult result) {
|
|
return const JsonEncoder.withIndent(' ').convert({
|
|
'code': result.code,
|
|
'message': result.message,
|
|
'data': result.data,
|
|
'raw': result.raw,
|
|
});
|
|
}
|
|
|
|
Map<String, TextStyle> _sqlCodeTheme(BuildContext context) {
|
|
final isDark = _isDarkTheme(context);
|
|
return isDark ? atomOneDarkTheme : atomOneLightTheme;
|
|
}
|
|
|
|
Widget _buildStatusGrid(BuildContext context) {
|
|
final items = <_StatusItem>[
|
|
_StatusItem('状态', _status),
|
|
_StatusItem('详情', _detail.isEmpty ? '-' : _detail),
|
|
_StatusItem(
|
|
'结果',
|
|
_resultRows.isEmpty
|
|
? '无结果'
|
|
: '共 ${_resultRows.length} 行,${_resultColumns.length} 列',
|
|
),
|
|
if (_isTemplateMode)
|
|
_StatusItem('模板', '${_selectedTid()} / ${_selectedOid()}'),
|
|
];
|
|
|
|
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(),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildTemplateBody(BuildContext context) {
|
|
final tids = _templateIndex.keys.toList();
|
|
final selectedTid = _selectedTid();
|
|
final oids = selectedTid.isEmpty
|
|
? const <String>[]
|
|
: _templateIndex[selectedTid] ?? const <String>[];
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
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'),
|
|
items: tids
|
|
.map(
|
|
(tid) => DropdownMenuItem(value: tid, child: Text(tid)),
|
|
)
|
|
.toList(),
|
|
onChanged: (value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_selectedTidValue = value;
|
|
_selectedOidValue =
|
|
(_templateIndex[value] ?? const <String>[]).isNotEmpty
|
|
? _templateIndex[value]!.first
|
|
: '';
|
|
});
|
|
},
|
|
),
|
|
);
|
|
final oidField = SizedBox(
|
|
width: wide ? 260 : double.infinity,
|
|
child: DropdownButtonFormField<String>(
|
|
value: oids.contains(_selectedOid()) ? _selectedOid() : null,
|
|
decoration: const InputDecoration(labelText: 'oid'),
|
|
items: oids
|
|
.map(
|
|
(oid) => DropdownMenuItem(value: oid, child: Text(oid)),
|
|
)
|
|
.toList(),
|
|
onChanged: (value) {
|
|
if (value == null) {
|
|
return;
|
|
}
|
|
setState(() {
|
|
_selectedOidValue = value;
|
|
});
|
|
},
|
|
),
|
|
);
|
|
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),
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (_isTemplateMode &&
|
|
_selectedTidValue.isEmpty &&
|
|
_templateIndex.isNotEmpty) {
|
|
_selectedTidValue = _templateIndex.keys.first;
|
|
_selectedOidValue = _templateIndex[_selectedTidValue]!.first;
|
|
}
|
|
|
|
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: [
|
|
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),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
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 _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),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
if (includeTemplateFields) ...[
|
|
Wrap(
|
|
spacing: 10,
|
|
runSpacing: 10,
|
|
children: [
|
|
SizedBox(
|
|
width: 200,
|
|
child: TextField(
|
|
controller: _countSqlController,
|
|
decoration: const InputDecoration(labelText: 'count SQL'),
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 200,
|
|
child: TextField(
|
|
controller: _summarySqlController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'summary SQL',
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _criteriaController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'criteria',
|
|
alignLabelWithHint: true,
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextField(
|
|
controller: _extConfigController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'extConfig',
|
|
alignLabelWithHint: true,
|
|
),
|
|
maxLines: 3,
|
|
),
|
|
const SizedBox(height: 14),
|
|
],
|
|
SizedBox(
|
|
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: 10),
|
|
Row(
|
|
children: [
|
|
SizedBox(
|
|
width: 150,
|
|
child: TextField(
|
|
controller: _limitController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: const InputDecoration(labelText: 'limit'),
|
|
),
|
|
),
|
|
const SizedBox(width: 10),
|
|
FilledButton.tonalIcon(
|
|
onPressed: _loading ? null : _formatSql,
|
|
icon: const Icon(Icons.format_align_left_rounded),
|
|
label: const Text('格式化'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
String _formatCellValue(Object? value) {
|
|
if (value == null) {
|
|
return '';
|
|
}
|
|
if (value is Map || value is List) {
|
|
return const JsonEncoder.withIndent(' ').convert(value);
|
|
}
|
|
return value.toString();
|
|
}
|
|
}
|
|
|
|
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),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|