21 changed files with 1000 additions and 501 deletions
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
|
||||
class FilePathManager { |
||||
static const String _lastOpenedFolderKey = 'last_opened_folder'; |
||||
|
||||
// 保存上次打开的文件夹路径 |
||||
static Future<void> saveLastOpenedFolder(String folderPath) async { |
||||
final prefs = await SharedPreferences.getInstance(); |
||||
await prefs.setString(_lastOpenedFolderKey, folderPath); |
||||
Logger().info("保存最新打开的文件夹地址:$folderPath"); |
||||
} |
||||
|
||||
// 读取上次打开的文件夹路径 |
||||
static Future<String?> getLastOpenedFolder() async { |
||||
final prefs = await SharedPreferences.getInstance(); |
||||
final folderPath = prefs.getString(_lastOpenedFolderKey); |
||||
Logger().info("加载最后保存的文件夹地址:$folderPath"); |
||||
return folderPath; |
||||
} |
||||
} |
@ -0,0 +1,229 @@
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/modules/memory_table/services/memory_table_service.dart'; |
||||
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||
|
||||
// 字段数据模型 |
||||
class Field { |
||||
Field(this.id, this.name, this.chineseName, this.type, this.remark, [this.isSelected = false]); |
||||
|
||||
final String id; // 序号 |
||||
final String name; // 名称 |
||||
final String chineseName; // 中文名 |
||||
final String type; // 类型 |
||||
final String remark; // 备注 |
||||
bool isSelected; |
||||
} |
||||
|
||||
// 索引数据模型 |
||||
class Index { |
||||
Index(this.indexName, this.isPrimary, this.indexFields, this.rule, [this.isSelected = false]); |
||||
|
||||
final String indexName; // 索引名称 |
||||
final String isPrimary; // 是否主键 |
||||
final String indexFields; // 索引字段 |
||||
final String rule; // 规则 |
||||
bool isSelected; |
||||
} |
||||
|
||||
// 字段数据源 |
||||
class FieldsDataSource extends DataGridSource { |
||||
FieldsDataSource(this.fields); |
||||
|
||||
List<Field> fields; |
||||
|
||||
void toggleAllSelection(bool? value) { |
||||
for (var field in fields) { |
||||
field.isSelected = value ?? false; |
||||
} |
||||
notifyListeners(); |
||||
} |
||||
|
||||
void toggleRowSelection(int index, bool? value) { |
||||
fields[index].isSelected = value ?? false; |
||||
notifyListeners(); |
||||
} |
||||
|
||||
void updateData(List<Field> newFields) { |
||||
fields = newFields; |
||||
notifyListeners(); // 关键:通知DataGrid更新 |
||||
} |
||||
|
||||
@override |
||||
List<DataGridRow> get rows => |
||||
fields |
||||
.map( |
||||
(field) => DataGridRow( |
||||
cells: [ |
||||
DataGridCell<bool>(columnName: 'select', value: field.isSelected), |
||||
DataGridCell<String>(columnName: 'id', value: field.id), |
||||
DataGridCell<String>(columnName: 'name', value: field.name), |
||||
DataGridCell<String>(columnName: 'chineseName', value: field.chineseName), |
||||
DataGridCell<String>(columnName: 'type', value: field.type), |
||||
DataGridCell<String>(columnName: 'remark', value: field.remark), |
||||
], |
||||
), |
||||
) |
||||
.toList(); |
||||
|
||||
@override |
||||
DataGridRowAdapter? buildRow(DataGridRow row) { |
||||
final int rowIndex = effectiveRows.indexOf(row); |
||||
return DataGridRowAdapter( |
||||
cells: |
||||
row.getCells().map<Widget>((dataGridCell) { |
||||
if (dataGridCell.columnName == 'select') { |
||||
return Center( |
||||
child: Checkbox( |
||||
value: fields[rowIndex].isSelected, |
||||
onChanged: (value) { |
||||
toggleRowSelection(rowIndex, value); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
return Container( |
||||
alignment: Alignment.centerLeft, |
||||
padding: const EdgeInsets.symmetric(horizontal: 8), |
||||
child: Text(dataGridCell.value.toString(), overflow: TextOverflow.ellipsis), |
||||
); |
||||
}).toList(), |
||||
); |
||||
} |
||||
} |
||||
|
||||
// 索引数据源 |
||||
class IndexesDataSource extends DataGridSource { |
||||
IndexesDataSource(this.indexes); |
||||
|
||||
List<Index> indexes; |
||||
|
||||
void toggleAllSelection(bool? value) { |
||||
for (var index in indexes) { |
||||
index.isSelected = value ?? false; |
||||
} |
||||
notifyListeners(); |
||||
} |
||||
|
||||
void toggleRowSelection(int index, bool? value) { |
||||
indexes[index].isSelected = value ?? false; |
||||
notifyListeners(); |
||||
} |
||||
|
||||
void updateData(List<Index> newIndexes) { |
||||
indexes = newIndexes; |
||||
notifyListeners(); // 关键:通知DataGrid更新 |
||||
} |
||||
|
||||
@override |
||||
List<DataGridRow> get rows => |
||||
indexes |
||||
.map( |
||||
(index) => DataGridRow( |
||||
cells: [ |
||||
DataGridCell<bool>(columnName: 'select', value: index.isSelected), |
||||
DataGridCell<String>(columnName: 'indexName', value: index.indexName), |
||||
DataGridCell<String>(columnName: 'isPrimary', value: index.isPrimary), |
||||
DataGridCell<String>(columnName: 'indexFields', value: index.indexFields), |
||||
DataGridCell<String>(columnName: 'rule', value: index.rule), |
||||
], |
||||
), |
||||
) |
||||
.toList(); |
||||
|
||||
@override |
||||
DataGridRowAdapter? buildRow(DataGridRow row) { |
||||
final int rowIndex = effectiveRows.indexOf(row); |
||||
return DataGridRowAdapter( |
||||
cells: |
||||
row.getCells().map<Widget>((dataGridCell) { |
||||
if (dataGridCell.columnName == 'select') { |
||||
return Center( |
||||
child: Checkbox( |
||||
value: indexes[rowIndex].isSelected, |
||||
onChanged: (value) { |
||||
toggleRowSelection(rowIndex, value); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
return Container( |
||||
alignment: Alignment.centerLeft, |
||||
padding: const EdgeInsets.symmetric(horizontal: 8), |
||||
child: Text(dataGridCell.value.toString(), overflow: TextOverflow.ellipsis), |
||||
); |
||||
}).toList(), |
||||
); |
||||
} |
||||
|
||||
bool _getSelectedValue(DataGridRow row) { |
||||
final int rowIndex = rows.indexOf(row); |
||||
if (rowIndex == -1) { |
||||
return false; |
||||
} |
||||
return indexes[rowIndex].isSelected; |
||||
} |
||||
} |
||||
|
||||
class MemoryTableController extends BaseContentController { |
||||
String? _errorMessage; |
||||
String tableName = ""; |
||||
String objectId = ""; |
||||
String chineseName = ""; |
||||
|
||||
late DataGridSource fieldsSource; |
||||
late DataGridSource indexesSource; |
||||
late MemoryTableService _service; |
||||
|
||||
MemoryTableController() : _service = MemoryTableService(Logger()) { |
||||
// 初始化空数据 |
||||
fieldsSource = FieldsDataSource([ |
||||
Field('1', '', '', '', ''), // 序号1的空字段 |
||||
Field('2', '', '', '', ''), // 序号2的空字段 |
||||
Field('3', '', '', '', ''), // 序号3的空字段 |
||||
]); |
||||
|
||||
indexesSource = IndexesDataSource([ |
||||
Index('', '', '', ''), // 空索引1 |
||||
Index('', '', '', ''), // 空索引2 |
||||
]); |
||||
} |
||||
|
||||
String? get errorMessage => _errorMessage; |
||||
|
||||
@override |
||||
Future<void> onOpenFile(String filePath) async { |
||||
try { |
||||
final tableData = await _service.parseStructureFile(filePath); |
||||
|
||||
// Update controller state |
||||
tableName = tableData.tableName; |
||||
chineseName = tableData.chineseName; |
||||
objectId = tableData.objectId; |
||||
|
||||
// Update data sources |
||||
(fieldsSource as FieldsDataSource).updateData(tableData.fields); |
||||
(indexesSource as IndexesDataSource).updateData(tableData.indexes); |
||||
|
||||
// Clear any previous error |
||||
_errorMessage = null; |
||||
|
||||
// Notify UI to update |
||||
notifyListeners(); |
||||
} catch (e) { |
||||
_errorMessage = e.toString(); |
||||
notifyListeners(); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void onOpenFolder(String folderPath) { |
||||
// 不支持打开文件夹 |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
super.dispose(); |
||||
} |
||||
} |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
// memory_table_service.dart |
||||
import 'dart:io'; |
||||
|
||||
import 'package:win_text_editor/modules/memory_table/controllers/memory_table_controller.dart'; |
||||
import 'package:xml/xml.dart' as xml; |
||||
import 'package:path/path.dart' as path; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
|
||||
class MemoryTableService { |
||||
final Logger _logger; |
||||
|
||||
MemoryTableService(this._logger); |
||||
|
||||
Future<TableData> parseStructureFile(String filePath) async { |
||||
try { |
||||
// 1. Check file extension |
||||
if (!filePath.toLowerCase().endsWith('.uftstructure')) { |
||||
throw const FormatException("文件扩展名必须是.uftstructure"); |
||||
} |
||||
|
||||
// 2. Read and parse file content |
||||
final file = File(filePath); |
||||
final content = await file.readAsString(); |
||||
|
||||
final document = xml.XmlDocument.parse(content); |
||||
final structureNode = document.findAllElements('structure:Structure').firstOrNull; |
||||
|
||||
if (structureNode == null) { |
||||
throw const FormatException("文件格式错误:缺少structure:Structure节点"); |
||||
} |
||||
|
||||
// 3. Get basic info |
||||
final fileNameWithoutExt = path.basenameWithoutExtension(filePath); |
||||
final chineseName = structureNode.getAttribute('chineseName') ?? ''; |
||||
final objectId = structureNode.getAttribute('objectId') ?? ''; |
||||
|
||||
// 4. Process properties (fields) |
||||
final properties = document.findAllElements('properties'); |
||||
final fields = <Field>[]; |
||||
int index = 1; |
||||
|
||||
for (final property in properties) { |
||||
final id = property.getAttribute('id') ?? ''; |
||||
fields.add( |
||||
Field( |
||||
(index++).toString(), // 序号 |
||||
id, // 名称 |
||||
'', // 中文名 |
||||
'', // 类型 |
||||
'', // 备注 |
||||
), |
||||
); |
||||
} |
||||
|
||||
// 5. Process indexes |
||||
final indexes = document.findAllElements('indexs'); |
||||
final indexList = <Index>[]; |
||||
|
||||
for (final indexNode in indexes) { |
||||
final name = indexNode.getAttribute('name') ?? ''; |
||||
final containerType = indexNode.getAttribute('containerType'); |
||||
final isPrimary = containerType == null ? '是' : '否'; |
||||
final rule = containerType ?? ''; |
||||
|
||||
// Get all index fields |
||||
final items = indexNode.findAllElements('items'); |
||||
final fieldsList = |
||||
items |
||||
.map((item) => item.getAttribute('attrname') ?? '') |
||||
.where((f) => f.isNotEmpty) |
||||
.toList(); |
||||
final indexFields = fieldsList.join(','); |
||||
|
||||
indexList.add( |
||||
Index( |
||||
name, // 索引名称 |
||||
isPrimary, // 是否主键 |
||||
indexFields, // 索引字段 |
||||
rule, // 规则 |
||||
), |
||||
); |
||||
} |
||||
|
||||
return TableData( |
||||
tableName: fileNameWithoutExt, |
||||
chineseName: chineseName, |
||||
objectId: objectId, |
||||
fields: fields.isNotEmpty ? fields : _getDefaultFields(), |
||||
indexes: indexList.isNotEmpty ? indexList : _getDefaultIndexes(), |
||||
); |
||||
} on xml.XmlParserException catch (e) { |
||||
_logger.error("XML解析错误: ${e.message}"); |
||||
rethrow; |
||||
} catch (e) { |
||||
_logger.error("处理文件时发生错误: $e"); |
||||
rethrow; |
||||
} |
||||
} |
||||
|
||||
List<Field> _getDefaultFields() { |
||||
return [Field('1', '', '', '', ''), Field('2', '', '', '', ''), Field('3', '', '', '', '')]; |
||||
} |
||||
|
||||
List<Index> _getDefaultIndexes() { |
||||
return [Index('', '', '', ''), Index('', '', '', '')]; |
||||
} |
||||
} |
||||
|
||||
class TableData { |
||||
final String tableName; |
||||
final String chineseName; |
||||
final String objectId; |
||||
final List<Field> fields; |
||||
final List<Index> indexes; |
||||
|
||||
TableData({ |
||||
required this.tableName, |
||||
required this.chineseName, |
||||
required this.objectId, |
||||
required this.fields, |
||||
required this.indexes, |
||||
}); |
||||
} |
@ -0,0 +1,256 @@
@@ -0,0 +1,256 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
||||
import 'package:win_text_editor/modules/memory_table/controllers/memory_table_controller.dart'; |
||||
|
||||
class MemoryTableLeftSide extends StatelessWidget { |
||||
final MemoryTableController controller; |
||||
const MemoryTableLeftSide({super.key, required this.controller}); |
||||
|
||||
Widget _buildTextFieldRow(String label, String value) { |
||||
return Row( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
Text('$label:'), |
||||
SizedBox( |
||||
width: 200, |
||||
child: TextField( |
||||
controller: TextEditingController(text: value), |
||||
readOnly: true, |
||||
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)), |
||||
), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
|
||||
Container _buildGridHeader(String text) { |
||||
return Container( |
||||
alignment: Alignment.center, |
||||
color: Colors.grey[200], |
||||
child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), |
||||
); |
||||
} |
||||
|
||||
Widget _buildFieldsDataGrid(MemoryTableController controller) { |
||||
final fieldsSource = controller.fieldsSource as FieldsDataSource; |
||||
|
||||
return Card( |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
Expanded( |
||||
child: LayoutBuilder( |
||||
builder: (context, constraints) { |
||||
return SingleChildScrollView( |
||||
scrollDirection: Axis.horizontal, |
||||
child: SizedBox( |
||||
width: constraints.maxWidth > 800 ? constraints.maxWidth : 800, |
||||
child: SfDataGrid( |
||||
source: fieldsSource, |
||||
gridLinesVisibility: GridLinesVisibility.both, |
||||
headerGridLinesVisibility: GridLinesVisibility.both, |
||||
columnWidthMode: ColumnWidthMode.fitByCellValue, |
||||
selectionMode: SelectionMode.none, |
||||
columns: [ |
||||
GridColumn( |
||||
columnName: 'select', |
||||
label: _buildCheckboxHeader( |
||||
context, |
||||
fieldsSource, |
||||
controller.fieldsSource.rows.length, |
||||
), |
||||
width: 60, |
||||
), |
||||
GridColumn( |
||||
columnName: 'id', |
||||
label: _buildGridHeader('序号'), |
||||
minimumWidth: 80, |
||||
), |
||||
GridColumn( |
||||
columnName: 'name', |
||||
label: _buildGridHeader('名称'), |
||||
minimumWidth: 120, |
||||
), |
||||
GridColumn( |
||||
columnName: 'chineseName', |
||||
label: _buildGridHeader('中文名'), |
||||
minimumWidth: 120, |
||||
), |
||||
GridColumn( |
||||
columnName: 'type', |
||||
label: _buildGridHeader('类型'), |
||||
minimumWidth: 120, |
||||
), |
||||
GridColumn( |
||||
columnName: 'remark', |
||||
label: _buildGridHeader('备注'), |
||||
minimumWidth: 200, |
||||
), |
||||
], |
||||
onCellTap: (details) { |
||||
if (details.column.columnName == 'select') { |
||||
final rowIndex = details.rowColumnIndex.rowIndex - 1; |
||||
if (rowIndex >= 0 && rowIndex < fieldsSource.fields.length) { |
||||
fieldsSource.toggleRowSelection( |
||||
rowIndex, |
||||
!fieldsSource.fields[rowIndex].isSelected, |
||||
); |
||||
} |
||||
} |
||||
}, |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildCheckboxHeader(BuildContext context, FieldsDataSource dataSource, int rowCount) { |
||||
final allSelected = rowCount > 0 && dataSource.fields.every((item) => item.isSelected); |
||||
|
||||
return Container( |
||||
alignment: Alignment.center, |
||||
color: Colors.grey[200], |
||||
child: Checkbox( |
||||
value: allSelected, |
||||
tristate: true, |
||||
onChanged: (value) { |
||||
dataSource.toggleAllSelection(value ?? false); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildIndexesDataGrid(MemoryTableController controller) { |
||||
final indexesSource = controller.indexesSource as IndexesDataSource; |
||||
|
||||
return Card( |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
Expanded( |
||||
child: LayoutBuilder( |
||||
builder: (context, constraints) { |
||||
return SingleChildScrollView( |
||||
scrollDirection: Axis.horizontal, |
||||
child: SizedBox( |
||||
width: constraints.maxWidth > 800 ? constraints.maxWidth : 800, |
||||
child: SfDataGrid( |
||||
source: indexesSource, |
||||
gridLinesVisibility: GridLinesVisibility.both, |
||||
headerGridLinesVisibility: GridLinesVisibility.both, |
||||
columnWidthMode: ColumnWidthMode.fitByCellValue, |
||||
columns: [ |
||||
GridColumn( |
||||
columnName: 'select', |
||||
label: _buildCheckboxHeaderForIndexes( |
||||
context, |
||||
indexesSource, |
||||
controller.indexesSource.rows.length, |
||||
), |
||||
width: 60, |
||||
), |
||||
GridColumn( |
||||
columnName: 'indexName', |
||||
label: _buildGridHeader('索引名称'), |
||||
minimumWidth: 120, |
||||
), |
||||
GridColumn( |
||||
columnName: 'isPrimary', |
||||
label: _buildGridHeader('是否主键'), |
||||
minimumWidth: 100, |
||||
), |
||||
GridColumn( |
||||
columnName: 'indexFields', |
||||
label: _buildGridHeader('索引字段'), |
||||
minimumWidth: 150, |
||||
), |
||||
GridColumn( |
||||
columnName: 'rule', |
||||
label: _buildGridHeader('规则'), |
||||
minimumWidth: 200, |
||||
), |
||||
], |
||||
onCellTap: (details) { |
||||
if (details.column.columnName == 'select') { |
||||
final rowIndex = details.rowColumnIndex.rowIndex - 1; |
||||
if (rowIndex >= 0 && rowIndex < indexesSource.indexes.length) { |
||||
indexesSource.toggleRowSelection( |
||||
rowIndex, |
||||
!indexesSource.indexes[rowIndex].isSelected, |
||||
); |
||||
} |
||||
} |
||||
}, |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildCheckboxHeaderForIndexes( |
||||
BuildContext context, |
||||
IndexesDataSource dataSource, |
||||
int rowCount, |
||||
) { |
||||
final allSelected = rowCount > 0 && dataSource.indexes.every((item) => item.isSelected); |
||||
|
||||
return Container( |
||||
alignment: Alignment.center, |
||||
color: Colors.grey[200], |
||||
child: Checkbox( |
||||
value: allSelected, |
||||
tristate: true, |
||||
onChanged: (value) { |
||||
dataSource.toggleAllSelection(value ?? false); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
SizedBox( |
||||
width: double.infinity, |
||||
child: Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Wrap( |
||||
spacing: 16, |
||||
runSpacing: 8, |
||||
children: [ |
||||
_buildTextFieldRow('名称', controller.tableName), |
||||
_buildTextFieldRow('中文名', controller.chineseName), |
||||
_buildTextFieldRow('对象编号', controller.objectId), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
), |
||||
const SizedBox(height: 8), |
||||
const Padding( |
||||
padding: EdgeInsets.all(8.0), |
||||
child: Text('字段列表', style: TextStyle(fontWeight: FontWeight.bold)), |
||||
), |
||||
Expanded(flex: 6, child: _buildFieldsDataGrid(controller)), |
||||
const Padding( |
||||
padding: EdgeInsets.all(8.0), |
||||
child: Text('索引列表', style: TextStyle(fontWeight: FontWeight.bold)), |
||||
), |
||||
Expanded(flex: 4, child: _buildIndexesDataGrid(controller)), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,106 @@
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:flutter/services.dart'; |
||||
|
||||
class MemoryTableRightSide extends StatelessWidget { |
||||
final TextEditingController codeController; |
||||
const MemoryTableRightSide({super.key, required this.codeController}); |
||||
|
||||
Widget _buildCheckboxSection() { |
||||
return SizedBox( |
||||
width: double.infinity, |
||||
child: Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
Wrap( |
||||
spacing: 16, |
||||
runSpacing: 8, |
||||
children: [ |
||||
_buildCheckbox('获取记录'), |
||||
_buildCheckbox('获取记录数'), |
||||
_buildCheckbox('插入记录'), |
||||
_buildCheckbox('修改记录'), |
||||
_buildCheckbox('删除记录'), |
||||
_buildCheckbox('遍历记录'), |
||||
], |
||||
), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildCheckbox(String label) { |
||||
return Row( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
Checkbox( |
||||
value: false, // Replace with your actual value |
||||
onChanged: (bool? value) { |
||||
// Handle checkbox change |
||||
}, |
||||
), |
||||
Text(label), |
||||
], |
||||
); |
||||
} |
||||
|
||||
Widget _buildCodeEditor() { |
||||
return Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8), |
||||
// 保留内部的Expanded |
||||
child: Container( |
||||
decoration: BoxDecoration( |
||||
border: Border.all(color: Colors.grey), |
||||
borderRadius: BorderRadius.circular(4), |
||||
), |
||||
child: TextField( |
||||
controller: codeController, |
||||
maxLines: null, |
||||
expands: true, |
||||
decoration: const InputDecoration( |
||||
border: InputBorder.none, |
||||
contentPadding: EdgeInsets.all(8), |
||||
), |
||||
style: const TextStyle(fontFamily: 'monospace'), |
||||
), |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Column( |
||||
children: [ |
||||
_buildCheckboxSection(), |
||||
Padding( |
||||
padding: const EdgeInsets.only(left: 8.0, right: 8.0), |
||||
child: Row( |
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween, |
||||
children: [ |
||||
const Text('生成代码:', style: TextStyle(fontWeight: FontWeight.bold)), |
||||
IconButton( |
||||
icon: const Icon(Icons.content_copy, size: 20), |
||||
tooltip: '复制代码', |
||||
onPressed: () { |
||||
if (codeController.text.isNotEmpty) { |
||||
Clipboard.setData(ClipboardData(text: codeController.text)); |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(const SnackBar(content: Text('已复制到剪贴板'))); |
||||
} |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
Flexible(child: _buildCodeEditor()), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/memory_table/controllers/memory_table_controller.dart'; |
||||
import 'memory_table_left_side.dart'; |
||||
import 'memory_table_right_side.dart'; |
||||
|
||||
class MemoryTableView extends StatefulWidget { |
||||
final String tabId; |
||||
const MemoryTableView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
State<MemoryTableView> createState() => _MemoryTableViewState(); |
||||
} |
||||
|
||||
class _MemoryTableViewState extends State<MemoryTableView> { |
||||
late final MemoryTableController _controller; |
||||
final TextEditingController _codeController = TextEditingController(); |
||||
bool _isControllerFromTabManager = false; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
|
||||
final controllerFromManager = tabManager.getController(widget.tabId); |
||||
if (controllerFromManager != null) { |
||||
_controller = controllerFromManager; |
||||
_isControllerFromTabManager = true; |
||||
} else { |
||||
_controller = MemoryTableController(); |
||||
_isControllerFromTabManager = false; |
||||
tabManager.registerController(widget.tabId, _controller); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
if (!_isControllerFromTabManager) { |
||||
_controller.dispose(); |
||||
} |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return ChangeNotifierProvider.value( |
||||
value: _controller, |
||||
child: Consumer<MemoryTableController>( |
||||
builder: (context, controller, child) { |
||||
return Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Row( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
// 左侧部分 (50%) |
||||
Expanded(flex: 5, child: MemoryTableLeftSide(controller: controller)), |
||||
const SizedBox(width: 8), |
||||
// 右侧部分 (50%) |
||||
Expanded(flex: 5, child: MemoryTableRightSide(codeController: _codeController)), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -1,68 +0,0 @@
@@ -1,68 +0,0 @@
|
||||
// tree_view_controller.dart |
||||
|
||||
import 'package:win_text_editor/shared/models/template_node.dart'; |
||||
import '../../../shared/base/safe_notifier.dart'; |
||||
|
||||
class TreeViewController extends SafeNotifier { |
||||
//根节点 |
||||
List<TemplateNode> _treeNodes = []; |
||||
TemplateNode? _selectedNode; |
||||
String? _currentParentPath; |
||||
|
||||
List<TemplateNode> get treeNodes => _treeNodes; |
||||
TemplateNode? get selectedNode => _selectedNode; |
||||
|
||||
// 加载树视图,当文件路径改变时调用 |
||||
void updateTreeNodes(List<TemplateNode> nodes) { |
||||
_treeNodes = nodes; |
||||
safeNotify(); |
||||
} |
||||
|
||||
void selectTreeNode(TemplateNode node) { |
||||
_selectedNode = node; |
||||
safeNotify(); |
||||
} |
||||
|
||||
// 选择节点,多选时仅可选中同一层级的节点 |
||||
void toggleNodeCheck(TemplateNode node) { |
||||
final parentPath = node.path.substring(0, node.path.lastIndexOf('/')); |
||||
if (_currentParentPath != null && _currentParentPath != parentPath) { |
||||
clearAllChecked(); |
||||
} |
||||
node.isChecked = !node.isChecked; |
||||
_currentParentPath = parentPath; |
||||
safeNotify(); |
||||
} |
||||
|
||||
void clearAllChecked() { |
||||
void traverse(TemplateNode node) { |
||||
node.isChecked = false; |
||||
for (var child in node.children) { |
||||
traverse(child); |
||||
} |
||||
} |
||||
|
||||
for (var node in _treeNodes) { |
||||
traverse(node); |
||||
} |
||||
} |
||||
|
||||
List<String> get selectedNodeNames { |
||||
List<String> selectedNodeNames = []; |
||||
|
||||
void traverse(TemplateNode node) { |
||||
if (node.isChecked) { |
||||
selectedNodeNames.add(node.name); |
||||
} |
||||
for (var child in node.children) { |
||||
traverse(child); |
||||
} |
||||
} |
||||
|
||||
for (var node in _treeNodes) { |
||||
traverse(node); |
||||
} |
||||
|
||||
return selectedNodeNames; |
||||
} |
||||
} |
@ -1,145 +0,0 @@
@@ -1,145 +0,0 @@
|
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/shared/models/template_node.dart'; |
||||
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||
import 'package:xml/xml.dart' as xml; |
||||
import 'dart:io'; |
||||
import 'tree_view_controller.dart'; |
||||
|
||||
class UftFileController extends BaseContentController { |
||||
final TreeViewController treeController; |
||||
|
||||
String _filePath = ''; |
||||
String? _errorMessage; |
||||
|
||||
String get filePath => _filePath; |
||||
String? get errorMessage => _errorMessage; |
||||
|
||||
//---------------初始化方法---- |
||||
|
||||
UftFileController() : treeController = TreeViewController() { |
||||
_setupCrossControllerCommunication(); |
||||
} |
||||
|
||||
//设置跨控制器状态协同 |
||||
void _setupCrossControllerCommunication() {} |
||||
|
||||
//----------------业务入口方法----- |
||||
//widget调用入口:打开文件 |
||||
Future<void> pickFile() async { |
||||
final result = await FilePicker.platform.pickFiles( |
||||
type: FileType.custom, |
||||
allowedExtensions: ['xml', '*'], |
||||
); |
||||
if (result != null) { |
||||
_filePath = result.files.single.path!; |
||||
notifyListeners(); // 通知 Consumer 刷新 |
||||
await _loadTemplateData(); |
||||
} |
||||
} |
||||
|
||||
//执行框架回调入口:双击左侧资源管理文件 |
||||
Future<void> setFilePath(String path) async { |
||||
_filePath = path; |
||||
notifyListeners(); // 通知 Consumer 刷新 |
||||
await _loadTemplateData(); |
||||
} |
||||
|
||||
//加载xml文件 |
||||
Future<void> _loadTemplateData() async { |
||||
try { |
||||
_errorMessage = null; |
||||
final file = File(_filePath); |
||||
final content = await file.readAsString(); |
||||
final document = xml.XmlDocument.parse(content); |
||||
|
||||
// 更新各控制器 |
||||
//树视图展示文件结构 |
||||
treeController.updateTreeNodes( |
||||
_buildTreeNodes(document.rootElement, document.rootElement.localName, depth: 0), |
||||
); |
||||
//列表展示选中节点的内容 |
||||
} catch (e) { |
||||
_errorMessage = 'Failed to load XML: ${e.toString()}'; |
||||
Logger().error('XML加载错误$_errorMessage'); |
||||
} |
||||
} |
||||
|
||||
//--------------------私有方法--------- |
||||
// 构建树节点 |
||||
List<TemplateNode> _buildTreeNodes( |
||||
xml.XmlElement element, |
||||
String path, { |
||||
required int depth, |
||||
int repreatCount = 1, |
||||
}) { |
||||
final node = TemplateNode( |
||||
path: path, |
||||
name: element.qualifiedName, |
||||
children: [], |
||||
depth: depth, |
||||
isExpanded: depth < 5, // 默认展开前两层 |
||||
isRepeated: repreatCount > 1, |
||||
repreatCount: repreatCount, |
||||
); |
||||
|
||||
// 添加当前元素的所有属性节点 |
||||
if (element.attributes.isNotEmpty) { |
||||
node.children.addAll( |
||||
element.attributes.map( |
||||
(attr) => TemplateNode( |
||||
path: '$path/@${attr.name.local}', |
||||
name: '@${attr.qualifiedName}', |
||||
children: [], |
||||
depth: depth + 1, |
||||
isAttribute: true, |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
// 处理子元素节点(忽略文本节点) |
||||
final childElements = element.children.whereType<xml.XmlElement>(); |
||||
final groupedChildren = <String, List<xml.XmlElement>>{}; |
||||
|
||||
// 按元素名分组 |
||||
for (var child in childElements) { |
||||
groupedChildren.putIfAbsent(child.name.local, () => []).add(child); |
||||
} |
||||
|
||||
// 为每个唯一子元素创建节点 |
||||
groupedChildren.forEach((name, elements) { |
||||
String path0 = '$path/${elements.first.name.local}'; |
||||
if (elements.length == 1) { |
||||
// 单一节点直接添加(包含其所有属性) |
||||
node.children.addAll(_buildTreeNodes(elements.first, path0, depth: depth + 1)); |
||||
} else { |
||||
// 多个相同节点需要合并 |
||||
node.children.addAll( |
||||
_buildTreeNodes(elements.first, path0, depth: depth + 1, repreatCount: elements.length), |
||||
); |
||||
} |
||||
}); |
||||
|
||||
return [node]; |
||||
} |
||||
|
||||
//解析全量数据 |
||||
|
||||
//-----------框架回调-- |
||||
@override |
||||
void onOpenFile(String filePath) { |
||||
setFilePath(filePath); |
||||
} |
||||
|
||||
@override |
||||
void onOpenFolder(String folderPath) { |
||||
// 不支持打开文件夹 |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
treeController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
} |
@ -1,89 +0,0 @@
@@ -1,89 +0,0 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/modules/uft_file/controllers/tree_view_controller.dart'; |
||||
import 'package:win_text_editor/shared/models/template_node.dart'; |
||||
import 'package:win_text_editor/shared/components/tree_view.dart'; |
||||
|
||||
class FileTreeView extends StatelessWidget { |
||||
const FileTreeView({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Consumer<TreeViewController>( |
||||
builder: (context, controller, _) { |
||||
if (controller.treeNodes.isEmpty) { |
||||
return const Center(child: Text('No XML data available')); |
||||
} |
||||
|
||||
return TreeView( |
||||
nodes: controller.treeNodes, |
||||
config: const TreeViewConfig( |
||||
showIcons: true, |
||||
singleSelect: true, |
||||
selectedColor: Colors.lightBlueAccent, |
||||
icons: {'element': Icons.label_outline, 'attribute': Icons.code}, |
||||
), |
||||
onNodeTap: (node) { |
||||
controller.selectTreeNode; |
||||
}, |
||||
nodeBuilder: (context, node, isSelected, onTap) { |
||||
return _buildTreeNode(node, isSelected, onTap, controller); |
||||
}, |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
|
||||
Widget _buildTreeNode( |
||||
TreeNode node, |
||||
bool isSelected, |
||||
VoidCallback onTap, |
||||
TreeViewController controller, |
||||
) { |
||||
final templateNode = node as TemplateNode; |
||||
final isAttribute = node.isAttribute; |
||||
final isActuallySelected = controller.selectedNode?.id == templateNode.id; |
||||
|
||||
return Container( |
||||
color: isActuallySelected ? Colors.lightBlueAccent.withOpacity(0.2) : Colors.transparent, |
||||
child: Padding( |
||||
padding: EdgeInsets.only(left: 12.0 * node.depth), |
||||
child: ListTile( |
||||
dense: true, |
||||
leading: Row( |
||||
mainAxisSize: MainAxisSize.min, |
||||
children: [ |
||||
if (templateNode.children.isEmpty) // 仅在叶子节点显示复选框 |
||||
Checkbox( |
||||
value: templateNode.isChecked, |
||||
onChanged: (value) { |
||||
if (value != null) { |
||||
controller.toggleNodeCheck(templateNode); |
||||
} |
||||
}, |
||||
), |
||||
isAttribute |
||||
? const Icon(Icons.code, size: 16, color: Colors.grey) |
||||
: const Icon(Icons.label_outline, size: 18, color: Colors.blue), |
||||
], |
||||
), |
||||
title: Text( |
||||
isAttribute ? templateNode.name.substring(1) : templateNode.name, |
||||
style: TextStyle( |
||||
color: isAttribute ? Colors.grey[600] : Colors.black, |
||||
fontWeight: isAttribute ? FontWeight.normal : FontWeight.w500, |
||||
), |
||||
), |
||||
trailing: |
||||
templateNode.isRepeated |
||||
? Text( |
||||
"(${templateNode.repreatCount.toString()})", |
||||
style: const TextStyle(color: Colors.grey), |
||||
) |
||||
: null, |
||||
onTap: onTap, |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -1,112 +0,0 @@
@@ -1,112 +0,0 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/uft_file/controllers/uft_file_controller.dart'; |
||||
import 'package:win_text_editor/modules/uft_file/widgets/tree_view.dart'; |
||||
|
||||
class UftFileView extends StatefulWidget { |
||||
final String tabId; |
||||
const UftFileView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
State<UftFileView> createState() => _UftFileViewState(); |
||||
} |
||||
|
||||
class _UftFileViewState extends State<UftFileView> { |
||||
late final UftFileController _controller; |
||||
|
||||
bool _isControllerFromTabManager = false; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
|
||||
final controllerFromManager = tabManager.getController(widget.tabId); |
||||
if (controllerFromManager != null) { |
||||
_controller = controllerFromManager; |
||||
_isControllerFromTabManager = true; |
||||
} else { |
||||
_controller = UftFileController(); |
||||
_isControllerFromTabManager = false; |
||||
tabManager.registerController(widget.tabId, _controller); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
if (!_isControllerFromTabManager) { |
||||
_controller.dispose(); |
||||
} |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return MultiProvider( |
||||
providers: [ |
||||
ChangeNotifierProvider.value(value: _controller), |
||||
ChangeNotifierProvider.value(value: _controller.treeController), |
||||
], |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Column( |
||||
children: [ |
||||
_buildFilePathInput(), |
||||
const SizedBox(height: 8), |
||||
Expanded(child: _buildMainContent()), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Widget _buildFilePathInput() { |
||||
return Consumer<UftFileController>( |
||||
builder: (context, controller, _) { |
||||
return TextField( |
||||
decoration: InputDecoration( |
||||
labelText: 'UFT File', |
||||
hintText: 'UFT File (包括服务、原子层代码文件)', |
||||
suffixIcon: IconButton( |
||||
icon: const Icon(Icons.folder_open), |
||||
onPressed: controller.pickFile, |
||||
), |
||||
border: const OutlineInputBorder(), |
||||
errorText: controller.errorMessage, |
||||
), |
||||
controller: TextEditingController(text: controller.filePath), |
||||
readOnly: true, |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
|
||||
Widget _buildMainContent() { |
||||
return Consumer<UftFileController>( |
||||
builder: (context, controller, _) { |
||||
if (controller.errorMessage != null) { |
||||
return Center(child: Text(controller.errorMessage!)); |
||||
} |
||||
|
||||
return Row( |
||||
children: [ |
||||
SizedBox( |
||||
width: MediaQuery.of(context).size.width * 0.3, |
||||
child: const Column( |
||||
children: [ |
||||
Expanded(flex: 2, child: Card(child: FileTreeView())), |
||||
SizedBox(height: 8), |
||||
Expanded(flex: 4, child: Card(child: Text("UFT File Content1"))), |
||||
], |
||||
), |
||||
), |
||||
const SizedBox(width: 8), |
||||
const Expanded(child: Card(child: Text("UFT File Content2"))), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue