Files
TtxPageAdmin/lib/studio_app_pages_form_editor.dart
T
2026-05-16 19:03:01 +08:00

2076 lines
67 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
part of 'studio_app.dart';
class StudioFormEditorPage extends StatefulWidget {
const StudioFormEditorPage({
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<StudioFormEditorPage> createState() => _StudioFormEditorPageState();
}
class _StudioFormEditorPageState extends State<StudioFormEditorPage> {
late final TextEditingController _nameController;
late final TextEditingController _codeController;
late final TextEditingController _versionController;
late final TextEditingController _remarkController;
late final JsonTextFieldController _contentController;
late final FocusNode _contentFocusNode;
late final LinkedScrollControllerGroup _jsonScrollGroup;
late final ScrollController _jsonLineScrollController;
late final ScrollController _jsonFieldScrollController;
late final ScrollController _jsonDiffScrollController;
late final ScrollController _navigatorTreeScrollController;
late final TextEditingController _modelSearchController;
bool _loading = true;
bool _saving = false;
bool _loadedExisting = false;
String _originalSavePreview = '';
String _jsonErrorMessage = '';
int? _jsonErrorOffset;
int? _jsonErrorLine;
String _status = '加载中';
String _detail = '';
List<Map<String, dynamic>> _options = <Map<String, dynamic>>[];
String _selectedKey = '';
List<Map<String, dynamic>> _navigatorStructure = <Map<String, dynamic>>[];
String _navigatorSelectedId = '';
final Set<String> _navigatorExpandedIds = <String>{};
late final TextEditingController _navigatorIdController;
late final TextEditingController _navigatorNameController;
late final TextEditingController _navigatorTidController;
late final TextEditingController _navigatorOidController;
late final TextEditingController _navigatorIconController;
late final TextEditingController _navigatorParentController;
late final TextEditingController _navigatorMaxOpenedController;
late final TextEditingController _navigatorParamController;
bool _navigatorPublic = false;
bool _navigatorHidden = false;
bool get _isTableMode => widget.tab.id.toLowerCase().contains('table');
bool get _isBillMode => widget.tab.id.toLowerCase().contains('bill');
bool get _isViewMode =>
widget.tab.id.toLowerCase().contains('view') ||
widget.tab.id.toLowerCase().contains('wsodata');
bool get _isNavigatorMode =>
widget.tab.id.toLowerCase().contains('navigator') ||
widget.tab.id.toLowerCase().contains('navigation');
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: widget.tab.title);
_codeController = TextEditingController(text: widget.tab.id);
_versionController = TextEditingController(text: '1');
_remarkController = TextEditingController(text: widget.tab.summary);
_contentController = JsonTextFieldController()..text = '{}';
_contentFocusNode = FocusNode();
_jsonScrollGroup = LinkedScrollControllerGroup();
_jsonLineScrollController = _jsonScrollGroup.addAndGet();
_jsonFieldScrollController = _jsonScrollGroup.addAndGet();
_jsonDiffScrollController = ScrollController();
_navigatorTreeScrollController = ScrollController();
_navigatorIdController = TextEditingController();
_navigatorNameController = TextEditingController();
_navigatorTidController = TextEditingController();
_navigatorOidController = TextEditingController();
_navigatorIconController = TextEditingController();
_navigatorParentController = TextEditingController();
_navigatorMaxOpenedController = TextEditingController();
_navigatorParamController = TextEditingController();
_modelSearchController = TextEditingController();
_contentController.addListener(_onContentChanged);
_loadInitialData();
}
@override
void dispose() {
_nameController.dispose();
_codeController.dispose();
_versionController.dispose();
_remarkController.dispose();
_contentController.dispose();
_contentFocusNode.dispose();
_jsonLineScrollController.dispose();
_jsonFieldScrollController.dispose();
_jsonDiffScrollController.dispose();
_navigatorTreeScrollController.dispose();
_navigatorIdController.dispose();
_navigatorNameController.dispose();
_navigatorTidController.dispose();
_navigatorOidController.dispose();
_navigatorIconController.dispose();
_navigatorParentController.dispose();
_navigatorMaxOpenedController.dispose();
_navigatorParamController.dispose();
_modelSearchController.dispose();
super.dispose();
}
void _onContentChanged() {
final result = _decodeContent();
setState(() {
if (result.isSuccess) {
_jsonErrorMessage = '';
_jsonErrorOffset = null;
_jsonErrorLine = null;
} else {
_jsonErrorMessage = result.errorMessage;
_jsonErrorOffset = result.errorOffset;
_jsonErrorLine = _jsonLineFromOffset(result.errorOffset);
}
});
if (!result.isSuccess) {
_highlightJsonError();
}
}
Future<void> _loadInitialData() async {
if (_isTableMode) {
await _loadTableLikeOptions();
return;
}
if (_isBillMode) {
await _loadBillLikeOptions();
return;
}
if (_isViewMode) {
await _loadViewOptions();
return;
}
if (_isNavigatorMode) {
await _loadNavigator();
return;
}
setState(() {
_contentController.text = _prettyJson(_defaultPayload());
_status = '可直接编辑';
_loading = false;
});
_captureOriginalSavePreview(_defaultPayload());
}
Future<void> _loadTableLikeOptions() async {
try {
final items = await widget.runtime.client.fetchTableModels(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _itemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadTableLikeItem();
} else {
setState(() {
_loading = false;
_status = '没有找到数据模型';
_contentController.text = _prettyJson(_defaultPayload());
});
_captureOriginalSavePreview(_defaultPayload());
}
} catch (error, stackTrace) {
_reportError('加载数据模型失败', error, stackTrace);
}
}
Future<void> _loadBillLikeOptions() async {
try {
final items = await widget.runtime.client.fetchBillModels(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _itemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadBillLikeItem();
} else {
setState(() {
_loading = false;
_status = '没有找到单据模型';
_contentController.text = _prettyJson(_defaultPayload());
});
_captureOriginalSavePreview(_defaultPayload());
}
} catch (error, stackTrace) {
_reportError('加载单据模型失败', error, stackTrace);
}
}
Future<void> _loadViewOptions() async {
try {
final items = await widget.runtime.client.fetchViewModelOptions(
session: widget.runtime.session,
);
if (!mounted) {
return;
}
setState(() {
_options = items;
_selectedKey = items.isNotEmpty ? _viewItemKey(items.first) : '';
});
if (_selectedKey.isNotEmpty) {
await _loadViewItem();
} else {
setState(() {
_loading = false;
_status = '没有找到界面模型';
_contentController.text = _prettyJson(_defaultPayload());
});
_captureOriginalSavePreview(_defaultPayload());
}
} catch (error, stackTrace) {
_reportError('加载界面模型失败', error, stackTrace);
}
}
Future<void> _loadNavigator() async {
try {
final navigator = await widget.runtime.client
.fetchCreationNavigatorRecord(session: widget.runtime.session);
if (!mounted) {
return;
}
final structure = navigator['structure'] is List
? List<Map<String, dynamic>>.from(
(navigator['structure'] as List).whereType<Map>(),
)
: <Map<String, dynamic>>[];
setState(() {
_navigatorStructure = structure;
_navigatorExpandedIds.clear();
_navigatorSelectedId = '';
_versionController.text = _readValue(navigator, const ['version'], '1');
_contentController.text = _prettyJson(
navigator.isNotEmpty
? navigator
: <String, dynamic>{'structure': structure},
);
_status = '导航数据已加载';
_detail = 'root items: ${structure.length}';
_loadedExisting = true;
_loading = false;
});
final defaultSelectedId =
_navigatorDefaultSelectedId() ??
(structure.isNotEmpty
? _readValue(structure.first, const ['id'])
: '');
if (defaultSelectedId.isNotEmpty) {
_selectNavigatorNode(defaultSelectedId, scrollIntoView: false);
}
_refreshNavigatorContent();
_captureOriginalSavePreview(
navigator.isNotEmpty
? navigator
: <String, dynamic>{'structure': structure},
);
} catch (error, stackTrace) {
_reportError('加载导航失败', error, stackTrace);
}
}
String? _navigatorDefaultSelectedId() {
for (final item in _navigatorStructure) {
final id = _readValue(item, const ['id']);
if (id.isEmpty || id == 'root') {
continue;
}
if (_navigatorChildrenOf(id).isEmpty) {
return id;
}
}
for (final item in _navigatorStructure) {
final id = _readValue(item, const ['id']);
if (id.isNotEmpty && id != 'root') {
return id;
}
}
return null;
}
Map<String, dynamic>? _navigatorNodeById(String id) {
for (final item in _navigatorStructure) {
if (_readValue(item, const ['id']) == id) {
return item;
}
}
return null;
}
List<Map<String, dynamic>> _navigatorChildrenOf(String parentId) {
return _navigatorStructure
.where((item) => _readValue(item, const ['parent']) == parentId)
.toList();
}
List<String> _navigatorAncestorIds(String id) {
final ancestors = <String>[];
var current = _navigatorNodeById(id);
while (current != null) {
final parentId = _readValue(current, const ['parent']);
if (parentId.isEmpty || parentId == 'root') {
break;
}
ancestors.add(parentId);
current = _navigatorNodeById(parentId);
}
ancestors.add('root');
return ancestors;
}
void _selectNavigatorNode(String id, {bool scrollIntoView = true}) {
final node = _navigatorNodeById(id);
if (node == null) {
return;
}
setState(() {
_navigatorSelectedId = id;
_navigatorExpandedIds.addAll(_navigatorAncestorIds(id));
_navigatorIdController.text = _readValue(node, const ['id']);
_navigatorNameController.text = _readValue(node, const ['name', 'title']);
_navigatorTidController.text = _readValue(node, const ['tid']);
_navigatorOidController.text = _readValue(node, const ['oid']);
_navigatorIconController.text = _readValue(node, const ['icon']);
_navigatorParentController.text = _readValue(node, const [
'parent',
], 'root');
_navigatorMaxOpenedController.text = _readValue(node, const [
'maxOpened',
], '0');
_navigatorParamController.text = _readValue(node, const ['param']);
_navigatorPublic = _readBoolValue(node, 'public');
_navigatorHidden = _readBoolValue(node, 'hidden');
_detail = '$id / ${_navigatorNameController.text}';
});
if (scrollIntoView) {
_refreshNavigatorContent();
}
}
bool _readBoolValue(Map<String, dynamic> item, String key) {
final value = item[key];
if (value is bool) {
return value;
}
final text = value?.toString().trim().toLowerCase();
return text == '1' || text == 'true' || text == 'yes';
}
void _refreshNavigatorContent() {
if (!_isNavigatorMode) {
return;
}
final version = int.tryParse(_versionController.text.trim()) ?? 1;
final payload = <String, dynamic>{
'version': version,
'structure': _navigatorStructure,
};
_contentController.text = _prettyJson(payload);
}
void _updateNavigatorField(String key, dynamic value) {
final node = _navigatorNodeById(_navigatorSelectedId);
if (node == null) {
return;
}
final oldId = _readValue(node, const ['id']);
setState(() {
node[key] = value;
if (key == 'public') {
_navigatorPublic = value == 1 || value == true;
} else if (key == 'hidden') {
_navigatorHidden = value == 1 || value == true;
}
if (key == 'id' && value.toString().trim().isNotEmpty) {
final newId = value.toString().trim();
for (final item in _navigatorStructure) {
if (_readValue(item, const ['parent']) == oldId) {
item['parent'] = newId;
}
}
_navigatorSelectedId = newId;
}
_detail = '${_navigatorSelectedId} / ${_readValue(node, const ['name'])}';
});
_refreshNavigatorContent();
}
Future<void> _loadTableLikeItem() async {
final code = _selectedKey;
if (code.isEmpty) {
return;
}
setState(() {
_loading = true;
_status = '正在加载数据模型...';
});
try {
final item = await widget.runtime.client.fetchTableModel(
code: code,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = code;
_loading = false;
});
return;
}
_applyLoadedItem(item, code: _readValue(item, const ['code'], code));
} catch (error, stackTrace) {
_reportError('加载数据模型失败', error, stackTrace);
}
}
Future<void> _loadBillLikeItem() async {
final code = _selectedKey;
if (code.isEmpty) {
return;
}
setState(() {
_loading = true;
_status = '正在加载单据模型...';
});
try {
final item = await widget.runtime.client.fetchBillModel(
code: code,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = code;
_loading = false;
});
return;
}
_applyLoadedItem(item, code: _readValue(item, const ['code'], code));
} catch (error, stackTrace) {
_reportError('加载单据模型失败', error, stackTrace);
}
}
Future<void> _loadViewItem() async {
final parts = _selectedKey.split('::');
if (parts.length != 2) {
return;
}
final tid = parts[0];
final oid = parts[1];
setState(() {
_loading = true;
_status = '正在加载界面模型...';
});
try {
final item = await widget.runtime.client.fetchViewModel(
tid: tid,
oid: oid,
session: widget.runtime.session,
);
if (!mounted) {
return;
}
if (item == null) {
setState(() {
_status = '模型不存在';
_detail = '$tid / $oid';
_loading = false;
});
return;
}
_applyLoadedItem(
item,
code: _readValue(item, const ['tid', 'code'], tid),
version: _readValue(item, const ['oid', 'version'], oid),
);
} catch (error, stackTrace) {
_reportError('加载界面模型失败', error, stackTrace);
}
}
void _applyLoadedItem(
Map<String, dynamic> item, {
required String code,
String? version,
}) {
setState(() {
_nameController.text = _readValue(item, const [
'name',
'title',
], widget.tab.title);
_codeController.text = code;
_versionController.text =
version ?? _readValue(item, const ['version', 'oid'], '1');
_remarkController.text = _readValue(item, const [
'remark',
'description',
'summary',
], widget.tab.summary);
_contentController.text = _prettyJson(item);
_status = '已加载';
_detail = code;
_loadedExisting = true;
_loading = false;
});
_captureOriginalSavePreview(item);
}
Map<String, dynamic> _defaultPayload() {
return <String, dynamic>{
'name': widget.tab.title,
'code': widget.tab.id,
'version': '1',
'remark': widget.tab.summary,
'enabled': true,
};
}
dynamic _buildSavePreviewPayload(dynamic parsed) {
if (_isNavigatorMode) {
final map = parsed is Map<String, dynamic>
? Map<String, dynamic>.from(parsed)
: <String, dynamic>{'structure': parsed};
final structure = map['structure'] is List
? List<Map<String, dynamic>>.from(
(map['structure'] as List).whereType<Map>(),
)
: <Map<String, dynamic>>[];
final version = int.tryParse(_versionController.text.trim()) ?? 1;
return <String, dynamic>{'version': version, 'structure': structure};
}
final payload = parsed is Map<String, dynamic>
? Map<String, dynamic>.from(parsed)
: <String, dynamic>{'data': parsed};
payload['name'] = _nameController.text.trim();
payload['code'] = _codeController.text.trim();
payload['version'] = _versionController.text.trim();
payload['remark'] = _remarkController.text.trim();
if (_isViewMode) {
payload['tid'] = _codeController.text.trim();
payload['oid'] = _versionController.text.trim();
}
return payload;
}
void _captureOriginalSavePreview(dynamic parsed) {
_originalSavePreview = _prettyJson(_buildSavePreviewPayload(parsed));
}
String _itemKey(Map<String, dynamic> item) {
return _readValue(item, const ['code', 'id']);
}
String _itemLabel(Map<String, dynamic> item) {
if (_isViewMode) {
return '${_readValue(item, const ['tid'])} / ${_readValue(item, const ['oid'])}';
}
return _readValue(item, const ['code', 'id']);
}
List<DropdownMenuEntry<String>> _filterModelEntries(
List<DropdownMenuEntry<String>> entries,
String filter,
) {
final query = filter.trim().toLowerCase();
if (query.isEmpty) {
return entries;
}
final terms = query
.split(RegExp(r'\s+'))
.where((term) => term.isNotEmpty)
.toList();
return entries.where((entry) {
final haystack = '${entry.label} ${entry.value}'.toLowerCase();
return terms.every(haystack.contains);
}).toList();
}
String _viewItemKey(Map<String, dynamic> item) {
final tid = _readValue(item, const ['tid']);
final oid = _readValue(item, const ['oid']);
if (tid.isEmpty || oid.isEmpty) {
return '';
}
return '$tid::$oid';
}
String _readValue(
Map<String, dynamic> item,
List<String> keys, [
String fallback = '',
]) {
for (final key in keys) {
final value = item[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString();
}
}
return fallback;
}
String _prettyJson(dynamic value) {
try {
return const JsonEncoder.withIndent(' ').convert(value);
} catch (_) {
return value.toString();
}
}
void _reportError(String title, Object error, StackTrace stackTrace) {
debugPrint('========== $title ==========');
debugPrint('error: $error');
debugPrint('stackTrace:\n$stackTrace');
debugPrint('========== $title end ==========');
if (!mounted) {
return;
}
setState(() {
_status = '$title';
_detail = error.toString().replaceFirst('Bad state: ', '');
_loading = false;
});
}
Future<void> _save() async {
final result = _decodeContent();
if (!result.isSuccess) {
setState(() {
_status = 'JSON 格式错误';
_detail = '请先修正内容后再保存';
_jsonErrorMessage = result.errorMessage;
_jsonErrorOffset = result.errorOffset;
});
_highlightJsonError();
return;
}
final previewPayload = _buildSavePreviewPayload(result.value);
final previewText = _prettyJson(previewPayload);
if (_loadedExisting) {
final confirmed = await _confirmUpdateChanges(
before: _originalSavePreview,
after: previewText,
);
if (!confirmed) {
return;
}
}
setState(() {
_saving = true;
_status = _loadedExisting ? '正在更新...' : '正在创建...';
_detail = '';
});
try {
final wasExisting = _loadedExisting;
final result = await _saveByMode(previewPayload);
if (!mounted) {
return;
}
final resultMessage = result.message.isNotEmpty
? result.message
: result.success
? '保存已完成'
: '保存未成功';
setState(() {
_loadedExisting = true;
_status = result.success
? '保存成功:$resultMessage'
: '保存失败:$resultMessage';
_detail = _buildActionResultDetail(result);
if (result.success) {
_jsonErrorMessage = '';
_jsonErrorOffset = null;
}
});
if (result.success) {
_originalSavePreview = previewText;
}
widget.onAction(
result.success
? (wasExisting ? '更新成功' : '创建成功')
: '保存失败',
'${widget.tab.title} / $resultMessage',
);
} catch (error, stackTrace) {
_reportError('保存失败', error, stackTrace);
} finally {
if (mounted) {
setState(() {
_saving = false;
});
}
}
}
Future<StudioBackendActionResult> _saveByMode(dynamic previewPayload) async {
if (_isNavigatorMode) {
final map = previewPayload is Map<String, dynamic>
? Map<String, dynamic>.from(previewPayload)
: <String, dynamic>{'structure': previewPayload};
final structure = map['structure'] is List
? List<Map<String, dynamic>>.from(
(map['structure'] as List).whereType<Map>(),
)
: <Map<String, dynamic>>[];
final version = int.tryParse(map['version']?.toString() ?? '') ?? 1;
return widget.runtime.client.saveCreationNavigator(
data: structure,
version: version,
session: widget.runtime.session,
);
}
final payload = previewPayload is Map<String, dynamic>
? Map<String, dynamic>.from(previewPayload)
: <String, dynamic>{'data': previewPayload};
if (_isTableMode) {
return _loadedExisting
? widget.runtime.client.updateTableModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createTableModel(
payload: payload,
session: widget.runtime.session,
);
}
if (_isBillMode) {
return _loadedExisting
? widget.runtime.client.updateBillModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createBillModel(
payload: payload,
session: widget.runtime.session,
);
}
if (_isViewMode) {
return _loadedExisting
? widget.runtime.client.updateViewModel(
payload: payload,
session: widget.runtime.session,
)
: widget.runtime.client.createViewModel(
payload: payload,
session: widget.runtime.session,
);
}
return StudioBackendActionResult(
success: true,
code: '',
message: '无需保存',
data: payload,
raw: payload,
);
}
String _buildActionResultDetail(StudioBackendActionResult result) {
final buffer = StringBuffer()
..writeln('success: ${result.success}')
..writeln('code: ${result.code.isEmpty ? '空' : result.code}')
..writeln('message: ${result.message.isEmpty ? '无' : result.message}')
..writeln(
'data: ${const JsonEncoder.withIndent(' ').convert(result.data)}',
)
..writeln('raw: ${const JsonEncoder.withIndent(' ').convert(result.raw)}');
return buffer.toString();
}
Future<bool> _confirmUpdateChanges({
required String before,
required String after,
}) async {
final diffLines = _buildJsonDiffLines(before, after);
final addedCount = diffLines
.where((line) => line.type == _JsonDiffType.added)
.length;
final removedCount = diffLines
.where((line) => line.type == _JsonDiffType.removed)
.length;
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
final theme = Theme.of(dialogContext);
final hasChanges = addedCount > 0 || removedCount > 0;
return AlertDialog(
title: const Text('确认更新'),
content: SizedBox(
width: 900,
height: 520,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
hasChanges
? 'JSON 内容已变化,确认后将提交更新。'
: '未检测到 JSON 内容变化,仍要继续更新吗?',
style: theme.textTheme.bodyMedium,
),
const SizedBox(height: 8),
Text(
'新增 $addedCount 行,删除 $removedCount 行',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
Expanded(
child: Container(
decoration: BoxDecoration(
color: _softPanelColor(dialogContext),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.outlineVariant,
),
),
child: Scrollbar(
controller: _jsonDiffScrollController,
child: ListView.builder(
controller: _jsonDiffScrollController,
padding: const EdgeInsets.all(12),
itemCount: diffLines.length,
itemBuilder: (context, index) {
return _buildJsonDiffLine(
context,
diffLines[index],
);
},
),
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('确认更新'),
),
],
);
},
) ??
false;
}
List<_JsonDiffLine> _buildJsonDiffLines(String before, String after) {
if (before.trim() == after.trim()) {
return <_JsonDiffLine>[
const _JsonDiffLine(_JsonDiffType.unchanged, '未检测到 JSON 内容变化'),
];
}
final oldLines = before.split('\n');
final newLines = after.split('\n');
final lcs = List.generate(
oldLines.length + 1,
(_) => List<int>.filled(newLines.length + 1, 0),
);
for (var i = oldLines.length - 1; i >= 0; i--) {
for (var j = newLines.length - 1; j >= 0; j--) {
if (oldLines[i] == newLines[j]) {
lcs[i][j] = lcs[i + 1][j + 1] + 1;
} else {
lcs[i][j] = lcs[i + 1][j] >= lcs[i][j + 1]
? lcs[i + 1][j]
: lcs[i][j + 1];
}
}
}
final result = <_JsonDiffLine>[];
var oldIndex = 0;
var newIndex = 0;
while (oldIndex < oldLines.length && newIndex < newLines.length) {
if (oldLines[oldIndex] == newLines[newIndex]) {
result.add(_JsonDiffLine(_JsonDiffType.unchanged, oldLines[oldIndex]));
oldIndex++;
newIndex++;
continue;
}
if (lcs[oldIndex + 1][newIndex] >= lcs[oldIndex][newIndex + 1]) {
result.add(_JsonDiffLine(_JsonDiffType.removed, oldLines[oldIndex]));
oldIndex++;
} else {
result.add(_JsonDiffLine(_JsonDiffType.added, newLines[newIndex]));
newIndex++;
}
}
while (oldIndex < oldLines.length) {
result.add(_JsonDiffLine(_JsonDiffType.removed, oldLines[oldIndex]));
oldIndex++;
}
while (newIndex < newLines.length) {
result.add(_JsonDiffLine(_JsonDiffType.added, newLines[newIndex]));
newIndex++;
}
return result;
}
Widget _buildJsonDiffLine(BuildContext context, _JsonDiffLine line) {
final theme = Theme.of(context);
final color = switch (line.type) {
_JsonDiffType.added => Colors.green.shade700,
_JsonDiffType.removed => Colors.red.shade700,
_JsonDiffType.unchanged => theme.colorScheme.onSurfaceVariant,
};
final prefix = switch (line.type) {
_JsonDiffType.added => '+',
_JsonDiffType.removed => '-',
_JsonDiffType.unchanged => ' ',
};
return Padding(
padding: const EdgeInsets.symmetric(vertical: 1),
child: SelectableText(
'$prefix ${line.text}',
style: theme.textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
height: 1.35,
color: color,
),
),
);
}
_JsonDecodeResult _decodeContent() {
try {
return _JsonDecodeResult.success(jsonDecode(_contentController.text));
} on FormatException catch (error) {
return _JsonDecodeResult.failure(error.message, error.offset);
} catch (error) {
return _JsonDecodeResult.failure(error.toString(), null);
}
}
int? _jsonLineFromOffset(int? offset) {
if (offset == null) {
return null;
}
final text = _contentController.text;
if (text.isEmpty) {
return null;
}
final clamped = offset.clamp(0, text.length) as int;
return '\n'.allMatches(text.substring(0, clamped)).length + 1;
}
void _formatContent() {
final result = _decodeContent();
if (!result.isSuccess) {
setState(() {
_status = 'JSON 格式错误,无法格式化';
_jsonErrorMessage = result.errorMessage;
_jsonErrorOffset = result.errorOffset;
_jsonErrorLine = _jsonLineFromOffset(result.errorOffset);
_detail = result.errorMessage;
});
_highlightJsonError();
return;
}
setState(() {
_contentController.text = _prettyJson(result.value);
_status = 'JSON 已格式化';
_jsonErrorMessage = '';
_jsonErrorOffset = null;
_jsonErrorLine = null;
_detail = '';
});
widget.onAction('格式化 JSON', widget.tab.title);
}
void _highlightJsonError() {
if (!mounted || _jsonErrorMessage.isEmpty) {
return;
}
}
void _clearJsonError() {
if (_jsonErrorMessage.isEmpty && _jsonErrorOffset == null) {
return;
}
setState(() {
_jsonErrorMessage = '';
_jsonErrorOffset = null;
_jsonErrorLine = null;
});
}
bool _canSaveCurrentJson() {
return _decodeContent().isSuccess;
}
Future<void> _reload() async {
setState(() {
_loading = true;
_status = '正在重新加载...';
});
await _loadInitialData();
widget.onAction('重新加载', widget.tab.title);
}
Future<void> _loadSelectedItem() async {
if (_isTableMode) {
await _loadTableLikeItem();
return;
}
if (_isBillMode) {
await _loadBillLikeItem();
return;
}
if (_isViewMode) {
await _loadViewItem();
return;
}
if (_isNavigatorMode) {
await _loadNavigator();
}
}
Future<void> _delete() async {
if (_isNavigatorMode) {
final selectedId = _navigatorSelectedId;
if (selectedId.isEmpty || selectedId == 'root') {
setState(() {
_status = '请选择要删除的菜单项';
});
return;
}
final children = _navigatorChildrenOf(selectedId);
if (children.isNotEmpty) {
setState(() {
_status = '请先删除子节点';
});
return;
}
setState(() {
_navigatorStructure.removeWhere(
(item) => _readValue(item, const ['id']) == selectedId,
);
_navigatorSelectedId = '';
_navigatorIdController.clear();
_navigatorNameController.clear();
_navigatorTidController.clear();
_navigatorOidController.clear();
_navigatorIconController.clear();
_navigatorParentController.text = 'root';
_navigatorMaxOpenedController.text = '0';
_navigatorParamController.clear();
_navigatorPublic = false;
_navigatorHidden = false;
_detail = '已删除 $selectedId';
_status = '已删除菜单项,等待保存';
_loadedExisting = false;
});
_refreshNavigatorContent();
return;
}
if (!_loadedExisting) {
setState(() {
_status = '当前内容还没有保存过,无需删除';
});
return;
}
try {
StudioBackendActionResult result;
if (_isTableMode) {
result = await widget.runtime.client.deleteTableModel(
code: _codeController.text.trim(),
session: widget.runtime.session,
);
} else if (_isBillMode) {
result = await widget.runtime.client.deleteBillModel(
code: _codeController.text.trim(),
session: widget.runtime.session,
);
} else if (_isViewMode) {
result = await widget.runtime.client.deleteViewModel(
tid: _codeController.text.trim(),
oid: _versionController.text.trim(),
session: widget.runtime.session,
);
} else {
setState(() {
_status = '当前页面不支持删除';
});
return;
}
if (!mounted) {
return;
}
setState(() {
_status = result.success ? '删除成功' : '删除失败';
_detail = _prettyJson(result.raw);
});
widget.onAction('删除', widget.tab.title);
} catch (error, stackTrace) {
_reportError('删除失败', error, stackTrace);
}
}
void _createNewNavigatorNode() {
if (!_isNavigatorMode) {
return;
}
final parentId = _navigatorSelectedId.isEmpty
? 'root'
: (_navigatorNodeById(_navigatorSelectedId)?['parent']?.toString() ??
'root');
final baseId = 'new_menu';
var candidateId = baseId;
var index = 1;
while (_navigatorNodeById(candidateId) != null) {
candidateId = '${baseId}_$index';
index += 1;
}
setState(() {
_navigatorStructure.add({
'id': candidateId,
'name': '新菜单项',
'tid': '',
'oid': '',
'icon': 'nav-report',
'parent': parentId,
'maxOpened': '0',
'public': 0,
'hidden': 0,
'param': '',
'checked': false,
'label': '',
'hintCode1': '',
'hintCode2': '',
});
_navigatorExpandedIds.add(parentId);
_navigatorSelectedId = candidateId;
_navigatorIdController.text = candidateId;
_navigatorNameController.text = '新菜单项';
_navigatorTidController.text = '';
_navigatorOidController.text = '';
_navigatorIconController.text = 'nav-report';
_navigatorParentController.text = parentId;
_navigatorMaxOpenedController.text = '0';
_navigatorParamController.text = '';
_navigatorPublic = false;
_navigatorHidden = false;
_detail = '$candidateId / 新菜单项';
_loadedExisting = false;
_status = '已创建新菜单项草稿';
});
_refreshNavigatorContent();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, _) {
final headerCard = Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
alignment: WrapAlignment.spaceBetween,
children: [
SizedBox(
width: 260,
child: SelectableText(
_status,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _reload,
icon: const Icon(Icons.refresh_rounded),
label: const Text('重新加载'),
),
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _formatContent,
icon: const Icon(Icons.data_object_rounded),
label: const Text('格式化'),
),
FilledButton.icon(
onPressed: _loading || _saving || !_canSaveCurrentJson()
? null
: _save,
icon: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_rounded),
label: Text(_loadedExisting ? '更新' : '保存'),
),
if (!_isNavigatorMode)
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _delete,
icon: const Icon(Icons.delete_rounded),
label: const Text('删除'),
),
],
),
],
),
),
);
if (_isNavigatorMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
headerCard,
const SizedBox(height: 12),
Expanded(child: _buildNavigatorWorkspace(context)),
],
);
}
final bodyPanel = _buildEditorPanel(context);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [headerCard, const SizedBox(height: 12), bodyPanel],
),
);
},
);
}
Widget _buildEditorPanel(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'属性编辑',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
if (_options.isNotEmpty && !_isNavigatorMode) ...[
DropdownMenu<String>(
controller: _modelSearchController,
initialSelection: _selectedKey.isEmpty ? null : _selectedKey,
enableFilter: true,
enableSearch: true,
requestFocusOnTap: true,
leadingIcon: const Icon(Icons.search_rounded),
hintText: _isViewMode ? '搜索并选择 tid / oid' : '搜索并选择模型',
inputDecorationTheme: const InputDecorationTheme(
isDense: true,
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
expandedInsets: EdgeInsets.zero,
dropdownMenuEntries: _options.map((item) {
final value = _isViewMode
? _viewItemKey(item)
: _itemKey(item);
return DropdownMenuEntry<String>(
value: value,
label: _itemLabel(item),
);
}).toList(),
filterCallback: (entries, filter) =>
_filterModelEntries(entries, filter),
onSelected: (value) {
if (value == null || value.isEmpty) {
return;
}
setState(() {
_selectedKey = value;
});
_loadSelectedItem();
},
),
const SizedBox(height: 10),
],
const SizedBox(height: 10),
Text(
'JSON 内容',
style: Theme.of(
context,
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 8),
SizedBox(
height: _jsonEditorHeight(context),
child: Container(
decoration: BoxDecoration(
color: _jsonErrorMessage.isEmpty
? _softPanelColor(context)
: Theme.of(context).colorScheme.error.withOpacity(0.05),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _jsonErrorMessage.isEmpty
? Theme.of(context).colorScheme.outlineVariant
: Theme.of(context).colorScheme.error,
width: _jsonErrorMessage.isEmpty ? 1 : 2,
),
boxShadow: _jsonErrorMessage.isEmpty
? null
: [
BoxShadow(
color: Theme.of(
context,
).colorScheme.error.withOpacity(0.14),
blurRadius: 16,
spreadRadius: 1,
),
],
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
width: 54,
padding: const EdgeInsets.fromLTRB(12, 10, 8, 12),
decoration: BoxDecoration(
color: _jsonErrorMessage.isEmpty
? Theme.of(
context,
).colorScheme.surface.withOpacity(0.30)
: Theme.of(
context,
).colorScheme.error.withOpacity(0.12),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
bottomLeft: Radius.circular(16),
),
),
child: SingleChildScrollView(
controller: _jsonLineScrollController,
child: RichText(text: _jsonLineNumberTextSpan(context)),
),
),
Expanded(
child: Theme(
data: Theme.of(context).copyWith(
textSelectionTheme: TextSelectionThemeData(
cursorColor: Theme.of(context).colorScheme.primary,
selectionColor: _jsonErrorMessage.isEmpty
? Theme.of(
context,
).colorScheme.primary.withOpacity(0.20)
: Theme.of(
context,
).colorScheme.error.withOpacity(0.34),
),
),
child: JsonField(
controller: _contentController,
focusNode: _contentFocusNode,
scrollController: _jsonFieldScrollController,
expands: true,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13.5,
height: 1.45,
),
cursorColor: Theme.of(context).colorScheme.primary,
decoration: const InputDecoration(
border: InputBorder.none,
isCollapsed: true,
contentPadding: EdgeInsets.fromLTRB(12, 10, 12, 12),
),
keyHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w700,
),
stringHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.tertiary,
),
numberHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.secondary,
),
boolHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w700,
),
nullHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.outline,
fontWeight: FontWeight.w700,
),
specialCharHighlightStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.outlineVariant,
),
commonTextStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.onSurface,
),
errorTextStyle: _jsonSyntaxStyle(
context,
color: Theme.of(context).colorScheme.error,
),
errorContainerDecoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.error.withOpacity(0.10),
borderRadius: BorderRadius.circular(10),
),
showErrorMessage: false,
isFormatting: true,
doInitFormatting: false,
onError: (error) {
if (error == null) {
return;
}
setState(() {
_jsonErrorMessage = error;
});
},
),
),
),
],
),
),
),
if (_jsonErrorMessage.isNotEmpty) ...[
const SizedBox(height: 8),
SelectableText.rich(
TextSpan(
children: [
TextSpan(
text: _jsonErrorMessage,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
if (_jsonErrorLine != null)
TextSpan(
text: '(第 $_jsonErrorLine 行)',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.error.withOpacity(0.75),
fontWeight: FontWeight.w700,
),
),
],
),
),
],
],
),
),
);
}
Widget _buildNavigatorWorkspace(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(width: 300, child: _buildNavigatorTreePanel(context)),
const SizedBox(width: 12),
Expanded(child: _buildNavigatorDetailPanel(context)),
],
);
}
Widget _buildNavigatorTreePanel(BuildContext context) {
final rootNode = _navigatorNodeById('root');
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: Text(
'菜单树',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
),
),
const Divider(height: 1),
Expanded(
child: Scrollbar(
controller: _navigatorTreeScrollController,
child: ListView(
controller: _navigatorTreeScrollController,
padding: const EdgeInsets.symmetric(vertical: 8),
children: [
if (rootNode != null)
_buildNavigatorTreeNode(context, rootNode, depth: 0),
],
),
),
),
],
),
);
}
Widget _buildNavigatorTreeNode(
BuildContext context,
Map<String, dynamic> node, {
required int depth,
}) {
final id = _readValue(node, const ['id']);
final title = _readValue(node, const ['name'], id);
final children = _navigatorChildrenOf(id);
final isExpanded = _navigatorExpandedIds.contains(id);
final isSelected = id == _navigatorSelectedId;
final colorScheme = Theme.of(context).colorScheme;
final hasChildren = children.isNotEmpty;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
InkWell(
onTap: () {
_selectNavigatorNode(id);
if (hasChildren) {
setState(() {
_navigatorExpandedIds.add(id);
});
}
},
child: Container(
color: isSelected
? colorScheme.primary.withOpacity(0.22)
: Colors.transparent,
padding: EdgeInsets.fromLTRB(12 + depth * 18, 8, 12, 8),
child: Row(
children: [
if (hasChildren)
IconButton(
onPressed: () {
setState(() {
if (isExpanded) {
_navigatorExpandedIds.remove(id);
} else {
_navigatorExpandedIds.add(id);
}
});
},
icon: Icon(
isExpanded
? Icons.keyboard_arrow_down_rounded
: Icons.keyboard_arrow_right_rounded,
size: 18,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints.tightFor(
width: 24,
height: 24,
),
visualDensity: VisualDensity.compact,
)
else
const SizedBox(width: 24),
Icon(
_navigatorNodeIcon(node),
size: 18,
color: isSelected
? colorScheme.primary
: colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isSelected ? colorScheme.primary : null,
fontWeight: isSelected
? FontWeight.w700
: FontWeight.w400,
),
),
),
],
),
),
),
if (hasChildren && isExpanded)
...children.map(
(child) =>
_buildNavigatorTreeNode(context, child, depth: depth + 1),
),
],
);
}
IconData _navigatorNodeIcon(Map<String, dynamic> node) {
final id = _readValue(node, const ['id']);
final iconText = _readValue(node, const ['icon']).toLowerCase();
if (id == 'root') {
return Icons.home_rounded;
}
if (iconText.contains('permission')) {
return Icons.verified_user_outlined;
}
if (iconText.contains('global')) {
return Icons.public_rounded;
}
if (iconText.contains('setting')) {
return Icons.settings_rounded;
}
if (_navigatorChildrenOf(id).isNotEmpty) {
return Icons.folder_rounded;
}
return Icons.description_rounded;
}
Widget _buildNavigatorDetailPanel(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox.expand(
child: Card(
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'菜单项编辑',
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 12),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
FilledButton.tonalIcon(
onPressed: _loading || _saving
? null
: _createNewNavigatorNode,
icon: const Icon(Icons.add_rounded),
label: const Text('添加'),
),
FilledButton.tonalIcon(
onPressed:
_loading || _saving || !_isNavigatorMode
? null
: _navigatorSelectedId.isEmpty
? null
: () => _selectNavigatorNode(_navigatorSelectedId),
icon: const Icon(Icons.refresh_rounded),
label: const Text('更新'),
),
FilledButton.icon(
onPressed: _loading || _saving || !_canSaveCurrentJson()
? null
: _save,
icon: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_rounded),
label: const Text('保存'),
),
FilledButton.tonalIcon(
onPressed:
_loading ||
_saving ||
(_isNavigatorMode && _navigatorSelectedId.isEmpty)
? null
: _delete,
icon: const Icon(Icons.delete_rounded),
label: const Text('删除'),
),
],
),
const SizedBox(height: 12),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildNavigatorTextRow(
context,
label: 'Id',
controller: _navigatorIdController,
onChanged: (value) =>
_updateNavigatorField('id', value.trim()),
),
const SizedBox(height: 10),
_buildNavigatorTextRow(
context,
label: '界面类型',
controller: _navigatorTidController,
onChanged: (value) =>
_updateNavigatorField('tid', value),
),
const SizedBox(height: 10),
_buildNavigatorTextRow(
context,
label: 'ICON',
controller: _navigatorIconController,
onChanged: (value) =>
_updateNavigatorField('icon', value),
),
const SizedBox(height: 10),
_buildNavigatorTextRow(
context,
label: '最多额外打开数量',
controller: _navigatorMaxOpenedController,
onChanged: (value) =>
_updateNavigatorField('maxOpened', value),
),
const SizedBox(height: 10),
_buildNavigatorCheckboxRow(
context,
label: '不受权限控制',
value: _navigatorPublic,
onChanged: (value) =>
_updateNavigatorField('public', value ? 1 : 0),
),
],
),
),
const SizedBox(width: 24),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildNavigatorTextRow(
context,
label: '名称',
controller: _navigatorNameController,
onChanged: (value) =>
_updateNavigatorField('name', value),
),
const SizedBox(height: 10),
_buildNavigatorTextRow(
context,
label: '界面名称',
controller: _navigatorOidController,
onChanged: (value) =>
_updateNavigatorField('oid', value),
),
const SizedBox(height: 10),
_buildNavigatorTextRow(
context,
label: '父节点',
controller: _navigatorParentController,
onChanged: (value) =>
_updateNavigatorField('parent', value),
),
const SizedBox(height: 10),
_buildNavigatorCheckboxRow(
context,
label: '隐藏',
value: _navigatorHidden,
onChanged: (value) =>
_updateNavigatorField('hidden', value ? 1 : 0),
),
],
),
),
],
),
const SizedBox(height: 12),
_buildNavigatorMultilineRow(
context,
label: '页面参数',
controller: _navigatorParamController,
minLines: 8,
onChanged: (value) => _updateNavigatorField('param', value),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest.withOpacity(0.35),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Text(
_detail.isEmpty ? '选择左侧菜单项进行编辑' : _detail,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
],
),
),
),
);
}
Widget _buildNavigatorTextRow(
BuildContext context, {
required String label,
required TextEditingController controller,
required ValueChanged<String> onChanged,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 124,
child: Align(
alignment: Alignment.centerRight,
child: Text(
'$label',
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: controller,
onChanged: onChanged,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
),
),
],
);
}
Widget _buildNavigatorMultilineRow(
BuildContext context, {
required String label,
required TextEditingController controller,
required int minLines,
required ValueChanged<String> onChanged,
}) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 124,
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: Align(
alignment: Alignment.topRight,
child: Text(
'$label',
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: controller,
onChanged: onChanged,
minLines: minLines,
maxLines: null,
decoration: InputDecoration(
isDense: true,
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
contentPadding: const EdgeInsets.all(10),
),
),
),
],
);
}
Widget _buildNavigatorCheckboxRow(
BuildContext context, {
required String label,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return Row(
children: [
SizedBox(
width: 124,
child: Align(
alignment: Alignment.centerRight,
child: Text(
'$label',
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
const SizedBox(width: 8),
Checkbox(
value: value,
onChanged: (checked) => onChanged(checked ?? false),
),
],
);
}
double _jsonEditorHeight(BuildContext context) {
final screenHeight = MediaQuery.sizeOf(context).height;
return (screenHeight * 0.58).clamp(420.0, 760.0);
}
TextSpan _jsonLineNumberTextSpan(BuildContext context) {
final lineCount = _contentController.text.split('\n').length;
final baseStyle =
Theme.of(context).textTheme.bodySmall?.copyWith(
fontFamily: 'monospace',
fontSize: 13.5,
height: 1.45,
color: Theme.of(context).colorScheme.onSurfaceVariant,
) ??
const TextStyle(fontFamily: 'monospace', fontSize: 13.5, height: 1.45);
final errorColor = Theme.of(context).colorScheme.error;
return TextSpan(
children: List.generate(lineCount, (index) {
final lineNumber = index + 1;
final isErrorLine = lineNumber == _jsonErrorLine;
return TextSpan(
text: '$lineNumber${lineNumber == lineCount ? '' : '\n'}',
style: baseStyle.copyWith(
color: isErrorLine ? errorColor : baseStyle.color,
fontWeight: isErrorLine ? FontWeight.w700 : baseStyle.fontWeight,
backgroundColor: isErrorLine ? errorColor.withOpacity(0.12) : null,
),
);
}),
);
}
TextStyle _jsonSyntaxStyle(
BuildContext context, {
required Color color,
FontWeight? fontWeight,
}) {
return Theme.of(context).textTheme.bodyMedium?.copyWith(
fontFamily: 'monospace',
fontSize: 13.5,
height: 1.45,
color: color,
fontWeight: fontWeight,
) ??
TextStyle(
fontFamily: 'monospace',
fontSize: 13.5,
height: 1.45,
color: color,
fontWeight: fontWeight,
);
}
}
class _JsonDecodeResult {
const _JsonDecodeResult._({
required this.isSuccess,
required this.value,
required this.errorMessage,
required this.errorOffset,
});
final bool isSuccess;
final dynamic value;
final String errorMessage;
final int? errorOffset;
factory _JsonDecodeResult.success(dynamic value) {
return _JsonDecodeResult._(
isSuccess: true,
value: value,
errorMessage: '',
errorOffset: null,
);
}
factory _JsonDecodeResult.failure(String message, int? offset) {
return _JsonDecodeResult._(
isSuccess: false,
value: null,
errorMessage: message,
errorOffset: offset,
);
}
}
enum _JsonDiffType { unchanged, added, removed }
class _JsonDiffLine {
const _JsonDiffLine(this.type, this.text);
final _JsonDiffType type;
final String text;
}