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

826 lines
25 KiB
Dart

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 TextEditingController _contentController;
bool _loading = true;
bool _saving = false;
bool _loadedExisting = false;
String _status = '加载中';
String _detail = '';
List<Map<String, dynamic>> _options = <Map<String, dynamic>>[];
String _selectedKey = '';
bool get _isTableMode =>
widget.tab.id.contains('Table') || widget.tab.id.contains('table');
bool get _isBillMode =>
widget.tab.id.contains('Bill') || widget.tab.id.contains('bill');
bool get _isViewMode =>
widget.tab.id.contains('View') ||
widget.tab.id.contains('WsoData') ||
widget.tab.id.contains('view');
bool get _isNavigatorMode =>
widget.tab.id.contains('Navigator') ||
widget.tab.id.contains('Navigation') ||
widget.tab.id.contains('navigator') ||
widget.tab.id.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 = TextEditingController(text: '{}');
_loadInitialData();
}
@override
void dispose() {
_nameController.dispose();
_codeController.dispose();
_versionController.dispose();
_remarkController.dispose();
_contentController.dispose();
super.dispose();
}
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;
});
}
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());
});
}
} 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());
});
}
} 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());
});
}
} 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(() {
_nameController.text = widget.tab.title;
_codeController.text = widget.tab.id;
_versionController.text = _readValue(navigator, const ['version'], '1');
_remarkController.text = widget.tab.summary;
_contentController.text = _prettyJson(navigator.isNotEmpty
? navigator
: <String, dynamic>{'structure': structure});
_status = '导航数据已加载';
_detail = 'root items: ${structure.length}';
_loadedExisting = true;
_loading = false;
});
} catch (error, stackTrace) {
_reportError('加载导航失败', error, stackTrace);
}
}
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;
});
}
Map<String, dynamic> _defaultPayload() {
return <String, dynamic>{
'name': widget.tab.title,
'code': widget.tab.id,
'version': '1',
'remark': widget.tab.summary,
'enabled': true,
};
}
String _itemKey(Map<String, dynamic> item) {
return _readValue(item, const ['code', 'id']);
}
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 parsed = _decodeContent();
if (parsed == null) {
setState(() {
_status = 'JSON 格式错误';
_detail = '请先修正内容后再保存';
});
return;
}
setState(() {
_saving = true;
_status = _loadedExisting ? '正在更新...' : '正在创建...';
_detail = '';
});
try {
final wasExisting = _loadedExisting;
final result = await _saveByMode(parsed);
if (!mounted) {
return;
}
setState(() {
_loadedExisting = true;
_status = result.success ? '保存成功' : '保存失败';
_detail = _prettyJson(result.raw);
});
widget.onAction(wasExisting ? '更新' : '创建', widget.tab.title);
} catch (error, stackTrace) {
_reportError('保存失败', error, stackTrace);
} finally {
if (mounted) {
setState(() {
_saving = false;
});
}
}
}
Future<StudioBackendActionResult> _saveByMode(dynamic parsed) async {
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 widget.runtime.client.saveCreationNavigator(
data: structure,
version: version,
session: widget.runtime.session,
);
}
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();
}
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,
);
}
dynamic _decodeContent() {
try {
return jsonDecode(_contentController.text);
} catch (_) {
return null;
}
}
void _formatContent() {
final parsed = _decodeContent();
if (parsed == null) {
setState(() {
_status = 'JSON 格式错误,无法格式化';
});
return;
}
setState(() {
_contentController.text = _prettyJson(parsed);
_status = 'JSON 已格式化';
});
widget.onAction('格式化 JSON', widget.tab.title);
}
Future<void> _reload() async {
if (_isTableMode) {
await _loadTableLikeItem();
return;
}
if (_isBillMode) {
await _loadBillLikeItem();
return;
}
if (_isViewMode) {
await _loadViewItem();
return;
}
if (_isNavigatorMode) {
await _loadNavigator();
return;
}
}
Future<void> _delete() async {
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);
}
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: LayoutBuilder(
builder: (context, constraints) {
final wide = constraints.maxWidth >= 980;
final leftPanel = _buildEditorPanel(context);
final rightPanel = _buildPreviewPanel(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (wide)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: leftPanel),
const SizedBox(width: 16),
Expanded(child: rightPanel),
],
)
else ...[
leftPanel,
const SizedBox(height: 16),
rightPanel,
],
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Row(
children: [
Expanded(
child: SelectableText(
_status,
style: Theme.of(context).textTheme.bodyLarge
?.copyWith(fontWeight: FontWeight.w700),
),
),
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _reload,
icon: const Icon(Icons.refresh_rounded),
label: const Text('重新加载'),
),
const SizedBox(width: 10),
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _formatContent,
icon: const Icon(Icons.data_object_rounded),
label: const Text('格式化'),
),
const SizedBox(width: 10),
FilledButton.icon(
onPressed: _loading || _saving ? null : _save,
icon: _saving
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.save_rounded),
label: Text(_loadedExisting ? '更新' : '保存'),
),
const SizedBox(width: 10),
if (!_isNavigatorMode)
FilledButton.tonalIcon(
onPressed: _loading || _saving ? null : _delete,
icon: const Icon(Icons.delete_rounded),
label: const Text('删除'),
),
],
),
),
),
],
);
},
),
);
}
Widget _buildEditorPanel(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'属性编辑',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 14),
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: '名称',
prefixIcon: Icon(Icons.title_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _codeController,
decoration: InputDecoration(
labelText: _isViewMode ? 'tid' : '编码',
prefixIcon: const Icon(Icons.code_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _versionController,
decoration: InputDecoration(
labelText: _isViewMode ? 'oid' : '版本',
prefixIcon: const Icon(Icons.sell_rounded),
),
),
const SizedBox(height: 12),
TextField(
controller: _remarkController,
maxLines: 3,
decoration: const InputDecoration(
labelText: '说明',
alignLabelWithHint: true,
prefixIcon: Icon(Icons.notes_rounded),
),
),
const SizedBox(height: 14),
if (_options.isNotEmpty && !_isNavigatorMode) ...[
DropdownButtonFormField<String>(
value: _selectedKey.isNotEmpty ? _selectedKey : null,
decoration: InputDecoration(
labelText: _isViewMode ? '选择 tid / oid' : '选择模型',
),
items: _options.map((item) {
final value = _isViewMode ? _viewItemKey(item) : _itemKey(item);
final label = _isViewMode
? '${_readValue(item, const ['tid'])} / ${_readValue(item, const ['oid'])}'
: _readValue(item, const ['code', 'id']);
return DropdownMenuItem<String>(
value: value,
child: Text(label),
);
}).toList(),
onChanged: (value) {
if (value == null || value.isEmpty) {
return;
}
setState(() {
_selectedKey = value;
});
_reload();
},
),
const SizedBox(height: 12),
],
Text(
'JSON 内容',
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 10),
SizedBox(
height: 360,
child: TextField(
controller: _contentController,
expands: true,
maxLines: null,
minLines: null,
keyboardType: TextInputType.multiline,
textAlignVertical: TextAlignVertical.top,
style: const TextStyle(fontFamily: 'monospace'),
decoration: const InputDecoration(
hintText: '编辑或粘贴 JSON',
alignLabelWithHint: true,
prefixIcon: Icon(Icons.description_rounded),
),
),
),
],
),
),
);
}
Widget _buildPreviewPanel(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(18),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'预览与原文',
style: Theme.of(context).textTheme.titleLarge
?.copyWith(fontWeight: FontWeight.w700),
),
const SizedBox(height: 14),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _softPanelColor(context),
borderRadius: BorderRadius.circular(18),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: SelectableText(
_prettyJson(_decodeContent() ?? _currentPreview()),
style: const TextStyle(fontFamily: 'monospace', height: 1.5),
),
),
const SizedBox(height: 14),
Wrap(
spacing: 10,
runSpacing: 10,
children: [
_MiniBadge(text: widget.tab.pageType.name),
_MiniBadge(text: widget.tab.id),
_MiniBadge(text: _loadedExisting ? '已加载' : '新建'),
],
),
const SizedBox(height: 14),
SelectableText(
_detail.isEmpty ? '暂无详情' : _detail,
style: const TextStyle(fontFamily: 'monospace', height: 1.45),
),
],
),
),
);
}
dynamic _currentPreview() {
if (_isNavigatorMode) {
return <String, dynamic>{
'structure': <Map<String, dynamic>>[],
'version': _versionController.text.trim(),
};
}
return <String, dynamic>{
'name': _nameController.text.trim(),
'code': _codeController.text.trim(),
'version': _versionController.text.trim(),
'remark': _remarkController.text.trim(),
};
}
}