hejl 1 month ago
parent
commit
bcc94541b4
  1. 3
      .gitignore
  2. 3
      .gitmodules
  3. BIN
      documents/PB UFT模块迁移方案.docx
  4. BIN
      documents/斯帝罗兰/附件1.jpg
  5. BIN
      documents/斯帝罗兰/附件2.jpg
  6. BIN
      documents/斯帝罗兰/附件3.jpg
  7. BIN
      documents/斯帝罗兰/附件4.jpg
  8. 22
      uft_dev_server/.vscode/c_cpp_properties.json
  9. 26
      uft_dev_server/.vscode/launch.json
  10. 76
      uft_dev_server/.vscode/settings.json
  11. 33
      uft_dev_server/.vscode/tasks.json
  12. 84
      uft_dev_server/CMakeLists.txt
  13. 9
      uft_dev_server/cmd.txt
  14. 1
      uft_dev_server/imgui
  15. 25
      uft_dev_server/src/config/config.json
  16. 48
      uft_dev_server/src/controllers/ApiController.cpp
  17. 12
      uft_dev_server/src/controllers/HelloWorldController.cpp
  18. 15
      uft_dev_server/src/controllers/HelloWorldController.h
  19. 24
      uft_dev_server/src/filters/ApiFilter.cpp
  20. 14
      uft_dev_server/src/filters/ApiFilter.h
  21. 38
      uft_dev_server/src/main.cpp
  22. 25
      win_text_editor/.vscode/launch.json
  23. 4
      win_text_editor/.vscode/settings.json
  24. 342
      win_text_editor/assets/config/uft_macro_list.yaml
  25. 2
      win_text_editor/assets/config/words_classes.yaml
  26. 349045
      win_text_editor/assets/dict.txt
  27. 0
      win_text_editor/assets/prob_emit.txt
  28. 104
      win_text_editor/lib/app/app.dart
  29. 66
      win_text_editor/lib/app/menus/menu_actions.dart
  30. 75
      win_text_editor/lib/app/services/file_service.dart
  31. 31
      win_text_editor/lib/app/services/syntax_service.dart
  32. 59
      win_text_editor/lib/app/utils/file_utils.dart
  33. 84
      win_text_editor/lib/app/widgets/editor_pane.dart
  34. 144
      win_text_editor/lib/app/widgets/template_parser_tab.dart
  35. 328
      win_text_editor/lib/app/widgets/text_editor.dart
  36. 7
      win_text_editor/lib/framework/common/constants.dart
  37. 33
      win_text_editor/lib/framework/controllers/file_provider.dart
  38. 1
      win_text_editor/lib/framework/controllers/logger.dart
  39. 165
      win_text_editor/lib/framework/controllers/tab_items_controller.dart
  40. 26
      win_text_editor/lib/framework/models/file_node.dart
  41. 4
      win_text_editor/lib/framework/models/tab_model.dart
  42. 74
      win_text_editor/lib/framework/services/fast_xml_parser.dart
  43. 21
      win_text_editor/lib/framework/services/file_path_manager.dart
  44. 124
      win_text_editor/lib/framework/services/file_service.dart
  45. 73
      win_text_editor/lib/framework/services/macro_template_service.dart
  46. 119
      win_text_editor/lib/framework/widgets/app_scaffold.dart
  47. 2
      win_text_editor/lib/framework/widgets/console_panel.dart
  48. 100
      win_text_editor/lib/framework/widgets/file_explorer_pane.dart
  49. 99
      win_text_editor/lib/framework/widgets/tab_view.dart
  50. 79
      win_text_editor/lib/main copy.dart
  51. 36
      win_text_editor/lib/main.dart
  52. 61
      win_text_editor/lib/menus/app_menu.dart
  53. 131
      win_text_editor/lib/menus/menu_actions.dart
  54. 18
      win_text_editor/lib/menus/menu_constants.dart
  55. 132
      win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart
  56. 110
      win_text_editor/lib/modules/call_function/models/call_function.dart
  57. 127
      win_text_editor/lib/modules/call_function/services/call_function_service.dart
  58. 78
      win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart
  59. 62
      win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart
  60. 76
      win_text_editor/lib/modules/call_function/widgets/call_function_view.dart
  61. 91
      win_text_editor/lib/modules/code_creater/controllers/code_creater_controller.dart
  62. 341
      win_text_editor/lib/modules/code_creater/services/code_create_service.dart
  63. 168
      win_text_editor/lib/modules/code_creater/widgets/code_creater_view.dart
  64. 313
      win_text_editor/lib/modules/code_creater/widgets/node_table.dart
  65. 263
      win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart
  66. 10
      win_text_editor/lib/modules/content_search/models/count_result.dart
  67. 6
      win_text_editor/lib/modules/content_search/models/match_result.dart
  68. 1
      win_text_editor/lib/modules/content_search/models/search_mode.dart
  69. 24
      win_text_editor/lib/modules/content_search/models/search_result.dart
  70. 187
      win_text_editor/lib/modules/content_search/services/base_search_service.dart
  71. 225
      win_text_editor/lib/modules/content_search/services/count_search_service.dart
  72. 133
      win_text_editor/lib/modules/content_search/services/custom_search_service.dart
  73. 112
      win_text_editor/lib/modules/content_search/services/locate_search_service.dart
  74. 63
      win_text_editor/lib/modules/content_search/widgets/content_search_view.dart
  75. 105
      win_text_editor/lib/modules/content_search/widgets/directory_settings.dart
  76. 318
      win_text_editor/lib/modules/content_search/widgets/results_view.dart
  77. 298
      win_text_editor/lib/modules/content_search/widgets/search_settings.dart
  78. 258
      win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart
  79. 93
      win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart
  80. 95
      win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart
  81. 231
      win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart
  82. 99
      win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart
  83. 28
      win_text_editor/lib/modules/data_extract/models/search_result.dart
  84. 21
      win_text_editor/lib/modules/data_extract/models/xml_rule.dart
  85. 120
      win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart
  86. 164
      win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart
  87. 63
      win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart
  88. 86
      win_text_editor/lib/modules/data_extract/widgets/directory.dart
  89. 157
      win_text_editor/lib/modules/data_extract/widgets/results_view.dart
  90. 116
      win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart
  91. 24
      win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart
  92. 70
      win_text_editor/lib/modules/data_format/models/template_node.dart
  93. 57
      win_text_editor/lib/modules/data_format/services/mustache_service.dart
  94. 67
      win_text_editor/lib/modules/data_format/widgets/data_format_view.dart
  95. 59
      win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart
  96. 221
      win_text_editor/lib/modules/data_format/widgets/grid_view.dart
  97. 19
      win_text_editor/lib/modules/demo/controllers/demo_controller.dart
  98. 47
      win_text_editor/lib/modules/demo/widgets/demo_view.dart
  99. 83
      win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart
  100. 141
      win_text_editor/lib/modules/memory_table/controllers/memory_table_controller.dart
  101. Some files were not shown because too many files have changed in this diff Show More

3
.gitignore vendored

@ -6,3 +6,6 @@ @@ -6,3 +6,6 @@
/win_text_editor/macos
/win_text_editor/web
/win_text_editor/windows/runner
/cpp_server/swagger
/uft_dev_server/build
/uft_dev_server/third_party

3
.gitmodules vendored

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
[submodule "cpp_server/RuntimeCompiledCPlusPlus"]
path = cpp_server/RuntimeCompiledCPlusPlus
url = https://github.com/RuntimeCompiledCPlusPlus/RuntimeCompiledCPlusPlus.git

BIN
documents/PB UFT模块迁移方案.docx

Binary file not shown.

BIN
documents/斯帝罗兰/附件1.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 KiB

BIN
documents/斯帝罗兰/附件2.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

BIN
documents/斯帝罗兰/附件3.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

BIN
documents/斯帝罗兰/附件4.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

22
uft_dev_server/.vscode/c_cpp_properties.json vendored

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
{
"configurations": [
{
"name": "Win32",
"includePath": [
"${workspaceFolder}/**",
"${vcpkgRoot}/x64-mingw-static/include/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"compilerPath": "C:/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin/g++.exe",
"cStandard": "c17",
"cppStandard": "c++17",
"intelliSenseMode": "windows-gcc-x64",
"configurationProvider": "ms-vscode.cmake-tools"
}
],
"version": 4
}

26
uft_dev_server/.vscode/launch.json vendored

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "g++.exe - Build and debug active file",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/UftDevServer.exe",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "C:/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin/gdb.exe",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: g++.exe build active file"
}
]
}

76
uft_dev_server/.vscode/settings.json vendored

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
{
"files.associations": {
"*.dbclient-js": "javascript",
"any": "cpp",
"array": "cpp",
"atomic": "cpp",
"strstream": "cpp",
"bit": "cpp",
"bitset": "cpp",
"cctype": "cpp",
"chrono": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"codecvt": "cpp",
"compare": "cpp",
"concepts": "cpp",
"condition_variable": "cpp",
"coroutine": "cpp",
"csignal": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdint": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"deque": "cpp",
"list": "cpp",
"map": "cpp",
"set": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"functional": "cpp",
"iterator": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"regex": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"future": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"numbers": "cpp",
"ostream": "cpp",
"semaphore": "cpp",
"shared_mutex": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"stop_token": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"cinttypes": "cpp",
"typeinfo": "cpp",
"variant": "cpp"
}
}

33
uft_dev_server/.vscode/tasks.json vendored

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "cppbuild",
"label": "C/C++: g++.exe build active file",
"command": "C:/ProgramData/chocolatey/lib/mingw/tools/install/mingw64/bin/g++.exe",
"args": [
"-fdiagnostics-color=always",
"-g",
"${workspaceFolder}/*.cpp",
"-o",
"${workspaceFolder}/build/${fileBasenameNoExtension}.exe",
"-I${vcpkgRoot}/x64-mingw-static/include",
"-L${vcpkgRoot}/x64-mingw-static/lib",
"-ldrogon",
"-ljsoncpp",
"-lrcpp"
],
"options": {
"cwd": "${workspaceFolder}"
},
"problemMatcher": [
"$gcc"
],
"group": {
"kind": "build",
"isDefault": true
},
"detail": "Task generated by Debugger."
}
]
}

84
uft_dev_server/CMakeLists.txt

@ -0,0 +1,84 @@ @@ -0,0 +1,84 @@
cmake_minimum_required(VERSION 3.12)
project(UftDevServer)
#
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
endif()
# vcpkg
set(CMAKE_TOOLCHAIN_FILE "D:/aigc/vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "Vcpkg toolchain file")
# C++
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
#
set(SOURCE_DIR src)
add_subdirectory(imgui)
#
find_package(Drogon CONFIG REQUIRED)
find_package(RapidJSON CONFIG REQUIRED)
set(SOURCES
src/main.cpp
src/controllers/*.cpp
src/filters/*.cpp
)
# 使GLOB
file(GLOB_RECURSE SOURCES_EXPANDED ${SOURCES})
#
add_executable(${PROJECT_NAME}
${SOURCES_EXPANDED}
)
#
target_link_libraries(${PROJECT_NAME} PRIVATE
Drogon::Drogon
imgui
${CMAKE_DL_LIBS} #
)
#
target_include_directories(${PROJECT_NAME} PRIVATE
${SOURCE_DIR}
${SOURCE_DIR}/controllers
${SOURCE_DIR}/filters
)
#
set(CONFIG_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/config/config.json")
set(CONFIG_DEST "${CMAKE_CURRENT_BINARY_DIR}/Debug/config.json")
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_CURRENT_BINARY_DIR}/Debug"
COMMAND ${CMAKE_COMMAND} -E copy
"${CONFIG_SRC}"
"${CONFIG_DEST}"
COMMENT "Copying config.json to output directory"
VERBATIM
)
#
if(CMAKE_CONFIGURATION_TYPES)
foreach(CFG_TYPE ${CMAKE_CONFIGURATION_TYPES})
set(CFG_DIR "${CMAKE_CURRENT_BINARY_DIR}/${CFG_TYPE}")
add_custom_command(
TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${CFG_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy
"${CONFIG_SRC}"
"${CFG_DIR}/config.json"
COMMENT "Copying config.json to ${CFG_TYPE} directory"
VERBATIM
)
endforeach()
endif()

9
uft_dev_server/cmd.txt

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
#工程编译
cd D:/aigc/manta/uft_dev_server
rm -Path build -Recurse -Force
cmake -B build -DCMAKE_TOOLCHAIN_FILE="D:/aigc/vcpkg/scripts/buildsystems/vcpkg.cmake"
cmake --build build

1
uft_dev_server/imgui

@ -0,0 +1 @@ @@ -0,0 +1 @@
Subproject commit 407a0b972eac6166095d2b5b5b0896bad6e9687a

25
uft_dev_server/src/config/config.json

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
{
"controllers": [
{
"name": "HelloWorldController",
"path": "/"
},
{
"name": "ApiController",
"path": "/"
}
],
"filters": [
{
"name": "ApiFilter",
"path": "/api/*"
}
],
"listeners": [
{
"address": "0.0.0.0",
"port": 8080,
"ssl": false
}
]
}

48
uft_dev_server/src/controllers/ApiController.cpp

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
#pragma once
#include <drogon/HttpController.h>
#include <drogon/HttpResponse.h>
#include <json/json.h>
using namespace drogon;
class ApiController : public HttpController<ApiController>
{
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(ApiController::getInfo, "/api/info", Get);
ADD_METHOD_TO(ApiController::postData, "/api/data", Post);
METHOD_LIST_END
void getInfo(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
Json::Value ret;
ret["status"] = "ok";
ret["version"] = "1.0.0";
ret["message"] = "Welcome to CppServerProject";
auto resp = HttpResponse::newHttpJsonResponse(ret);
callback(resp);
}
void postData(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto json = req->getJsonObject();
if (!json)
{
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("Invalid JSON data");
callback(resp);
return;
}
Json::Value ret;
ret["status"] = "received";
ret["data"] = *json;
auto resp = HttpResponse::newHttpJsonResponse(ret);
callback(resp);
}
};

12
uft_dev_server/src/controllers/HelloWorldController.cpp

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
#include "HelloWorldController.h"
using namespace drogon;
void HelloWorldController::asyncHandleHttpRequest(
const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto resp = HttpResponse::newHttpResponse();
resp->setBody("Hello, World!");
callback(resp);
}

15
uft_dev_server/src/controllers/HelloWorldController.h

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
#pragma once
#include <drogon/HttpSimpleController.h>
using namespace drogon;
class HelloWorldController : public HttpSimpleController<HelloWorldController>
{
public:
PATH_LIST_BEGIN
PATH_ADD("/hello", Get);
PATH_LIST_END
void asyncHandleHttpRequest(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) override;
};

24
uft_dev_server/src/filters/ApiFilter.cpp

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
#include "ApiFilter.h"
using namespace drogon;
ApiFilter::ApiFilter() {}
void ApiFilter::doFilter(const HttpRequestPtr &req,
FilterCallback &&fcb,
FilterChainCallback &&fccb)
{
// 简单的API密钥验证
auto apiKey = req->getHeader("X-API-KEY");
if (apiKey.empty() || apiKey != "secret-key")
{
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k401Unauthorized);
resp->setBody("Unauthorized: Invalid API Key");
fcb(resp);
return;
}
// 通过验证,继续处理
fccb();
};

14
uft_dev_server/src/filters/ApiFilter.h

@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
#include <drogon/HttpFilter.h>
using namespace drogon;
class ApiFilter : public HttpFilter<ApiFilter>
{
public:
ApiFilter();
virtual void doFilter(const HttpRequestPtr &req,
FilterCallback &&fcb,
FilterChainCallback &&fccb) override;
};

38
uft_dev_server/src/main.cpp

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
int main()
{
// 初始化窗口和渲染上下文(例如GLFW+OpenGL)
glfwInit();
GLFWwindow *window = glfwCreateWindow(1280, 720, "ImGui Demo", NULL, NULL);
// 初始化ImGui
ImGui::CreateContext();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
while (!glfwWindowShouldClose(window))
{
// 开始新帧
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// 创建UI
ImGui::Begin("Demo Window");
ImGui::Text("Hello, world!");
if (ImGui::Button("Save"))
{
// 按钮点击处理
}
ImGui::End();
// 渲染
ImGui::Render();
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
}

25
win_text_editor/.vscode/launch.json vendored

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
{
// 使 IntelliSense
//
// 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "win_text_editor",
"request": "launch",
"type": "dart"
},
{
"name": "win_text_editor (profile mode)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "win_text_editor (release mode)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

4
win_text_editor/.vscode/settings.json vendored

@ -1 +1,3 @@ @@ -1 +1,3 @@
{}
{
"cmake.sourceDirectory": "D:/aigc/manta/win_text_editor/linux"
}

342
win_text_editor/assets/config/uft_macro_list.yaml

@ -0,0 +1,342 @@ @@ -0,0 +1,342 @@
templates:
获取记录:
body: |
<M>[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
][
{{#selectedFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
[继续执行]
[记录为空][{{tableName}}]
{
[报错返回][ERR_???][{{#keyFields}}@{{name}}{{^isLast}}, {{/isLast}}{{/keyFields}}]
}else{
}
获取记录_ALL:
body: |
[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
][]
获取记录数:
body: "[获取记录数][{{tableName}}][@count]"
插入记录:
body: |
<C>[插入记录][{{tableName}}][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
[索引冲突]
{
[继续执行]
[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}} {{^isLast}}, {{/isLast}}
{{/keyFields}}
]
[修改记录][{{tableName}}][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
<F>[修改索引字段][{{tableName}}][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
}
修改记录:
body: |
<M>[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
[继续执行]
[记录不为空][{{tableName}}]
{
[修改记录][{{tableName}}][
{{#selectedFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
<F>[修改索引字段][{{tableName}}][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
}
修改记录_ALL:
body: |
<M>[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
[继续执行]
[记录不为空][{{tableName}}]
{
[修改记录][{{tableName}}][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
<F>[修改索引字段][{{tableName}}][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
}
删除记录:
body: |
<M>[获取记录][{{tableName}}({{keyName}})][
{{#keyFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/keyFields}}
]
[继续执行]
[记录不为空][{{tableName}}]
{
[删除记录][{{tableName}}]
}
遍历记录:
body: |
[遍历记录开始][{{tableName}}({{selectIndexOrKey.name}})][
{{#selectIndexOrKey.fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/selectIndexOrKey.fields}}
][
{{#selectedFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
{
//balabala
}
[遍历记录结束]
遍历记录_ALL:
body: |
[遍历记录开始][{{tableName}}][][]
遍历记录结束_ALL:
body: |
[遍历记录结束]
插入组件:
body: |
[插入组件][{{name}}][0][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
修改组件:
body: |
[修改组件][{{name}}][@num][
{{#selectedFields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
修改组件_ALL:
body: |
[修改组件][{{name}}][@num][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
获取组件:
body: |
{{#hasIndex}}[组件排序][{{name}}(index.name)]{{/hasIndex}}
[获取组件][{{name}}{{#hasIndex}}({{index.name}}){{/hasIndex}}][{{^hasIndex}}@num][{{/hasIndex}}
{{#hasIndex}}
{{#index.fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/index.fields}}
][
{{/hasIndex}}
{{#selectedFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
获取组件_ALL:
body: |
{{#hasIndex}}[组件排序][{{name}}(index.name)]{{/hasIndex}}
[获取组件][{{name}}{{#hasIndex}}({{index.name}}){{/hasIndex}}][{{^hasIndex}}@num][{{/hasIndex}}
{{#hasIndex}}
{{#index.fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/index.fields}}
][
{{/hasIndex}}]
遍历组件:
body: |
{{#hasIndex}}[组件排序][{{name}}({{index.name}})]{{/hasIndex}}
[遍历组件开始][{{name}}{{#hasIndex}}({{index.name}}){{/hasIndex}}][
{{#hasIndex}}
{{#index.fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/index.fields}}
{{/hasIndex}}
][
{{#selectedFields}}
{{name}} = @{{name}}{{^isLast}}, {{/isLast}}
{{/selectedFields}}
]
{
}
[遍历组件结束]
组件大小:
body: "[组件大小][{{name}}][@num]"
尾部插入组件:
body: |
[尾部插入组件][{{name}}][
{{#fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}}, {{/isLast}}
{{/fields}}
]
遍历组件_ALL:
body: |
{{#hasIndex}}[组件排序][{{name}}({{index.name}})]{{/hasIndex}}
[遍历组件开始][{{name}}{{#hasIndex}}({{index.name}}){{/hasIndex}}][
{{#hasIndex}}
{{#index.fields}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}} {{^isLast}}, {{/isLast}}
{{/index.fields}}
{{/hasIndex}}
]
遍历组件结束_ALL:
body: |
[遍历组件结束]
普通调用_ALL:
body: |
[{{chineseName}}][
{{#input}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}},{{/isLast}}
{{/input}}
][
{{#output}}
{{name}} = @{{name}}{{^isLast}},{{/isLast}}
{{/output}}
]
普通调用:
body: |
<M>[{{chineseName}}][
{{#selectedInput}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}},{{/isLast}}
{{/selectedInput}}
][
{{#selectedOutput}}
{{name}} = @{{name}}{{^isLast}},{{/isLast}}
{{/selectedOutput}}
]
[处理失败]
{
[错误信息获取]
//TODO:处理异常
[继续执行]
}
else
{
//balabala
}
事务调用:
body: |
[事务处理开始]
<M>[{{chineseName}}][
{{#selectedInput}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}},{{/isLast}}
{{/selectedInput}}
][
{{#selectedOutput}}
{{name}} = @{{name}}{{^isLast}},{{/isLast}}
{{/selectedOutput}}
]
[处理失败]
{
[错误信息获取]
//TODO:处理异常
[事务回滚]
[继续执行]
}
else
{
[事务处理结束]
}
事务调用_ALL:
body: |
[事务处理开始]
<M>[{{chineseName}}][
{{#input}}
{{name}} = @{{#partnerName}}{{partnerName}}.{{/partnerName}}{{name}}{{^isLast}},{{/isLast}}
{{/input}}
][
{{#output}}
{{name}} = @{{name}}{{^isLast}},{{/isLast}}
{{/output}}
]
[处理失败]
{
[错误信息获取]
//TODO:处理异常
[事务回滚]
[继续执行]
}
else
{
[事务处理结束]
}
因子服务:
body: |
LPFN_RISK lpOpenRisk = lpIUFTContext->GetRiskEntry({{functionNo}});
if(NULL == lpOpenRisk)
{
[记录日志][CNST_DLOG_ERROR][]["获取因子RS_{{functionNo}}失败"][]
}
lpIUFTContext->AsyncExecMsg((void*) lpOpenRisk, lpIUFTContext, (void*)&p_C{{factorParam}});
手工打包:
body: |
[手工打包头][{{#selectedFields}}{{name}}{{^isLast}}, {{/isLast}}{{/selectedFields}}]
[手工打包体][{{#selectedFields}}@{{parentName}}.{{name}}{{^isLast}}, {{/isLast}}{{/selectedFields}}]
[手工打包结束]

2
win_text_editor/assets/config/words_classes.yaml

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
outline_name_black_list:
- 历史,日志,名称,比例,数量,金额,次数,属性,对应,分类,姓名,单位,总数,行使,子项,占比,记录,列表,目标,字段,字符串,动作,方式,类型,类别

349045
win_text_editor/assets/dict.txt

File diff suppressed because it is too large Load Diff

0
win_text_editor/assets/prob_emit.txt

104
win_text_editor/lib/app/app.dart

@ -1,104 +0,0 @@ @@ -1,104 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/menus/app_menu.dart';
import 'package:win_text_editor/app/providers/tab_provider.dart';
import 'package:win_text_editor/app/providers/file_provider.dart';
import 'package:win_text_editor/app/widgets/editor_pane.dart';
import 'package:win_text_editor/app/widgets/file_explorer.dart';
import 'package:win_text_editor/app/widgets/console_panel.dart'; //
class AppScaffold extends StatelessWidget {
const AppScaffold({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => FileProvider()),
ChangeNotifierProvider(create: (_) => TabProvider()),
],
child: const Scaffold(
body: Column(
children: [
//
AppMenu(),
//
Expanded(child: _ResizablePanel()),
],
),
),
);
}
}
class _ResizablePanel extends StatefulWidget {
const _ResizablePanel();
@override
State<_ResizablePanel> createState() => _ResizablePanelState();
}
class _ResizablePanelState extends State<_ResizablePanel> {
double _leftWidth = 0.2; // 20%
final double _minWidth = 100; //
final double _maxWidth = 400; //
@override
Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
return LayoutBuilder(
builder: (context, constraints) {
//
final leftPanelWidth = (_leftWidth * screenWidth).clamp(_minWidth, _maxWidth);
return Column(
children: [
Expanded(
child: Row(
children: [
// - Material小部件包裹
Material(
elevation: 1, //
child: SizedBox(
width: leftPanelWidth,
child: const ClipRect(
//
child: FileExplorer(),
),
),
),
//
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (details) {
setState(() {
_leftWidth = ((leftPanelWidth + details.delta.dx) / screenWidth).clamp(
_minWidth / screenWidth,
_maxWidth / screenWidth,
);
});
},
child: MouseRegion(
cursor: SystemMouseCursors.resizeLeftRight,
child: Container(width: 4, color: Colors.grey[300]),
),
),
// - Material背景
const Expanded(
child: Material(
color: Colors.white,
child: Column(children: [Expanded(child: EditorPane())]),
),
),
],
),
),
//
const ConsolePanel(),
],
);
},
);
}
}

66
win_text_editor/lib/app/menus/menu_actions.dart

@ -1,66 +0,0 @@ @@ -1,66 +0,0 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/menus/menu_constants.dart';
import 'package:win_text_editor/app/providers/file_provider.dart';
import 'package:win_text_editor/app/providers/tab_provider.dart';
import 'package:collection/collection.dart';
import 'dart:io';
class MenuActions {
//
static const String templateParserTabType = "template_parser";
static const String templateParserTabTitle = "模板解析";
static const IconData templateParserTabIcon = Icons.auto_awesome_mosaic;
static const String templateParserDefaultContent = "";
static Future<void> handleMenuAction(String value, BuildContext context) async {
switch (value) {
case MenuConstants.openFolder:
await _openFolder(context);
break;
case MenuConstants.templateParser:
await _openTemplateParser(context);
break;
case MenuConstants.exit:
_exitApplication();
break;
//
}
}
static Future<void> _openFolder(BuildContext context) async {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
final String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory != null) {
await fileProvider.loadDirectory(selectedDirectory);
}
}
static Future<void> _openTemplateParser(BuildContext context) async {
final editorProvider = Provider.of<TabProvider>(context, listen: false);
// 使 firstWhereOrNull
final existingTab = editorProvider.tabs.firstWhereOrNull(
(tab) => tab.type == templateParserTabType,
);
if (existingTab != null) {
//
editorProvider.setActiveTab(existingTab.id);
} else {
//
await editorProvider.addTab(
title: templateParserTabTitle,
type: templateParserTabType,
icon: templateParserTabIcon,
content: templateParserDefaultContent,
);
}
}
static void _exitApplication() {
exit(0);
}
}

75
win_text_editor/lib/app/services/file_service.dart

@ -1,75 +0,0 @@ @@ -1,75 +0,0 @@
import 'dart:io';
import 'package:win_text_editor/app/models/file_node.dart';
class FileService {
///
static Future<List<FileNode>> listDirectory(String path, {int parentDepth = 0}) async {
final directory = Directory(path);
final List<FileNode> nodes = [];
if (await directory.exists()) {
final entities = directory.listSync();
// FileNode
for (final entity in entities) {
nodes.add(
FileNode(
name: entity.path.split(Platform.pathSeparator).last,
path: entity.path,
isDirectory: entity is Directory,
depth: parentDepth + 1,
children: [],
),
);
}
//
nodes.sort((a, b) {
if (a.isDirectory && !b.isDirectory) {
return -1; // a是文件夹b是文件a排在前面
} else if (!a.isDirectory && b.isDirectory) {
return 1; // a是文件b是文件夹b排在前面
} else {
//
return a.name.compareTo(b.name);
}
});
}
return nodes;
}
///
static Future<List<FileNode>> buildFileTree(String rootPath) async {
final rootDirectory = Directory(rootPath);
final List<FileNode> nodes = [];
if (await rootDirectory.exists()) {
final entities = rootDirectory.listSync();
for (final entity in entities) {
final node = FileNode(
name: entity.path.split(Platform.pathSeparator).last,
path: entity.path,
isDirectory: entity is Directory,
);
if (entity is Directory) {
node.children.addAll(await buildFileTree(entity.path));
}
nodes.add(node);
}
}
return nodes;
}
static Future<String> readFile(String filePath) async {
return await File(filePath).readAsString();
}
static Future<void> writeFile(String filePath, String content) async {
await File(filePath).writeAsString(content);
}
}

31
win_text_editor/lib/app/services/syntax_service.dart

@ -1,31 +0,0 @@ @@ -1,31 +0,0 @@
class SyntaxService {
static String detectFileType(String fileName) {
final extension = fileName.split('.').last.toLowerCase();
switch (extension) {
case 'dart':
return 'dart';
case 'java':
return 'java';
case 'js':
return 'javascript';
case 'py':
return 'python';
case 'html':
return 'html';
case 'css':
return 'css';
case 'json':
return 'json';
case 'xml':
return 'xml';
case 'md':
return 'markdown';
case 'yaml':
case 'yml':
return 'yaml';
default:
return 'text';
}
}
}

59
win_text_editor/lib/app/utils/file_utils.dart

@ -1,59 +0,0 @@ @@ -1,59 +0,0 @@
// file_utils.dart
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/app/providers/logger.dart';
class FileUtils {
static Future<String?> pickFile(BuildContext context) async {
try {
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);
return result?.files.single.path;
} catch (e) {
Logger().error('选择文件失败: ${e.toString()}');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('选择文件失败: ${e.toString()}')));
}
return null;
}
}
static Future<String?> readFileContent(
BuildContext context,
String filePath, {
Duration timeout = const Duration(seconds: 30),
}) async {
try {
final content = await File(filePath).readAsString().timeout(
timeout,
onTimeout: () {
throw TimeoutException('文件加载超时,可能文件过大');
},
);
return content;
} on FormatException {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
return null;
} on FileSystemException catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}')));
}
return null;
} catch (e) {
Logger().error('读取文件失败: ${e.toString()}');
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
return null;
}
}
// showLoadingDialog方法
}

84
win_text_editor/lib/app/widgets/editor_pane.dart

@ -1,84 +0,0 @@ @@ -1,84 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/providers/tab_provider.dart';
import 'template_parser_tab.dart';
class EditorPane extends StatelessWidget {
const EditorPane({super.key});
@override
Widget build(BuildContext context) {
final provider = Provider.of<TabProvider>(context);
return Column(
children: [
//
SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: provider.tabs.length,
itemBuilder: (ctx, index) {
final tab = provider.tabs[index];
return _TabItem(
title: tab.title,
icon: tab.icon, //
isActive: tab.id == provider.activeTabId,
onClose: () => provider.closeTab(tab.id),
onTap: () => provider.setActiveTab(tab.id),
);
},
),
),
//
Expanded(
child:
provider.activeTabId != null && provider.tabs.any((t) => t.id == provider.activeTabId)
? TemplateParserTab(tabId: provider.activeTabId!)
: const Center(child: Text('无活动标签页')),
),
],
);
}
}
class _TabItem extends StatelessWidget {
final String title;
final IconData? icon; //
final bool isActive;
final VoidCallback onClose;
final VoidCallback onTap;
const _TabItem({
required this.title,
this.icon,
required this.isActive,
required this.onClose,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: isActive ? Colors.blue[100] : Colors.grey[200],
border: Border(
bottom: BorderSide(color: isActive ? Colors.blue : Colors.transparent, width: 2),
),
),
child: Row(
children: [
if (icon != null) Icon(icon, size: 16),
if (icon != null) const SizedBox(width: 4),
Text(title),
const SizedBox(width: 8),
IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose),
],
),
),
);
}
}

144
win_text_editor/lib/app/widgets/template_parser_tab.dart

@ -1,144 +0,0 @@ @@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/app/providers/tab_provider.dart';
import 'package:win_text_editor/app/widgets/text_editor.dart';
class TemplateParserTab extends StatefulWidget {
final String tabId;
const TemplateParserTab({super.key, required this.tabId});
@override
State<TemplateParserTab> createState() => TemplateParserTabState();
}
class TemplateParserTabState extends State<TemplateParserTab> {
late TabProvider _provider;
String? _editor1FileName;
String? _editor2FileName;
String _editor1Content = '';
String _editor2Content = '';
int _activeEditorIndex = 0; // 01
final GlobalKey<TextEditorState> _editor1Key = GlobalKey();
final GlobalKey<TextEditorState> _editor2Key = GlobalKey();
//
void _setupFocusListeners() {
_editor1Key.currentState?.focusNode.addListener(() {
if (_editor1Key.currentState?.hasFocus ?? false) {
setState(() => _activeEditorIndex = 0);
}
});
_editor2Key.currentState?.focusNode.addListener(() {
if (_editor2Key.currentState?.hasFocus ?? false) {
setState(() => _activeEditorIndex = 1);
}
});
}
@override
void initState() {
super.initState();
_provider = Provider.of<TabProvider>(context, listen: false);
_provider.registerTextTabController(widget.tabId, this);
final tab = _provider.getTabById(widget.tabId);
if (tab != null) {
_editor1Content = tab.content;
_editor2Content = tab.content;
_editor1FileName = tab.fileName;
_editor2FileName = tab.fileName;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_setupFocusListeners();
});
}
// loadFile方法使用_activeEditorIndex
Future<void> loadFile(BuildContext context, String filePath) async {
if (_activeEditorIndex == 0) {
_editor1Key.currentState?.loadFile(context, filePath);
} else {
_editor2Key.currentState?.loadFile(context, filePath);
}
}
@override
Widget build(BuildContext context) {
final tab = _provider.getTabById(widget.tabId);
if (tab == null) {
return const Center(child: Text('选项卡不存在'));
}
return SingleChildScrollView(
child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 400),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: SizedBox(
height: MediaQuery.of(context).size.height / 2 - 100,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: _activeEditorIndex == 0 ? Colors.blue : Colors.transparent,
width: 2.0,
),
),
child: TextEditor(
key: _editor1Key,
tabId: '${widget.tabId}_1',
title: '源文本', //
initialContent: _editor1Content,
fileName: _editor1FileName,
onContentChanged: (content, fileName) {
setState(() {
_editor1Content = content;
_editor1FileName = fileName;
});
_provider.updateContent(widget.tabId, content, fileName);
},
onFileLoaded: (filePath) {
//
},
),
),
),
),
Flexible(
child: SizedBox(
height: MediaQuery.of(context).size.height / 2 - 100,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: _activeEditorIndex == 1 ? Colors.blue : Colors.transparent,
width: 2.0,
),
),
child: TextEditor(
key: _editor2Key,
tabId: '${widget.tabId}_2',
title: '目标文本', //
initialContent: _editor2Content,
fileName: _editor2FileName,
onContentChanged: (content, fileName) {
setState(() {
_editor2Content = content;
_editor2FileName = fileName;
});
_provider.updateContent(widget.tabId, content, fileName);
},
onFileLoaded: (filePath) {
//
},
),
),
),
),
],
),
),
);
}
}

328
win_text_editor/lib/app/widgets/text_editor.dart

@ -1,328 +0,0 @@ @@ -1,328 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:file_picker/file_picker.dart';
class TextEditor extends StatefulWidget {
final String tabId;
final String? initialContent;
final String? fileName;
final String title;
final Function(String, String?)? onContentChanged;
final Function(String)? onFileLoaded;
const TextEditor({
super.key,
required this.tabId,
this.initialContent,
this.fileName,
this.title = '未命名',
this.onContentChanged,
this.onFileLoaded,
});
@override
State<TextEditor> createState() => TextEditorState();
}
class TextEditorState extends State<TextEditor> {
late TextEditingController _controller;
late ScrollController _scrollController;
bool _isLoading = false;
static const int maxFileSize = 1024 * 1024; // 1MB
bool get hasFocus => _focusNode.hasFocus;
FocusNode get _focusNode => focusNode; // _focusNode改为focusNode
late FocusNode focusNode = FocusNode(); //
void loadFile(BuildContext context, String filePath) async {
await _loadFile(context, filePath);
}
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialContent ?? '');
focusNode = FocusNode();
_scrollController = ScrollController();
WidgetsBinding.instance.addPostFrameCallback((_) {
FocusScope.of(context).requestFocus(focusNode);
});
}
@override
void didUpdateWidget(TextEditor oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.initialContent != widget.initialContent) {
_controller.text = widget.initialContent ?? '';
}
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: hasFocus ? Colors.blue[50] : Colors.grey[100],
border: Border(
bottom: BorderSide(
color: hasFocus ? Colors.blue : Colors.grey[300]!,
width: hasFocus ? 2.0 : 1.0,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${widget.title}${_controller.text.isEmpty ? '' : ' (${widget.fileName ?? ''}${_controller.text.length}字符)'}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.folder_open, size: 20),
tooltip: '打开文件',
onPressed: _isLoading ? null : () => _openFile(context),
),
IconButton(
icon: const Icon(Icons.content_copy, size: 20),
tooltip: '复制内容',
onPressed:
_controller.text.isEmpty
? null
: () => _copyToClipboard(context, _controller.text),
),
IconButton(
icon: const Icon(Icons.save, size: 20),
tooltip: '保存到文件',
onPressed:
_controller.text.isEmpty
? null
: () => _saveFile(context, _controller.text),
),
if (_isLoading)
const Padding(
padding: EdgeInsets.only(left: 8),
child: SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
],
),
],
),
),
Expanded(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
dragDevices: {PointerDeviceKind.touch, PointerDeviceKind.mouse},
),
child: SingleChildScrollView(
controller: _scrollController,
child: Stack(
children: [
TextField(
controller: _controller,
focusNode: _focusNode,
maxLines: null,
onChanged: (text) {
widget.onContentChanged?.call(text, widget.fileName);
},
decoration: const InputDecoration(
border: InputBorder.none,
contentPadding: EdgeInsets.all(16),
),
style: const TextStyle(
fontFamily: 'Courier New',
fontSize: 16,
color: Colors.black,
),
),
],
),
),
),
),
],
);
}
Future<void> _openFile(BuildContext context) async {
final result = await FilePicker.platform.pickFiles(type: FileType.any, allowMultiple: false);
if (result != null && result.files.single.path != null) {
await _loadFile(context, result.files.single.path!);
}
}
Future<void> _copyToClipboard(BuildContext context, String content) async {
await Clipboard.setData(ClipboardData(text: content));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('已复制到剪贴板')));
}
}
Future<void> _saveFile(BuildContext context, String content) async {
try {
String? outputPath = await FilePicker.platform.saveFile(
dialogTitle: '保存文件',
fileName: 'untitled.txt',
allowedExtensions: ['txt'],
type: FileType.any,
);
if (outputPath == null) return;
final file = File(outputPath);
if (await file.exists()) {
final shouldOverwrite = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('文件已存在'),
content: const Text('要覆盖现有文件吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('覆盖'),
),
],
),
);
if (shouldOverwrite != true) return;
}
await file.writeAsString(content);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已保存到: ${file.path}')));
}
} on FileSystemException catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存失败: ${e.message}')));
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('保存失败: ${e.toString()}')));
}
}
}
Future<void> _loadFile(BuildContext context, String filePath) async {
try {
setState(() => _isLoading = true);
final file = File(filePath);
final fileSize = await file.length();
//
if (fileSize > maxFileSize) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('文件过大(超过1MB),无法处理')));
}
return;
}
//
if (_controller.text.isNotEmpty) {
final confirm = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('确认提示'),
content: const Text('确认是否打开新的文件?打开后将覆盖当前内容'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('确认'),
),
],
),
);
if (confirm != true) {
return;
}
}
final fileName = file.path.split('\\').last;
//
_controller.text = '';
widget.onContentChanged?.call('', fileName);
//
final stream = file.openRead();
final lines = stream.transform(utf8.decoder).transform(const LineSplitter());
await for (final line in lines) {
if (!mounted) break;
setState(() {
_controller.text += '$line\n';
widget.onContentChanged?.call(_controller.text, fileName);
});
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
});
await Future.delayed(const Duration(milliseconds: 10));
}
widget.onFileLoaded?.call(file.path);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('已加载: ${file.path}')));
}
} on FormatException {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('这不是可读的文本文件')));
}
} on FileSystemException catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件访问错误: ${e.message}')));
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('读取失败: ${e.toString()}')));
}
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
}

7
win_text_editor/lib/framework/common/constants.dart

@ -0,0 +1,7 @@ @@ -0,0 +1,7 @@
class Constants {
static const String uftTable = 'uftTable';
static const String component = 'component';
static const String business = 'business';
static const String atomService = 'atomService';
static const String atomFunction = 'atomFunction';
}

33
win_text_editor/lib/app/providers/file_provider.dart → win_text_editor/lib/framework/controllers/file_provider.dart

@ -2,9 +2,9 @@ import 'dart:io'; @@ -2,9 +2,9 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:win_text_editor/app/models/file_node.dart';
import 'package:win_text_editor/app/providers/logger.dart';
import 'package:win_text_editor/app/services/file_service.dart';
import 'package:win_text_editor/framework/models/file_node.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/services/file_service.dart';
class FileProvider with ChangeNotifier {
List<FileNode> _fileNodes = [];
@ -18,9 +18,12 @@ class FileProvider with ChangeNotifier { @@ -18,9 +18,12 @@ class FileProvider with ChangeNotifier {
// _initFileTree调用
FileProvider();
String? get rootPath => _currentRootPath;
//
Future<void> setRootPath(String path) async {
_currentRootPath = path;
notifyListeners();
await _loadRootDirectory();
}
@ -58,14 +61,15 @@ class FileProvider with ChangeNotifier { @@ -58,14 +61,15 @@ class FileProvider with ChangeNotifier {
try {
final directory = Directory(path);
final displayName = await FileService.getModuleDisplayName(directory.path);
final rootNode = FileNode(
name: directory.path.split(Platform.pathSeparator).last,
name: displayName ?? directory.path.split(Platform.pathSeparator).last,
path: directory.path,
isDirectory: true,
isRoot: true, //
isRoot: true,
children: await FileService.buildFileTree(directory.path),
);
_fileNodes = [rootNode]; //
_fileNodes = [rootNode];
} catch (e) {
Logger().error('Error loading directory: $e');
_fileNodes = [];
@ -82,9 +86,10 @@ class FileProvider with ChangeNotifier { @@ -82,9 +86,10 @@ class FileProvider with ChangeNotifier {
notifyListeners();
try {
final displayName = await FileService.getModuleDisplayName(_currentRootPath!);
_fileNodes = [
FileNode(
name: _currentRootPath!.split(Platform.pathSeparator).last,
name: displayName ?? _currentRootPath!.split(Platform.pathSeparator).last,
path: _currentRootPath!,
isDirectory: true,
isRoot: true,
@ -106,9 +111,10 @@ class FileProvider with ChangeNotifier { @@ -106,9 +111,10 @@ class FileProvider with ChangeNotifier {
notifyListeners();
try {
final displayName = await FileService.getModuleDisplayName(path);
_fileNodes = [
FileNode(
name: path.split(Platform.pathSeparator).last,
name: displayName ?? path.split(Platform.pathSeparator).last,
path: path,
isDirectory: true,
isRoot: true,
@ -159,16 +165,9 @@ class FileProvider with ChangeNotifier { @@ -159,16 +165,9 @@ class FileProvider with ChangeNotifier {
notifyListeners();
try {
final contents = await FileService.listDirectory(
dirNode.path,
parentDepth: dirNode.depth, //
);
final contents = await FileService.listDirectory(dirNode.path, parentDepth: dirNode.depth);
final updatedNode = dirNode.copyWith(
children: contents,
isExpanded: true,
// depth copyWith
);
final updatedNode = dirNode.copyWith(children: contents, isExpanded: true);
_replaceNodeInTree(dirNode, updatedNode);
} catch (e) {

1
win_text_editor/lib/app/providers/logger.dart → win_text_editor/lib/framework/controllers/logger.dart

@ -53,7 +53,6 @@ class Logger with ChangeNotifier { @@ -53,7 +53,6 @@ class Logger with ChangeNotifier {
}
void _addLog(LogEntry entry) {
print('Adding log: $entry');
if (entry.level.index >= _minimumLevel.index) {
_logs.add(entry);
notifyListeners();

165
win_text_editor/lib/framework/controllers/tab_items_controller.dart

@ -0,0 +1,165 @@ @@ -0,0 +1,165 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_js/quickjs/ffi.dart';
import 'package:win_text_editor/framework/models/tab_model.dart';
import 'package:win_text_editor/modules/module_router.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
class TabItemsController with ChangeNotifier {
final List<AppTab> _tabs = [];
final Map<String, BaseContentController> _contentControllers = {}; // Tab的Controller
String? _activeTabId;
List<AppTab> get tabs => _tabs;
String? get activeTabId => _activeTabId;
AppTab? get activeTab {
if (_activeTabId == null) return null;
try {
return _tabs.firstWhere((tab) => tab.id == _activeTabId);
} catch (e) {
Logger().error("找不到活动选项卡: $_activeTabId", source: 'EditorProvider');
return null;
}
}
Future<void> addTab(
String id, {
String title = '未命名',
String? type,
IconData? icon,
String content = '',
}) async {
//tab组件
final newTab = AppTab(id: id, title: title, type: type, icon: icon, content: content);
_tabs.add(newTab);
// provider
final controller = createContentController(newTab);
if (controller != null) {
_contentControllers[id] = controller;
}
_activeTabId = id;
notifyListeners();
}
AppTab? getTabById(String tabId) {
try {
return _tabs.firstWhere((tab) => tab.id == tabId);
} catch (e) {
Logger().error("找不到选项卡: $tabId", source: 'EditorProvider');
return null;
}
}
BaseContentController? createContentController(AppTab tab) {
return ModuleRouter.createControllerForTab(tab);
}
BaseContentController? get activeContentController {
if (_activeTabId == null) {
return null;
}
final controller = _contentControllers[_activeTabId];
if (controller == null) {
Logger().error("没有活动的内容控制器,activeTabId:$_activeTabId", source: 'TabItemsController');
return null;
}
// ignore: unnecessary_type_check
if (controller is! BaseContentController) {
Logger().error("活动内容控制器不是BaseContentController类型", source: 'TabItemsController');
return null;
}
return controller;
}
void closeTab(String tabId) {
final controller = _contentControllers[tabId];
controller?.dispose(); // Controller
_contentControllers.remove(tabId); // Controller引用
_tabs.removeWhere((tab) => tab.id == tabId); // Tab
if (_activeTabId == tabId) {
_activeTabId = _tabs.isNotEmpty ? _tabs.last.id : null;
}
notifyListeners();
}
void registerController(String tabId, BaseContentController controller) {
_contentControllers[tabId] = controller;
}
T? getController<T extends ChangeNotifier>(String tabId) {
return _contentControllers[tabId] as T?;
}
void disposeController(String tabId) {
_contentControllers[tabId]?.dispose();
_contentControllers.remove(tabId);
}
void setActiveTab(String tabId) {
if (_activeTabId == tabId) return; // Tab则不通知
_activeTabId = tabId;
notifyListeners();
}
void handleFolderDoubleTap(String folderPath) {
activeContentController?.onOpenFolder(folderPath);
}
void handleFileDoubleTap(String filePath, dynamic appendArg) {
Logger().debug('双击事件参数:filePath:$filePath,appendArg:$appendArg');
final fileName = filePath.split(Platform.pathSeparator).last;
if (fileName == "component.xml") {
openOrActivateTab("标准组件", RouterKey.uftComponent, Icons.extension);
} else {
final fileExtension = _getFileExtension(fileName);
switch (fileExtension) {
case 'uftstructure':
openOrActivateTab("内存表", RouterKey.memoryTable, Icons.list);
break;
case 'uftfunction':
case 'uftatomfunction':
case 'uftatomservice':
case 'uftfactorfunction':
case 'uftfactorservice':
openOrActivateTab("函数调用", RouterKey.callFunction, Icons.functions);
break;
default:
Logger().error("没有活动的内容控制器", source: 'TabItemsController');
}
}
activeContentController?.onOpenFile(filePath, appendArg: appendArg);
}
bool hasController(String tabId) {
return _contentControllers.containsKey(tabId);
}
//
Future<void> openOrActivateTab(String title, String type, IconData icon) async {
try {
final existingTab = _tabs.firstWhereOrNull((tab) => tab.type == type);
Logger().debug("获取活动Tab页${existingTab?.title}");
if (existingTab != null) {
setActiveTab(existingTab.id);
} else {
final tabId = DateTime.now().millisecondsSinceEpoch.toString();
await addTab(tabId, title: title, type: type, icon: icon, content: "");
}
} catch (e) {
Logger().error("打开或激活标签页失败: $e", source: 'TabItemsController');
}
}
static String _getFileExtension(String filePath) {
final parts = filePath.split('.');
return parts.length > 1 ? parts.last.toLowerCase() : '';
}
}

26
win_text_editor/lib/app/models/file_node.dart → win_text_editor/lib/framework/models/file_node.dart

@ -1,15 +1,27 @@ @@ -1,15 +1,27 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class FileNode {
class FileNode implements TreeNode {
@override
final String name;
final String path;
@override
final bool isDirectory;
final bool isRoot;
@override
final int depth;
@override
List<FileNode> children;
@override
bool isExpanded;
@override
String get title => name;
@override
bool get isVisible => true;
FileNode({
required this.name,
required this.path,
@ -20,7 +32,11 @@ class FileNode { @@ -20,7 +32,11 @@ class FileNode {
List<FileNode>? children,
}) : children = children ?? [];
@override
String get id => path;
//
@override
IconData get iconData {
if (isDirectory) {
return Icons.folder;
@ -87,19 +103,19 @@ class FileNode { @@ -87,19 +103,19 @@ class FileNode {
String? name,
String? path,
bool? isDirectory,
bool? isExpanded,
bool? isRoot,
int? depth, // depth参数
List<FileNode>? children,
bool? isExpanded,
int? depth,
}) {
return FileNode(
name: name ?? this.name,
path: path ?? this.path,
isDirectory: isDirectory ?? this.isDirectory,
isExpanded: isExpanded ?? this.isExpanded,
isRoot: isRoot ?? this.isRoot,
depth: depth ?? this.depth, // depth或使用新值
children: children ?? this.children,
isExpanded: isExpanded ?? this.isExpanded,
depth: depth ?? this.depth,
);
}

4
win_text_editor/lib/app/models/tab_model.dart → win_text_editor/lib/framework/models/tab_model.dart

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
class ContentTab {
class AppTab {
final String id;
final String title;
final String? type; //
@ -8,7 +8,7 @@ class ContentTab { @@ -8,7 +8,7 @@ class ContentTab {
String content;
String? fileName;
ContentTab({
AppTab({
required this.id,
required this.title,
this.type,

74
win_text_editor/lib/framework/services/fast_xml_parser.dart

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
import 'dart:convert';
import 'dart:io';
import 'package:xml/xml.dart';
class FastXmlParser {
/// XML文件根节点属性
static Future<List<Map<String, String>>> batchParse(
List<String> filePaths, {
int maxFastParseBytes = 1024,
}) async {
return await Future.wait(filePaths.map((path) => _parseWithFallback(path, maxFastParseBytes)));
}
static Future<Map<String, String>> parse(String filePath, {int maxFastParseBytes = 512}) async {
return _parseWithFallback(filePath, maxFastParseBytes);
}
///
static Future<Map<String, String>> _parseWithFallback(String path, int maxBytes) async {
try {
//
return await _fastParseRootAttributes(path, maxBytes);
} catch (e) {
// DOM解析
return await _domParseRootAttributes(path);
}
}
///
static Future<Map<String, String>> _fastParseRootAttributes(String path, int maxBytes) async {
final file = File(path);
final raf = await file.open();
try {
//
final buffer = await raf.read(maxBytes);
final content = utf8.decode(buffer);
// 使
final startTag = RegExp(
r'(?:<\?xml[^?>]*\?>[\s\r\n]*)?<([^\s>]+)([^>]*)>',
caseSensitive: false,
).firstMatch(content);
if (startTag == null) throw Exception('No start tag found');
final attrs = _parseAttributes(startTag.group(2)!);
return _extractRequiredAttributes(attrs);
} finally {
await raf.close();
}
}
/// DOM解析降级方案
static Future<Map<String, String>> _domParseRootAttributes(String path) async {
final content = await File(path).readAsString();
final document = XmlDocument.parse(content);
final attrs = document.rootElement.attributes.fold<Map<String, String>>(
{},
(map, attr) => map..[attr.name.qualified] = attr.value,
);
return _extractRequiredAttributes(attrs);
}
///
static Map<String, String> _parseAttributes(String attrStr) {
return RegExp(r'(\w+)="([^"]*)"')
.allMatches(attrStr)
.fold<Map<String, String>>({}, (map, match) => map..[match.group(1)!] = match.group(2)!);
}
///
static Map<String, String> _extractRequiredAttributes(Map<String, String> attrs) {
return {'chineseName': attrs['chineseName'] ?? '', 'objectId': attrs['objectId'] ?? ''};
}
}

21
win_text_editor/lib/framework/services/file_path_manager.dart

@ -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;
}
}

124
win_text_editor/lib/framework/services/file_service.dart

@ -0,0 +1,124 @@ @@ -0,0 +1,124 @@
import 'dart:io';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/models/file_node.dart';
import 'package:win_text_editor/framework/services/fast_xml_parser.dart';
import 'package:xml/xml.dart';
class FileService {
static const _specialExtensions = [
'.uftfunction',
'.uftservice',
'.uftatomfunction',
'.uftatomservice',
'.uftfactorfunction',
'.uftfactorservice',
];
static const Map<String, String> _uftFloders = {
'.settings': '项目设置',
'metadata': '元数据',
'tools': '工具资源',
'uftatom': 'UFT原子',
'uftbusiness': 'UFT业务逻辑',
'uftfactor': 'UFT因子',
'uftstructure': 'UFT对象',
};
static const _hiddenFiles = ['.classpath', '.project', '.respath', 'project.xml', 'module.xml'];
static Future<String?> getSpecialFileName(String filePath) async {
final extension = filePath.substring(filePath.lastIndexOf('.'));
if (!_specialExtensions.contains(extension)) {
return null;
}
try {
final result = await FastXmlParser.parse(filePath);
return ('[${result['objectId']}]${result['chineseName']}');
} catch (e) {
Logger().debug('Error reading special file: $e');
}
return null;
}
///
static Future<List<FileNode>> listDirectory(String path, {int parentDepth = 0}) async {
final dir = Directory(path);
final List<FileSystemEntity> entities = await dir.list().toList();
final List<FileNode> nodes = [];
// final stopwatch = Stopwatch()..start();
for (final entity in entities) {
final pathName = entity.path.split(Platform.pathSeparator).last;
if (_hiddenFiles.contains(pathName)) continue;
final isDirectory = await FileSystemEntity.isDirectory(entity.path);
final displayName =
isDirectory
? await getModuleDisplayName(entity.path)
: await getSpecialFileName(entity.path);
nodes.add(
FileNode(
name: displayName ?? pathName,
path: entity.path,
isDirectory: isDirectory,
depth: parentDepth + 1,
),
);
}
// stopwatch.stop();
// Logger().debug('执行耗时: ${stopwatch.elapsedMilliseconds} 毫秒 (ms)');
return nodes;
}
static Future<String?> getModuleDisplayName(String dirPath) async {
try {
final floderName = dirPath.split(Platform.pathSeparator).last;
if (_uftFloders.containsKey(floderName)) return _uftFloders[floderName];
final moduleFile = File('$dirPath${Platform.pathSeparator}module.xml');
if (await moduleFile.exists()) {
final content = await moduleFile.readAsString();
final xmlDoc = XmlDocument.parse(content);
final infoNode = xmlDoc.findAllElements('info').firstOrNull;
return infoNode?.getAttribute('cname');
}
} catch (e) {
Logger().debug('Error reading module.xml: $e');
}
return null;
}
///
static Future<List<FileNode>> buildFileTree(String rootPath) async {
final rootDirectory = Directory(rootPath);
final List<FileNode> nodes = [];
if (await rootDirectory.exists()) {
final entities = rootDirectory.listSync();
for (final entity in entities) {
final pathName = entity.path.split(Platform.pathSeparator).last;
if (_hiddenFiles.contains(pathName)) continue;
final node = FileNode(name: pathName, path: entity.path, isDirectory: entity is Directory);
if (entity is Directory) {
node.children.addAll(await buildFileTree(entity.path));
}
nodes.add(node);
}
}
return nodes;
}
static Future<String> readFile(String filePath) async {
return await File(filePath).readAsString();
}
static Future<void> writeFile(String filePath, String content) async {
await File(filePath).writeAsString(content);
}
}

73
win_text_editor/lib/framework/services/macro_template_service.dart

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import 'package:flutter/services.dart' show rootBundle;
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:yaml/yaml.dart';
import 'package:mustache_template/mustache_template.dart' as mustache;
class MacroTemplateService {
final Map<String, dynamic> _templates = {};
bool _inited = false;
bool get inited => _inited;
Future<void> init() async {
try {
final configString = await rootBundle.loadString('assets/config/uft_macro_list.yaml');
final yamlMap = loadYaml(configString);
if (yamlMap is! YamlMap) {
throw const FormatException('Invalid YAML structure: root should be a map');
}
final templates = yamlMap['templates'];
if (templates is YamlMap) {
_templates.clear();
templates.forEach((key, value) {
if (value is YamlMap) {
_templates[key] = {
'header': value['header']?.toString() ?? '',
'body': value['body']?.toString() ?? '',
'footer': value['footer']?.toString() ?? '',
};
}
});
}
_inited = true;
} catch (e) {
throw Exception('Failed to load macro templates: $e');
}
}
Map<String, dynamic>? getTemplate(String templateName) {
return _templates[templateName] is Map
? Map<String, dynamic>.from(_templates[templateName])
: null;
}
String renderTemplate(
List<String> templateList,
Map<String, dynamic> context, {
bool selectAll = false,
}) {
StringBuffer buffer = StringBuffer();
for (var templateName in templateList) {
Map<String, dynamic>? template = getTemplate('$templateName${selectAll ? '_ALL' : ''}');
if (template == null) {
Logger().warning("没有找到模板$templateName${selectAll ? '_ALL' : ''}");
template = getTemplate(templateName);
if (template == null) {
throw Exception('Template $templateName not found');
}
}
buffer.writeln(_render(template['body'], context));
buffer.writeln();
}
return buffer.toString();
}
String _render(String template, Map<String, dynamic> context) {
final t = mustache.Template(template);
return t.renderString(context);
}
}

119
win_text_editor/lib/framework/widgets/app_scaffold.dart

@ -0,0 +1,119 @@ @@ -0,0 +1,119 @@
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/framework/widgets/file_explorer_pane.dart';
import 'package:win_text_editor/framework/widgets/tab_view.dart';
import 'package:win_text_editor/menus/app_menu.dart';
import 'package:win_text_editor/framework/controllers/file_provider.dart';
import 'package:win_text_editor/framework/widgets/console_panel.dart';
import 'package:win_text_editor/modules/outline/controllers/outline_provider.dart';
import 'package:win_text_editor/modules/outline/widgets/outline_explorer.dart';
class AppScaffold extends StatefulWidget {
const AppScaffold({super.key});
@override
State<AppScaffold> createState() => _AppScaffoldState();
}
class _AppScaffoldState extends State<AppScaffold> {
bool _isFileExplorerCollapsed = false; //
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => FileProvider()),
ChangeNotifierProvider(create: (_) => TabItemsController()),
ChangeNotifierProvider(create: (_) => OutlineProvider()),
],
child: Scaffold(
backgroundColor: Colors.grey[100],
body: Column(
children: [
const AppMenu(),
Expanded(
child: Row(
children: [
//
AnimatedContainer(
duration: const Duration(milliseconds: 200),
width: _isFileExplorerCollapsed ? 48 : 200,
child: Material(
elevation: 2,
child: Column(
children: [
//
if (_isFileExplorerCollapsed)
InkWell(
onTap: () {
setState(() {
_isFileExplorerCollapsed = false;
});
},
child: Container(
height: 40,
alignment: Alignment.center,
child: const Icon(Icons.chevron_right, size: 20),
),
),
//
if (!_isFileExplorerCollapsed)
Expanded(
child: Consumer<TabItemsController>(
builder: (context, tabManager, child) {
return FileExplorerPane(
onFileDoubleTap:
(path) => tabManager.handleFileDoubleTap(path, null),
onFolderDoubleTap: (path) {
if (path == 'toggle_collapse') {
setState(() {
_isFileExplorerCollapsed = true;
});
} else {
tabManager.handleFolderDoubleTap(path);
}
},
);
},
),
),
],
),
),
),
//
Expanded(
child: Consumer<TabItemsController>(
builder:
(_, manager, __) =>
TabView(tabs: manager.tabs, currentTabId: manager.activeTabId),
),
),
//
SizedBox(
width: 300,
child: Consumer<TabItemsController>(
builder: (context, tabManager, child) {
return OutlineExplorer(
onFileDoubleTap: (path, dynamic appendArg) {
// Handle file double tap if needed
tabManager.handleFileDoubleTap(path, appendArg);
},
onFolderDoubleTap: (path) {
tabManager.handleFolderDoubleTap(path);
},
);
},
),
),
],
),
),
const ConsolePanel(),
],
),
),
);
}
}

2
win_text_editor/lib/app/widgets/console_panel.dart → win_text_editor/lib/framework/widgets/console_panel.dart

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter/services.dart'; //
import 'package:win_text_editor/app/providers/logger.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
class ConsolePanel extends StatefulWidget {
const ConsolePanel({super.key});

100
win_text_editor/lib/framework/widgets/file_explorer_pane.dart

@ -0,0 +1,100 @@ @@ -0,0 +1,100 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/file_provider.dart';
import 'package:win_text_editor/framework/services/file_path_manager.dart';
import 'package:win_text_editor/shared/components/file_explorer.dart';
class FileExplorerPane extends StatelessWidget {
final Function(String)? onFileDoubleTap;
final Function(String)? onFolderDoubleTap;
const FileExplorerPane({super.key, this.onFileDoubleTap, this.onFolderDoubleTap});
@override
Widget build(BuildContext context) {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
_loadLastOpenedFolder(fileProvider);
return Consumer<FileProvider>(
builder: (context, fileProvider, child) {
return Material(
elevation: 1,
child: SizedBox(
width: _calculateWidth(context, fileProvider),
child: Column(
children: [
_buildHeader(context),
Expanded(
child: FileExplorer(
onFileDoubleTap: onFileDoubleTap,
onFolderDoubleTap: onFolderDoubleTap,
),
),
],
),
),
);
},
);
}
Widget _buildHeader(BuildContext context) {
return Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Colors.grey[100],
border: const Border(bottom: BorderSide(color: Colors.grey)),
),
child: Row(
children: [
//
IconButton(
icon: const Icon(Icons.chevron_left, size: 20),
tooltip: '折叠',
onPressed: () {
//
if (onFolderDoubleTap != null) {
onFolderDoubleTap!('toggle_collapse');
}
},
),
const Text('项目资源管理器', style: TextStyle(fontWeight: FontWeight.bold)),
const Spacer(),
IconButton(
icon: const Icon(Icons.folder_open, size: 20),
tooltip: '打开项目文件夹',
onPressed: () => _openDirectory(context),
),
],
),
);
}
double _calculateWidth(BuildContext context, FileProvider fileProvider) {
final screenWidth = MediaQuery.of(context).size.width;
final defaultWidth = screenWidth * 0.2; // 20%
return defaultWidth.clamp(200, 400); // 200400
}
Future<void> _openDirectory(BuildContext context) async {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
final String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory != null) {
await FilePathManager.saveLastOpenedFolder(selectedDirectory);
await fileProvider.setRootPath(selectedDirectory);
}
}
Future<void> _loadLastOpenedFolder(FileProvider fileProvider) async {
if (fileProvider.rootPath != null && fileProvider.rootPath!.isNotEmpty) {
return;
}
final String? lastOpenedFolder = await FilePathManager.getLastOpenedFolder();
if (lastOpenedFolder != null) {
await fileProvider.setRootPath(lastOpenedFolder);
}
}
}

99
win_text_editor/lib/framework/widgets/tab_view.dart

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/module_router.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/framework/models/tab_model.dart';
class TabView extends StatefulWidget {
final List<AppTab> tabs;
final String? currentTabId;
const TabView({super.key, required this.tabs, required this.currentTabId});
@override
State<TabView> createState() => _TabViewState();
}
class _TabViewState extends State<TabView> {
@override
Widget build(BuildContext context) {
return Column(children: [_buildTabBar(context), Expanded(child: _buildTabContent())]);
}
Widget _buildTabBar(BuildContext context) {
return SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.tabs.length,
itemBuilder: (ctx, index) {
final tab = widget.tabs[index];
return _TabItem(
tab: tab,
isActive: tab.id == widget.currentTabId,
onClose: () => context.read<TabItemsController>().closeTab(tab.id),
onTap: () => context.read<TabItemsController>().setActiveTab(tab.id),
);
},
),
);
}
Widget _buildTabContent() {
final tabManager = Provider.of<TabItemsController>(context, listen: false);
final activeIndex = widget.tabs.indexWhere((t) => t.id == widget.currentTabId);
if (activeIndex == -1) return const Center(child: Text('欢迎光临,敬请指导!'));
return IndexedStack(
index: activeIndex,
children:
widget.tabs.map((tab) {
final controller = tabManager.getController<ChangeNotifier>(tab.id);
return KeyedSubtree(key: ValueKey(tab.id), child: _buildTabItem(tab, controller));
}).toList(),
);
}
Widget _buildTabItem(AppTab tab, ChangeNotifier? controller) {
return Container(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: Colors.lightBlue, width: 1)),
),
child: ModuleRouter.buildWidgetForTab(tab, controller),
);
}
}
class _TabItem extends StatelessWidget {
final AppTab tab;
final bool isActive;
final VoidCallback onClose;
final VoidCallback onTap;
const _TabItem({
required this.tab,
required this.isActive,
required this.onClose,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(color: isActive ? Colors.blue[100] : Colors.grey[200]),
child: Row(
children: [
if (tab.icon != null) Icon(tab.icon, size: 16),
if (tab.icon != null) const SizedBox(width: 4),
Text(tab.title),
const SizedBox(width: 8),
IconButton(icon: const Icon(Icons.close, size: 16), onPressed: onClose),
],
),
),
);
}
}

79
win_text_editor/lib/main copy.dart

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
void main2() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: DragIconDemo());
}
}
class DragIconDemo extends StatefulWidget {
@override
_DragIconDemoState createState() => _DragIconDemoState();
}
class _DragIconDemoState extends State<DragIconDemo> {
// 12
int iconContainer = 1;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('图标拖拽示例')),
body: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//
buildContainer(
containerNumber: 1,
child: iconContainer == 1 ? buildDraggableIcon() : null,
),
//
buildContainer(
containerNumber: 2,
child: iconContainer == 2 ? buildDraggableIcon() : null,
),
],
),
);
}
//
Widget buildDraggableIcon() {
return Draggable<int>(
data: iconContainer, //
feedback: const Icon(Icons.star, size: 50, color: Colors.amber),
childWhenDragging: const Opacity(
opacity: 0.5,
child: Icon(Icons.star, size: 50, color: Colors.amber),
),
child: const Icon(Icons.star, size: 50, color: Colors.amber),
);
}
//
Widget buildContainer({required int containerNumber, Widget? child}) {
return DragTarget<int>(
builder: (context, candidateData, rejectedData) {
return Container(
width: 150,
height: 150,
decoration: BoxDecoration(
border: Border.all(color: Colors.blue, width: 2),
borderRadius: BorderRadius.circular(10),
),
child: Center(child: child),
);
},
onWillAcceptWithDetails: (data) => true, //
onAcceptWithDetails: (data) {
setState(() {
iconContainer = containerNumber; //
});
},
);
}
}

36
win_text_editor/lib/main.dart

@ -1,8 +1,11 @@ @@ -1,8 +1,11 @@
import 'package:flutter/material.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/widgets/app_scaffold.dart';
import 'package:win_text_editor/shared/data/std_fields_cache.dart';
import 'package:window_manager/window_manager.dart';
import 'app/app.dart';
import 'app/providers/logger.dart'; //
import 'framework/controllers/logger.dart'; //
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -10,15 +13,25 @@ void main() async { @@ -10,15 +13,25 @@ void main() async {
//
await windowManager.ensureInitialized();
WindowOptions windowOptions = const WindowOptions(
size: Size(1200, 700),
size: Size(1600, 1000),
center: true,
title: '文本转换',
title: '编程辅助工具',
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
});
//
WidgetsFlutterBinding.ensureInitialized();
//
await Hive.initFlutter();
await Hive.deleteBoxFromDisk('uft_stdFieldsCache');
//
await StdFieldsCache.init();
runApp(
MultiProvider(
providers: [
@ -36,11 +49,20 @@ class MyApp extends StatelessWidget { @@ -36,11 +49,20 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '文本转换',
title: '编程辅助工具',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
useMaterial3: true, // Material 3
cardTheme: const CardTheme(color: Colors.white),
// SfDataGrid
textTheme: const TextTheme(
bodyMedium: TextStyle(fontSize: 12), //
titleMedium: TextStyle(fontSize: 14), //
),
colorScheme: const ColorScheme.light(
primary: Colors.blue, //
onPrimary: Colors.white, //
),
),
home: const AppScaffold(),
);

61
win_text_editor/lib/app/menus/app_menu.dart → win_text_editor/lib/menus/app_menu.dart

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/app/menus/menu_constants.dart';
import 'package:win_text_editor/menus/menu_constants.dart';
import 'menu_actions.dart';
@ -14,7 +14,8 @@ class AppMenu extends StatelessWidget { @@ -14,7 +14,8 @@ class AppMenu extends StatelessWidget {
child: Row(
children: [
_buildMenuButton(context, '文件', _buildFileMenuItems()),
_buildMenuButton(context, '工具', _buildToolsMenuItems()),
_buildMenuButton(context, '文本工具', _buildToolsMenuItems()),
_buildMenuButton(context, 'UFT工具', _buildUftToolsMenuItems()),
_buildMenuButton(context, '编辑', _buildEditMenuItems()),
_buildMenuButton(context, '窗口', _buildWindowMenuItems()),
_buildMenuButton(context, '帮助', _buildHelpMenuItems()),
@ -24,7 +25,59 @@ class AppMenu extends StatelessWidget { @@ -24,7 +25,59 @@ class AppMenu extends StatelessWidget {
}
List<PopupMenuEntry<String>> _buildToolsMenuItems() {
return [const PopupMenuItem<String>(value: MenuConstants.templateParser, child: Text('模板解析'))];
return [
const PopupMenuItem<String>(
value: MenuConstants.templateParser,
child: ListTile(leading: Icon(Icons.auto_awesome_mosaic), title: Text('XML解析')),
),
const PopupMenuItem<String>(
value: MenuConstants.contentSearch,
child: ListTile(leading: Icon(Icons.search), title: Text('内容搜索')),
),
const PopupMenuItem<String>(
value: MenuConstants.dataCompare,
child: ListTile(leading: Icon(Icons.compare), title: Text('数据对比')),
),
const PopupMenuItem<String>(
value: MenuConstants.dataFormat,
child: ListTile(leading: Icon(Icons.date_range), title: Text('数据格式化')),
),
const PopupMenuItem<String>(
value: MenuConstants.dataExtract,
child: ListTile(leading: Icon(Icons.outbox), title: Text('XML数据提取')),
),
const PopupMenuItem<String>(
value: MenuConstants.xmlSearch,
child: ListTile(leading: Icon(Icons.find_in_page), title: Text('XML搜索')),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: MenuConstants.demo,
child: ListTile(leading: Icon(Icons.view_agenda), title: Text('Demo')),
),
];
}
List<PopupMenuEntry<String>> _buildUftToolsMenuItems() {
return [
const PopupMenuItem<String>(
value: MenuConstants.memoryTable,
child: ListTile(leading: Icon(Icons.list), title: Text('内存表')),
),
const PopupMenuItem<String>(
value: MenuConstants.uftComponent,
child: ListTile(leading: Icon(Icons.extension), title: Text('标准组件')),
),
const PopupMenuItem<String>(
value: MenuConstants.callFunction,
child: ListTile(leading: Icon(Icons.functions), title: Text('功能号调用')),
),
const PopupMenuDivider(),
const PopupMenuItem<String>(
value: MenuConstants.codeCreater,
child: ListTile(leading: Icon(Icons.code), title: Text('代码生成器')),
),
];
}
Widget _buildMenuButton(BuildContext context, String label, List<PopupMenuEntry<String>> items) {
@ -38,7 +91,7 @@ class AppMenu extends StatelessWidget { @@ -38,7 +91,7 @@ class AppMenu extends StatelessWidget {
List<PopupMenuEntry<String>> _buildFileMenuItems() {
return [
const PopupMenuItem<String>(value: MenuConstants.openFolder, child: Text('打开文件夹...')),
// const PopupMenuItem<String>(value: MenuConstants.openFolder, child: Text('打开文件夹...')),
const PopupMenuItem<String>(value: MenuConstants.save, child: Text('保存')),
const PopupMenuItem<String>(value: MenuConstants.saveAs, child: Text('另存为...')),
const PopupMenuItem<String>(value: MenuConstants.exit, child: Text('退出')),

131
win_text_editor/lib/menus/menu_actions.dart

@ -0,0 +1,131 @@ @@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:file_picker/file_picker.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/menus/menu_constants.dart';
import 'package:win_text_editor/framework/controllers/file_provider.dart';
import 'dart:io';
import 'package:win_text_editor/modules/module_router.dart';
class MenuActions {
static final Map<String, Function(BuildContext)> _actionHandlers = {
MenuConstants.openFolder: _openFolder,
MenuConstants.contentSearch: _openContentSearch,
MenuConstants.templateParser: _openTemplateParser,
MenuConstants.dataFormat: _dataFormat,
MenuConstants.dataCompare: _dataCompare,
MenuConstants.dataExtract: _dataExtract,
MenuConstants.xmlSearch: _xmlSearch,
MenuConstants.memoryTable: _memoryTable,
MenuConstants.uftComponent: _uftComponent,
MenuConstants.callFunction: _callFunction,
MenuConstants.demo: _demo,
MenuConstants.outline: _outline,
MenuConstants.codeCreater: _codeCreater,
MenuConstants.exit: _exitApplication,
};
static Future<void> handleMenuAction(String value, BuildContext context) async {
final handler = _actionHandlers[value];
if (handler != null) {
await handler(context);
}
}
static Future<void> _openFolder(BuildContext context) async {
try {
final fileProvider = Provider.of<FileProvider>(context, listen: false);
final String? selectedDirectory = await FilePicker.platform.getDirectoryPath();
if (selectedDirectory != null) {
await fileProvider.loadDirectory(selectedDirectory);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('打开文件夹失败: $e')));
}
}
static Future<void> _openContentSearch(BuildContext context) async {
await _openOrActivateTab(context, "内容搜索", RouterKey.contentSearch, Icons.search);
}
static Future<void> _openTemplateParser(BuildContext context) async {
await _openOrActivateTab(context, "XML解析", RouterKey.templateParser, Icons.auto_awesome_mosaic);
}
static Future<void> _memoryTable(BuildContext context) async {
await _openOrActivateTab(context, "内存表", RouterKey.memoryTable, Icons.drive_file_move);
}
static Future<void> _uftComponent(BuildContext context) async {
await _openOrActivateTab(context, "标准组件", RouterKey.uftComponent, Icons.extension);
}
static Future<void> _callFunction(BuildContext context) async {
await _openOrActivateTab(context, "功能号调用", RouterKey.callFunction, Icons.extension);
}
static Future<void> _dataFormat(BuildContext context) async {
await _openOrActivateTab(context, "数据格式化", RouterKey.dataFormat, Icons.date_range);
}
static Future<void> _dataCompare(BuildContext context) async {
await _openOrActivateTab(context, "数据对比", RouterKey.dataCompare, Icons.compare);
}
static Future<void> _dataExtract(BuildContext context) async {
await _openOrActivateTab(context, "XML数据提取", RouterKey.dataExtract, Icons.outbox);
}
static Future<void> _xmlSearch(BuildContext context) async {
await _openOrActivateTab(context, "XML搜索", RouterKey.xmlSearch, Icons.find_in_page);
}
static Future<void> _demo(BuildContext context) async {
await _openOrActivateTab(context, "Demo", RouterKey.demo, Icons.code);
}
static Future<void> _codeCreater(BuildContext context) async {
await _openOrActivateTab(context, "代码生成器", RouterKey.codeCreater, Icons.code);
}
static Future<void> _outline(BuildContext context) async {
await _openOrActivateTab(context, "大纲", RouterKey.outline, Icons.outlined_flag_rounded);
}
static Future<void> _openOrActivateTab(
BuildContext context,
String title,
String type,
IconData icon,
) async {
try {
final tabManager = Provider.of<TabItemsController>(context, listen: false);
await tabManager.openOrActivateTab(title, type, icon);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('打开标签页失败: $e')));
}
}
static Future<void> _exitApplication(BuildContext context) async {
final shouldExit = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('退出应用'),
content: const Text('确定要退出吗?未保存的内容可能会丢失。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('取消'),
),
TextButton(onPressed: () => Navigator.of(context).pop(true), child: const Text('退出')),
],
),
);
if (shouldExit ?? false) {
exit(0);
}
}
}

18
win_text_editor/lib/app/menus/menu_constants.dart → win_text_editor/lib/menus/menu_constants.dart

@ -13,7 +13,23 @@ class MenuConstants { @@ -13,7 +13,23 @@ class MenuConstants {
static const String exit = 'exit';
//
static const String contentSearch = "content_search";
static const String templateParser = 'template_parser';
static const String dataFormat = 'data_format';
static const String dataCompare = 'data_compare';
static const String dataExtract = 'data_extract';
static const String xmlSearch = 'xml_search';
static const String demo = 'demo';
// AIGC菜单项
static const String aigc = 'aigc';
//Uft菜单
static const String memoryTable = 'memory_table';
static const String uftComponent = 'uft_component';
static const String callFunction = 'call_function';
static const String uftTools = 'uft_tools';
static const String outline = 'outline';
//
static const String undo = 'undo';
@ -30,4 +46,6 @@ class MenuConstants { @@ -30,4 +46,6 @@ class MenuConstants {
//
static const String about = 'about';
static const String help = 'help';
static const String codeCreater = 'code_creater';
}

132
win_text_editor/lib/modules/call_function/controllers/call_function_controller.dart

@ -0,0 +1,132 @@ @@ -0,0 +1,132 @@
import 'package:collection/collection.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/services/macro_template_service.dart';
import 'package:win_text_editor/modules/call_function/models/call_function.dart';
import 'package:win_text_editor/modules/call_function/services/call_function_service.dart';
import 'package:win_text_editor/modules/outline/models/code_partner.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class CallFunctionController extends BaseContentController {
CallFunction modle = CallFunction(
functionType: '',
functionNo: '',
chineseName: '',
inputParameters: [],
outputParameters: [],
);
late DataGridSource inputSource;
late DataGridSource outputSource;
final MacroTemplateService templateService = MacroTemplateService();
CallFunctionController() {
inputSource = FieldsDataSource(
[],
onSelectionChanged: (index, isSelected) {
updateInputSelection(index, isSelected);
},
);
outputSource = FieldsDataSource(
[],
onSelectionChanged: (index, isSelected) {
updateOutputSelection(index, isSelected);
},
);
}
void initTemplateService() {
if (!templateService.inited) {
templateService.init();
}
}
String? genCodeString(List<String> macroList) {
initTemplateService();
StringBuffer sb = StringBuffer();
//
if (modle.functionType == 'uftfactorservice') {
sb.write(templateService.renderTemplate(['因子服务'], modle.toMap()));
return sb.toString();
}
//
// for (final input in modle.inputParameters) {
// if (input.isSelected && input.type == CallFunction.componentType) {
// final component = modle.componentList?.firstWhereOrNull((c) => c.name == input.name);
// if (component != null) {
// sb.write(templateService.renderTemplate(['插入组件'], component.toMap()));
// }
// }
// }
sb.write(templateService.renderTemplate(macroList, modle.toMap()));
//
// for (final output in modle.outputParameters) {
// if (output.isSelected && output.type == CallFunction.componentType) {
// final component = modle.componentList?.firstWhereOrNull((c) => c.name == output.name);
// if (component != null) {
// sb.write(templateService.renderTemplate(['遍历组件所有'], component.toMap()));
// }
// }
// }
return sb.toString();
}
@override
Future<void> onOpenFile(String filePath, {dynamic appendArg}) async {
Logger().info("Opening file: $filePath");
try {
modle = await CallFunctionService.parseXmlFile(filePath);
// datagrid显示的数据源
(inputSource as FieldsDataSource).updateData(modle.inputParameters);
(outputSource as FieldsDataSource).updateData(modle.outputParameters);
// Notify UI to update
notifyListeners();
} catch (e) {
notifyListeners();
Logger().error("Error opening file: $e");
}
}
//
void updateInputSelection(int index, bool isSelected) {
final fields = (inputSource as FieldsDataSource).data;
if (index >= 0 && index < fields.length) {
fields[index].isSelected = isSelected;
inputSource.notifyListeners();
// CallFunction
modle.inputParameters[index].isSelected = isSelected;
notifyListeners();
}
}
void updateOutputSelection(int index, bool isSelected) {
final fields = (outputSource as FieldsDataSource).data;
if (index >= 0 && index < fields.length) {
fields[index].isSelected = isSelected;
outputSource.notifyListeners();
// CallFunction
modle.outputParameters[index].isSelected = isSelected;
notifyListeners();
}
}
@override
void onOpenFolder(String folderPath) {
//
}
@override
void onDropOutlineNode(OutlineNode node) {
modle.codePartners.add(CodePartner.fromOutlineNode(node));
notifyListeners();
}
}

110
win_text_editor/lib/modules/call_function/models/call_function.dart

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
import 'package:win_text_editor/modules/outline/models/code_partner.dart';
import 'package:win_text_editor/modules/uft_component/models/uft_component.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
class CallFunction {
static const String componentType = 'COMPONENT';
final String functionType;
final String functionNo;
final String chineseName;
final List<Field> inputParameters;
final List<Field> outputParameters;
List<UftComponent>? componentList;
List<UftComponent>? outComponentList;
String? factorParam;
final List<CodePartner> codePartners = [];
CallFunction({
required this.functionType,
required this.functionNo,
required this.chineseName,
required this.inputParameters,
required this.outputParameters,
this.componentList,
this.outComponentList,
this.factorParam,
});
Map<String, dynamic> toMap() {
return {
'functionNo': functionNo,
'chineseName': chineseName,
'factorParam': factorParam ?? '',
'input':
inputParameters
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': inputParameters.indexOf(field) == inputParameters.length - 1,
'partnerName':
codePartners
.firstWhere(
(partner) => partner.fields.any((f) => f == field.name),
orElse: () => CodePartner(name: null, fields: []),
)
.name,
},
)
.toList(),
'selectedInput':
inputParameters
.where((field) => field.isSelected)
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': inputParameters.indexOf(field) == inputParameters.length - 1,
'partnerName':
codePartners
.firstWhere(
(partner) => partner.fields.any((f) => f == field.name),
orElse: () => CodePartner(name: null, fields: []),
)
.name,
},
)
.toList(),
'selectedOutput':
outputParameters
.where((field) => field.isSelected)
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': outputParameters.indexOf(field) == outputParameters.length - 1,
},
)
.toList(),
'output':
outputParameters
.map(
(field) => {
'id': field.id,
'name': field.name,
'chineseName': field.chineseName,
'type': field.type,
'isLast': outputParameters.indexOf(field) == outputParameters.length - 1,
},
)
.toList(),
'inputComps':
componentList
?.map(
(componen) => {
'id': componen.id,
'name': componen.name,
'chineseName': componen.chineseName,
'fields': componen.fields.map((field) => {'name': field.name}),
},
)
.toList(),
};
}
}

127
win_text_editor/lib/modules/call_function/services/call_function_service.dart

@ -0,0 +1,127 @@ @@ -0,0 +1,127 @@
// memory_table_service.dart
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:win_text_editor/modules/call_function/models/call_function.dart';
import 'package:win_text_editor/modules/uft_component/models/uft_component.dart';
import 'package:win_text_editor/modules/uft_component/services/uft_component_service.dart';
import 'package:win_text_editor/shared/data/std_fields_cache.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
import 'package:xml/xml.dart' as xml;
import 'package:win_text_editor/framework/controllers/logger.dart';
class CallFunctionService {
static const Map<String, String> _functionRootTypeMap = {
'uftfunction': 'business:Function',
'uftservice': 'business:Service',
'uftatomfunction': 'business:Function',
'uftatomservice': 'business:Service',
'uftfactorfunction': 'business:FactorFunction',
'uftfactorservice': 'business:FactorService',
};
static Future<CallFunction> parseXmlFile(String filePath) async {
try {
// 1. Check file extension
final extendFileName = filePath.toLowerCase().split('.').last;
final String? rootNodeType = _functionRootTypeMap[extendFileName];
if (rootNodeType == null) {
Logger().error('文件扩展名不正确:$filePath');
throw const FormatException("文件扩展名不正确!");
}
// 2. metadata stdfield.stfield
await StdFieldsCache.loadForFile(filePath);
// 3. Read and parse structure file content
final file = File(filePath);
final content = await file.readAsString();
final document = xml.XmlDocument.parse(content);
final rootNode = document.findAllElements(rootNodeType).firstOrNull;
if (rootNode == null) {
throw const FormatException("文件格式错误:缺少支持的根节点");
}
// 4. Get basic info
final chineseName = rootNode.getAttribute('chineseName') ?? '';
final objectId = rootNode.getAttribute('objectId') ?? '';
List<UftComponent> componentList = [];
List<UftComponent> outComponentList = [];
// 5. Process inputParameters (fields)
final inputParameters = document.findAllElements(
extendFileName == "uftfactorservice" ? 'internalParams' : 'inputParameters',
);
final inputFields = await parserFields(filePath, inputParameters, componentList);
final factorParam =
extendFileName == "uftfactorservice"
? inputFields
.firstWhereOrNull((field) => field.type == CallFunction.componentType)
?.name
: '';
// 6. Process outputParameters
final outputParameters = document.findAllElements('outputParameters');
final outputFields = await parserFields(filePath, outputParameters, outComponentList);
return CallFunction(
functionType: extendFileName,
chineseName: chineseName,
functionNo: objectId,
inputParameters: inputFields,
outputParameters: outputFields,
componentList: componentList,
outComponentList: outComponentList,
factorParam: factorParam,
);
} on xml.XmlParserException catch (e) {
Logger().error("XML解析错误: ${e.message}");
rethrow;
} catch (e) {
Logger().error("处理文件时发生错误: $e");
rethrow;
}
}
static Future<List<Field>> parserFields(
String filePath,
Iterable<xml.XmlElement> parameters,
List<UftComponent> componentList,
) async {
final fields = <Field>[];
int index = 0;
for (final parameter in parameters) {
index++;
final id = parameter.getAttribute('id') ?? '';
final paramType = parameter.getAttribute('paramType') ?? 'FIELD';
if (paramType == CallFunction.componentType) {
var component = await UftComponentService.getUftComponent(filePath, id);
if (component != null) {
componentList.add(component);
}
fields.add(
Field(
index.toString(),
id,
component?.chineseName ?? '', // 使
paramType,
),
);
} else {
final stdField = StdFieldsCache.getData(id);
fields.add(
Field(index.toString(), id, stdField?.chineseName ?? '', stdField?.dateType ?? ''),
);
}
}
return fields;
}
}

78
win_text_editor/lib/modules/call_function/widgets/call_function_left_side.dart

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart';
import 'package:win_text_editor/shared/uft_std_fields/fields_data_grid.dart';
class CallFunctionLeftSide extends StatelessWidget {
final CallFunctionController controller;
const CallFunctionLeftSide({super.key, required this.controller});
Widget _buildTextFieldRow(String label, String value, double width) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('$label:'),
SizedBox(
width: width,
child: TextField(
controller: TextEditingController(text: value),
readOnly: true,
decoration: const InputDecoration(isDense: true, contentPadding: EdgeInsets.all(8)),
),
),
],
);
}
@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.modle.functionNo, 100),
_buildTextFieldRow('中文名', controller.modle.chineseName, 350),
],
),
),
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('入参', style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
flex: 6,
child: FieldsDataGrid(
fieldsSource: controller.inputSource as FieldsDataSource,
onSelectionChanged: (index, isSelected) {
controller.updateInputSelection(index, isSelected);
},
),
),
const Padding(
padding: EdgeInsets.all(8.0),
child: Text('出参', style: TextStyle(fontWeight: FontWeight.bold)),
),
Expanded(
flex: 4,
child: FieldsDataGrid(
fieldsSource: controller.outputSource as FieldsDataSource,
onSelectionChanged: (index, isSelected) {
controller.updateOutputSelection(index, isSelected);
},
),
),
],
);
}
}

62
win_text_editor/lib/modules/call_function/widgets/call_function_right_side.dart

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
// call_function_right_side.dart
import 'package:flutter/material.dart';
import 'package:win_text_editor/modules/call_function/controllers/call_function_controller.dart';
import 'package:win_text_editor/shared/components/code_generation_components.dart';
class CallFunctionRightSide extends StatefulWidget {
final CallFunctionController controller;
final TextEditingController codeController;
const CallFunctionRightSide({super.key, required this.controller, required this.codeController});
@override
State<CallFunctionRightSide> createState() => _CallFunctionRightSideState();
}
class _CallFunctionRightSideState extends State<CallFunctionRightSide> {
String? _selectedOperation;
@override
void initState() {
super.initState();
widget.controller.initTemplateService();
widget.controller.addListener(_updateDisplay);
}
@override
void dispose() {
widget.controller.removeListener(_updateDisplay);
super.dispose();
}
void _updateDisplay() {
if (_selectedOperation != null) {
widget.codeController.text = widget.controller.genCodeString([_selectedOperation!]) ?? '';
} else {
widget.codeController.text = '';
}
}
void _selectOperation(String? operation) {
setState(() {
_selectedOperation = operation;
_updateDisplay();
});
}
@override
Widget build(BuildContext context) {
final operations = ['普通调用', '事务调用'];
return CodeGenerationSection(
title: '生成代码',
codeController: widget.codeController,
onNodeDropped: (node) => widget.controller.onDropOutlineNode(node),
child: OperationRadioSection(
operations: operations,
selectedOperation: _selectedOperation,
onOperationSelected: _selectOperation,
),
);
}
}

76
win_text_editor/lib/modules/call_function/widgets/call_function_view.dart

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
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/call_function/controllers/call_function_controller.dart';
import 'call_function_left_side.dart';
import 'call_function_right_side.dart';
class CallFunctionView extends StatefulWidget {
final String tabId;
const CallFunctionView({super.key, required this.tabId});
@override
State<CallFunctionView> createState() => _CallFunctionViewState();
}
class _CallFunctionViewState extends State<CallFunctionView> {
late final CallFunctionController _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 = CallFunctionController();
_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<CallFunctionController>(
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (50%)
Expanded(flex: 5, child: CallFunctionLeftSide(controller: controller)),
const SizedBox(width: 8),
// (50%)
Expanded(
flex: 5,
child: CallFunctionRightSide(
codeController: _codeController,
controller: controller,
),
),
],
),
);
},
),
);
}
}

91
win_text_editor/lib/modules/code_creater/controllers/code_creater_controller.dart

@ -0,0 +1,91 @@ @@ -0,0 +1,91 @@
import 'package:win_text_editor/framework/common/constants.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/code_creater/services/code_create_service.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class CodeCreaterController extends BaseContentController {
final Map<String, List<String>> unMatchMap = {
Constants.atomFunction: ['逻辑服务', '逻辑函数'],
Constants.atomService: ['原子服务', '原子函数'],
Constants.business: ['原子服务', '原子函数'],
Constants.uftTable: [],
Constants.component: [],
};
List<OutlineNode> _members = [];
List<OutlineNode> get members => _members;
String _selectedOperation = '原子函数';
String get selectedOperation => _selectedOperation;
set selectedOperation(String value) {
final filtered =
_members.where((node) => !(unMatchMap[node.value]?.contains(value) ?? false)).toList();
_members = filtered; //
_selectedOperation = value;
}
void updateMembers(List<OutlineNode> newMembers) {
_members = newMembers;
notifyListeners(); //
}
@override
void onOpenFile(String filePath, {dynamic appendArg}) {}
@override
void onOpenFolder(String folderPath) {}
@override
void onDropOutlineNode(OutlineNode node) {
if (unMatchMap[node.value]?.contains(selectedOperation) ?? false) {
Logger().error('当前节点不支持操作: $selectedOperation');
return;
}
if (members.contains(node)) {
Logger().error('不支持重复节点: ${node.title}');
return;
}
//
node.selectedAction = CodeCreateService.getNodeActions(node.name)[0];
members.add(node);
notifyListeners();
}
Future<String> genCodeString() {
return CodeCreateService.genCodeString(members, selectedOperation);
}
void handleActionTypeChange(int index, String newAction) {
final memberName = members[index].name;
if (newAction == "遍历记录") {
members.add(
OutlineNode(
name: '${members[index].name}_END',
title: members[index].title,
value: '${members[index].value}_END',
selectedAction: "遍历记录结束",
),
);
} else if (newAction == "遍历组件") {
members.add(
OutlineNode(
name: '${members[index].name}_END',
title: members[index].title,
value: '${members[index].value}_END',
selectedAction: "遍历组件结束",
),
);
} else {
members.removeWhere((member) => member.name == '${memberName}_END');
}
// members[index].selectedAction = newAction;
notifyListeners();
}
}

341
win_text_editor/lib/modules/code_creater/services/code_create_service.dart

@ -0,0 +1,341 @@ @@ -0,0 +1,341 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/framework/common/constants.dart';
import 'package:win_text_editor/framework/services/macro_template_service.dart';
import 'package:win_text_editor/modules/call_function/models/call_function.dart';
import 'package:win_text_editor/modules/call_function/services/call_function_service.dart';
import 'package:win_text_editor/modules/memory_table/models/memory_table.dart';
import 'package:win_text_editor/modules/memory_table/services/memory_table_service.dart';
import 'package:win_text_editor/modules/outline/models/code_partner.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/modules/uft_component/models/uft_component.dart';
import 'package:win_text_editor/modules/uft_component/services/uft_component_service.dart';
class CodeCreateService {
static List<String> getNodeActions(String type) {
switch (type) {
case Constants.atomFunction:
case Constants.atomService:
case Constants.business:
return ['普通调用', '事务调用'];
case Constants.uftTable:
return ['获取记录', '遍历记录', '插入记录', '修改记录', '删除记录'];
case Constants.component:
return ['获取组件', '遍历组件', '插入组件', '修改组件', '尾部插入组件'];
case "${Constants.component}_END":
return ['遍历组件结束'];
case "${Constants.uftTable}_END":
return ['遍历记录结束'];
default:
return ['']; //
}
}
static final MacroTemplateService templateService = MacroTemplateService();
static void initTemplateService() {
if (!templateService.inited) {
templateService.init();
}
}
static Future<String> genCodeString(List<OutlineNode> members, String selectedOperation) async {
initTemplateService();
//members,value
StringBuffer codeBuffer = StringBuffer("//生成$selectedOperation代码\n\n");
List<String> inputFields = []; //
List<String> outputFields = []; //
List<String> macroCodeList = []; //
List<CodePartner> beforePartner = []; //
Map<String, List<String>> iterateOutputFields = {}; //-使
Map<String, List<CodePartner>> iteratePartnerMap = {}; //
List<String> iterateStack = []; //
//
Future<void> parseRelationShips(List<OutlineNode> nodes, String selectedOperation) async {
for (int i = 0; i < members.length; i++) {
final member = members[i];
final action =
member.selectedAction!.isEmpty
? getNodeActions(member.value)[0]
: member.selectedAction;
//
if (action!.startsWith("遍历") && action.endsWith("结束")) {
//
final memberName = iterateStack.removeLast();
iterateOutputFields.remove(memberName);
iteratePartnerMap.remove(memberName);
final macroCode = templateService.renderTemplate([action], {}, selectAll: true);
macroCodeList.add(macroCode);
continue;
}
switch (member.value) {
case Constants.atomFunction:
case Constants.atomService:
case Constants.business:
CallFunction model = await CallFunctionService.parseXmlFile(member.path!);
model.codePartners.clear(); //
//
inputFields.addAll(
model.inputParameters
.where(
(field) =>
!outputFields.contains(field.name) &&
!inputFields.contains(field.name) &&
!iterateOutputFields.values.expand((list) => list).contains(field.name),
)
.map((field) => field.name),
);
//
model.codePartners.addAll(beforePartner.toSet());
//
model.codePartners.addAll(
iteratePartnerMap.values.expand((partners) => partners).toList(),
);
//
if (iterateStack.isNotEmpty) {
//
iterateOutputFields
.putIfAbsent(iterateStack.last, () => [])
.addAll(model.outputParameters.map((field) => field.name).toSet());
//:
for (UftComponent component in model.outComponentList!) {
List<String> fieldNames = [];
for (var field in component.fields) {
fieldNames.add(field.name);
}
iterateOutputFields
.putIfAbsent(iterateStack.last, () => [])
.addAll(component.fields.map((field) => field.name).toSet());
iteratePartnerMap
.putIfAbsent(component.name, () => [])
.add(CodePartner(name: component.name, fields: fieldNames));
}
} else {
//
outputFields.addAll(
model.outputParameters
.where((field) => !outputFields.contains(field.name))
.map((field) => field.name),
);
//
for (UftComponent component in model.outComponentList!) {
List<String> fieldNames = component.fields.map((col) => col.name).toList();
outputFields.addAll(fieldNames.toSet());
beforePartner.add(CodePartner(name: member.name, fields: fieldNames));
}
}
final macroCode = templateService.renderTemplate(
[action],
model.toMap(),
selectAll: true,
);
macroCodeList.add(macroCode);
break;
case Constants.uftTable:
TableData tableData = await MemoryTableService.parseStructureFile(member.path!);
final memoryTable = MemoryTable(
tableName: tableData.tableName,
columns: tableData.fields,
indexes: tableData.indexes,
);
memoryTable.codePartners.clear();
//
memoryTable.codePartners.addAll(beforePartner.toSet());
//
memoryTable.codePartners.addAll(
iteratePartnerMap.values.expand((partners) => partners).toList(),
);
List<String> fieldNames = memoryTable.columns.map((col) => col.name).toList();
switch (action) {
case '删除记录':
for (var fieldName in memoryTable.keyFields) {
if (!outputFields.contains(fieldName) &&
!inputFields.contains(fieldName) &&
!iterateOutputFields.values.expand((list) => list).contains(fieldName)) {
inputFields.add(fieldName);
}
}
break;
case '获取记录':
for (var fieldName in memoryTable.keyFields) {
if (!outputFields.contains(fieldName) &&
!inputFields.contains(fieldName) &&
!iterateOutputFields.values.expand((list) => list).contains(fieldName)) {
inputFields.add(fieldName);
}
}
if (iterateStack.isNotEmpty) {
//
iterateOutputFields.putIfAbsent(iterateStack.last, () => []).addAll(fieldNames);
iteratePartnerMap
.putIfAbsent(member.name, () => [])
.add(CodePartner(name: member.name, fields: fieldNames));
} else {
//
outputFields.addAll(fieldNames);
beforePartner.add(CodePartner(name: member.name, fields: fieldNames));
}
break;
case '遍历记录':
List<String> fieldNames = memoryTable.columns.map((col) => col.name).toList();
iterateStack.add(memoryTable.tableName); //
//
iterateOutputFields.putIfAbsent(iterateStack.last, () => []).addAll(fieldNames);
iteratePartnerMap
.putIfAbsent(member.name, () => [])
.add(CodePartner(name: member.name, fields: fieldNames));
break;
case '插入记录':
case '修改记录':
for (var fieldName in fieldNames) {
if (!outputFields.contains(fieldName) &&
!inputFields.contains(fieldName) &&
!iterateOutputFields.values.expand((list) => list).contains(fieldName)) {
inputFields.add(fieldName);
}
}
break;
}
final macroCode = templateService.renderTemplate(
[action!],
memoryTable.toMap(),
selectAll: true,
);
macroCodeList.add(macroCode);
break;
case Constants.component:
final component = await UftComponentService.getUftComponent(member.path!, member.name);
component!.codePartners.clear();
component.codePartners.addAll(beforePartner);
component.codePartners.addAll(
iteratePartnerMap.values.expand((partners) => partners).toList(),
);
List<String> fieldNames = component.fields.map((col) => col.name).toList();
final componentName = component.name;
switch (action) {
case "遍历组件":
if (!outputFields.contains(componentName) &&
!inputFields.contains(componentName) &&
!iterateOutputFields.values.expand((list) => list).contains(componentName)) {
inputFields.add(componentName);
}
iterateStack.add(component.name); //
iterateOutputFields.putIfAbsent(iterateStack.last, () => []).addAll(fieldNames);
iterateOutputFields[iterateStack.last]!.add(component.name);
iteratePartnerMap
.putIfAbsent(member.name, () => [])
.add(CodePartner(name: member.name, fields: fieldNames));
break;
case "修改组件":
//
if (!outputFields.contains(componentName) &&
!inputFields.contains(componentName) &&
!iterateOutputFields.values.expand((list) => list).contains(componentName)) {
inputFields.add(componentName);
}
continue case_insert;
case_insert:
case "插入组件":
case "尾部插入组件":
//
for (var name in fieldNames) {
if (!outputFields.contains(name) &&
!inputFields.contains(name) &&
!iterateOutputFields.values.expand((list) => list).contains(name)) {
inputFields.add(name);
}
}
if (iterateStack.isNotEmpty) {
//
iterateOutputFields.putIfAbsent(iterateStack.last, () => []).add(componentName);
iteratePartnerMap
.putIfAbsent(member.name, () => [])
.add(CodePartner(name: member.name, fields: fieldNames));
} else {
//
outputFields.add(componentName);
beforePartner.add(CodePartner(name: member.name, fields: fieldNames));
}
break;
case "获取组件":
//
if (!outputFields.contains(componentName) &&
!inputFields.contains(componentName) &&
!iterateOutputFields.values.expand((list) => list).contains(componentName)) {
inputFields.add(componentName);
}
//
if (iterateStack.isNotEmpty) {
//
iterateOutputFields.putIfAbsent(iterateStack.last, () => []).addAll(fieldNames);
iterateOutputFields[iterateStack.last]!.add(componentName);
iteratePartnerMap
.putIfAbsent(member.name, () => [])
.add(CodePartner(name: member.name, fields: fieldNames));
} else {
//
outputFields.addAll(fieldNames);
outputFields.add(componentName);
beforePartner.add(CodePartner(name: member.name, fields: fieldNames));
}
break;
}
final macroCode = templateService.renderTemplate(
[action],
component.toMap(),
selectAll: true,
);
macroCodeList.add(macroCode);
break;
default:
macroCodeList.add('//未知类型: ${member.name}');
}
}
}
await parseRelationShips(members, selectedOperation);
if (inputFields.isNotEmpty) codeBuffer.writeln("//可能需要传入的参数");
for (var field in inputFields) {
codeBuffer.writeln('@$field = ,');
}
codeBuffer.writeln('');
for (final mc in macroCodeList) {
codeBuffer.write(mc);
}
return codeBuffer.toString();
}
}

168
win_text_editor/lib/modules/code_creater/widgets/code_creater_view.dart

@ -0,0 +1,168 @@ @@ -0,0 +1,168 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/code_creater/controllers/code_creater_controller.dart';
import 'package:win_text_editor/modules/code_creater/widgets/node_table.dart';
import 'package:win_text_editor/shared/components/code_generation_components.dart';
class CodeCreaterView extends StatefulWidget {
final String tabId;
const CodeCreaterView({super.key, required this.tabId});
@override
State<CodeCreaterView> createState() => _CodeCreaterViewState();
}
class _CodeCreaterViewState extends State<CodeCreaterView> {
late final CodeCreaterController _controller;
bool _isControllerFromTabManager = false;
final TextEditingController _codeController = TextEditingController();
final List<String> operations = ['逻辑服务', '逻辑函数', '原子服务', '原子函数'];
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 = CodeCreaterController();
_isControllerFromTabManager = false;
tabManager.registerController(widget.tabId, _controller);
}
_controller.addListener(_handleControllerUpdate);
_controller.addListener(_updateDisplay);
}
void _handleControllerUpdate() {
setState(() {});
}
@override
void dispose() {
_controller.removeListener(_handleControllerUpdate);
_controller.removeListener(_updateDisplay);
if (!_isControllerFromTabManager) {
_controller.dispose();
}
_codeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
children: [
// (40%)
Expanded(
flex: 4,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: NodeTable(
key: ValueKey(_controller.members.length), //
members: _controller.members,
onMoveMember: _moveMember,
onDeleteMember: _deleteMember,
onDeleteAll: _deleteAll,
onMoveToTop: _moveToTop,
onMoveToBottom: _moveToBottom,
onActionTypeChanged:
(index, newAction) => _controller.handleActionTypeChange(index, newAction),
),
),
),
// (60%)
Expanded(
flex: 6,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: CodeGenerationSection(
title: '生成代码',
codeController: _codeController,
onNodeDropped: (node) => _controller.onDropOutlineNode(node),
child: OperationRadioSection(
operations: operations,
selectedOperation: _controller.selectedOperation,
onOperationSelected: _selectOperation,
),
),
),
),
],
);
}
void _moveMember(int index, int direction) {
_move(index, index + direction);
}
void _move(int from, int to) async {
if (from < 0 ||
from >= _controller.members.length ||
to < 0 ||
to >= _controller.members.length) {
return; //
}
final member = _controller.members[from];
setState(() {
_controller.members.removeAt(from);
});
await Future.delayed(const Duration(milliseconds: 50)); // UI
setState(() {
_controller.members.insert(to, member); //
_updateDisplay();
});
}
void _moveToTop(int index) {
_move(index, 0);
}
void _moveToBottom(int index) {
_move(index, _controller.members.length - 1);
}
void _deleteMember(int index) {
setState(() {
final memberName = _controller.members[index].name;
//
_controller.members.removeWhere((member) => member.name == '${memberName}_END');
_controller.members.removeAt(index);
_updateDisplay();
});
}
void _deleteAll() {
setState(() {
_controller.members.clear();
_updateDisplay();
});
}
void _selectOperation(String? operation) {
if (operation == null || operation.isEmpty) {
return;
}
setState(() {
_controller.selectedOperation = operation;
_updateDisplay();
});
}
Future<void> _updateDisplay() async {
_codeController.text = await _controller.genCodeString();
}
}

313
win_text_editor/lib/modules/code_creater/widgets/node_table.dart

@ -0,0 +1,313 @@ @@ -0,0 +1,313 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:pluto_grid/pluto_grid.dart';
import 'package:win_text_editor/framework/common/constants.dart';
import 'package:win_text_editor/modules/code_creater/services/code_create_service.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/components/my_pluto_column.dart';
import 'package:win_text_editor/shared/components/my_pluto_configuration.dart';
import 'package:win_text_editor/shared/components/my_pluto_dropdown_column.dart';
class NodeTable extends StatefulWidget {
final List<OutlineNode> members;
final Function(int, int) onMoveMember;
final Function(int) onDeleteMember;
final Function() onDeleteAll;
final Function(int) onMoveToTop;
final Function(int) onMoveToBottom;
final Function(int, String)? onActionTypeChanged; //
const NodeTable({
super.key,
required this.members,
required this.onMoveMember,
required this.onDeleteMember,
required this.onDeleteAll,
required this.onMoveToTop,
required this.onMoveToBottom,
this.onActionTypeChanged, //
});
@override
State<NodeTable> createState() => NodeTableState();
}
class _NodeTypeData {
final String originalType;
String currentSelection;
_NodeTypeData(this.originalType, this.currentSelection);
}
class NodeTableState extends State<NodeTable> {
PlutoGridStateManager? _stateManager;
int? _selectedRowIndex;
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Column(
children: [
// Header with controls
_buildHeader(),
// Table
Expanded(
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
PointerDeviceKind.stylus,
PointerDeviceKind.trackpad,
},
),
child: Scrollbar(
controller: _scrollController,
thickness: 12.0,
thumbVisibility: true,
interactive: true,
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.4,
child: PlutoGrid(
key: widget.key,
configuration: MyPlutoGridConfiguration(),
columns: _buildColumns(),
mode: PlutoGridMode.normal,
rows: widget.members.isEmpty ? [] : _buildRows(widget.members),
noRowsWidget: const Center(child: Text('')),
onLoaded: (PlutoGridOnLoadedEvent event) {
_stateManager = event.stateManager;
_stateManager?.setSelectingMode(PlutoGridSelectingMode.row);
_stateManager?.addListener(_handleRowSelection);
},
onChanged: (PlutoGridOnChangedEvent event) {
_handleCellChanged(event);
},
onRowDoubleTap: (event) {
_selectedRowIndex = event.rowIdx;
},
rowColorCallback: (rowContext) {
//
return rowContext.stateManager.currentRow?.key == rowContext.row.key
? Colors.blue[200]! //
: Colors.white; // null
},
),
),
),
),
),
),
],
);
}
Widget _buildHeader() {
final canMoveUp = _selectedRowIndex != null && _selectedRowIndex! > 0;
final canMoveDown = _selectedRowIndex != null && _selectedRowIndex! < widget.members.length - 1;
final canDelete = _selectedRowIndex != null;
final canDeleteAll = widget.members.isNotEmpty;
return Padding(
padding: const EdgeInsets.all(2.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('已添加节点', style: TextStyle(fontWeight: FontWeight.bold)),
Row(
children: [
IconButton(
icon: Icon(
Icons.arrow_upward,
size: 14,
color: canMoveUp ? Colors.blue : Colors.grey,
),
onPressed: canMoveUp ? () => widget.onMoveMember(_selectedRowIndex!, -1) : null,
tooltip: '上移一行',
),
IconButton(
icon: Icon(
Icons.vertical_align_top,
size: 14,
color: canMoveUp ? Colors.blue : Colors.grey,
),
onPressed: canMoveUp ? () => widget.onMoveToTop(_selectedRowIndex!) : null,
tooltip: '上移到顶',
),
IconButton(
icon: Icon(
Icons.arrow_downward,
size: 14,
color: canMoveDown ? Colors.blue : Colors.grey,
),
onPressed: canMoveDown ? () => widget.onMoveMember(_selectedRowIndex!, 1) : null,
tooltip: '下移一行',
),
IconButton(
icon: Icon(
Icons.vertical_align_bottom,
size: 14,
color: canMoveDown ? Colors.blue : Colors.grey,
),
onPressed: canMoveDown ? () => widget.onMoveToBottom(_selectedRowIndex!) : null,
tooltip: '下移到底',
),
IconButton(
icon: Icon(Icons.delete, size: 14, color: canDelete ? Colors.red : Colors.grey),
onPressed: canDelete ? () => widget.onDeleteMember(_selectedRowIndex!) : null,
tooltip: '删除行',
),
IconButton(
icon: Icon(
Icons.delete_forever,
size: 14,
color: canDeleteAll ? Colors.red : Colors.grey,
),
onPressed: canDeleteAll ? _showDeleteAllConfirmation : null,
tooltip: '全部删除',
),
],
),
],
),
);
}
void _showDeleteAllConfirmation() {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('确认删除'),
content: const Text('确定要删除所有节点吗?此操作不可撤销。'),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(false), child: const Text('取消')),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
_deleteAllMembers();
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
);
},
);
}
//
void _deleteAllMembers() {
setState(() {
_selectedRowIndex = null;
});
widget.onDeleteAll();
}
void _handleRowSelection() {
if (_stateManager?.currentRow == null) return;
setState(() {
_selectedRowIndex = _stateManager?.currentRow?.sortIdx;
});
}
void _handleCellChanged(PlutoGridOnChangedEvent event) {
if (event.column?.field == 'type' && event.rowIdx != null) {
final rowIndex = event.rowIdx!;
if (rowIndex >= 0 && rowIndex < widget.members.length) {
final newValue = event.value?.toString() ?? '';
setState(() {
widget.members[rowIndex].selectedAction = newValue;
});
// controller
if (widget.onActionTypeChanged != null) {
widget.onActionTypeChanged!(rowIndex, newValue);
}
}
}
}
final Map<Key, _NodeTypeData> _nodeTypeData = {};
List<PlutoColumn> _buildColumns() {
return [
MyPlutoColumn(
title: '#',
field: 'rowNum',
width: 40,
enableRowChecked: false, //
),
MyPlutoColumn(
title: '成员',
field: 'content',
width: 200,
enableRowChecked: false, //
),
MyPlutoDropdownColumn(
title: '操作类型',
field: 'type',
width: 120,
enableRowChecked: false, //
optionsBuilder: (rowKey) {
final data = _nodeTypeData[rowKey as ValueKey];
return _getOptions(data?.originalType ?? '');
},
),
];
}
List<PlutoRow> _buildRows(List<OutlineNode> members) {
return members
.asMap()
.map((index, member) {
final rowKey = ValueKey('${member.hashCode}_$index');
final options = _getOptions(member.value);
// Use selectedAction if it exists, otherwise use the first option
final initialValue =
member.selectedAction?.isNotEmpty == true
? member.selectedAction!
: options.isNotEmpty
? options.first
: '';
_nodeTypeData[rowKey] = _NodeTypeData(member.value, initialValue);
return MapEntry(
index,
PlutoRow(
key: rowKey,
cells: {
'rowNum': PlutoCell(value: index + 1),
'content': PlutoCell(value: member.title),
'type': PlutoCell(value: initialValue),
},
),
);
})
.values
.toList();
}
void refreshTable() {
if (_stateManager != null) {
_stateManager!.refRows.clear();
_stateManager!.refRows.addAll(_buildRows(widget.members));
_stateManager!.notifyListeners();
}
}
@override
void dispose() {
_stateManager?.removeListener(_handleRowSelection);
_scrollController.dispose();
super.dispose();
}
List<String> _getOptions(String s) {
return CodeCreateService.getNodeActions(s);
}
}

263
win_text_editor/lib/modules/content_search/controllers/content_search_controller.dart

@ -0,0 +1,263 @@ @@ -0,0 +1,263 @@
// content_search_controller.dart
import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/modules/content_search/services/count_search_service.dart';
import 'package:win_text_editor/modules/content_search/services/locate_search_service.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
import '../services/custom_search_service.dart';
class ContentSearchController extends BaseContentController {
String _searchQuery = '';
String _searchDirectory = '';
String _fileType = '*.*';
String _jumpFiles = '';
bool _caseSensitive = false;
bool _wholeWord = false;
bool _useRegex = false;
bool _customRule = false;
SearchMode _searchMode = SearchMode.locate;
final List<SearchResult> _results = [];
final List<SearchResult> _allResults = [];
bool _onlyZeroCounts = false;
bool _isSearching = false;
bool _shouldStop = false;
// Getters
String get searchQuery => _searchQuery;
String get searchDirectory => _searchDirectory;
String get fileType => _fileType;
String get jumpFiles => _jumpFiles;
bool get caseSensitive => _caseSensitive;
bool get wholeWord => _wholeWord;
bool get useRegex => _useRegex;
bool get customRule => _customRule;
SearchMode get searchMode => _searchMode;
List<SearchResult> get results => _results;
bool get onlyZeroCounts => _onlyZeroCounts;
double _progress = 0;
double get progress => _progress;
bool get isSearching => _isSearching;
//
void _updateProgress(double value) {
final newProgress = value.clamp(0.0, 100.0);
if (newProgress <= _progress) return;
_progress = newProgress;
notifyListeners();
}
//
void stopSearch() {
_shouldStop = true;
_isSearching = false;
notifyListeners();
}
set customRule(bool value) {
_customRule = value;
notifyListeners();
}
void setInitialSearchQuery(String query) {
if (_searchQuery != query) {
_searchQuery = query;
notifyListeners();
}
}
// Setters with notifyListeners
Timer? _searchDebounce;
set searchQuery(String value) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 500), () {
_searchQuery = value;
notifyListeners();
});
}
set searchDirectory(String value) {
_searchDirectory = value;
notifyListeners();
}
set fileType(String value) {
_fileType = value;
notifyListeners();
}
set jumpFiles(String value) {
_jumpFiles = value;
notifyListeners();
}
set caseSensitive(bool value) {
_caseSensitive = value;
notifyListeners();
}
set wholeWord(bool value) {
_wholeWord = value;
notifyListeners();
}
set useRegex(bool value) {
_useRegex = value;
notifyListeners();
}
set searchMode(SearchMode value) {
_searchMode = value;
notifyListeners();
}
void clearResults() {
results.clear();
_allResults.clear();
notifyListeners();
}
set onlyZeroCounts(bool value) {
_onlyZeroCounts = value;
_filterResults();
notifyListeners();
}
void _filterResults() {
_results.clear();
if (_searchMode == SearchMode.count) {
_results.addAll(
_allResults.where((r) => _onlyZeroCounts ? r.lineNumber == 0 : r.lineNumber > 0),
);
} else {
_results.addAll(_allResults);
}
}
// ContentSearchController
void removeResultByFilePath(String filePath) {
results.removeWhere((result) => result.filePath == filePath);
notifyListeners(); // ChangeNotifier
}
Future<void> startSearch() async {
_shouldStop = false;
_isSearching = true;
_progress = 0;
_results.clear();
_allResults.clear();
notifyListeners();
//
if (searchQuery.isEmpty) {
Logger().info("搜索内容不能为空");
return;
}
String first50Chars = searchQuery.length > 50 ? searchQuery.substring(0, 50) : searchQuery;
Logger().debug("开始搜索: $first50Chars...");
if (searchDirectory.isEmpty || !Directory(searchDirectory).existsSync()) {
Logger().info("搜索目录不能为空");
return;
}
try {
if (customRule) {
final validationResult = CustomSearchService.validateJsRule(searchQuery);
if (validationResult.isError) {
Logger().error('JavaScript 语法错误: ${validationResult.rawResult.toString()}');
return;
}
_allResults.addAll(
await CustomSearchService.performCustomSearch(
directory: searchDirectory,
fileType: fileType,
jsFunction: searchQuery,
searchMode: searchMode,
onProgress: _updateProgress,
shouldStop: () => _shouldStop,
),
);
} else {
if (searchMode == SearchMode.locate) {
_allResults.addAll(
await LocateSearchService.performSearch(
directory: searchDirectory,
query: searchQuery,
fileType: fileType,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
onProgress: _updateProgress,
shouldStop: () => _shouldStop,
),
);
} else {
final counts = await CountSearchService.performSearch(
directory: searchDirectory,
query: searchQuery,
fileType: fileType,
jumpFiles: jumpFiles,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
onProgress: _updateProgress,
shouldStop: () => _shouldStop,
);
counts.forEach((keyword, count) {
_allResults.add(
SearchResult(
filePath: keyword,
lineNumber: count,
lineContent: '',
matches: [],
queryTerm: keyword,
),
);
});
}
}
} catch (e) {
Logger().error("搜索出错: $e");
} finally {
_isSearching = false;
_progress = 100;
_shouldStop = false;
_filterResults();
notifyListeners();
}
}
Future<void> pickDirectory() async {
final dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
searchDirectory = dir;
}
}
@override
void onOpenFile(String filePath, {dynamic appendArg}) {
//searchDirectory = filePath;
//notifyListeners();
}
@override
void onOpenFolder(String folderPath) {
searchDirectory = folderPath;
}
@override
void onDropOutlineNode(OutlineNode node) {
// TODO: implement onDropOutlineNode
}
}

10
win_text_editor/lib/modules/content_search/models/count_result.dart

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
import 'package:win_text_editor/shared/base/selectable_item.dart';
class CountResult implements SelectableItem {
final String keyword;
final int matchCount;
@override
bool isSelected;
CountResult({required this.keyword, required this.matchCount, this.isSelected = false});
}

6
win_text_editor/lib/modules/content_search/models/match_result.dart

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
class MatchResult {
final int start;
final int end;
const MatchResult({required this.start, required this.end});
}

1
win_text_editor/lib/modules/content_search/models/search_mode.dart

@ -0,0 +1 @@ @@ -0,0 +1 @@
enum SearchMode { locate, count }

24
win_text_editor/lib/modules/content_search/models/search_result.dart

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
import 'package:win_text_editor/modules/content_search/models/match_result.dart';
import 'package:win_text_editor/shared/base/selectable_item.dart';
class SearchResult implements SelectableItem {
final String filePath;
final int lineNumber;
final String lineContent;
final List<MatchResult> matches;
final String queryTerm; //
@override
bool isSelected;
SearchResult({
required this.filePath,
required this.lineNumber,
required this.lineContent,
required this.matches,
required this.queryTerm,
this.isSelected = false,
});
@override
String toString() => lineContent;
}

187
win_text_editor/lib/modules/content_search/services/base_search_service.dart

@ -0,0 +1,187 @@ @@ -0,0 +1,187 @@
// lib/app/modules/content_search/services/base_search_service.dart
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart';
import 'package:docx_to_text/docx_to_text.dart';
import 'dart:convert';
import 'package:flutter_charset_detector/flutter_charset_detector.dart';
typedef ProgressCallback = void Function(double progress);
typedef FileProcessor = Future<void> Function(File file, String content);
abstract class BaseSearchService {
///
static Future<void> processFiles({
required String directory,
required String fileType,
required FileProcessor onFile,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final dir = Directory(directory);
final totalFiles = await countFilesInDirectory(dir, fileType, shouldStop);
if (totalFiles == 0) return;
onProgress?.call(1);
int processedFiles = 0;
int oldProgress = 1;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return;
if (entity is! File || !FileUtils.matchesFileType(entity.path, fileType)) continue;
// 1.
if (fileType != 'docx' && await _isBinaryFile(entity)) continue;
processedFiles++;
final progress = (processedFiles / totalFiles) * 99 + 1;
if (progress - oldProgress >= 1) {
oldProgress = progress.floor();
onProgress?.call(progress);
}
try {
// 2.
final content = await _readFileWithEncoding(entity);
await onFile(entity, content);
} catch (e) {
// 3. docx等文档
if (fileType == 'docx') {
final docxContent = await _extractTextFromDocx(entity);
if (docxContent != null) {
await onFile(entity, docxContent);
}
}
continue; //
}
}
onProgress?.call(100);
}
static Future<String> _readFileWithEncoding(File file) async {
try {
final bytes = await file.readAsBytes();
// 'UTF-8'
DecodingResult result = await CharsetDetector.autoDecode(bytes);
final encodingName = result.charset.toLowerCase();
// Dart Encoding
final encoding = _getEncodingFromName(encodingName);
return encoding.decode(bytes);
} catch (e) {
// 退UTF-8
return utf8.decode(await file.readAsBytes(), allowMalformed: true);
}
}
/// Dart Encoding
static Encoding _getEncodingFromName(String name) {
return Encoding.getByName(name) ?? latin1;
}
static Future<String?> _extractTextFromDocx(File file) async {
try {
final bytes = await file.readAsBytes();
return docxToText(bytes);
} catch (e) {
return null;
}
}
static Future<bool> _isBinaryFile(File file, {int sampleSize = 512}) async {
if (isBinaryFileByExtension(file.path)) return true;
final bytes = await file.openRead().take(sampleSize).expand((chunk) => chunk).toList();
if (await isBinaryFileByHeader(bytes)) return true;
//
return await isLikelyBinary(bytes);
}
static bool isBinaryFileByExtension(String filePath) {
final binaryExtensions = [
'.png', '.jpg', '.jpeg', '.gif', '.bmp', //
'.mp4', '.avi', '.mov', '.mkv', //
'.zip', '.rar', '.7z', '.tar', '.gz', //
'.exe', '.dll', '.so', '.a', //
];
return binaryExtensions.any((ext) => filePath.toLowerCase().endsWith(ext));
}
static Future<bool> isBinaryFileByHeader(List<int> bytes) async {
//
final magicNumbers = {
'PNG': [0x89, 0x50, 0x4E, 0x47],
'JPEG': [0xFF, 0xD8, 0xFF],
'GIF': [0x47, 0x49, 0x46],
'ZIP': [0x50, 0x4B, 0x03, 0x04],
'MP4': [0x00, 0x00, 0x00, 0x20, 0x66, 0x74, 0x79, 0x70],
};
return magicNumbers.values.any(
(header) =>
bytes.length >= header.length &&
const ListEquality().equals(bytes.sublist(0, header.length), header),
);
}
static Future<bool> isLikelyBinary(List<int> bytes) async {
int controlChars = 0;
for (final byte in bytes) {
// ASCII控制字符/
if (byte < 32 && byte != 9 && byte != 10 && byte != 13) {
controlChars++;
//
if (controlChars > 5) return true; //
}
}
return false;
}
///
static List<String> splitQuery(String query) {
return query
.split('\n') //
.expand((line) => line.split(',').map((q) => q.trim())) //
.where((q) => q.isNotEmpty) //
.toList();
}
static RegExp buildSearchPattern({
required String query,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
}) {
String pattern;
if (useRegex) {
pattern = query;
} else {
pattern = RegExp.escape(query);
if (wholeWord) {
pattern = '\\b$pattern\\b';
}
}
return RegExp(pattern, caseSensitive: caseSensitive, multiLine: true);
}
static Future<int> countFilesInDirectory(
Directory dir,
String fileType,
bool Function()? shouldStop,
) async {
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return totalFiles;
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
return totalFiles;
}
}

225
win_text_editor/lib/modules/content_search/services/count_search_service.dart

@ -0,0 +1,225 @@ @@ -0,0 +1,225 @@
import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'package:path/path.dart' as path;
import 'package:win_text_editor/modules/content_search/services/base_search_service.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart';
class CountSearchService {
static const _maxConcurrentIsolates = 8; // CPU核心数调整
static const _queriesPerBatch = 500; //
static List<List<T>> _chunkList<T>(List<T> list, int chunkSize) {
return List.generate(
(list.length / chunkSize).ceil(),
(i) => list.sublist(
i * chunkSize,
i * chunkSize + chunkSize > list.length ? list.length : i * chunkSize + chunkSize,
),
);
}
static Future<Map<String, int>> performSearch({
required String directory,
required String query,
required String fileType,
required String jumpFiles,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
if (shouldStop?.call() == true) return {};
final counts = <String, int>{};
final allQueries = BaseSearchService.splitQuery(query);
//
final queryBatches = _chunkList(allQueries, _queriesPerBatch);
//
final filePaths = await _collectFilePaths(directory, fileType, jumpFiles, shouldStop);
if (filePaths.isEmpty) return counts;
// Isolate池
final resultPort = ReceivePort();
final stopPort = ReceivePort();
final completer = Completer<Map<String, int>>();
int activeIsolates = 0;
//
int processedFiles = 0;
void updateProgress() {
final progress = (processedFiles / filePaths.length) * 100;
onProgress?.call(progress);
}
final initIsolates = min(2, _maxConcurrentIsolates); // 2
for (int i = 0; i < initIsolates; i++) {
_spawnIsolateWorker(
filePaths.sublist(i, i + 1), // Isolate处理部分文件
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
}
// Isolate
Future.delayed(const Duration(milliseconds: 300), () {
for (int i = activeIsolates; i < min(_maxConcurrentIsolates, filePaths.length); i++) {
_spawnIsolateWorker(
filePaths.sublist(i, i + 1), // Isolate处理部分文件
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
}
});
//
resultPort.listen((msg) {
if (msg is Map<String, int>) {
msg.forEach((query, count) {
counts.update(query, (v) => v + count, ifAbsent: () => count);
});
processedFiles++;
updateProgress();
//
if (activeIsolates < filePaths.length) {
_spawnIsolateWorker(
[filePaths[activeIsolates]],
queryBatches,
resultPort.sendPort,
stopPort.sendPort,
caseSensitive,
wholeWord,
useRegex,
);
activeIsolates++;
} else if (processedFiles == filePaths.length) {
completer.complete(counts);
}
}
});
//
stopPort.listen((_) => completer.complete(counts));
return completer.future.whenComplete(() {
resultPort.close();
stopPort.close();
});
}
static Future<List<String>> _collectFilePaths(
String directory,
String fileType,
String jumpFiles,
bool Function()? shouldStop,
) async {
final dir = Directory(directory);
final paths = <String>[];
final jumpFileList = BaseSearchService.splitQuery(jumpFiles.toLowerCase());
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) break;
if (entity is File &&
!jumpFileList.contains(path.basename(entity.path).toLowerCase()) &&
FileUtils.matchesFileType(entity.path, fileType)) {
paths.add(entity.path);
}
}
return paths;
}
static void _spawnIsolateWorker(
List<String> filePaths,
List<List<String>> queryBatches,
SendPort resultPort,
SendPort stopPort,
bool caseSensitive,
bool wholeWord,
bool useRegex,
) async {
await Isolate.spawn(
_isolateEntry,
_IsolateData(
filePaths,
queryBatches,
resultPort,
stopPort,
caseSensitive,
wholeWord,
useRegex,
),
);
}
static void _isolateEntry(_IsolateData data) {
final localCounts = <String, int>{};
for (final filePath in data.filePaths) {
try {
final content = File(filePath).readAsStringSync();
for (final batch in data.queryBatches) {
//
final batchResults = ParallelTask.runSync(() {
final batchCounts = <String, int>{};
for (final query in batch) {
final pattern = BaseSearchService.buildSearchPattern(
query: query,
caseSensitive: data.caseSensitive,
wholeWord: data.wholeWord,
useRegex: data.useRegex,
);
batchCounts[query] = pattern.allMatches(content).length;
}
return batchCounts;
});
batchResults.forEach((query, count) {
localCounts.update(query, (v) => v + count, ifAbsent: () => count);
});
}
} catch (e) {
print('Isolate error: $e');
}
}
data.resultPort.send(localCounts);
}
}
// Isolate通信数据结构
class _IsolateData {
final List<String> filePaths;
final List<List<String>> queryBatches;
final SendPort resultPort;
final SendPort stopPort;
final bool caseSensitive;
final bool wholeWord;
final bool useRegex;
_IsolateData(
this.filePaths,
this.queryBatches,
this.resultPort,
this.stopPort,
this.caseSensitive,
this.wholeWord,
this.useRegex,
);
}
//
class ParallelTask {
static T runSync<T>(T Function() task) => task();
}

133
win_text_editor/lib/modules/content_search/services/custom_search_service.dart

@ -0,0 +1,133 @@ @@ -0,0 +1,133 @@
// lib/app/modules/content_search/content_search_service.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter_js/flutter_js.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart';
typedef ProgressCallback = void Function(double progress);
class CustomSearchService {
/// JavaScript
static Future<List<SearchResult>> performCustomSearch({
required String directory,
required String fileType,
required String jsFunction,
required SearchMode searchMode,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
int count = 0;
final dir = Directory(directory);
final jsRuntime = getJavascriptRuntime();
try {
// JavaScript
final jsCode = 'function match(content){$jsFunction};';
jsRuntime.evaluate(jsCode);
int totalFiles = 0;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) {
totalFiles++;
}
}
onProgress?.call(1);
int processedFiles = 0;
int calledProcessedFiles = 1;
await for (final entity in dir.list(recursive: true)) {
if (shouldStop?.call() == true) return results;
if (entity is File && FileUtils.matchesFileType(entity.path, fileType)) {
try {
final lines = await entity.readAsLines();
for (int i = 0; i < lines.length; i++) {
if (shouldStop?.call() == true) return results;
final line = lines[i].trim();
if (line.length < 3) continue; //
final result = jsRuntime.evaluate('match(${jsonEncode(line)});');
if (result.isError) {
throw Exception('JS Error: ${result.stringResult}');
}
if (result.stringResult == 'true') {
if (searchMode == SearchMode.locate) {
results.add(
SearchResult(
filePath: entity.path,
lineNumber: i + 1,
lineContent: line,
matches: [],
queryTerm: "Custom Rule",
),
);
} else {
count++;
}
}
}
} catch (e) {
Logger().error('Error in file ${entity.path}: $e');
}
processedFiles++;
final progress = (processedFiles / totalFiles) * 99 + 1;
if (processedFiles - calledProcessedFiles >= 1) {
calledProcessedFiles = processedFiles;
onProgress?.call(progress);
}
}
}
//
if (searchMode == SearchMode.count) {
results.add(
SearchResult(
filePath: "Custom Rule",
lineNumber: count,
lineContent: '',
matches: [],
queryTerm: "Custom Rule",
),
);
}
} finally {
onProgress?.call(100);
jsRuntime.dispose();
}
return results;
}
/// JavaScript
static JsEvalResult validateJsRule(String jsFunction) {
final jsRuntime = getJavascriptRuntime();
try {
final jsCode = 'function match(content) {$jsFunction};';
// JS
final result = jsRuntime.evaluate(jsCode);
if (result.isError) {
return result;
}
//
final test = jsRuntime.evaluate("match('test');");
if (test.stringResult != 'true' && test.stringResult != 'false') {
return JsEvalResult('Custom rule must return boolean (true/false)', null, isError: true);
}
return test;
} finally {
jsRuntime.dispose();
}
}
}

112
win_text_editor/lib/modules/content_search/services/locate_search_service.dart

@ -0,0 +1,112 @@ @@ -0,0 +1,112 @@
// lib/app/modules/content_search/services/locate_search_service.dart
import 'dart:io';
import 'package:win_text_editor/modules/content_search/models/match_result.dart';
import '../models/search_result.dart';
import 'base_search_service.dart';
class LocateSearchService {
static Future<List<SearchResult>> performSearch({
required String directory,
required String query,
required String fileType,
required bool caseSensitive,
required bool wholeWord,
required bool useRegex,
ProgressCallback? onProgress,
bool Function()? shouldStop,
}) async {
final results = <SearchResult>[];
final queries = BaseSearchService.splitQuery(query);
await BaseSearchService.processFiles(
directory: directory,
fileType: fileType,
onProgress: onProgress,
shouldStop: shouldStop,
onFile: (file, content) async {
for (final q in queries) {
final pattern = BaseSearchService.buildSearchPattern(
query: q,
caseSensitive: caseSensitive,
wholeWord: wholeWord,
useRegex: useRegex,
);
final matches = pattern.allMatches(content);
if (matches.isNotEmpty) {
results.addAll(_convertMatchesToResults(file, content, matches, q));
}
}
},
);
return results;
}
/// SearchResult
static List<SearchResult> _convertMatchesToResults(
File file,
String content,
Iterable<RegExpMatch> matches,
String queryTerm,
) {
final results = <SearchResult>[];
final lines = content.split('\n');
// ->
final lineMatches = <int, List<RegExpMatch>>{};
for (final match in matches) {
//
final lineNumber = _getLineNumber(lines, match.start);
lineMatches.putIfAbsent(lineNumber, () => []).add(match);
}
//
lineMatches.forEach((lineNumber, matchesInLine) {
final lineContent = lines[lineNumber - 1]; // 1
final lineResults =
matchesInLine.map((m) {
//
final lineStartPos = _getLineStartPos(lines, lineNumber - 1);
final startInLine = m.start - lineStartPos;
final endInLine = m.end - lineStartPos;
return MatchResult(start: startInLine, end: endInLine);
}).toList();
results.add(
SearchResult(
filePath: file.path,
lineNumber: lineNumber,
lineContent: lineContent,
matches: lineResults,
queryTerm: queryTerm,
),
);
});
return results;
}
///
static int _getLineNumber(List<String> lines, int charPos) {
int currentPos = 0;
for (int i = 0; i < lines.length; i++) {
currentPos += lines[i].length + 1; // +1
if (charPos < currentPos) {
return i + 1; // 1
}
}
return lines.length; //
}
///
static int _getLineStartPos(List<String> lines, int lineIndex) {
int pos = 0;
for (int i = 0; i < lineIndex; i++) {
pos += lines[i].length + 1; // +1
}
return pos;
}
}

63
win_text_editor/lib/modules/content_search/widgets/content_search_view.dart

@ -0,0 +1,63 @@ @@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import '../controllers/content_search_controller.dart';
import 'directory_settings.dart';
import 'search_settings.dart';
import 'results_view.dart';
class ContentSearchView extends StatefulWidget {
final String tabId;
const ContentSearchView({super.key, required this.tabId});
@override
ContentSearchViewState createState() => ContentSearchViewState();
}
class ContentSearchViewState extends State<ContentSearchView> {
late final ContentSearchController _controller;
get tabManager => Provider.of<TabItemsController>(context, listen: false);
@override
void initState() {
super.initState();
_controller = tabManager.getController(widget.tabId) ?? ContentSearchController();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
children: [
const DirectorySettings(), // controller
const SearchSettings(),
Consumer<ContentSearchController>(
builder: (context, controller, _) {
return Column(
children: [
LinearProgressIndicator(
value: controller.progress / 100,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
minHeight: 8,
),
],
);
},
),
const Expanded(child: ResultsView()),
],
),
),
);
}
}

105
win_text_editor/lib/modules/content_search/widgets/directory_settings.dart

@ -0,0 +1,105 @@ @@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
class DirectorySettings extends StatefulWidget {
const DirectorySettings({super.key});
@override
State<DirectorySettings> createState() => _DirectorySettingsState();
}
class _DirectorySettingsState extends State<DirectorySettings> {
late TextEditingController _searchDirectoryController;
late TextEditingController _fileTypeController;
late TextEditingController _jumpFilesController;
@override
void initState() {
super.initState();
final controller = context.read<ContentSearchController>();
_searchDirectoryController = TextEditingController(text: controller.searchDirectory);
_fileTypeController = TextEditingController(text: controller.fileType);
_jumpFilesController = TextEditingController(text: controller.jumpFiles);
}
@override
void dispose() {
_searchDirectoryController.dispose();
_fileTypeController.dispose();
_jumpFilesController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// 使 Consumer ContentSearchController
return Consumer<ContentSearchController>(
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),
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
await controller.pickDirectory();
// _searchDirectoryController.text
// Consumer
},
),
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),
SizedBox(
width: 200,
child: TextField(
controller: _jumpFilesController,
decoration: const InputDecoration(
labelText: '跳过文件(列表)',
border: OutlineInputBorder(),
),
onChanged: (value) => controller.jumpFiles = value,
),
),
],
),
),
);
},
);
}
}

318
win_text_editor/lib/modules/content_search/widgets/results_view.dart

@ -0,0 +1,318 @@ @@ -0,0 +1,318 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:path/path.dart' as path;
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/modules/content_search/models/count_result.dart';
import 'dart:io';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
import 'package:win_text_editor/modules/content_search/models/search_result.dart';
import 'package:win_text_editor/shared/base/my_sf_data_grid.dart';
import 'package:win_text_editor/shared/base/my_sf_data_source.dart';
import 'package:win_text_editor/shared/components/my_grid_column.dart';
// import 'package:recycle_bin/recycle_bin.dart';
class ResultsView extends StatelessWidget {
const ResultsView({super.key});
@override
Widget build(BuildContext context) {
final controller = context.watch<ContentSearchController>();
//
final previousSearchMode = context.select<ContentSearchController, SearchMode>(
(c) => c.searchMode,
);
if (previousSearchMode != controller.searchMode) {
controller.clearResults();
}
return Card(
child:
controller.searchMode == SearchMode.locate
? _buildLocateGrid(controller, context)
: _buildCountGrid(controller, context),
);
}
Widget _buildLocateGrid(ContentSearchController controller, BuildContext context) {
return MySfDataGrid<SearchResult>(
source: LocateDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'index', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件(行号)', maximumWidth: 400),
MyGridColumn(columnName: 'content', label: '内容'),
ShortGridColumn(columnName: 'action', label: '操作', width: 90),
],
selectable: false,
);
}
Widget _buildCountGrid(ContentSearchController controller, BuildContext context) {
return MySfDataGrid<CountResult>(
source: CountDataSource(controller, context),
columns: [
ShortGridColumn(columnName: 'index', label: '序号'),
MyGridColumn(columnName: 'keyword', label: '关键词', minimumWidth: 300),
MyGridColumn(columnName: 'matchCount', label: '匹配数量'),
],
selectable: false,
);
}
}
class LocateDataSource extends MySfDataSource<SearchResult> {
final ContentSearchController controller;
final BuildContext context;
LocateDataSource(this.controller, this.context)
: super(controller.results, onSelectionChanged: (index, isSelected) {});
@override
List<DataGridRow> get rows =>
controller.results.asMap().entries.map((result) {
final index = result.key + 1;
return DataGridRow(
cells: [
DataGridCell(columnName: 'index', value: index),
DataGridCell(
columnName: 'file',
value: '${path.basename(result.value.filePath)}(${result.value.lineNumber})',
),
DataGridCell(
columnName: 'content',
value: result.value, //
),
DataGridCell(
columnName: 'action',
value: result.value.filePath, // Store file path for delete action
),
],
);
}).toList();
@override
DataGridRowAdapter buildRow(DataGridRow row) {
final cells = row.getCells();
final result = cells[2].value as SearchResult;
return DataGridRowAdapter(
cells: [
Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cells[0].value.toString()),
),
//
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cells[1].value.toString(), overflow: TextOverflow.ellipsis, maxLines: 1),
),
//
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: _buildHighlightedText(result.lineContent, result.matches),
),
),
Container(
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.open_in_new, size: 18, color: Colors.blue),
onPressed: () => _openFile(result.filePath),
),
IconButton(
icon: const Icon(Icons.delete_forever, size: 18, color: Colors.red),
onPressed: () => _showDeleteConfirmation(result.filePath),
),
],
),
),
],
);
}
Future<void> _openFile(String filePath) async {
try {
final file = File(filePath);
if (await file.exists()) {
// Use Process.run to open the file with the default system handler
if (Platform.isWindows) {
await Process.run('start', ['""', filePath], runInShell: true);
} else if (Platform.isMacOS) {
await Process.run('open', [filePath], runInShell: true);
} else if (Platform.isLinux) {
await Process.run('xdg-open', [filePath], runInShell: true);
}
} else {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('文件不存在: $filePath')));
}
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('打开文件失败: ${e.toString()}')));
}
}
}
Future<void> _showDeleteConfirmation(String filePath) async {
bool confirmed = false;
await showDialog<bool>(
context: context,
builder: (context) {
//
return Shortcuts(
shortcuts: const {
//
SingleActivator(LogicalKeyboardKey.enter): _ConfirmAction(),
},
child: Actions(
actions: {
_ConfirmAction: CallbackAction<_ConfirmAction>(
onInvoke: (_) {
confirmed = true;
Navigator.pop(context, true);
return null;
},
),
},
child: Focus(
autofocus: true, //
child: AlertDialog(
title: const Text('确认删除'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('将所选文件删除吗?'),
const SizedBox(height: 8),
Text(filePath, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('取消'),
),
TextButton(
onPressed: () {
confirmed = true;
Navigator.pop(context, true);
},
child: const Text('确认'),
),
],
),
),
),
);
},
);
if (confirmed && context.mounted) {
try {
await File(filePath).delete();
controller.removeResultByFilePath(filePath);
notifyListeners();
if (context.mounted) {
Logger().info('已删除文件: $filePath');
}
} catch (e) {
if (context.mounted) {
Logger().error('删除失败: ${e.toString()}');
}
}
}
}
Widget _buildHighlightedText(String text, matches) {
final spans = <TextSpan>[];
int lastEnd = 0;
for (final match in matches) {
if (match.start > lastEnd) {
spans.add(
TextSpan(
text: text.substring(lastEnd, match.start),
style: const TextStyle(color: Colors.black),
),
);
}
spans.add(
TextSpan(
text: text.substring(match.start, match.end),
style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
),
);
lastEnd = match.end;
}
if (lastEnd < text.length) {
spans.add(
TextSpan(text: text.substring(lastEnd), style: const TextStyle(color: Colors.black)),
);
}
return RichText(text: TextSpan(children: spans), overflow: TextOverflow.clip);
}
}
class CountDataSource extends MySfDataSource<CountResult> {
final ContentSearchController controller;
final BuildContext context;
late final List<MapEntry<String, int>> _counts;
CountDataSource(this.controller, this.context)
: super([], onSelectionChanged: (index, isSelected) {}) {
final counts = <String, int>{};
for (var result in controller.results) {
counts[result.filePath] = (counts[result.filePath] ?? 0) + result.lineNumber;
}
_counts = counts.entries.toList();
}
@override
List<DataGridRow> get rows =>
_counts.asMap().entries.map((entry) {
final index = entry.key + 1;
return DataGridRow(
cells: [
DataGridCell(columnName: 'index', value: index),
DataGridCell(columnName: 'keyword', value: entry.value.key),
DataGridCell(columnName: 'count', value: '${entry.value.value}'),
],
);
}).toList();
@override
DataGridRowAdapter buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map((cell) {
return Container(
alignment: cell.columnName == 'count' ? Alignment.centerRight : Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis, maxLines: 1),
);
}).toList(),
);
}
}
class _ConfirmAction extends Intent {
const _ConfirmAction();
}

298
win_text_editor/lib/modules/content_search/widgets/search_settings.dart

@ -0,0 +1,298 @@ @@ -0,0 +1,298 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:win_text_editor/shared/components/my_checkbox.dart';
import 'package:win_text_editor/shared/components/text_editor.dart';
import 'package:win_text_editor/modules/content_search/controllers/content_search_controller.dart';
import 'package:win_text_editor/modules/content_search/models/search_mode.dart';
class SearchSettings extends StatefulWidget {
const SearchSettings({super.key});
@override
State<SearchSettings> createState() => _SearchSettingsState();
}
class _SearchSettingsState extends State<SearchSettings> {
Duration _elapsedTime = Duration.zero;
final Stopwatch _stopwatch = Stopwatch();
Timer? _updateTimer;
@override
void dispose() {
_updateTimer?.cancel();
_stopwatch.stop();
super.dispose();
}
void _startTimer() {
_elapsedTime = Duration.zero;
_stopwatch
..reset()
..start();
// 100UI
_updateTimer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
setState(() {
_elapsedTime = _stopwatch.elapsed;
});
});
}
void _stopTimer() {
_updateTimer?.cancel();
_updateTimer = null;
_stopwatch.stop();
//
_elapsedTime = _stopwatch.elapsed;
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String twoDigitMinutes = twoDigits(duration.inMinutes.remainder(60));
String twoDigitSeconds = twoDigits(duration.inSeconds.remainder(60));
String twoDigitMillis = twoDigits(duration.inMilliseconds.remainder(1000) ~/ 10);
return "${twoDigits(duration.inHours)}:$twoDigitMinutes:$twoDigitSeconds.$twoDigitMillis";
}
@override
Widget build(BuildContext context) {
final controller = context.watch<ContentSearchController>();
//
if (controller.isSearching && _updateTimer == null) {
_startTimer();
} else if (!controller.isSearching && _updateTimer != null) {
_stopTimer();
}
return Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ()
Expanded(
child: SizedBox(
height: 360,
child: TextEditor(
tabId: 'search_content_${controller.hashCode}',
title: '搜索内容[列表以换行或半角逗号分隔]',
initialContent: controller.searchQuery, //
onContentChanged: (content) {
controller.searchQuery = content; //
},
),
),
),
const SizedBox(width: 8),
//
SizedBox(
width: 310,
child: Container(
height: 360,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(4),
),
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// -
const Text('搜索方式:', style: TextStyle(fontSize: 12)),
const SizedBox(height: 4),
Row(
children: [
Expanded(
child: Row(
children: [
Transform.scale(
scale: 0.75,
child: Radio<SearchMode>(
value: SearchMode.locate,
groupValue: controller.searchMode,
onChanged: (value) {
if (value != null) {
controller.clearResults(); //
controller.searchMode = value;
}
},
),
),
const Text('定位', style: TextStyle(fontSize: 12)),
],
),
),
Expanded(
child: Row(
children: [
Transform.scale(
scale: 0.75,
child: Radio<SearchMode>(
value: SearchMode.count,
groupValue: controller.searchMode,
onChanged: (value) {
if (value != null) {
controller.clearResults(); //
controller.searchMode = value;
}
},
),
),
const Text('计数', style: TextStyle(fontSize: 12)),
],
),
),
],
),
Visibility(
visible: controller.searchMode == SearchMode.count,
child: Column(
children: [
MyCheckbox(
title: '显示未匹配列表',
value: controller.onlyZeroCounts,
onChanged:
controller.searchMode == SearchMode.count
? (value) {
controller.onlyZeroCounts = value!;
}
: null,
),
const SizedBox(height: 4),
],
),
),
const SizedBox(height: 4),
//
const Text('匹配规则:', style: TextStyle(fontSize: 12)),
const SizedBox(height: 4),
//
Row(
children: [
Expanded(
child: MyCheckbox(
title: '大小写敏感',
value: controller.caseSensitive,
onChanged: (value) {
controller.caseSensitive = value!;
if (value) {
controller.customRule = false;
controller.useRegex = false;
}
},
),
),
Expanded(
child: MyCheckbox(
title: '全字匹配',
value: controller.wholeWord,
onChanged: (value) {
controller.wholeWord = value!;
if (value) {
controller.customRule = false;
controller.useRegex = false;
}
},
),
),
],
),
Row(
children: [
Expanded(
child: MyCheckbox(
title: '正则匹配',
value: controller.useRegex,
onChanged: (value) {
controller.useRegex = value!;
if (value) {
controller.caseSensitive = false;
controller.wholeWord = false;
controller.customRule = false;
}
},
),
),
Expanded(
child: MyCheckbox(
title: '自定义规则',
value: controller.customRule,
onChanged: (value) {
controller.customRule = value!;
if (value) {
controller.caseSensitive = false;
controller.wholeWord = false;
controller.useRegex = false;
}
},
),
),
],
),
const SizedBox(height: 8),
// JS函数说明
Visibility(
visible: controller.customRule,
child: const Column(
children: [
Text.rich(
TextSpan(
text:
"在搜索内容输入框中输入js函数体:\n"
"function boolean match(content) {//content为单行文本\n"
"}//返回是否匹配",
style: TextStyle(fontSize: 12, fontFamily: 'monospace'),
),
),
SizedBox(height: 8),
],
),
),
//
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.search, size: 20),
label: const Text('开始搜索'),
onPressed:
controller.isSearching ? null : () => controller.startSearch(),
),
),
const SizedBox(width: 8),
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.stop, color: Colors.red),
label: const Text('停止', style: TextStyle(color: Colors.red)),
onPressed:
controller.isSearching
? () {
controller.stopSearch();
_stopTimer();
}
: null,
),
),
],
),
const SizedBox(height: 8),
Text(
' 搜索计时:[${_formatDuration(_elapsedTime)}]',
style: const TextStyle(fontSize: 12),
),
],
),
),
),
],
),
),
);
}
}

258
win_text_editor/lib/modules/data_compare/controllers/data_compare_controller.dart

@ -0,0 +1,258 @@ @@ -0,0 +1,258 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class DataCompareController extends BaseContentController {
List<String> leftColumns = [];
List<String> rightColumns = [];
List<Map<String, dynamic>> leftData = [];
List<Map<String, dynamic>> rightData = [];
//
List<Map<String, dynamic>> comparedData = [];
Future<void> importCsv(bool isLeftTable, String csvContent) async {
try {
// 1.
final lines = csvContent.split('\n');
if (lines.isEmpty) return;
String delimiter = lines[0].contains('\t') ? '\t' : ',';
// 2.
final headers = lines[0].split(delimiter).skip(1).toList();
// 3.
List<Map<String, dynamic>> data = [];
for (int i = 1; i < lines.length; i++) {
if (lines[i].trim().isEmpty) continue;
final values = lines[i].split(delimiter);
if (values.isEmpty) continue;
//
final row = {
'serial': i.toString(),
'key': values[0],
...Map.fromIterables(
headers,
List.generate(headers.length, (j) => j + 1 < values.length ? values[j + 1] : ''),
),
};
data.add(row);
}
if (isLeftTable) {
leftColumns = headers;
leftData = data;
} else {
rightColumns = headers;
rightData = data;
}
_compareData();
notifyListeners();
} catch (e) {
Logger().error('${isLeftTable ? '左表' : '右表'}导入失败: $e');
rethrow;
}
}
void _compareData() {
comparedData = [];
// 1.
if (leftData.isEmpty && rightData.isEmpty) return;
//
if (leftData.isEmpty || rightData.isEmpty) {
final source = leftData.isEmpty ? rightData : leftData;
comparedData =
source
.map((row) => {...row, 'match_status': 'no_match', 'is_left': leftData.isNotEmpty})
.toList();
return;
}
// 3.
final rightMap = {for (var r in rightData) r['key']: r};
//
for (var leftRow in leftData) {
final rightRow = rightMap[leftRow['key']];
if (rightRow != null) {
bool isFullMatch = leftColumns.every((col) => leftRow[col] == rightRow[col]);
comparedData.add({
...leftRow,
'match_status': isFullMatch ? 'full_match' : 'key_match',
'is_left': true,
'right_data': rightRow,
});
rightMap.remove(leftRow['key']);
} else {
comparedData.add({...leftRow, 'match_status': 'no_match', 'is_left': true});
}
}
//
comparedData.addAll(
rightMap.values.map((row) => ({...row, 'match_status': 'no_match', 'is_left': false})),
);
// 4.
comparedData.sort((a, b) {
const order = {'full_match': 0, 'key_match': 1, 'no_match': 2};
return order[a['match_status']]!.compareTo(order[b['match_status']]!);
});
}
void exportLeftTable(String matchType) async {
try {
final filteredData =
comparedData.where((row) {
if (!row['is_left']) return false;
return row['match_status'] == matchType;
}).toList();
if (filteredData.isEmpty) {
Logger().info('没有匹配类型的左表数据可导出');
return;
}
// CSV内容
final csvContent = _generateCsvContent(filteredData, leftColumns);
final fileName = 'left_table_${_getMatchTypeName(matchType)}.csv';
await _saveCsvFile(fileName, csvContent);
Logger().info('左表(${_getMatchTypeName(matchType)})导出成功');
} catch (e) {
Logger().error('左表导出失败: $e');
rethrow;
}
}
void exportRightTable(String matchType) async {
try {
final List<Map<String, dynamic>> filteredData = [];
//
if (matchType == 'full_match') {
filteredData.addAll(
comparedData.where((row) {
return row['match_status'] == "full_match";
}).toList(),
);
} else if (matchType == 'key_match') {
//right_data中取出右表数据
filteredData.addAll(
comparedData
.where((row) => row['match_status'] == "key_match")
.map((row) {
// right_data存在且是Map类型
if (row['right_data'] is Map<String, dynamic>) {
return row['right_data'] as Map<String, dynamic>;
} else {
return <String, dynamic>{}; // Map作为保护
}
})
.where((row) => row.isNotEmpty) //
.toList(),
);
} else {
filteredData.addAll(
comparedData.where((row) {
if (row['is_left']) return false;
return row['match_status'] == matchType;
}).toList(),
);
}
if (filteredData.isEmpty) {
Logger().info('没有匹配类型的右表数据可导出');
return;
}
// CSV内容
final csvContent = _generateCsvContent(filteredData, rightColumns);
final fileName = 'right_table_${_getMatchTypeName(matchType)}.csv';
await _saveCsvFile(fileName, csvContent);
Logger().info('右表(${_getMatchTypeName(matchType)})导出成功');
} catch (e) {
Logger().error('右表导出失败: $e');
rethrow;
}
}
// CSV内容
String _generateCsvContent(List<Map<String, dynamic>> data, List<String> columns) {
final buffer = StringBuffer();
// ()
buffer.write('主键'); // "序号"
for (var column in columns) {
buffer.write('\t$column');
}
buffer.writeln();
//
for (var row in data) {
buffer.write('${row['key']}'); // serial
for (var column in columns) {
buffer.write('\t${row[column] ?? ''}');
}
buffer.writeln();
}
return buffer.toString();
}
// CSV文件
Future<void> _saveCsvFile(String fileName, String content) async {
final filePath = await FilePicker.platform.saveFile(
dialogTitle: '保存对比结果',
fileName: fileName,
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (filePath != null) {
final file = File(filePath);
await file.writeAsString(content);
}
}
//
String _getMatchTypeName(String matchType) {
switch (matchType) {
case 'full_match':
return '整行匹配';
case 'key_match':
return '主键匹配';
case 'no_match':
return '不匹配';
default:
return '未知类型';
}
}
@override
void onOpenFile(String filePath, {dynamic appendArg}) {
// TODO: implement onOpenFile
}
@override
void onOpenFolder(String folderPath) {
// TODO: implement onOpenFolder
}
@override
void onDropOutlineNode(OutlineNode node) {
// TODO: implement onDropOutlineNode
}
}

93
win_text_editor/lib/modules/data_compare/widgets/data_compare_data_source.dart

@ -0,0 +1,93 @@ @@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart';
class DataCompareDataSource extends DataGridSource {
final DataCompareController controller;
DataCompareDataSource(this.controller) {
controller.addListener(_handleDataChange);
}
void _handleDataChange() {
notifyListeners();
}
@override
void dispose() {
controller.removeListener(_handleDataChange);
super.dispose();
}
@override
List<DataGridRow> get rows {
return controller.comparedData.map((row) {
final isLeft = row['is_left'] as bool;
final rightData = row['right_data'] as Map<String, dynamic>?;
final status = row['match_status'] as String;
return DataGridRow(
cells: [
// -
DataGridCell<String>(
columnName: 'left_serial',
value: (isLeft || status != 'no_match') ? row['serial'] : '',
),
DataGridCell<String>(
columnName: 'left_key',
value: (isLeft || status != 'no_match') ? row['key'] : '',
),
...controller.leftColumns.map(
(col) => DataGridCell<String>(
columnName: 'left_$col',
value: (isLeft || status != 'no_match') ? row[col] : '',
),
),
//
DataGridCell<Icon>(columnName: 'comparison', value: _getStatusIcon(status)),
// -
DataGridCell<String>(
columnName: 'right_serial',
value: (!isLeft || status != 'no_match') ? (rightData?['serial'] ?? row['serial']) : '',
),
DataGridCell<String>(
columnName: 'right_key',
value: (!isLeft || status != 'no_match') ? (rightData?['key'] ?? row['key']) : '',
),
...controller.rightColumns.map(
(col) => DataGridCell<String>(
columnName: 'right_$col',
value: (!isLeft || status != 'no_match') ? (rightData?[col] ?? row[col]) : '',
),
),
],
);
}).toList();
}
Icon _getStatusIcon(String status) {
switch (status) {
case 'full_match':
return const Icon(Icons.double_arrow, color: Colors.green);
case 'key_match':
return const Icon(Icons.arrow_forward_ios, color: Colors.blue);
default:
return const Icon(Icons.close, color: Colors.red);
}
}
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map<Widget>((cell) {
return Container(
alignment: Alignment.center,
child: cell.value.runtimeType == Icon ? cell.value : Text(cell.value.toString()),
);
}).toList(),
);
}
}

95
win_text_editor/lib/modules/data_compare/widgets/data_compare_grid.dart

@ -0,0 +1,95 @@ @@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart';
class DataCompareGrid extends StatelessWidget {
final DataCompareController controller;
final DataGridSource dataSource;
const DataCompareGrid({super.key, required this.dataSource, required this.controller});
@override
Widget build(BuildContext context) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(2.0), //
),
child: SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: dataSource,
columns: _buildColumns(),
stackedHeaderRows: _buildStackedHeaders(),
columnWidthMode: ColumnWidthMode.fill,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
),
);
}
List<GridColumn> _buildColumns() {
return [
//
GridColumn(columnName: 'left_serial', width: 60, label: _buildHeaderCell('序号')),
GridColumn(columnName: 'left_key', label: _buildHeaderCell('主键')),
...controller.leftColumns.map(
(col) => GridColumn(columnName: 'left_$col', label: _buildHeaderCell(col)),
),
//
GridColumn(
columnName: 'comparison',
label: _buildHeaderCell('对比', color: Colors.purple[50]),
width: 80,
),
//
GridColumn(columnName: 'right_serial', width: 60, label: _buildHeaderCell('序号')),
GridColumn(columnName: 'right_key', label: _buildHeaderCell('主键')),
...controller.rightColumns.map(
(col) => GridColumn(columnName: 'right_$col', label: _buildHeaderCell(col)),
),
];
}
List<StackedHeaderRow> _buildStackedHeaders() {
return [
//
StackedHeaderRow(
cells: [
StackedHeaderCell(
columnNames: [
'left_serial',
'left_key',
...controller.leftColumns.map((col) => 'left_$col'),
],
child: _buildGroupHeader('左表', color: Colors.green[50]),
),
StackedHeaderCell(
columnNames: [
'right_serial',
'right_key',
...controller.rightColumns.map((col) => 'right_$col'),
],
child: _buildGroupHeader('右表', color: Colors.blue[50]),
),
],
),
];
}
Container _buildHeaderCell(String text, {Color? color}) {
return Container(
alignment: Alignment.center,
color: color ?? Colors.grey[200],
child: Text(text, style: const TextStyle(fontWeight: FontWeight.normal)),
);
}
Container _buildGroupHeader(String text, {Color? color}) {
return Container(
alignment: Alignment.center,
color: color ?? Colors.purple[50],
child: Text(text, style: const TextStyle(fontWeight: FontWeight.normal)),
);
}
}

231
win_text_editor/lib/modules/data_compare/widgets/data_compare_view.dart

@ -0,0 +1,231 @@ @@ -0,0 +1,231 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart';
import 'package:win_text_editor/modules/data_compare/controllers/data_compare_controller.dart';
import 'package:win_text_editor/modules/data_compare/widgets/data_compare_data_source.dart';
import 'package:win_text_editor/shared/utils/file_utils.dart';
import 'data_compare_grid.dart'; //
class DataCompareView extends StatefulWidget {
final String tabId;
const DataCompareView({super.key, required this.tabId});
@override
State<DataCompareView> createState() => _DataCompareViewState();
}
class _DataCompareViewState extends State<DataCompareView> {
late final DataCompareController _controller;
late final DataGridController _dataGridController;
bool _isControllerFromTabManager = false;
late final DataCompareDataSource _dataSource;
get tabManager => Provider.of<TabItemsController>(context, listen: false);
@override
void initState() {
super.initState();
_dataGridController = DataGridController();
final controllerFromManager = tabManager.getController(widget.tabId);
if (controllerFromManager != null) {
_controller = controllerFromManager;
_isControllerFromTabManager = true;
} else {
_controller = DataCompareController();
_isControllerFromTabManager = false;
tabManager.registerController(widget.tabId, _controller);
}
// DataSource
_dataSource = DataCompareDataSource(_controller);
}
@override
void dispose() {
_dataGridController.dispose();
if (!_isControllerFromTabManager) {
_controller.dispose();
}
super.dispose();
}
Future<void> _importLeftTable() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (result != null) {
final file = File(result.files.single.path!);
final content = await FileUtils.readFileWithAutoEncoding(file);
await _controller.importCsv(true, content);
}
} catch (e) {
Logger().error('导入失败: ${e.toString()}');
}
}
Future<void> _importRightTable() async {
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (result != null) {
final file = File(result.files.single.path!);
final content = await FileUtils.readFileWithAutoEncoding(file);
await _controller.importCsv(false, content);
}
} catch (e) {
Logger().error('右表导入失败: ${e.toString()}');
}
}
//
Widget _buildExportButton(bool isLeftTable) {
return Builder(
builder: (btnContext) {
return ElevatedButton(
onPressed: () {
final RenderBox renderBox = btnContext.findRenderObject() as RenderBox;
final buttonSize = renderBox.size;
final offset = renderBox.localToGlobal(Offset.zero);
_showExportMenu(
isLeftTable,
RelativeRect.fromLTRB(
offset.dx,
offset.dy + buttonSize.height,
offset.dx + buttonSize.width,
offset.dy + buttonSize.height,
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: isLeftTable ? Colors.green[50] : Colors.blue[50],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Row(
children: [
const Icon(Icons.output, size: 18),
const SizedBox(width: 4),
Text('导出${isLeftTable ? '左表' : '右表'}'),
],
),
);
},
);
}
//
void _showExportMenu(bool isLeftTable, RelativeRect position) {
showMenu<String>(
context: context,
position: position,
items: [
const PopupMenuItem(
value: 'full_match',
child: Row(
children: [
Icon(Icons.double_arrow, color: Colors.green),
SizedBox(width: 8),
Text('整行匹配'),
],
),
),
const PopupMenuItem(
value: 'key_match',
child: Row(
children: [
Icon(Icons.arrow_forward_ios, color: Colors.blue),
SizedBox(width: 8),
Text('主键匹配'),
],
),
),
const PopupMenuItem(
value: 'no_match',
child: Row(
children: [Icon(Icons.close, color: Colors.red), SizedBox(width: 8), Text('不匹配')],
),
),
],
).then((value) {
if (value != null) {
if (isLeftTable) {
_controller.exportLeftTable(value);
} else {
_controller.exportRightTable(value);
}
}
});
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider.value(
value: _controller,
child: Consumer<DataCompareController>(
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.only(left: 4.0, right: 4.0),
child: Column(
children: [
Container(
height: 50,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
ElevatedButton(
onPressed: _importLeftTable,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green[50],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('导入左表(CSV)'),
),
const SizedBox(width: 8),
_buildExportButton(true), // 使
],
),
Row(
children: [
ElevatedButton(
onPressed: _importRightTable,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue[50],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('导入右表(CSV)'),
),
const SizedBox(width: 8),
_buildExportButton(false), // 使
],
),
],
),
),
Expanded(child: DataCompareGrid(dataSource: _dataSource, controller: controller)),
],
),
);
},
),
);
}
}

99
win_text_editor/lib/modules/data_extract/controllers/data_extract_controller.dart

@ -0,0 +1,99 @@ @@ -0,0 +1,99 @@
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/modules/outline/models/outline_node.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', matchCount: 0),
);
} 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, {dynamic appendArg}) {
// TODO: implement onOpenFile
}
@override
void onOpenFolder(String folderPath) {
searchDirectory = folderPath;
}
void cancelExtraction() {}
@override
void onDropOutlineNode(OutlineNode node) {
// TODO: implement onDropOutlineNode
}
}

28
win_text_editor/lib/modules/data_extract/models/search_result.dart

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
// search_result.dart
class SearchResult {
final int rowNum;
final String filePath;
final String content;
final int matchCount;
SearchResult({
required this.rowNum,
required this.filePath,
required this.content,
required this.matchCount,
});
SearchResult copyWith({
int? rowNum,
String? filePath,
String? content,
int? matchCount,
}) {
return SearchResult(
rowNum: rowNum ?? this.rowNum,
filePath: filePath ?? this.filePath,
content: content ?? this.content,
matchCount: matchCount ?? this.matchCount,
);
}
}

21
win_text_editor/lib/modules/data_extract/models/xml_rule.dart

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
// xml_rule.dart
class XmlRule {
final String nodePath;
final String attributeName;
final bool isFirstOccurrence;
final String? namespacePrefix;
final bool isDuplicatesOnly;
XmlRule({
required this.nodePath,
required this.attributeName,
this.isFirstOccurrence = false,
this.namespacePrefix,
required this.isDuplicatesOnly,
});
String toxPath() {
return '${namespacePrefix != null ? '$namespacePrefix:' : ''}$nodePath${isFirstOccurrence ? '[0]' : ''}/${attributeName.isNotEmpty ? '@$attributeName' : 'text()'}';
}
}

120
win_text_editor/lib/modules/data_extract/services/xml_extract_service.dart

@ -0,0 +1,120 @@ @@ -0,0 +1,120 @@
// xml_extract_service.dart
import 'dart:io';
import 'package:win_text_editor/framework/controllers/logger.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 fileResults = _extractWithRule(document, rule, entity.path);
// "仅提取重复项"matchCount>1
if (rule.isDuplicatesOnly) {
results.addAll(fileResults.where((r) => r.matchCount > 1));
} else {
results.addAll(fileResults);
}
} catch (e) {
Logger().error('XmlExtractService.extractFromDirectory方法执行出错: $e');
results.add(
SearchResult(
rowNum: rowNum++,
filePath: entity.path,
content: 'Error: $e',
matchCount: 1,
),
);
}
}
}
//
for (int i = 0; i < results.length; i++) {
results[i] = results[i].copyWith(rowNum: i + 1);
}
return results;
}
List<SearchResult> _extractWithRule(xml.XmlDocument document, XmlRule rule, String filePath) {
final List<xml.XmlElement> nodes = document.findAllElements(rule.nodePath).toList();
if (rule.isFirstOccurrence && nodes.isNotEmpty) {
final attr = nodes.first.getAttribute(rule.attributeName);
return attr != null
? [SearchResult(rowNum: 0, filePath: filePath, content: attr, matchCount: 1)]
: [];
} else if (rule.isDuplicatesOnly) {
return _findDuplicateAttributes(nodes, rule.attributeName, filePath);
} else {
final attributeCounts = <String, int>{};
// Count occurrences of each attribute value
for (final node in nodes) {
final attr = node.getAttribute(rule.attributeName);
if (attr != null) {
attributeCounts[attr] = (attributeCounts[attr] ?? 0) + 1;
}
}
return nodes
.map((node) => node.getAttribute(rule.attributeName))
.where((attr) => attr != null)
.map(
(attr) => SearchResult(
rowNum: 0, // Will be updated later
filePath: filePath,
content: attr!,
matchCount: attributeCounts[attr]!,
),
)
.toList();
}
}
List<SearchResult> _findDuplicateAttributes(
List<xml.XmlElement> nodes,
String attributeName,
String filePath,
) {
final attributeCounts = <String, int>{};
final attributeFirstOccurrence = <String, xml.XmlElement>{};
//
for (final node in nodes) {
final attr = node.getAttribute(attributeName);
if (attr != null) {
attributeCounts[attr] = (attributeCounts[attr] ?? 0) + 1;
attributeFirstOccurrence.putIfAbsent(attr, () => node);
}
}
// >1
final duplicateAttributes =
attributeCounts.entries.where((entry) => entry.value > 1).map((entry) {
final attr = entry.key;
return SearchResult(
rowNum: 0, //
filePath: filePath,
content: attr,
matchCount: entry.value,
);
}).toList();
return duplicateAttributes;
}
}

164
win_text_editor/lib/modules/data_extract/widgets/condition_setting.dart

@ -0,0 +1,164 @@ @@ -0,0 +1,164 @@
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';
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();
int _extractionOption = 0; // 0: all, 1: first occurrence, 2: duplicates only
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: '节点名称',
hintText: '如: business:Service',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
TextField(
controller: _attributeNameController,
decoration: const InputDecoration(
labelText: '属性名称',
hintText: '如: chineseName 或 name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 12),
// Radio buttons for extraction options
Row(
children: [
Radio<int>(
value: 1,
groupValue: _extractionOption,
onChanged: (value) {
setState(() {
_extractionOption = value ?? 0;
});
},
),
const Text('仅提取第一个匹配项'),
const SizedBox(width: 16),
Radio<int>(
value: 2,
groupValue: _extractionOption,
onChanged: (value) {
setState(() {
_extractionOption = value ?? 0;
});
},
),
const Text('仅提取重复项'),
],
),
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: _extractionOption == 1,
isDuplicatesOnly: _extractionOption == 2,
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);
}
}

63
win_text_editor/lib/modules/data_extract/widgets/data_extract_view.dart

@ -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()),
],
),
),
],
),
);
},
),
);
}
}

86
win_text_editor/lib/modules/data_extract/widgets/directory.dart

@ -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
},
),
],
),
),
);
},
);
}
}

157
win_text_editor/lib/modules/data_extract/widgets/results_view.dart

@ -0,0 +1,157 @@ @@ -0,0 +1,157 @@
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: 'exportFileName', child: Text('导出文件名(csv)')),
const PopupMenuItem<String>(value: 'exportContent', child: Text('导出内容(csv)')),
const PopupMenuItem<String>(value: 'exportAll', child: Text('导出全部(csv)')),
],
);
//
if (result != null && result.startsWith('export') && context.mounted) {
try {
await _exportToCsv(controller, result);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('导出失败: ${e.toString()}')));
}
}
}
}
Future<void> _exportToCsv(DataExtractController controller, String? exportType) async {
String csvData = '';
csvData =
exportType == 'exportFileName'
? '文件名称\n'
: (exportType == 'exportContent' ? '内容\n' : '文件名称\t内容\n');
for (var result in controller.results) {
switch (exportType) {
case 'exportFileName':
csvData += '${path.basename(result.filePath)}\n';
break;
case 'exportContent':
csvData += '${result.content}\n';
break;
default:
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: [
ShortGridColumn(columnName: 'rowNum', label: '序号'),
MyGridColumn(columnName: 'file', label: '文件名称', minimumWidth: 300),
MyGridColumn(columnName: 'content', label: '内容'),
ShortGridColumn(columnName: 'matchCount', 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),
DataGridCell(columnName: 'matchCount', value: result.matchCount),
],
);
}).toList();
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells: row.getCells().map<Widget>((cell) {
return Container(
alignment: cell.columnName == 'matchCount'
? Alignment.center
: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis),
);
}).toList(),
);
}
}

116
win_text_editor/lib/modules/data_format/controllers/data_format_controller.dart

@ -0,0 +1,116 @@ @@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/modules/data_format/services/mustache_service.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
import 'grid_view_controller.dart';
class DataFormatController extends BaseContentController {
final GridViewController gridController;
String _templateText = '';
// 使ValueNotifier来管理结果文本
final ValueNotifier<String> _resultTextNotifier = ValueNotifier('');
// notifier给外部访问
ValueNotifier<String> get resultTextNotifier => _resultTextNotifier;
//-------------------
DataFormatController() : gridController = GridViewController() {
_setupCrossControllerCommunication();
}
//
void _setupCrossControllerCommunication() {}
// Set template text from the editor
void setTemplateText(String text) {
_templateText = text;
}
// Apply the template to grid data
void applyTemplate() {
if (_templateText.isEmpty) {
_resultTextNotifier.value = '错误:模板文本不能为空';
Logger().error(_resultTextNotifier.value);
return;
}
// Validate template syntax
if (!MustacheService.validateTemplate(_templateText)) {
_resultTextNotifier.value = '错误:模板语法不正确';
Logger().error(_resultTextNotifier.value);
return;
}
// Get variables from template
final templateVars = MustacheService.getTemplateVariables(_templateText);
if (templateVars.isEmpty) {
_resultTextNotifier.value = '错误:模板中没有找到变量';
Logger().error(_resultTextNotifier.value);
return;
}
// Get grid data
final gridData = gridController.csvData;
if (gridData.isEmpty || gridData.length < 2) {
_resultTextNotifier.value = '错误:没有CSV数据';
Logger().error(_resultTextNotifier.value);
return;
}
final headers = gridData.first;
final dataRows = gridData.sublist(1);
// Check if all template variables exist in headers
final missingVars = templateVars.where((varName) => !headers.contains(varName)).toList();
if (missingVars.isNotEmpty) {
_resultTextNotifier.value = '错误:没有找到模板变量: ${missingVars.join(', ')}';
Logger().error(_resultTextNotifier.value);
return;
}
// Process each row
final resultBuffer = StringBuffer();
for (final row in dataRows) {
final rowData = <String, dynamic>{};
for (var i = 0; i < headers.length; i++) {
if (i < row.length) {
rowData[headers[i]] = row[i];
} else {
rowData[headers[i]] = '';
}
}
try {
final rendered = MustacheService.applyTemplate(_templateText, rowData);
resultBuffer.writeln(rendered);
} catch (e) {
resultBuffer.writeln('Error processing row: $e');
}
}
_resultTextNotifier.value = resultBuffer.toString();
}
@override
void onOpenFile(String filePath, {dynamic appendArg}) {}
@override
void onOpenFolder(String folderPath) {
//
}
@override
void dispose() {
_resultTextNotifier.dispose(); // dispose时释放资源
gridController.dispose();
super.dispose();
}
@override
void onDropOutlineNode(OutlineNode node) {
// TODO: implement onDropOutlineNode
}
}

24
win_text_editor/lib/modules/data_format/controllers/grid_view_controller.dart

@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
// grid_view_controller.dart
import 'package:win_text_editor/shared/base/safe_notifier.dart';
class GridViewController extends SafeNotifier {
List<List<dynamic>> _csvData = [];
List<List<dynamic>> get csvData => _csvData;
void setCsvData(List<List<dynamic>> data) {
_csvData = data;
notifyListeners();
}
@override
void dispose() {
// Clean up any resources
super.dispose();
}
void reset() {
//
safeNotify();
}
}

70
win_text_editor/lib/modules/data_format/models/template_node.dart

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/shared/components/tree_view.dart';
class TemplateNode implements TreeNode {
@override
final String name;
@override
final List<TemplateNode> children;
@override
final int depth;
@override
bool isExpanded;
final String path;
bool isRepeated;
bool isAttribute;
int repreatCount;
bool isChecked; //
@override
String get title => name;
TemplateNode({
required this.name,
required this.children,
required this.depth,
required this.path,
this.isExpanded = false,
this.isRepeated = false,
this.isAttribute = false,
this.repreatCount = 1,
this.isChecked = false, //
});
@override
bool get isDirectory => children.isNotEmpty;
@override
IconData? get iconData => isAttribute ? Icons.code : Icons.label_outline;
@override
String get id => path;
@override
bool get isVisible => true;
}
enum NodeType { element, attribute, text }
class TemplateItem {
final int id;
final String rowId;
final String content;
final String xPath;
final String value;
final NodeType nodeType;
TemplateItem({
required this.id,
required this.rowId,
required this.content,
required this.xPath,
required this.value,
required this.nodeType,
});
bool matchesPath(String path) {
return xPath == path;
}
}

57
win_text_editor/lib/modules/data_format/services/mustache_service.dart

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
// mustache_service.dart
import 'package:mustache_template/mustache_template.dart';
class MustacheService {
// Validate Mustache template syntax
static bool validateTemplate(String template) {
try {
Template(template);
return true;
} catch (e) {
return false;
}
}
// Get variables from template
static Set<String> getTemplateVariables(String template) {
try {
final parsed = Template(template);
// Use the source to parse variables manually
return _parseVariablesFromSource(parsed.source);
} catch (e) {
return {};
}
}
// Apply template to a row of data
static String applyTemplate(String template, Map<String, dynamic> data) {
try {
return Template(template).renderString(data);
} catch (e) {
return 'Error applying template: $e';
}
}
// Helper method to parse variables from template source
static Set<String> _parseVariablesFromSource(String source) {
final variables = <String>{};
final pattern = RegExp(r'{{\s*([^{}\s]+)\s*}}');
final matches = pattern.allMatches(source);
for (final match in matches) {
if (match.groupCount >= 1) {
final variable = match.group(1)!;
// Skip sections and special tags
if (!variable.startsWith('#') &&
!variable.startsWith('/') &&
!variable.startsWith('^') &&
!variable.startsWith('>') &&
!variable.startsWith('!')) {
variables.add(variable);
}
}
}
return variables;
}
}

67
win_text_editor/lib/modules/data_format/widgets/data_format_view.dart

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
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/data_format/controllers/data_format_controller.dart';
import 'package:win_text_editor/modules/data_format/widgets/grid_view.dart';
import 'package:win_text_editor/modules/data_format/widgets/format_text_panel.dart';
class DataFormatView extends StatefulWidget {
final String tabId;
const DataFormatView({super.key, required this.tabId});
@override
State<DataFormatView> createState() => _DataFormatViewState();
}
class _DataFormatViewState extends State<DataFormatView> {
late final DataFormatController _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 = DataFormatController();
_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.gridController),
],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
flex: 1,
child: Card(child: DataGridView(controller: _controller.gridController)),
),
// FormatText (50%)
Expanded(flex: 1, child: FormatTextPanel(controller: _controller)),
],
),
),
);
}
}

59
win_text_editor/lib/modules/data_format/widgets/format_text_panel.dart

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:win_text_editor/modules/data_format/controllers/data_format_controller.dart';
import 'package:win_text_editor/shared/components/editor_toolbar.dart';
import 'package:win_text_editor/shared/components/text_editor.dart';
class FormatTextPanel extends StatelessWidget {
final DataFormatController controller;
const FormatTextPanel({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return Card(
child: Column(
children: [
// (200px)
SizedBox(
height: 200,
child: TextEditor(
tabId: 'format_template',
title: 'Mustache模板',
onContentChanged: controller.setTemplateText,
),
),
const SizedBox(height: 6),
// ()
Expanded(
child: ValueListenableBuilder<String>(
valueListenable: controller.resultTextNotifier,
builder: (context, resultText, _) {
return TextEditor(
tabId: 'format_result',
title: '转换结果',
initialContent: resultText,
toolbarBuilder:
(context, state) => EditorToolbar(
title: '转换结果',
text: state.currentText,
isLoading: state.isLoading,
showOpenFileButton: false,
customButtons: [
ToolbarButtonConfig(
icon: Icons.code,
tooltip: '格式化',
onPressed: () => controller.applyTemplate(),
),
],
onCopyToClipboard: state.copyToClipboard,
onSaveFile: state.saveFile,
),
);
},
),
),
],
),
);
}
}

221
win_text_editor/lib/modules/data_format/widgets/grid_view.dart

@ -0,0 +1,221 @@ @@ -0,0 +1,221 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:file_picker/file_picker.dart';
import 'dart:io';
import 'package:csv/csv.dart';
import 'package:win_text_editor/modules/data_format/controllers/grid_view_controller.dart';
import 'package:win_text_editor/shared/components/my_grid_column.dart';
class DataGridView extends StatefulWidget {
final GridViewController controller;
const DataGridView({super.key, required this.controller});
@override
State<DataGridView> createState() => _DataGridViewState();
}
class _DataGridViewState extends State<DataGridView> {
final TextEditingController _filePathController = TextEditingController();
List<List<dynamic>> _csvData = [];
String? _delimiter;
final ScrollController _horizontalScrollController = ScrollController();
@override
void dispose() {
_filePathController.dispose();
_horizontalScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
//
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: TextField(
controller: _filePathController,
decoration: const InputDecoration(
labelText: 'CSV文件路径',
border: OutlineInputBorder(),
),
readOnly: true,
),
),
const SizedBox(width: 8),
ElevatedButton(onPressed: _pickAndLoadCsvFile, child: const Text('选择文件')),
],
),
),
//
Expanded(
child: Row(
children: [
_csvData.isEmpty ? const Center(child: Text('请选择CSV文件')) : _buildScrollableDataGrid(),
],
),
),
],
);
}
Widget _buildScrollableDataGrid() {
return Align(
// Align 使
alignment: Alignment.topLeft, //
child: Scrollbar(
controller: _horizontalScrollController,
thumbVisibility: true,
trackVisibility: true,
child: SingleChildScrollView(
controller: _horizontalScrollController,
scrollDirection: Axis.horizontal,
child: SizedBox(width: _calculateTableWidth(), child: _buildCsvDataGrid()),
),
),
);
}
double _calculateTableWidth() {
if (_csvData.isEmpty || _csvData.length < 2) return 0;
final columnCount = _csvData.first.length;
return columnCount * 150.0; // 150
}
Future<void> _pickAndLoadCsvFile() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ['csv'],
);
if (result != null) {
final file = File(result.files.single.path!);
_filePathController.text = file.path;
final content = await file.readAsString();
//
final firstLine = content.split('\n').first.trim();
_delimiter = firstLine.contains('\t') ? '\t' : ',';
// CSV - 使
final csvTable = const CsvToListConverter(
shouldParseNumbers: false,
allowInvalid: false,
eol: '\n',
).convert(content, fieldDelimiter: _delimiter);
//
final cleanedData =
csvTable
.where(
(row) => row.isNotEmpty && row.any((cell) => cell.toString().trim().isNotEmpty),
)
.toList(); // List
//
final dataWithIndex =
cleanedData.asMap().entries.map((entry) {
final index = entry.key;
final row = entry.value;
// ()"序号"
if (index == 0) {
return ['序号', ...row.map((cell) => cell.toString().trim()).toList()];
}
// (1)
return [index, ...row.map((cell) => cell.toString().trim()).toList()];
}).toList();
setState(() {
_csvData = dataWithIndex;
});
widget.controller.setCsvData(dataWithIndex);
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('加载CSV文件失败: $e')));
}
}
Widget _buildCsvDataGrid() {
if (_csvData.isEmpty || _csvData.length < 2) {
return const Center(child: Text('没有有效数据或数据格式不正确'));
}
final headers = _csvData.first;
final dataRows = _csvData.sublist(1);
final columns = [
//
MyGridColumn(columnName: '序号', label: '序号', minimumWidth: 60),
//
...headers.sublist(1).map<GridColumn>((header) {
return MyGridColumn(
columnName: header.toString(),
label: header.toString(),
minimumWidth: 150,
);
}).toList(),
];
final dataSource = _CsvDataSource(headers: headers, rows: dataRows);
return SfDataGrid(
rowHeight: 32,
headerRowHeight: 32,
source: dataSource,
columns: columns,
gridLinesVisibility: GridLinesVisibility.both,
headerGridLinesVisibility: GridLinesVisibility.both,
columnWidthMode: ColumnWidthMode.fill,
allowColumnsResizing: true,
columnResizeMode: ColumnResizeMode.onResizeEnd,
);
}
}
class _CsvDataSource extends DataGridSource {
final List<dynamic> headers;
final List<List<dynamic>> _rows;
_CsvDataSource({required this.headers, required List<List<dynamic>> rows}) : _rows = rows;
@override
List<DataGridRow> get rows {
return _rows.map((row) {
return DataGridRow(
cells:
headers.asMap().entries.map((entry) {
final columnIndex = entry.key;
final columnName = entry.value.toString();
final cellValue = columnIndex < row.length ? row[columnIndex].toString() : '';
return DataGridCell(columnName: columnName, value: cellValue);
}).toList(),
);
}).toList();
}
@override
DataGridRowAdapter? buildRow(DataGridRow row) {
return DataGridRowAdapter(
cells:
row.getCells().map<Widget>((dataGridCell) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.centerLeft,
child: Text(
dataGridCell.value.toString(),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
}).toList(),
);
}
}

19
win_text_editor/lib/modules/demo/controllers/demo_controller.dart

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/base/base_content_controller.dart';
class DemoController extends BaseContentController {
@override
void onOpenFile(String filePath, {dynamic appendArg}) {
// TODO: implement onOpenFile
}
@override
void onOpenFolder(String folderPath) {
// TODO: implement onOpenFolder
}
@override
void onDropOutlineNode(OutlineNode node) {
// TODO: implement onDropOutlineNode
}
}

47
win_text_editor/lib/modules/demo/widgets/demo_view.dart

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
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/demo/controllers/demo_controller.dart';
class DemoView extends StatefulWidget {
final String tabId;
const DemoView({super.key, required this.tabId});
@override
State<DemoView> createState() => _DemoViewState();
}
class _DemoViewState extends State<DemoView> {
late final DemoController _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 = DemoController();
_isControllerFromTabManager = false;
tabManager.registerController(widget.tabId, _controller);
}
}
@override
void dispose() {
if (!_isControllerFromTabManager) {
_controller.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Center(child: Text('demo'));
}
}

83
win_text_editor/lib/modules/memory_table/controllers/index_data_source.dart

@ -0,0 +1,83 @@ @@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/shared/base/selectable_data_source.dart';
import 'package:win_text_editor/modules/memory_table/models/memory_table.dart';
//
class IndexesDataSource extends SelectableDataSource<Index> {
// ID
String? _selectedId;
IndexesDataSource(
List<Index> indexes, {
required Null Function(dynamic index, dynamic isSelected) onSelectionChanged,
}) : super(indexes, onSelectionChanged: onSelectionChanged);
@override
List<DataGridRow> get rows =>
items
.map(
(index) => DataGridRow(
cells: [
DataGridCell<bool>(columnName: 'select', value: index.isSelected),
DataGridCell<String>(columnName: 'indexName', value: index.indexName),
DataGridCell<bool>(columnName: 'isPrimary', value: index.isPrimary),
DataGridCell<String>(columnName: 'indexFields', value: index.indexFields),
DataGridCell<String>(columnName: 'rule', value: index.rule),
],
),
)
.toList();
get data => items;
@override
DataGridRowAdapter buildRow(DataGridRow row) {
final rowIndex = effectiveRows.indexOf(row);
final index = items[rowIndex];
return DataGridRowAdapter(
cells:
row.getCells().map<Widget>((cell) {
if (cell.columnName == 'select') {
return Center(
child: Transform.scale(
scale: 0.75,
child: Radio<String>(
value: index.id, // 使value
groupValue: _selectedId, // ID
onChanged: (String? value) {
_selectedId = value;
toggleRowSelection(rowIndex, value != null);
notifyListeners(); //
},
),
),
);
}
return Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(cell.value.toString(), overflow: TextOverflow.ellipsis),
);
}).toList(),
);
}
//
@override
void toggleRowSelection(int index, bool? value) {
final newValue = value ?? false;
if (newValue) {
//
for (var item in items) {
item.isSelected = false;
}
_selectedId = items[index].id; // ID
}
items[index].isSelected = newValue;
updateSelectionState();
notifyListeners();
onSelectionChanged?.call(index, newValue);
}
}

141
win_text_editor/lib/modules/memory_table/controllers/memory_table_controller.dart

@ -0,0 +1,141 @@ @@ -0,0 +1,141 @@
import 'package:syncfusion_flutter_datagrid/datagrid.dart';
import 'package:win_text_editor/framework/controllers/logger.dart';
import 'package:win_text_editor/framework/services/macro_template_service.dart';
import 'package:win_text_editor/modules/outline/models/code_partner.dart';
import 'package:win_text_editor/modules/outline/models/outline_node.dart';
import 'package:win_text_editor/shared/models/std_filed.dart';
import 'package:win_text_editor/shared/uft_std_fields/field_data_source.dart';
import 'package:win_text_editor/modules/memory_table/controllers/index_data_source.dart';
import 'package:win_text_editor/modules/memory_table/models/memory_table.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 MemoryTableController extends BaseContentController {
String? _errorMessage;
String tableName = "";
String objectId = "";
String chineseName = "";
late DataGridSource fieldsSource;
late DataGridSource indexesSource;
final MacroTemplateService templateService = MacroTemplateService();
// MemoryTable对象
late MemoryTable _memoryTable;
MemoryTableController() {
//
final initialFields = [Field('1', '', '', '', false), Field('2', '', '', '', false)];
final initialIndexes = [Index('', false, '', '', false)];
fieldsSource = FieldsDataSource(
initialFields,
onSelectionChanged: (index, isSelected) {
updateFieldSelection(index, isSelected);
},
);
indexesSource = IndexesDataSource(
initialIndexes,
onSelectionChanged: (index, isSelected) {
updateIndexSelection(index, isSelected);
},
);
// MemoryTable
_memoryTable = MemoryTable(tableName: '', columns: initialFields, indexes: initialIndexes);
}
String? get errorMessage => _errorMessage;
// MemoryTable
MemoryTable get memoryTable => _memoryTable;
void initTemplateService() {
if (!templateService.inited) {
templateService.init();
}
}
String? genCodeString(List<String> macroList) {
initTemplateService();
return templateService.renderTemplate(macroList, _memoryTable.toMap());
}
@override
Future<void> onOpenFile(String filePath, {dynamic appendArg}) async {
Logger().info("Opening file: $filePath");
try {
final tableData = await MemoryTableService.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);
// MemoryTable对象
_memoryTable = MemoryTable(
tableName: tableName,
columns: tableData.fields,
indexes: tableData.indexes,
);
// Clear any previous error
_errorMessage = null;
// Notify UI to update
notifyListeners();
} catch (e) {
_errorMessage = e.toString();
notifyListeners();
Logger().error("Error opening file: $e");
}
}
//
void updateFieldSelection(int index, bool isSelected) {
final fields = (fieldsSource as FieldsDataSource).data;
if (index >= 0 && index < fields.length) {
fields[index].isSelected = isSelected;
fieldsSource.notifyListeners();
// MemoryTable
_memoryTable.columns[index].isSelected = isSelected;
notifyListeners();
}
}
//
void updateIndexSelection(int index, bool isSelected) {
final indexes = (indexesSource as IndexesDataSource).data;
if (index >= 0 && index < indexes.length) {
indexes[index].isSelected = isSelected;
indexesSource.notifyListeners();
// MemoryTable
_memoryTable.indexes[index].isSelected = isSelected;
notifyListeners();
}
}
@override
void onOpenFolder(String folderPath) {
//
}
@override
void dispose() {
super.dispose();
}
@override
void onDropOutlineNode(OutlineNode node) {
_memoryTable.codePartners.add(CodePartner.fromOutlineNode(node));
notifyListeners();
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save