25 changed files with 1025 additions and 175 deletions
@ -0,0 +1,91 @@
@@ -0,0 +1,91 @@
|
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/models/search_result.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/services/xml_extract_service.dart'; |
||||
import 'package:win_text_editor/shared/base/base_content_controller.dart'; |
||||
|
||||
class DataExtractController extends BaseContentController { |
||||
String _searchDirectory = ''; |
||||
String _fileType = '*.*'; |
||||
final List<SearchResult> _results = []; |
||||
final List<XmlRule> _rules = []; |
||||
final XmlExtractService _extractService = XmlExtractService(); |
||||
|
||||
List<SearchResult> get results => _results; |
||||
List<XmlRule> get rules => _rules; |
||||
|
||||
String get searchDirectory => _searchDirectory; |
||||
String get fileType => _fileType; |
||||
|
||||
bool _isExtracting = false; |
||||
|
||||
bool onlyFileName = false; |
||||
bool get isExtracting => _isExtracting; |
||||
|
||||
Future<void> executeExtraction() async { |
||||
Logger().info("开始提取目录:$_searchDirectory, 文件名:$_fileType"); |
||||
if (_searchDirectory.isEmpty || _rules.isEmpty) return; |
||||
|
||||
_isExtracting = true; |
||||
notifyListeners(); |
||||
|
||||
_results.clear(); |
||||
notifyListeners(); |
||||
|
||||
try { |
||||
final newResults = await _extractService.extractFromDirectory( |
||||
directory: _searchDirectory, |
||||
fileType: _fileType, |
||||
rule: _rules[0], |
||||
); |
||||
_results.addAll(newResults); |
||||
} catch (e) { |
||||
Logger().error("提取目录出错:$e"); |
||||
_results.add(SearchResult(rowNum: 1, filePath: 'Error', content: 'Extraction failed: $e')); |
||||
} finally { |
||||
_isExtracting = false; |
||||
notifyListeners(); |
||||
} |
||||
} |
||||
|
||||
set searchDirectory(String value) { |
||||
_searchDirectory = value; |
||||
notifyListeners(); |
||||
} |
||||
|
||||
set fileType(String value) { |
||||
_fileType = value; |
||||
notifyListeners(); |
||||
} |
||||
|
||||
Future<void> pickDirectory() async { |
||||
final dir = await FilePicker.platform.getDirectoryPath(); |
||||
if (dir != null) { |
||||
searchDirectory = dir; |
||||
} |
||||
} |
||||
|
||||
void setRule(XmlRule rule) { |
||||
_rules.clear(); |
||||
rules.add(rule); |
||||
notifyListeners(); |
||||
} |
||||
|
||||
void removeRule(int index) { |
||||
_rules.removeAt(index); |
||||
notifyListeners(); |
||||
} |
||||
|
||||
@override |
||||
void onOpenFile(String filePath) { |
||||
// TODO: implement onOpenFile |
||||
} |
||||
|
||||
@override |
||||
void onOpenFolder(String folderPath) { |
||||
searchDirectory = folderPath; |
||||
} |
||||
|
||||
void cancelExtraction() {} |
||||
} |
@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
|
||||
// search_result.dart |
||||
class SearchResult { |
||||
final int rowNum; |
||||
final String filePath; |
||||
final String content; |
||||
|
||||
SearchResult({required this.rowNum, required this.filePath, required this.content}); |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
// xml_rule.dart |
||||
class XmlRule { |
||||
final String nodePath; |
||||
final String attributeName; |
||||
final bool isFirstOccurrence; |
||||
final String? namespacePrefix; |
||||
|
||||
XmlRule({ |
||||
required this.nodePath, |
||||
required this.attributeName, |
||||
this.isFirstOccurrence = false, |
||||
this.namespacePrefix, |
||||
}); |
||||
|
||||
String toxPath() { |
||||
return '${namespacePrefix != null ? '$namespacePrefix:' : ''}$nodePath${isFirstOccurrence ? '[0]' : ''}/${attributeName.isNotEmpty ? '@$attributeName' : 'text()'}'; |
||||
} |
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
import 'package:xml/xml.dart' as xml; |
||||
|
||||
class SimpleXPath { |
||||
static List<xml.XmlNode> query(xml.XmlNode node, String path) { |
||||
final segments = path.split('/').where((s) => s.isNotEmpty).toList(); |
||||
var current = [node]; |
||||
|
||||
for (var i = 0; i < segments.length; i++) { |
||||
final segment = segments[i]; |
||||
final isRecursive = segment.isEmpty; // 修正:空段表示前导// |
||||
|
||||
current = current.expand((n) => _findNodes(n, segment, isRecursive)).toList(); |
||||
if (current.isEmpty) break; |
||||
} |
||||
|
||||
return current; |
||||
} |
||||
|
||||
static Iterable<xml.XmlNode> _findNodes(xml.XmlNode node, String segment, bool recursive) sync* { |
||||
if (segment == '..') { |
||||
if (node.parent != null) yield node.parent!; |
||||
} else if (segment == '.') { |
||||
yield node; |
||||
} else if (segment == 'text()') { |
||||
if (node is xml.XmlText || node is xml.XmlAttribute) { |
||||
yield node; |
||||
} else { |
||||
yield* node.children.whereType<xml.XmlText>(); |
||||
} |
||||
} else if (segment.startsWith('@')) { |
||||
final attrName = segment.substring(1); |
||||
if (node is xml.XmlElement) { |
||||
yield* node.attributes.where((a) => a.name.local == attrName); |
||||
} |
||||
} else { |
||||
if (node is xml.XmlElement) { |
||||
if (recursive) { |
||||
// 递归查找所有子节点 |
||||
yield* _findNodesRecursive(node, segment); |
||||
} else { |
||||
// 只查找直接子节点 |
||||
yield* node.children.whereType<xml.XmlElement>().where( |
||||
(child) => child.name.local == segment, |
||||
); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 新增递归查找方法 |
||||
static Iterable<xml.XmlNode> _findNodesRecursive(xml.XmlNode node, String segment) sync* { |
||||
if (node is xml.XmlElement) { |
||||
if (node.name.local == segment) { |
||||
yield node; |
||||
} |
||||
for (final child in node.children) { |
||||
yield* _findNodesRecursive(child, segment); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
void main() { |
||||
final doc = xml.XmlDocument.parse(''' |
||||
<root> |
||||
<items> |
||||
<item id="1">Text 1</item> |
||||
<items> |
||||
<item id="2">Text 2</item> |
||||
</items> |
||||
</items> |
||||
</root> |
||||
'''); |
||||
|
||||
void printResults(String query, List<xml.XmlNode> results) { |
||||
print('\nQuery: "$query"'); |
||||
if (results.isEmpty) { |
||||
print('No results found'); |
||||
} else { |
||||
print('Found ${results.length} results:'); |
||||
results.forEach((node) { |
||||
if (node is xml.XmlAttribute) { |
||||
print('Attribute: ${node.name}=${node.value}'); |
||||
} else if (node is xml.XmlText) { |
||||
print('Text: ${node.text}'); |
||||
} else { |
||||
print('Element: ${node.toXmlString()}'); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
// 测试查询 |
||||
printResults('//items', SimpleXPath.query(doc, '//items')); |
||||
printResults('//item', SimpleXPath.query(doc, '//item')); |
||||
printResults('//item/text()', SimpleXPath.query(doc, '//item/text()')); |
||||
printResults('//item/@id', SimpleXPath.query(doc, '//item/@id')); |
||||
printResults('/root/items/item', SimpleXPath.query(doc, '/root/items/item')); |
||||
printResults('//nonexistent', SimpleXPath.query(doc, '//nonexistent')); |
||||
} |
@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
|
||||
// xml_extract_service.dart |
||||
import 'dart:io'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/services/simple_xpath.dart'; |
||||
import 'package:win_text_editor/shared/utils/file_utils.dart'; |
||||
import 'package:xml/xml.dart' as xml; |
||||
import 'package:win_text_editor/modules/data_extract/models/search_result.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart'; |
||||
|
||||
class XmlExtractService { |
||||
Future<List<SearchResult>> extractFromDirectory({ |
||||
required String directory, |
||||
required String fileType, |
||||
required XmlRule rule, |
||||
}) async { |
||||
final results = <SearchResult>[]; |
||||
final dir = Directory(directory); |
||||
int rowNum = 1; |
||||
|
||||
await for (var entity in dir.list(recursive: true)) { |
||||
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) { |
||||
try { |
||||
final fileContent = await entity.readAsString(); |
||||
final document = xml.XmlDocument.parse(fileContent); |
||||
final values = _extractWithRule(document, rule); |
||||
for (var value in values) { |
||||
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: value)); |
||||
} |
||||
} catch (e) { |
||||
Logger().error('XmlExtractService.extractFromDirectory方法执行出错: $e'); |
||||
results.add(SearchResult(rowNum: rowNum++, filePath: entity.path, content: 'Error: $e')); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return results; |
||||
} |
||||
|
||||
List<String> _extractWithRule(xml.XmlDocument document, XmlRule rule) { |
||||
//final nodes = document.findAllElements(rule.nodePath); |
||||
final nodes = SimpleXPath.query(document, rule.toxPath()); |
||||
if (rule.isFirstOccurrence && nodes.isNotEmpty) { |
||||
final attr = nodes.first.getAttribute(rule.attributeName); |
||||
return attr != null ? [attr] : []; |
||||
} else { |
||||
return nodes |
||||
.map((node) => node.getAttribute(rule.attributeName)) |
||||
.where((attr) => attr != null) |
||||
.cast<String>() |
||||
.toList(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,150 @@
@@ -0,0 +1,150 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/models/xml_rule.dart'; |
||||
import 'package:win_text_editor/shared/components/my_checkbox.dart'; |
||||
|
||||
class ConditionSetting extends StatefulWidget { |
||||
const ConditionSetting({super.key}); |
||||
|
||||
@override |
||||
State<ConditionSetting> createState() => _ConditionSettingState(); |
||||
} |
||||
|
||||
class _ConditionSettingState extends State<ConditionSetting> { |
||||
final _nodePathController = TextEditingController(); |
||||
final _attributeNameController = TextEditingController(); |
||||
final _namespacePrefixController = TextEditingController(); |
||||
bool _isFirstOccurrence = false; |
||||
bool _isExtracting = false; |
||||
|
||||
@override |
||||
void dispose() { |
||||
_nodePathController.dispose(); |
||||
_attributeNameController.dispose(); |
||||
_namespacePrefixController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final controller = context.watch<DataExtractController>(); |
||||
|
||||
return Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(16.0), |
||||
child: Column( |
||||
crossAxisAlignment: CrossAxisAlignment.stretch, |
||||
children: [ |
||||
const Text('数据提取规则设置:', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), |
||||
const SizedBox(height: 16), |
||||
|
||||
// 规则输入表单 |
||||
TextField( |
||||
controller: _nodePathController, |
||||
decoration: const InputDecoration( |
||||
labelText: '节点路径 (XPath)', |
||||
hintText: '如: //business:Service 或 /root/items', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
), |
||||
const SizedBox(height: 12), |
||||
TextField( |
||||
controller: _attributeNameController, |
||||
decoration: const InputDecoration( |
||||
labelText: '属性名称', |
||||
hintText: '如: chineseName 或 name', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
), |
||||
|
||||
const SizedBox(height: 12), |
||||
const Text('命名空间配置 (可选):'), |
||||
TextField( |
||||
controller: _namespacePrefixController, |
||||
decoration: const InputDecoration( |
||||
labelText: '命名空间前缀', |
||||
hintText: '如: business', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
), |
||||
const SizedBox(height: 12), |
||||
|
||||
MyCheckbox( |
||||
title: '仅提取第一个匹配项', |
||||
value: _isFirstOccurrence, |
||||
onChanged: (value) => setState(() => _isFirstOccurrence = value ?? false), |
||||
), |
||||
const SizedBox(height: 16), |
||||
// 操作按钮行 |
||||
Row( |
||||
children: [ |
||||
Expanded( |
||||
child: ElevatedButton.icon( |
||||
icon: const Icon(Icons.play_arrow), |
||||
label: const Text('开始'), |
||||
onPressed: _isExtracting ? null : () => _startExtraction(controller), |
||||
), |
||||
), |
||||
const SizedBox(width: 12), |
||||
Expanded( |
||||
child: ElevatedButton.icon( |
||||
icon: const Icon(Icons.stop, color: Colors.red), |
||||
label: const Text('结束', style: TextStyle(color: Colors.red)), |
||||
onPressed: _isExtracting ? _stopExtraction : null, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
|
||||
void _setRule() { |
||||
final nodePath = _nodePathController.text.trim(); |
||||
final attributeName = _attributeNameController.text.trim(); |
||||
|
||||
if (nodePath.isNotEmpty && attributeName.isNotEmpty) { |
||||
final controller = Provider.of<DataExtractController>(context, listen: false); |
||||
controller.setRule( |
||||
XmlRule( |
||||
nodePath: nodePath, |
||||
attributeName: attributeName, |
||||
isFirstOccurrence: _isFirstOccurrence, |
||||
namespacePrefix: |
||||
_namespacePrefixController.text.trim().isNotEmpty |
||||
? _namespacePrefixController.text.trim() |
||||
: null, |
||||
), |
||||
); |
||||
} |
||||
} |
||||
|
||||
Future<void> _startExtraction(DataExtractController controller) async { |
||||
if (controller.searchDirectory.isEmpty) { |
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('请先选择搜索目录'))); |
||||
return; |
||||
} |
||||
|
||||
_setRule(); |
||||
|
||||
setState(() => _isExtracting = true); |
||||
try { |
||||
await controller.executeExtraction(); |
||||
} finally { |
||||
if (mounted) { |
||||
setState(() => _isExtracting = false); |
||||
} |
||||
} |
||||
} |
||||
|
||||
void _stopExtraction() { |
||||
// 这里需要确保控制器中有取消提取的逻辑 |
||||
final controller = Provider.of<DataExtractController>(context, listen: false); |
||||
// 假设控制器中有cancelExtraction方法 |
||||
controller.cancelExtraction(); |
||||
setState(() => _isExtracting = false); |
||||
} |
||||
} |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'results_view.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/widgets/condition_setting.dart'; |
||||
import 'directory.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
|
||||
class DataExtractView extends StatefulWidget { |
||||
final String tabId; |
||||
const DataExtractView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
DataExtractViewState createState() => DataExtractViewState(); |
||||
} |
||||
|
||||
class DataExtractViewState extends State<DataExtractView> { |
||||
late final DataExtractController _controller; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_controller = tabManager.getController(widget.tabId) ?? DataExtractController(); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return ChangeNotifierProvider.value( |
||||
value: _controller, |
||||
child: Consumer<DataExtractController>( |
||||
builder: (context, controller, child) { |
||||
return const Padding( |
||||
padding: EdgeInsets.all(4.0), |
||||
child: Column( |
||||
children: [ |
||||
Directory(), |
||||
Expanded( |
||||
child: Row( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
// 左侧部分 (50%) |
||||
Expanded(flex: 3, child: ConditionSetting()), |
||||
SizedBox(width: 8), |
||||
// 右侧部分 (50%) |
||||
Expanded(flex: 7, child: ResultsView()), |
||||
], |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
|
||||
class Directory extends StatefulWidget { |
||||
const Directory({super.key}); |
||||
|
||||
@override |
||||
State<Directory> createState() => _DirectoryState(); |
||||
} |
||||
|
||||
class _DirectoryState extends State<Directory> { |
||||
late TextEditingController _searchDirectoryController; |
||||
late TextEditingController _fileTypeController; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
final controller = context.read<DataExtractController>(); |
||||
_searchDirectoryController = TextEditingController(text: controller.searchDirectory); |
||||
_fileTypeController = TextEditingController(text: controller.fileType); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
_searchDirectoryController.dispose(); |
||||
_fileTypeController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Consumer<DataExtractController>( |
||||
builder: (context, controller, child) { |
||||
// 同步 TextEditingController 的值 |
||||
if (_searchDirectoryController.text != controller.searchDirectory) { |
||||
_searchDirectoryController.text = controller.searchDirectory; |
||||
} |
||||
if (_fileTypeController.text != controller.fileType) { |
||||
_fileTypeController.text = controller.fileType; |
||||
} |
||||
|
||||
return Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Row( |
||||
children: [ |
||||
Expanded( |
||||
child: TextField( |
||||
controller: _searchDirectoryController, |
||||
decoration: const InputDecoration( |
||||
labelText: '搜索目录', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
onChanged: (value) => controller.searchDirectory = value, |
||||
), |
||||
), |
||||
const SizedBox(width: 8), |
||||
SizedBox( |
||||
width: 100, |
||||
child: TextField( |
||||
controller: _fileTypeController, |
||||
decoration: const InputDecoration( |
||||
labelText: '文件类型', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
onChanged: (value) => controller.fileType = value, |
||||
), |
||||
), |
||||
const SizedBox(width: 8), |
||||
IconButton( |
||||
icon: const Icon(Icons.folder_open), |
||||
onPressed: () async { |
||||
await controller.pickDirectory(); |
||||
// 不需要手动更新 _searchDirectoryController.text, |
||||
// 因为 Consumer 会触发重建并自动同步 |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:syncfusion_flutter_datagrid/datagrid.dart'; |
||||
import 'package:path/path.dart' as path; |
||||
|
||||
import 'package:file_picker/file_picker.dart'; |
||||
import 'dart:io'; |
||||
|
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
import 'package:win_text_editor/shared/components/my_grid_column.dart'; |
||||
|
||||
class ResultsView extends StatelessWidget { |
||||
const ResultsView({super.key}); |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
final controller = context.watch<DataExtractController>(); |
||||
|
||||
return Card( |
||||
child: GestureDetector( |
||||
onSecondaryTapDown: (details) { |
||||
_showContextMenu(context, details.globalPosition, controller); |
||||
}, |
||||
child: _buildLocateGrid(controller), |
||||
), |
||||
); |
||||
} |
||||
|
||||
Future<void> _showContextMenu( |
||||
BuildContext context, |
||||
Offset position, |
||||
DataExtractController controller, |
||||
) async { |
||||
// 获取渲染对象以正确定位菜单 |
||||
final renderBox = context.findRenderObject() as RenderBox; |
||||
final localPosition = renderBox.globalToLocal(position); |
||||
|
||||
// 显示菜单并等待选择结果 |
||||
final result = await showMenu<String>( |
||||
context: context, |
||||
position: RelativeRect.fromLTRB( |
||||
position.dx, |
||||
position.dy, |
||||
position.dx + renderBox.size.width - localPosition.dx, |
||||
position.dy + renderBox.size.height - localPosition.dy, |
||||
), |
||||
items: [const PopupMenuItem<String>(value: 'export', child: Text('导出(csv)'))], |
||||
); |
||||
|
||||
// 处理菜单选择结果 |
||||
if (result == 'export' && context.mounted) { |
||||
try { |
||||
await _exportToCsv(controller); |
||||
} catch (e) { |
||||
if (context.mounted) { |
||||
ScaffoldMessenger.of( |
||||
context, |
||||
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}'))); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Future<void> _exportToCsv(DataExtractController controller) async { |
||||
String csvData = ''; |
||||
csvData = '文件\t行号\t内容\n'; |
||||
for (var result in controller.results) { |
||||
csvData += '${path.basename(result.filePath)}\t${result.content}\n'; |
||||
} |
||||
|
||||
final filePath = await FilePicker.platform.saveFile( |
||||
dialogTitle: '保存导出结果', |
||||
fileName: 'search_results.csv', |
||||
type: FileType.custom, |
||||
allowedExtensions: ['csv'], |
||||
); |
||||
|
||||
if (filePath != null) { |
||||
final file = File(filePath); |
||||
await file.writeAsString(csvData); |
||||
} |
||||
} |
||||
|
||||
Widget _buildLocateGrid(DataExtractController controller) { |
||||
return SfDataGrid( |
||||
rowHeight: 32, |
||||
headerRowHeight: 32, |
||||
source: LocateDataSource(controller), |
||||
columns: [ |
||||
MyGridColumn(columnName: 'rowNum', label: '序号'), |
||||
MyGridColumn(columnName: 'file', label: '文件', minimumWidth: 300), |
||||
MyGridColumn(columnName: 'content', label: '内容'), |
||||
], |
||||
selectionMode: SelectionMode.multiple, |
||||
navigationMode: GridNavigationMode.cell, |
||||
gridLinesVisibility: GridLinesVisibility.both, |
||||
headerGridLinesVisibility: GridLinesVisibility.both, |
||||
allowSorting: false, |
||||
allowFiltering: false, |
||||
columnWidthMode: ColumnWidthMode.fill, |
||||
isScrollbarAlwaysShown: true, |
||||
allowColumnsResizing: true, // 关键开关 |
||||
columnResizeMode: ColumnResizeMode.onResizeEnd, |
||||
); |
||||
} |
||||
} |
||||
|
||||
class LocateDataSource extends DataGridSource { |
||||
final DataExtractController controller; |
||||
|
||||
LocateDataSource(this.controller); |
||||
|
||||
@override |
||||
List<DataGridRow> get rows => |
||||
controller.results.map((result) { |
||||
return DataGridRow( |
||||
cells: [ |
||||
DataGridCell(columnName: 'rowNum', value: result.rowNum), |
||||
DataGridCell(columnName: 'file', value: path.basename(result.filePath)), |
||||
DataGridCell(columnName: 'content', value: result.content), |
||||
], |
||||
); |
||||
}).toList(); |
||||
|
||||
@override |
||||
DataGridRowAdapter? buildRow(DataGridRow row) { |
||||
return DataGridRowAdapter( |
||||
cells: |
||||
row.getCells().map<Widget>((cell) { |
||||
return Container( |
||||
alignment: Alignment.centerLeft, |
||||
padding: const EdgeInsets.symmetric(horizontal: 8), |
||||
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis), |
||||
); |
||||
}).toList(), |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue