diff --git a/README.md b/README.md index b0e712d..01aaa4c 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ ### 📊 结构化数据可视化 - **JSON / XML / YAML** —— 可折叠**层级树**,以及卡片 + 连线的**关系图**两种可视化 +- **SQL** —— 基于 rusqlite 执行(无需外部 sqlite3),结果渲染为**表格**;可连接内存库或指定 `.sqlite` 文件,失败显示具体错误 - **Markdown** —— 实时渲染预览(支持内嵌 HTML,DOMPurify 净化防 XSS) - **GitHub Actions 工作流** —— 自动识别并渲染为 **Jobs 依赖 DAG 图**(触发事件 → 各 Job → Steps) @@ -102,6 +103,7 @@ + @@ -111,7 +113,7 @@
-`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `Text` +`Python` · `Node.js` · `TypeScript` · `JavaScript` · `Go` · `Rust` · `Java` · `Kotlin` · `Scala` · `Groovy` · `Clojure` · `C` · `C++` · `Objective-C/C++` · `Swift` · `Ruby` · `PHP` · `R` · `Lua` · `Haskell` · `Cangjie` · `Shell` · `AppleScript` · `SQL` · `HTML` · `CSS` · `SVG` · `JSON` · `XML` · `YAML` · `Markdown` · `Text`
diff --git a/package.json b/package.json index 50e2f35..e834eb8 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@xterm/xterm": "^6.0.0", "codemirror": "^6.0.2", "dompurify": "^3.4.8", + "echarts": "^6.1.0", "js-yaml": "^4.2.0", "lodash-es": "^4.17.21", "lucide-vue-next": "^0.539.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index dad6d15..90f98bf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "flate2", "futures-util", "log", + "mysql", "notify", "portable-pty", "regex", @@ -22,6 +23,7 @@ dependencies = [ "rusqlite", "serde", "serde_json", + "subprocess", "tar", "tauri", "tauri-build", @@ -99,6 +101,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -140,7 +148,7 @@ dependencies = [ "enumflags2", "futures-channel", "futures-util", - "rand", + "rand 0.9.2", "raw-window-handle", "serde", "serde_repr", @@ -339,6 +347,28 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.75" @@ -366,6 +396,24 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.12.1", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex 1.3.0", + "syn 2.0.104", +] + [[package]] name = "bit-set" version = "0.8.0" @@ -457,6 +505,21 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "btoi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" +dependencies = [ + "num-traits", +] + +[[package]] +name = "bufstream" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" + [[package]] name = "bumpalo" version = "3.19.0" @@ -572,13 +635,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.32" +version = "1.2.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" dependencies = [ + "find-msvc-tools", "jobserver", "libc", - "shlex", + "shlex 2.0.1", ] [[package]] @@ -587,6 +651,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -645,6 +718,26 @@ dependencies = [ "inout", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.9", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "combine" version = "4.6.7" @@ -776,6 +869,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -785,6 +891,34 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -934,6 +1068,17 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "derive_utils" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "digest" version = "0.10.7" @@ -995,7 +1140,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" dependencies = [ - "libloading", + "libloading 0.8.9", ] [[package]] @@ -1029,7 +1174,7 @@ checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" dependencies = [ "bit-set", "cssparser", - "foldhash", + "foldhash 0.2.0", "html5ever", "precomputed-hash", "selectors", @@ -1093,6 +1238,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + [[package]] name = "embed-resource" version = "3.0.5" @@ -1265,6 +1416,12 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "fix-path-env" version = "0.0.0" @@ -1282,6 +1439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", ] @@ -1291,6 +1449,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + [[package]] name = "foldhash" version = "0.2.0" @@ -1348,6 +1512,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -1764,6 +1934,17 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -2193,6 +2374,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-enum" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de9008599afe8527a8c9d70423437363b321649161e98473f433de802d76107" +dependencies = [ + "derive_utils", +] + [[package]] name = "io-uring" version = "0.7.9" @@ -2238,6 +2428,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -2390,7 +2589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -2419,6 +2618,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + [[package]] name = "libredox" version = "0.1.9" @@ -2441,6 +2650,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -2469,6 +2689,15 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "lzma-rs" version = "0.3.0" @@ -2531,6 +2760,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2585,6 +2820,76 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mysql" +version = "25.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ad644efb545e459029b1ffa7c969d830975bd76906820913247620df10050b" +dependencies = [ + "bufstream", + "bytes", + "crossbeam", + "flate2", + "io-enum", + "libc", + "lru", + "mysql_common", + "named_pipe", + "pem", + "percent-encoding", + "rustls", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "socket2 0.5.10", + "twox-hash", + "url", + "webpki", + "webpki-roots 0.26.11", +] + +[[package]] +name = "mysql_common" +version = "0.32.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478b0ff3f7d67b79da2b96f56f334431aef65e15ba4b29dd74a4236e29582bdc" +dependencies = [ + "base64 0.21.7", + "bindgen", + "bitflags 2.12.1", + "btoi", + "byteorder", + "bytes", + "cc", + "cmake", + "crc32fast", + "flate2", + "lazy_static", + "num-bigint", + "num-traits", + "rand 0.8.6", + "regex", + "saturating", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "subprocess", + "thiserror 1.0.69", + "uuid", + "zstd", +] + +[[package]] +name = "named_pipe" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad9c443cce91fc3e12f017290db75dde490d685cdaaf508d7159d7cf41f0eb2b" +dependencies = [ + "winapi", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -2659,6 +2964,16 @@ dependencies = [ "memoffset 0.9.1", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "6.1.1" @@ -2678,12 +2993,31 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3118,6 +3452,16 @@ dependencies = [ "hmac", ] +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3410,14 +3754,35 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + [[package]] name = "rand" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] @@ -3427,7 +3792,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", ] [[package]] @@ -3538,7 +3912,7 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", @@ -3638,6 +4012,20 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -3686,6 +4074,21 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" version = "1.0.4" @@ -3695,6 +4098,36 @@ dependencies = [ "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -3716,6 +4149,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "saturating" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" + [[package]] name = "schannel" version = "0.1.27" @@ -4104,6 +4543,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "sigchld" version = "0.2.4" @@ -4277,6 +4722,16 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subprocess" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5133,6 +5588,17 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "rand 0.8.6", + "static_assertions", +] + [[package]] name = "typeid" version = "1.0.3" @@ -5209,6 +5675,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -5551,6 +6023,34 @@ dependencies = [ "system-deps", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3372903..36b63e5 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,6 +35,9 @@ futures-util = "0.3" rfd = "0.15" fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } async-trait = "0.1" +mysql = { version = "25", default-features = false, features = ["minimal", "rustls-tls"] } +# 固定 mysql_common 的构建依赖 subprocess 到不依赖 let 链的旧版,兼容当前 Rust 工具链 +subprocess = "=0.2.9" portable-pty = "0.8" zip = "2.2.2" flate2 = "1.0" diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs new file mode 100644 index 0000000..a4eed95 --- /dev/null +++ b/src-tauri/src/db/mod.rs @@ -0,0 +1,206 @@ +//! 数据库执行器:插件式架构。 +//! 每种数据库类型实现 `DbExecutor` 并在 `executors()` 中注册一行,新增类型互不影响。 + +mod mysql; +mod sqlite; + +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +#[derive(Serialize)] +pub(crate) struct SqlResultSet { + pub(crate) columns: Vec, + pub(crate) rows: Vec>, +} + +#[derive(Serialize)] +pub struct SqlRunResult { + pub(crate) result_sets: Vec, + pub(crate) messages: Vec, + pub(crate) error: Option, + pub(crate) elapsed_ms: u128, +} + +impl SqlRunResult { + pub(crate) fn new() -> Self { + Self { + result_sets: Vec::new(), + messages: Vec::new(), + error: None, + elapsed_ms: 0, + } + } +} + +/// 数据源描述:内存 / SQLite 文件 / MySQL(后续可扩展更多字段) +#[derive(Deserialize)] +pub struct DataSource { + pub kind: String, + #[serde(default)] + pub file: Option, + #[serde(default)] + pub host: Option, + #[serde(default)] + pub port: Option, + #[serde(default)] + pub user: Option, + #[serde(default)] + pub password: Option, + #[serde(default)] + pub database: Option, +} + +/// 数据库执行器接口:新增数据库类型只需实现本 trait 并在 executors() 注册。 +pub(crate) trait DbExecutor: Send + Sync { + /// 是否处理该数据源类型(如 sqlite 同时处理 "sqlite" 与 "memory") + fn handles(&self, kind: &str) -> bool; + /// 执行脚本,返回结构化结果(错误写入 result.error,不以 Err 形式返回) + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult; +} + +/// 已注册的执行器。新增数据库类型:在此加一行。 +fn executors() -> Vec> { + vec![ + Box::new(sqlite::SqliteExecutor), + Box::new(mysql::MysqlExecutor), + ] +} + +/// 把脚本按分号切成多条语句(处理字符串字面量、反引号与注释,UTF-8 安全) +pub(crate) fn split_sql(sql: &str) -> Vec { + #[derive(PartialEq)] + enum S { + Normal, + Single, + Double, + Backtick, + Line, + Block, + } + let chars: Vec = sql.chars().collect(); + let n = chars.len(); + let mut out = Vec::new(); + let mut cur = String::new(); + let mut state = S::Normal; + let mut i = 0; + while i < n { + let c = chars[i]; + let next = if i + 1 < n { Some(chars[i + 1]) } else { None }; + match state { + S::Normal => match c { + '\'' => { + state = S::Single; + cur.push(c); + } + '"' => { + state = S::Double; + cur.push(c); + } + '`' => { + state = S::Backtick; + cur.push(c); + } + '-' if next == Some('-') => { + state = S::Line; + cur.push(c); + } + '/' if next == Some('*') => { + state = S::Block; + cur.push(c); + } + ';' => { + let t = cur.trim().to_string(); + if !t.is_empty() { + out.push(t); + } + cur.clear(); + } + _ => cur.push(c), + }, + S::Single => { + cur.push(c); + if c == '\'' { + state = S::Normal; + } + } + S::Double => { + cur.push(c); + if c == '"' { + state = S::Normal; + } + } + S::Backtick => { + cur.push(c); + if c == '`' { + state = S::Normal; + } + } + S::Line => { + cur.push(c); + if c == '\n' { + state = S::Normal; + } + } + S::Block => { + cur.push(c); + if c == '*' && next == Some('/') { + cur.push('/'); + i += 1; + state = S::Normal; + } + } + } + i += 1; + } + let t = cur.trim().to_string(); + if !t.is_empty() { + out.push(t); + } + out +} + +/// 执行 SQL 脚本,按 source.kind 派发到对应执行器,并写入执行历史。 +#[tauri::command] +pub async fn run_sql( + sql: String, + source: DataSource, + history: tauri::State<'_, crate::execution::ExecutionHistory>, +) -> Result { + let sql_for_record = sql.clone(); + let result = tokio::task::spawn_blocking(move || { + let start = std::time::Instant::now(); + let execs = executors(); + let mut result = match execs.iter().find(|e| e.handles(&source.kind)) { + Some(exec) => exec.run(&sql, &source), + None => { + let mut r = SqlRunResult::new(); + r.error = Some(format!("不支持的数据源类型: {}", source.kind)); + r + } + }; + result.elapsed_ms = start.elapsed().as_millis(); + result + }) + .await + .map_err(|e| format!("SQL 任务失败: {}", e))?; + + // 与其它语言一致:记录到执行历史 + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + // stdout 存完整结果 JSON,历史详情用 SqlTableView 渲染,与实时运行一致 + let record = crate::plugins::ExecutionResult { + id: None, + success: result.error.is_none(), + code: sql_for_record, + stdout: serde_json::to_string(&result).unwrap_or_default(), + stderr: result.error.clone().unwrap_or_default(), + execution_time: result.elapsed_ms, + timestamp, + language: "sql".to_string(), + }; + let _ = history.insert(&record); + + Ok(result) +} diff --git a/src-tauri/src/db/mysql.rs b/src-tauri/src/db/mysql.rs new file mode 100644 index 0000000..21e7e10 --- /dev/null +++ b/src-tauri/src/db/mysql.rs @@ -0,0 +1,98 @@ +use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, split_sql}; +use serde_json::Value as JsonValue; + +pub(crate) struct MysqlExecutor; + +fn value_to_json(v: &mysql::Value) -> JsonValue { + use mysql::Value::*; + match v { + NULL => JsonValue::Null, + Int(i) => JsonValue::from(*i), + UInt(u) => JsonValue::from(*u), + Float(f) => JsonValue::from(*f as f64), + Double(d) => JsonValue::from(*d), + Bytes(b) => match std::str::from_utf8(b) { + Ok(s) => JsonValue::from(s.to_string()), + Err(_) => JsonValue::from(format!("<{} bytes>", b.len())), + }, + Date(y, mo, d, h, mi, s, _us) => JsonValue::from(format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + y, mo, d, h, mi, s + )), + Time(neg, d, h, mi, s, _us) => JsonValue::from(format!( + "{}{} {:02}:{:02}:{:02}", + if *neg { "-" } else { "" }, + d, + h, + mi, + s + )), + } +} + +impl DbExecutor for MysqlExecutor { + fn handles(&self, kind: &str) -> bool { + kind == "mysql" + } + + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult { + use mysql::prelude::Queryable; + let mut result = SqlRunResult::new(); + + let opts = mysql::OptsBuilder::new() + .ip_or_hostname( + source + .host + .clone() + .or_else(|| Some("127.0.0.1".to_string())), + ) + .tcp_port(source.port.unwrap_or(3306)) + .user(source.user.clone()) + .pass(source.password.clone()) + .db_name(source.database.clone()); + + let mut conn = match mysql::Conn::new(opts) { + Ok(c) => c, + Err(e) => { + result.error = Some(format!("连接 MySQL 失败: {}", e)); + return result; + } + }; + + 'stmts: for stmt_sql in split_sql(sql) { + let mut qr = match conn.query_iter(&stmt_sql) { + Ok(q) => q, + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + }; + let columns: Vec = qr + .columns() + .as_ref() + .iter() + .map(|c| c.name_str().to_string()) + .collect(); + if columns.is_empty() { + let affected = qr.affected_rows(); + result.messages.push(format!("OK,影响 {} 行", affected)); + } else { + let mut rows = Vec::new(); + for r in qr.by_ref() { + match r { + Ok(row) => { + let vals = row.unwrap(); + rows.push(vals.iter().map(value_to_json).collect()); + } + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } + result.result_sets.push(SqlResultSet { columns, rows }); + } + } + result + } +} diff --git a/src-tauri/src/db/sqlite.rs b/src-tauri/src/db/sqlite.rs new file mode 100644 index 0000000..9c563a5 --- /dev/null +++ b/src-tauri/src/db/sqlite.rs @@ -0,0 +1,87 @@ +use super::{DataSource, DbExecutor, SqlResultSet, SqlRunResult, split_sql}; +use rusqlite::Connection; +use serde_json::Value as JsonValue; + +pub(crate) struct SqliteExecutor; + +fn value_to_json(v: rusqlite::types::Value) -> JsonValue { + use rusqlite::types::Value::*; + match v { + Null => JsonValue::Null, + Integer(i) => JsonValue::from(i), + Real(f) => JsonValue::from(f), + Text(s) => JsonValue::from(s), + Blob(b) => JsonValue::from(format!("", b.len())), + } +} + +impl DbExecutor for SqliteExecutor { + fn handles(&self, kind: &str) -> bool { + kind == "sqlite" || kind == "memory" + } + + fn run(&self, sql: &str, source: &DataSource) -> SqlRunResult { + let mut result = SqlRunResult::new(); + let conn = match source.file.as_deref() { + Some(p) if !p.trim().is_empty() => Connection::open(p), + _ => Connection::open_in_memory(), + }; + let conn = match conn { + Ok(c) => c, + Err(e) => { + result.error = Some(format!("打开数据库失败: {}", e)); + return result; + } + }; + + 'stmts: for stmt_sql in split_sql(sql) { + let mut stmt = match conn.prepare(&stmt_sql) { + Ok(s) => s, + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + }; + let ncol = stmt.column_count(); + if ncol > 0 { + let columns: Vec = + stmt.column_names().iter().map(|s| s.to_string()).collect(); + let rows_iter = stmt.query_map([], |row| { + let mut v = Vec::with_capacity(ncol); + for idx in 0..ncol { + v.push(value_to_json(row.get(idx)?)); + } + Ok(v) + }); + match rows_iter { + Ok(it) => { + let mut rows = Vec::new(); + for r in it { + match r { + Ok(rw) => rows.push(rw), + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } + result.result_sets.push(SqlResultSet { columns, rows }); + } + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } else { + match stmt.execute([]) { + Ok(affected) => result.messages.push(format!("OK,影响 {} 行", affected)), + Err(e) => { + result.error = Some(e.to_string()); + break 'stmts; + } + } + } + } + result + } +} diff --git a/src-tauri/src/execution.rs b/src-tauri/src/execution.rs index 761d6cf..9923867 100644 --- a/src-tauri/src/execution.rs +++ b/src-tauri/src/execution.rs @@ -67,7 +67,7 @@ impl ExecutionHistory { }) } - fn insert(&self, result: &ExecutionResult) -> Result { + pub(crate) fn insert(&self, result: &ExecutionResult) -> Result { let conn = self .conn .lock() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3f815d4..3f16a92 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -8,6 +8,7 @@ mod ai_history; mod cache; mod config; mod custom_plugin_commands; +mod db; mod env_commands; mod env_manager; mod env_providers; @@ -35,6 +36,7 @@ use crate::custom_plugin_commands::{ add_custom_plugin, get_custom_plugins, remove_custom_plugin, save_custom_icon, update_custom_plugin, }; +use crate::db::run_sql; use crate::env_commands::{ EnvironmentManagerState, download_and_install_version, get_environment_info, get_supported_environment_languages, switch_environment_version, uninstall_environment_version, @@ -222,7 +224,9 @@ fn main() { terminal_create, terminal_write, terminal_resize, - terminal_kill + terminal_kill, + // SQL 执行 + run_sql ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/plugins/sql.rs b/src-tauri/src/plugins/sql.rs index bd353d3..99cd738 100644 --- a/src-tauri/src/plugins/sql.rs +++ b/src-tauri/src/plugins/sql.rs @@ -23,11 +23,11 @@ impl LanguagePlugin for SqlPlugin { } fn get_version_args(&self) -> Vec<&'static str> { - vec!["--version"] + vec!["--"] } fn get_path_command(&self) -> String { - "sqlite3".to_string() + "--".to_string() } fn get_default_config(&self) -> PluginConfig { @@ -37,14 +37,14 @@ impl LanguagePlugin for SqlPlugin { before_compile: None, extension: String::from("sql"), execute_home: None, - // 在内存 SQLite 中执行脚本(需要本机有 sqlite3) - run_command: Some(String::from("sqlite3 :memory: .read $filename")), + // 在内存 SQLite 中执行脚本并以 JSON 输出(前端渲染为表格;需要本机有 sqlite3) + run_command: Some(String::from("sqlite3 -json :memory: .read $filename")), after_compile: None, template: Some(String::from( "-- 在这里输入 SQL(默认在内存 SQLite 中执行)\nSELECT 'Hello, CodeForge' AS message;\n", )), timeout: Some(30), - console_type: Some(String::from("console")), + console_type: Some(String::from("sqltable")), icon_path: None, } } diff --git a/src/App.vue b/src/App.vue index 02d6127..b6d2a77 100644 --- a/src/App.vue +++ b/src/App.vue @@ -89,6 +89,7 @@ · {{ currentFileName }} +
@@ -174,6 +175,14 @@ :is-running="isRunning" :execution-time="lastExecutionTime" @clear="clearOutput"/> + + +
@@ -197,6 +206,7 @@ · {{ currentFileName }} +
@@ -347,6 +357,8 @@ import JsonView from "./components/JsonView.vue"; import MarkdownView from "./components/MarkdownView.vue"; import XmlView from "./components/XmlView.vue"; import YamlView from "./components/YamlView.vue"; +import SqlTableView from "./components/SqlTableView.vue"; +import SqlSourceSelect from "./components/SqlSourceSelect.vue"; import StatusBar from './components/StatusBar.vue' import About from './components/About.vue' import Settings from './components/Settings.vue' @@ -375,6 +387,7 @@ import Terminal from './components/Terminal.vue' import Breadcrumbs from './components/Breadcrumbs.vue' import {initSnippets} from './composables/useSnippets' import {kvGet, kvGetJSON, kvSet, kvSetJSON} from './composables/useKvStore' +import {useDbConnections} from './composables/useDbConnections' import {useAiConfig} from './composables/useAiConfig' import {setGhost, clearGhostIn, ghostActive} from './editor/aiComplete' import {cursorInfo} from './editor/cursorInfo' @@ -1286,6 +1299,39 @@ const showRunPrompt = ref(false) // 包装运行:仅编辑器模式下点击运行时自动展开控制台;关联文件则按策略就地运行 // 运行选中片段:以选中文本作为临时代码运行(不就地、不关联文件) +// SQL 走专用执行(结构化结果 + 错误 + 数据源:内存/SQLite/MySQL) +const {resolveActiveSource} = useDbConnections() +const runSql = async (sqlOverride?: string) => { + const sql = sqlOverride ?? code.value + if (!sql.trim()) { + toast.info('没有可执行的 SQL') + return + } + if (layoutMode.value === 'editor') { + showConsole.value = true + } + isRunning.value = true + output.value = '' + isSuccess.value = false + try { + const source = resolveActiveSource() + const res = await invoke('run_sql', {sql, source}) + output.value = JSON.stringify(res) + isSuccess.value = !res.error + lastExecutionTime.value = res.elapsed_ms || 0 + if (res.error) { + toast.error('SQL 执行失败') + } + } + catch (error) { + output.value = JSON.stringify({result_sets: [], messages: [], error: String(error)}) + toast.error('SQL 执行失败: ' + error) + } + finally { + isRunning.value = false + } +} + const runSelection = () => { const view = editorView.value if (!view) { @@ -1297,6 +1343,10 @@ const runSelection = () => { return } const selected = view.state.sliceDoc(from, to) + if (currentLanguage.value === 'sql') { + runSql(selected) + return + } if (layoutMode.value === 'editor') { showConsole.value = true } @@ -1304,6 +1354,10 @@ const runSelection = () => { } const handleRunCode = async () => { + if (currentLanguage.value === 'sql') { + runSql() + return + } if (layoutMode.value === 'editor') { showConsole.value = true } diff --git a/src/components/ExecutionHistory.vue b/src/components/ExecutionHistory.vue index 70aeebe..9c21712 100644 --- a/src/components/ExecutionHistory.vue +++ b/src/components/ExecutionHistory.vue @@ -104,7 +104,11 @@
输出
-
+              
+ +
+
{{ selectedOutput }}
@@ -139,6 +143,7 @@ import { invoke } from '@tauri-apps/api/core' import { Copy, History, Play, RefreshCw, RotateCcw, Sparkles, Trash2 } from 'lucide-vue-next' import Modal from '../ui/Modal.vue' import Button from '../ui/Button.vue' +import SqlResultTable from './SqlResultTable.vue' import type { ExecutionResult, Language } from '../types/app' import { useToast } from '../plugins/toast' diff --git a/src/components/MarkdownView.vue b/src/components/MarkdownView.vue index fe753b0..1246312 100644 --- a/src/components/MarkdownView.vue +++ b/src/components/MarkdownView.vue @@ -8,7 +8,9 @@ 运行中… {{ executionTime }} ms - + @@ -22,7 +24,7 @@ diff --git a/src/components/SqlSourceSelect.vue b/src/components/SqlSourceSelect.vue new file mode 100644 index 0000000..fa3e6a4 --- /dev/null +++ b/src/components/SqlSourceSelect.vue @@ -0,0 +1,40 @@ + + + diff --git a/src/components/SqlTableView.vue b/src/components/SqlTableView.vue new file mode 100644 index 0000000..6ec43f3 --- /dev/null +++ b/src/components/SqlTableView.vue @@ -0,0 +1,87 @@ + + + diff --git a/src/components/charts/BarChart.vue b/src/components/charts/BarChart.vue new file mode 100644 index 0000000..bcbdae0 --- /dev/null +++ b/src/components/charts/BarChart.vue @@ -0,0 +1,75 @@ + + + diff --git a/src/components/charts/ChartPanel.vue b/src/components/charts/ChartPanel.vue new file mode 100644 index 0000000..c0a3241 --- /dev/null +++ b/src/components/charts/ChartPanel.vue @@ -0,0 +1,181 @@ + + + diff --git a/src/components/charts/shape.ts b/src/components/charts/shape.ts new file mode 100644 index 0000000..cdcf9cc --- /dev/null +++ b/src/components/charts/shape.ts @@ -0,0 +1,161 @@ +// 表格数据 → 图表数据的塑形工具(与具体数据源解耦:SQL 结果、CSV 等均可复用) + +export type AggKind = 'sum' | 'count' | 'avg' | 'max' | 'min' + +export interface TableData { + columns: string[] + rows: any[][] +} + +export interface ShapedSeries { + name: string + data: (number | null)[] +} + +export interface ShapedData { + categories: string[] + series: ShapedSeries[] +} + +export const AGG_LABELS: Record = { + sum: '求和', + count: '计数', + avg: '平均', + max: '最大', + min: '最小' +} + +/** 判断某列是否数值型(抽样前若干非空值) */ +export function isNumericColumn(rows: any[][], colIndex: number): boolean { + let seen = 0 + for (const row of rows) { + const v = row[colIndex] + if (v === null || v === undefined || v === '') { + continue + } + seen++ + if (typeof v !== 'number' && isNaN(Number(v))) { + return false + } + if (seen >= 20) { + break + } + } + return seen > 0 +} + +function aggregate(values: number[], kind: AggKind): number { + if (values.length === 0) { + return 0 + } + switch (kind) { + case 'sum': + return values.reduce((a, b) => a + b, 0) + case 'avg': + return values.reduce((a, b) => a + b, 0) / values.length + case 'max': + return Math.max(...values) + case 'min': + return Math.min(...values) + case 'count': + return values.length + } +} + +const norm = (v: any): string => (v === null || v === undefined || v === '' ? '(空)' : String(v)) + +/** + * 多维度透视聚合: + * - dimensions[0]:分类轴(X) + * - dimensions[1..]:分组维度,每个不同组合拆成一条 series + * - metrics:一个或多个数值列;多指标时与分组组合,series 名为「组合 · 指标」 + * - agg:聚合方式;count 统计非空行数 + */ +export function pivot( + data: TableData, + dimensions: string[], + metrics: string[], + agg: AggKind +): ShapedData { + const dims = dimensions.filter(d => data.columns.includes(d)) + const mets = metrics.filter(m => data.columns.includes(m)) + if (dims.length === 0 || mets.length === 0) { + return {categories: [], series: []} + } + + const catIdx = data.columns.indexOf(dims[0]) + const groupIdx = dims.slice(1).map(d => data.columns.indexOf(d)) + const metricIdx = mets.map(m => data.columns.indexOf(m)) + const multiMetric = mets.length > 1 + + const categories: string[] = [] + const catSeen = new Set() + // series 名 -> (分类值 -> 待聚合数值数组) + const seriesMap = new Map>() + const seriesOrder: string[] = [] + + for (const row of data.rows) { + const catVal = norm(row[catIdx]) + if (!catSeen.has(catVal)) { + catSeen.add(catVal) + categories.push(catVal) + } + const groupVal = groupIdx.map(i => norm(row[i])).join(' / ') + + mets.forEach((m, j) => { + let name: string + if (groupVal) { + name = multiMetric ? `${groupVal} · ${m}` : groupVal + } + else { + name = m + } + if (!seriesMap.has(name)) { + seriesMap.set(name, new Map()) + seriesOrder.push(name) + } + const cm = seriesMap.get(name)! + if (!cm.has(catVal)) { + cm.set(catVal, []) + } + const v = row[metricIdx[j]] + if (agg === 'count') { + if (v !== null && v !== undefined && v !== '') { + cm.get(catVal)!.push(1) + } + } + else { + const num = typeof v === 'number' ? v : Number(v) + if (!isNaN(num)) { + cm.get(catVal)!.push(num) + } + } + }) + } + + const series: ShapedSeries[] = seriesOrder.map(name => ({ + name, + data: categories.map(c => { + const vals = seriesMap.get(name)!.get(c) + return vals && vals.length > 0 ? aggregate(vals, agg) : null + }) + })) + + return {categories, series} +} + +/** 按各分类的指标合计排序并截取前 N 项(topN<=0 表示不限制) */ +export function sortAndLimit(shaped: ShapedData, order: 'none' | 'asc' | 'desc', topN: number): ShapedData { + let idx = shaped.categories.map((_, i) => i) + if (order !== 'none') { + const totals = idx.map(i => shaped.series.reduce((s, ser) => s + (ser.data[i] || 0), 0)) + idx = [...idx].sort((a, b) => (order === 'asc' ? totals[a] - totals[b] : totals[b] - totals[a])) + } + if (topN > 0) { + idx = idx.slice(0, topN) + } + return { + categories: idx.map(i => shaped.categories[i]), + series: shaped.series.map(s => ({name: s.name, data: idx.map(i => s.data[i])})) + } +} diff --git a/src/components/setting/Database.vue b/src/components/setting/Database.vue new file mode 100644 index 0000000..f7df408 --- /dev/null +++ b/src/components/setting/Database.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/composables/useDbConnections.ts b/src/composables/useDbConnections.ts new file mode 100644 index 0000000..13dda6d --- /dev/null +++ b/src/composables/useDbConnections.ts @@ -0,0 +1,97 @@ +import {ref} from 'vue' +import {kvGet, kvGetJSON, kvSet, kvSetJSON} from './useKvStore' + +export interface DataSource +{ + kind: 'memory' | 'sqlite' | 'mysql' + file?: string + host?: string + port?: number + user?: string + password?: string + database?: string +} + +export interface DbConnection extends DataSource +{ + id: string + name: string +} + +const CONN_KEY = 'sql-connections' +const REF_KEY = 'sql-source-ref' + +const genId = () => `db-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` + +// 模块级共享:连接列表 + 当前数据源引用(token:memory / conn: / file:) +// 注意:不能在模块顶层读 KV——模块在 loadKvStore() 之前就被 import,缓存还是空的。 +// 改为首次调用 useDbConnections() 时(组件 setup 阶段,已在 loadKvStore 之后)再载入。 +const connections = ref([]) +const activeRef = ref('memory') +let loaded = false + +export function useDbConnections() +{ + if (!loaded) { + loaded = true + connections.value = kvGetJSON(CONN_KEY, []) + activeRef.value = kvGet(REF_KEY) || 'memory' + } + + const persist = () => kvSetJSON(CONN_KEY, connections.value) + + const add = (c: Omit) => { + connections.value.push({...c, id: genId()}) + persist() + } + const update = (id: string, patch: Partial) => { + const i = connections.value.findIndex(x => x.id === id) + if (i >= 0) { + connections.value[i] = {...connections.value[i], ...patch} + persist() + } + } + const remove = (id: string) => { + connections.value = connections.value.filter(x => x.id !== id) + persist() + if (activeRef.value === `conn:${id}`) { + setActiveRef('memory') + } + } + + const setActiveRef = (token: string) => { + activeRef.value = token + kvSet(REF_KEY, token) + } + + // 把当前引用解析为可执行的数据源 + const resolveActiveSource = (): DataSource => { + const t = activeRef.value + if (t.startsWith('conn:')) { + const conn = connections.value.find(c => c.id === t.slice(5)) + if (conn) { + const {id: _i, name: _n, ...rest} = conn + return rest + } + } + else if (t.startsWith('file:')) { + return {kind: 'sqlite', file: t.slice(5)} + } + return {kind: 'memory'} + } + + // 当前数据源展示名 + const activeLabel = (): string => { + const t = activeRef.value + if (t.startsWith('conn:')) { + return connections.value.find(c => c.id === t.slice(5))?.name || '(已删除)' + } + if (t.startsWith('file:')) { + const p = t.slice(5) + return p.split(/[\\/]/).pop() || p + } + return '内存数据库' + } + + return {connections, activeRef, add, update, remove, setActiveRef, resolveActiveSource, activeLabel} +} diff --git a/src/composables/useLanguageSettings.ts b/src/composables/useLanguageSettings.ts index ce4f18c..598d4dc 100644 --- a/src/composables/useLanguageSettings.ts +++ b/src/composables/useLanguageSettings.ts @@ -33,7 +33,7 @@ export function useLanguageSettings(emit: any) } ] - const consoleTypes = [{label: '控制台', value: 'console'}, {label: 'Web', value: 'web'}, {label: 'JSON', value: 'json'}, {label: 'Markdown', value: 'markdown'}, {label: 'XML', value: 'xml'}, {label: 'YAML', value: 'yaml'}] + const consoleTypes = [{label: '控制台', value: 'console'}, {label: 'Web', value: 'web'}, {label: 'JSON', value: 'json'}, {label: 'Markdown', value: 'markdown'}, {label: 'XML', value: 'xml'}, {label: 'YAML', value: 'yaml'}, {label: 'SQL 表格', value: 'sqltable'}] const { activePlugin, diff --git a/src/composables/useSettings.ts b/src/composables/useSettings.ts index 0925f98..3ec1e78 100644 --- a/src/composables/useSettings.ts +++ b/src/composables/useSettings.ts @@ -1,5 +1,5 @@ import { nextTick, ref } from 'vue' -import { BracesIcon, CodeIcon, Database, FileText, Globe, Keyboard, ShieldIcon, Sparkles } from 'lucide-vue-next' +import { BracesIcon, CodeIcon, Database, FileText, Globe, Keyboard, Server, ShieldIcon, Sparkles } from 'lucide-vue-next' export function useSettings(emit: any) { @@ -13,6 +13,7 @@ export function useSettings(emit: any) { key: 'editor', label: '编辑器', icon: CodeIcon }, { key: 'shortcut', label: '快捷键', icon: Keyboard }, { key: 'ai', label: 'AI', icon: Sparkles }, + { key: 'database', label: '数据库', icon: Server }, { key: 'language', label: '语言', icon: BracesIcon }, { key: 'network', label: '网络', icon: Globe }, { key: 'cache', label: '缓存', icon: Database },