168 changed files with 363834 additions and 1014 deletions
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
[submodule "cpp_server/RuntimeCompiledCPlusPlus"] |
||||
path = cpp_server/RuntimeCompiledCPlusPlus |
||||
url = https://github.com/RuntimeCompiledCPlusPlus/RuntimeCompiledCPlusPlus.git |
Binary file not shown.
After Width: | Height: | Size: 433 KiB |
After Width: | Height: | Size: 663 KiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 413 KiB |
@ -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 |
||||
} |
@ -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" |
||||
} |
||||
] |
||||
} |
@ -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" |
||||
} |
||||
} |
@ -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." |
||||
} |
||||
] |
||||
} |
@ -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() |
||||
|
@ -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 |
||||
|
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Subproject commit 407a0b972eac6166095d2b5b5b0896bad6e9687a |
@ -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 |
||||
} |
||||
] |
||||
} |
@ -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); |
||||
} |
||||
}; |
@ -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); |
||||
} |
@ -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; |
||||
}; |
@ -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(); |
||||
}; |
@ -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; |
||||
}; |
@ -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); |
||||
} |
||||
} |
@ -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" |
||||
} |
||||
] |
||||
} |
@ -1 +1,3 @@
@@ -1 +1,3 @@
|
||||
{} |
||||
{ |
||||
"cmake.sourceDirectory": "D:/aigc/manta/win_text_editor/linux" |
||||
} |
@ -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}}] |
||||
[手工打包结束] |
||||
|
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
outline_name_black_list: |
||||
- 历史,日志,名称,比例,数量,金额,次数,属性,对应,分类,姓名,单位,总数,行使,子项,占比,记录,列表,目标,字段,字符串,动作,方式,类型,类别 |
File diff suppressed because it is too large
Load Diff
@ -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(), |
||||
], |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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'; |
||||
} |
||||
} |
||||
} |
@ -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方法,因为现在直接在调用处处理 |
||||
} |
@ -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), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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; // 0表示第一个编辑器,1表示第二个 |
||||
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) { |
||||
// 文件加载处理 |
||||
}, |
||||
), |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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'; |
||||
} |
@ -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() : ''; |
||||
} |
||||
} |
@ -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'] ?? ''}; |
||||
} |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart'; |
||||
import 'package:win_text_editor/framework/controllers/logger.dart'; |
||||
|
||||
class FilePathManager { |
||||
static const String _lastOpenedFolderKey = 'last_opened_folder'; |
||||
|
||||
// 保存上次打开的文件夹路径 |
||||
static Future<void> saveLastOpenedFolder(String folderPath) async { |
||||
final prefs = await SharedPreferences.getInstance(); |
||||
await prefs.setString(_lastOpenedFolderKey, folderPath); |
||||
Logger().info("保存最新打开的文件夹地址:$folderPath"); |
||||
} |
||||
|
||||
// 读取上次打开的文件夹路径 |
||||
static Future<String?> getLastOpenedFolder() async { |
||||
final prefs = await SharedPreferences.getInstance(); |
||||
final folderPath = prefs.getString(_lastOpenedFolderKey); |
||||
Logger().info("加载最后保存的文件夹地址:$folderPath"); |
||||
return folderPath; |
||||
} |
||||
} |
@ -0,0 +1,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); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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(), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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}); |
@ -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); // 限制最小200,最大400 |
||||
} |
||||
|
||||
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); |
||||
} |
||||
} |
||||
} |
@ -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), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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> { |
||||
// 跟踪图标在哪个容器中(1或2) |
||||
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; // 更新图标位置 |
||||
}); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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(), |
||||
}; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
}, |
||||
), |
||||
), |
||||
], |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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}); |
||||
} |
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
class MatchResult { |
||||
final int start; |
||||
final int end; |
||||
|
||||
const MatchResult({required this.start, required this.end}); |
||||
} |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
enum SearchMode { locate, count } |
@ -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; |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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(); |
||||
} |
@ -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(); |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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()), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -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(); |
||||
} |
@ -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(); |
||||
|
||||
// 每100毫秒更新一次UI,保持流畅显示 |
||||
_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), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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(), |
||||
); |
||||
} |
||||
} |
@ -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)), |
||||
); |
||||
} |
||||
} |
@ -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)), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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, |
||||
); |
||||
} |
||||
} |
@ -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()'}'; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'results_view.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/widgets/condition_setting.dart'; |
||||
import 'directory.dart'; |
||||
import 'package:win_text_editor/framework/controllers/tab_items_controller.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
|
||||
class DataExtractView extends StatefulWidget { |
||||
final String tabId; |
||||
const DataExtractView({super.key, required this.tabId}); |
||||
|
||||
@override |
||||
DataExtractViewState createState() => DataExtractViewState(); |
||||
} |
||||
|
||||
class DataExtractViewState extends State<DataExtractView> { |
||||
late final DataExtractController _controller; |
||||
|
||||
get tabManager => Provider.of<TabItemsController>(context, listen: false); |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
_controller = tabManager.getController(widget.tabId) ?? DataExtractController(); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return ChangeNotifierProvider.value( |
||||
value: _controller, |
||||
child: Consumer<DataExtractController>( |
||||
builder: (context, controller, child) { |
||||
return const Padding( |
||||
padding: EdgeInsets.all(4.0), |
||||
child: Column( |
||||
children: [ |
||||
Directory(), |
||||
Expanded( |
||||
child: Row( |
||||
crossAxisAlignment: CrossAxisAlignment.start, |
||||
children: [ |
||||
// 左侧部分 (50%) |
||||
Expanded(flex: 3, child: ConditionSetting()), |
||||
SizedBox(width: 8), |
||||
// 右侧部分 (50%) |
||||
Expanded(flex: 7, child: ResultsView()), |
||||
], |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,86 @@
@@ -0,0 +1,86 @@
|
||||
import 'package:flutter/material.dart'; |
||||
import 'package:provider/provider.dart'; |
||||
import 'package:win_text_editor/modules/data_extract/controllers/data_extract_controller.dart'; |
||||
|
||||
class Directory extends StatefulWidget { |
||||
const Directory({super.key}); |
||||
|
||||
@override |
||||
State<Directory> createState() => _DirectoryState(); |
||||
} |
||||
|
||||
class _DirectoryState extends State<Directory> { |
||||
late TextEditingController _searchDirectoryController; |
||||
late TextEditingController _fileTypeController; |
||||
|
||||
@override |
||||
void initState() { |
||||
super.initState(); |
||||
final controller = context.read<DataExtractController>(); |
||||
_searchDirectoryController = TextEditingController(text: controller.searchDirectory); |
||||
_fileTypeController = TextEditingController(text: controller.fileType); |
||||
} |
||||
|
||||
@override |
||||
void dispose() { |
||||
_searchDirectoryController.dispose(); |
||||
_fileTypeController.dispose(); |
||||
super.dispose(); |
||||
} |
||||
|
||||
@override |
||||
Widget build(BuildContext context) { |
||||
return Consumer<DataExtractController>( |
||||
builder: (context, controller, child) { |
||||
// 同步 TextEditingController 的值 |
||||
if (_searchDirectoryController.text != controller.searchDirectory) { |
||||
_searchDirectoryController.text = controller.searchDirectory; |
||||
} |
||||
if (_fileTypeController.text != controller.fileType) { |
||||
_fileTypeController.text = controller.fileType; |
||||
} |
||||
|
||||
return Card( |
||||
child: Padding( |
||||
padding: const EdgeInsets.all(8.0), |
||||
child: Row( |
||||
children: [ |
||||
Expanded( |
||||
child: TextField( |
||||
controller: _searchDirectoryController, |
||||
decoration: const InputDecoration( |
||||
labelText: '搜索目录', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
onChanged: (value) => controller.searchDirectory = value, |
||||
), |
||||
), |
||||
const SizedBox(width: 8), |
||||
SizedBox( |
||||
width: 100, |
||||
child: TextField( |
||||
controller: _fileTypeController, |
||||
decoration: const InputDecoration( |
||||
labelText: '文件类型', |
||||
border: OutlineInputBorder(), |
||||
), |
||||
onChanged: (value) => controller.fileType = value, |
||||
), |
||||
), |
||||
const SizedBox(width: 8), |
||||
IconButton( |
||||
icon: const Icon(Icons.folder_open), |
||||
onPressed: () async { |
||||
await controller.pickDirectory(); |
||||
// 不需要手动更新 _searchDirectoryController.text, |
||||
// 因为 Consumer 会触发重建并自动同步 |
||||
}, |
||||
), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
}, |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,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(), |
||||
); |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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(); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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)), |
||||
], |
||||
), |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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, |
||||
), |
||||
); |
||||
}, |
||||
), |
||||
), |
||||
], |
||||
), |
||||
); |
||||
} |
||||
} |
@ -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(), |
||||
); |
||||
} |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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')); |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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…
Reference in new issue