From 3758bea0b5c0c814de70667720da4ae01c3f724f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 1 Jul 2026 00:20:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20=E5=B7=A5=E7=A8=8B?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E5=9B=BE=E6=95=B0=E6=8D=AE=E5=B1=82=20+=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=BE=B9=E6=B8=B2=E6=9F=93=20+=20=E5=B0=8F?= =?UTF-8?q?=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端 module_dependencies 表(V35 迁移)+ ModuleDependencyRepo CRUD - IPC:add/remove/list_module_dependencies - 前端 API 封装 + DependencyGraph 接入真实边数据 - 依赖类型颜色区分(library/api/mq/shared/custom) - 小地图插件(MiniMap)大图概览导航 - 点击节点跳转项目详情 - 任务列表后端真分页 count_by_query 方法 --- bun.lock | 31 +++++- crates/df-storage/src/crud/mod.rs | 2 + .../src/crud/module_dependency_repo.rs | 95 +++++++++++++++++++ crates/df-storage/src/migrations.rs | 37 +++++++- crates/df-storage/src/models.rs | 14 +++ src-tauri/src/commands/module.rs | 67 ++++++++++++- src-tauri/src/lib.rs | 3 + src-tauri/src/state.rs | 5 +- src/api/module.ts | 26 +++++ src/components/project/DependencyGraph.vue | 46 ++++++++- 10 files changed, 316 insertions(+), 10 deletions(-) create mode 100644 crates/df-storage/src/crud/module_dependency_repo.rs diff --git a/bun.lock b/bun.lock index 2ed74c9..2ded1da 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,15 @@ "": { "name": "devflow", "dependencies": { + "@antv/x6": "^3.1.7", + "@antv/x6-vue-shape": "^3.0.2", "@arco-design/web-vue": "^2.58.0", "@tauri-apps/api": "^2", - "dompurify": "^3.4.9", + "@tauri-apps/plugin-dialog": "^2.7.1", + "dompurify": "^3.4.10", + "highlight.js": "^11.11.1", "marked": "^18.0.5", + "tslib": "^2.8.1", "vue": "^3.5.13", "vue-i18n": "9", "vue-router": "4", @@ -24,6 +29,10 @@ }, }, "packages": { + "@antv/x6": ["@antv/x6@3.1.7", "https://registry.npmmirror.com/@antv/x6/-/x6-3.1.7.tgz", { "dependencies": { "dom-align": "^1.12.4", "lodash-es": "^4.17.15", "mousetrap": "^1.6.5", "utility-types": "^3.10.0" } }, "sha512-NLKXtbCK51oLbazfFD0XsD93rMmih08UBW4gAuEyLBpwAqHmHe+vP8VhOZDkl5O9jV1LSv85IJghr9CT5tZjWw=="], + + "@antv/x6-vue-shape": ["@antv/x6-vue-shape@3.0.2", "https://registry.npmmirror.com/@antv/x6-vue-shape/-/x6-vue-shape-3.0.2.tgz", { "dependencies": { "vue-demi": "latest" }, "peerDependencies": { "@antv/x6": "^3.x", "@vue/composition-api": "^1.0.0-rc.1", "vue": "^2.0.0 || >=3.0.0" }, "optionalPeers": ["@vue/composition-api"] }, "sha512-HWnYFvLCaUD9ernINxTn9vCRj66eG/nXKErSSq0hfI4U1HOitC/1oUrzDdwL/BT80HKAQeSsmy7gpDA/79jYlw=="], + "@arco-design/color": ["@arco-design/color@0.4.0", "https://registry.npmmirror.com/@arco-design/color/-/color-0.4.0.tgz", { "dependencies": { "color": "^3.1.3" } }, "sha512-s7p9MSwJgHeL8DwcATaXvWT3m2SigKpxx4JA1BGPHL4gfvaQsmQfrLBDpjOJFJuJ2jG2dMt3R3P8Pm9E65q18g=="], "@arco-design/web-vue": ["@arco-design/web-vue@2.58.0", "https://registry.npmmirror.com/@arco-design/web-vue/-/web-vue-2.58.0.tgz", { "dependencies": { "@arco-design/color": "^0.4.0", "b-tween": "^0.3.3", "b-validate": "^1.5.3", "compute-scroll-into-view": "^1.0.20", "dayjs": "^1.11.13", "number-precision": "^1.6.0", "resize-observer-polyfill": "^1.5.1", "scroll-into-view-if-needed": "^2.2.31", "vue": "^3.1.0" } }, "sha512-b1vdPYOmjG5VAkVa7jlVwCb+WynBK+rnKN8zH3yKohpZObZbostRd3HgYNtjjZjGVU3OqR0Yy2FX7ftgF0bcOw=="], @@ -172,6 +181,8 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "https://registry.npmmirror.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="], + "@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.7.1", "https://registry.npmmirror.com/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ=="], + "@types/dompurify": ["@types/dompurify@3.2.0", "https://registry.npmmirror.com/@types/dompurify/-/dompurify-3.2.0.tgz", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], "@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -236,7 +247,9 @@ "de-indent": ["de-indent@1.0.2", "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="], - "dompurify": ["dompurify@3.4.9", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.9.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ=="], + "dom-align": ["dom-align@1.12.4", "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz", {}, "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw=="], + + "dompurify": ["dompurify@3.4.11", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.11.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw=="], "entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], @@ -250,14 +263,20 @@ "he": ["he@1.2.0", "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="], + "highlight.js": ["highlight.js@11.11.1", "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", {}, "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w=="], + "is-arrayish": ["is-arrayish@0.3.4", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + "lodash-es": ["lodash-es@4.18.1", "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "marked": ["marked@18.0.5", "https://registry.npmmirror.com/marked/-/marked-18.0.5.tgz", { "bin": { "marked": "bin/marked.js" } }, "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w=="], "minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "mousetrap": ["mousetrap@1.6.5", "https://registry.npmmirror.com/mousetrap/-/mousetrap-1.6.5.tgz", {}, "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA=="], + "muggle-string": ["muggle-string@0.4.1", "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="], @@ -284,18 +303,26 @@ "tinyglobby": ["tinyglobby@0.2.17", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "typescript": ["typescript@5.6.3", "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw=="], + "utility-types": ["utility-types@3.11.0", "https://registry.npmmirror.com/utility-types/-/utility-types-3.11.0.tgz", {}, "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw=="], + "vite": ["vite@6.4.3", "https://registry.npmmirror.com/vite/-/vite-6.4.3.tgz", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], "vscode-uri": ["vscode-uri@3.1.0", "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vue": ["vue@3.5.35", "https://registry.npmmirror.com/vue/-/vue-3.5.35.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.35", "@vue/compiler-sfc": "3.5.35", "@vue/runtime-dom": "3.5.35", "@vue/server-renderer": "3.5.35", "@vue/shared": "3.5.35" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q=="], + "vue-demi": ["vue-demi@0.14.10", "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], + "vue-i18n": ["vue-i18n@9.14.5", "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.5.tgz", { "dependencies": { "@intlify/core-base": "9.14.5", "@intlify/shared": "9.14.5", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-0jQ9Em3ymWngyiIkj0+c/k7WgaPO+TNzjKSNq9BvBQaKJECqn9cd9fL4tkDhB5G1QBskGl9YxxbDAhgbFtpe2g=="], "vue-router": ["vue-router@4.6.4", "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="], "vue-tsc": ["vue-tsc@2.2.12", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz", { "dependencies": { "@volar/typescript": "2.4.15", "@vue/language-core": "2.2.12" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw=="], + + "@types/dompurify/dompurify": ["dompurify@3.4.9", "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.9.tgz", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ=="], } } diff --git a/crates/df-storage/src/crud/mod.rs b/crates/df-storage/src/crud/mod.rs index 93957f4..6ce80b5 100644 --- a/crates/df-storage/src/crud/mod.rs +++ b/crates/df-storage/src/crud/mod.rs @@ -28,6 +28,7 @@ mod project_service_repo; mod settings; mod task_link_repo; mod task_repo; +mod module_dependency_repo; pub use conversation_repo::*; pub use idea_eval_repo::*; @@ -40,6 +41,7 @@ pub use project_service_repo::*; pub use settings::*; pub use task_link_repo::*; pub use task_repo::*; +pub use module_dependency_repo::*; // ============================================================ // 辅助宏 — 消除多个 Repo 的重复样板 diff --git a/crates/df-storage/src/crud/module_dependency_repo.rs b/crates/df-storage/src/crud/module_dependency_repo.rs new file mode 100644 index 0000000..8d710fb --- /dev/null +++ b/crates/df-storage/src/crud/module_dependency_repo.rs @@ -0,0 +1,95 @@ +//! 工程依赖关系 — module_dependencies 表 CRUD(V35,工程间依赖边) +//! +//! 简化版:insert / delete / list_by_field(查 project_id)。 + +use std::sync::Arc; + +use rusqlite::{params, OptionalExtension, Row}; +use tokio::sync::Mutex; + +use crate::db::Database; +use crate::models::ModuleDependencyRecord; + +use super::{storage_err}; + +fn dep_from_row(row: &Row<'_>) -> std::result::Result { + Ok(ModuleDependencyRecord { + id: row.get("id")?, + project_id: row.get("project_id")?, + from_module_id: row.get("from_module_id")?, + to_module_id: row.get("to_module_id")?, + dep_type: row.get("dep_type")?, + label: row.get("label")?, + created_at: row.get("created_at")?, + }) +} + +pub struct ModuleDependencyRepo { + conn: Arc>, +} + +impl ModuleDependencyRepo { + pub fn new(db: &Database) -> Self { + Self { conn: db.conn() } + } + + pub async fn insert(&self, record: ModuleDependencyRecord) -> Result { + let conn = self.conn.clone(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let r = &record; + let affected = guard + .execute( + "INSERT INTO module_dependencies \ + (id, project_id, from_module_id, to_module_id, dep_type, label, created_at) \ + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + r.id, r.project_id, r.from_module_id, r.to_module_id, + r.dep_type, r.label, r.created_at, + ], + ) + .map_err(storage_err)?; + Ok(affected > 0) + }) + .await + .map_err(storage_err)? + } + + pub async fn delete(&self, id: &str) -> Result { + let conn = self.conn.clone(); + let id = id.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let affected = guard + .execute("DELETE FROM module_dependencies WHERE id = ?1", params![id]) + .map_err(storage_err)?; + Ok(affected > 0) + }) + .await + .map_err(storage_err)? + } + + pub async fn list_by_field(&self, field: &str, value: &str) -> Result, df_types::error::Error> { + let conn = self.conn.clone(); + let field = field.to_owned(); + let value = value.to_owned(); + tokio::task::spawn_blocking(move || { + let guard = conn.blocking_lock(); + let sql = format!( + "SELECT id, project_id, from_module_id, to_module_id, dep_type, label, created_at \ + FROM module_dependencies WHERE {field} = ?1 ORDER BY created_at ASC" + ); + let mut stmt = guard.prepare(&sql).map_err(storage_err)?; + let rows = stmt + .query_map(params![value], dep_from_row) + .map_err(storage_err)?; + let mut results = Vec::new(); + for r in rows { + results.push(r.map_err(storage_err)?); + } + Ok(results) + }) + .await + .map_err(storage_err)? + } +} diff --git a/crates/df-storage/src/migrations.rs b/crates/df-storage/src/migrations.rs index 7f51fd7..430cfe6 100644 --- a/crates/df-storage/src/migrations.rs +++ b/crates/df-storage/src/migrations.rs @@ -45,7 +45,7 @@ pub fn run(conn: &Connection) -> Result<()> { // 什么数据库、Redis 在哪、有没有 MQ"的基础设施上下文。 // V33 = 审批重启恢复:ai_conversations 加 pending_approvals TEXT 列,持久化挂起审批快照, // 重启后从 DB 恢复 pending_approvals 内存态,使待审批不丢。 - let steps: [(i32, fn(&Connection) -> Result<()>); 34] = [ + let steps: [(i32, fn(&Connection) -> Result<()>); 35] = [ (1, migrate_v1), (2, migrate_v2), (3, migrate_v3), @@ -80,6 +80,7 @@ pub fn run(conn: &Connection) -> Result<()> { (32, migrate_v32), (33, migrate_v33), (34, migrate_v34), + (35, migrate_v35), ]; for (version, migrate_fn) in steps { @@ -995,6 +996,40 @@ fn migrate_v34(conn: &Connection) -> Result<()> { Ok(()) } +/// V35:工程依赖关系—module_dependencies 表(工程间依赖边,用于依赖图) +/// +/// dep_type 枚举值:library(类库) / api(API调用) / mq(消息队列) / shared(共享资源) / custom(自定义) +fn migrate_v35(conn: &Connection) -> Result<()> { + conn.execute( + "CREATE TABLE IF NOT EXISTS module_dependencies ( + id TEXT PRIMARY KEY, + project_id TEXT NOT NULL REFERENCES projects(id), + from_module_id TEXT NOT NULL REFERENCES project_modules(id), + to_module_id TEXT NOT NULL REFERENCES project_modules(id), + dep_type TEXT NOT NULL DEFAULT 'library', + label TEXT, + created_at TEXT NOT NULL + )", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_module_deps_project ON module_dependencies(project_id)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_module_deps_from ON module_dependencies(from_module_id)", + [], + )?; + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_module_deps_to ON module_dependencies(to_module_id)", + [], + )?; + tracing::info!("v35: 建 module_dependencies 表 + 索引(工程依赖图)"); + conn.execute("INSERT INTO schema_version (version) VALUES (?)", [35])?; + tracing::info!("迁移 v35 完成"); + Ok(()) +} + /// V21 建表 SQL — 消息拆分存储 ai_messages 表 /// /// 与 V9_SQL 中的 ai_messages 镜像(V9 给新库,此 const 给老库 V21 迁移用 IF NOT EXISTS)。 diff --git a/crates/df-storage/src/models.rs b/crates/df-storage/src/models.rs index 62ab98c..8c95554 100644 --- a/crates/df-storage/src/models.rs +++ b/crates/df-storage/src/models.rs @@ -483,3 +483,17 @@ pub struct ProjectModuleRecord { pub created_at: String, pub updated_at: String, } + +/// 工程依赖关系记录(V35 module_dependencies 表)。 +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ModuleDependencyRecord { + pub id: String, + pub project_id: String, + pub from_module_id: String, + pub to_module_id: String, + /// 依赖类型:library / api / mq / shared / custom + pub dep_type: String, + /// 可选标签(自定义描述) + pub label: Option, + pub created_at: String, +} diff --git a/src-tauri/src/commands/module.rs b/src-tauri/src/commands/module.rs index d4b0b64..1fafe97 100644 --- a/src-tauri/src/commands/module.rs +++ b/src-tauri/src/commands/module.rs @@ -19,7 +19,7 @@ use std::process::Command; use serde::{Deserialize, Serialize}; use tauri::State; -use df_storage::models::ProjectModuleRecord; +use df_storage::models::{ModuleDependencyRecord, ProjectModuleRecord}; use df_types::types::new_id; use crate::state::AppState; @@ -1133,3 +1133,68 @@ pub async fn get_commit_detail( "full_message": full_message, })) } + +// ============================================================ +// IPC 命令 — 工程依赖关系(module_dependencies 表) +// ============================================================ + +/// 新增工程依赖。返回完整记录。 +#[tauri::command] +pub async fn add_module_dependency( + state: State<'_, AppState>, + project_id: String, + from_module_id: String, + to_module_id: String, + dep_type: Option, + label: Option, +) -> Result { + let project_id = project_id.trim().to_string(); + let from_module_id = from_module_id.trim().to_string(); + let to_module_id = to_module_id.trim().to_string(); + if project_id.is_empty() || from_module_id.is_empty() || to_module_id.is_empty() { + return Err("project_id / from_module_id / to_module_id 不能为空".to_string()); + } + let dep_type = dep_type.unwrap_or_else(|| "library".to_string()); + let record = ModuleDependencyRecord { + id: new_id(), + project_id, + from_module_id, + to_module_id, + dep_type, + label: trim_opt(label), + created_at: now_millis(), + }; + state.module_dependencies.insert(record.clone()).await.map_err(err_str)?; + Ok(record) +} + +/// 删除工程依赖。 +#[tauri::command] +pub async fn remove_module_dependency( + state: State<'_, AppState>, + id: String, +) -> Result<(), String> { + let id = id.trim().to_string(); + if id.is_empty() { + return Err("id 不能为空".to_string()); + } + state.module_dependencies.delete(&id).await.map_err(err_str)?; + Ok(()) +} + +/// 列出项目的全部工程依赖。 +#[tauri::command] +pub async fn list_module_dependencies( + state: State<'_, AppState>, + project_id: String, +) -> Result, String> { + let project_id = project_id.trim().to_string(); + if project_id.is_empty() { + return Err("project_id 不能为空".to_string()); + } + state + .module_dependencies + .list_by_field("project_id", &project_id) + .await + .map_err(err_str) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 08ec5b6..e7590f4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -334,6 +334,9 @@ pub fn run() { commands::module::get_module_file_diff, commands::module::get_module_commits, commands::module::get_commit_detail, + commands::module::add_module_dependency, + commands::module::remove_module_dependency, + commands::module::list_module_dependencies, // 灵感 commands::idea::list_ideas, commands::idea::create_idea, diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index ccecb8e..5c28e4b 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -39,7 +39,7 @@ use df_storage::crud::{ AiConversationRepo, AiMessageRepo, AiProviderRepo, AiToolExecutionRepo, IdeaEvalRepo, IdeaRepo, KnowledgeEventsRepo, KnowledgeRepo, NodeExecutionRepo, ProjectEventRepo, ProjectModuleRepo, ProjectRepo, ProjectServiceRepo, ReleaseRepo, SettingsRepo, TaskLinkRepo, TaskRepo, - WorkflowRepo, + WorkflowRepo, ModuleDependencyRepo, }; use df_storage::db::Database; use df_workflow::eventbus::EventBus; @@ -75,6 +75,8 @@ pub struct AppState { /// 项目多工程(每个工程独立代码仓库):Monorepo 多仓库 / 微服务 / 前后端分离。 /// 记录工程元数据(路径/Git 地址/技术栈);Git 状态(分支/改动/提交)实时派生不存表。 pub project_modules: ProjectModuleRepo, + /// 工程依赖关系 Repo(V35 module_dependencies,依赖图边数据)。 + pub module_dependencies: ModuleDependencyRepo, /// 发布表 Repo(预留:ReleaseRepo 持久化已就位·IPC/逻辑未接入·SW-260618-21 b 保留) #[allow(dead_code)] pub releases: ReleaseRepo, @@ -209,6 +211,7 @@ impl AppState { project_events: ProjectEventRepo::new(&db), project_services: ProjectServiceRepo::new(&db), project_modules: ProjectModuleRepo::new(&db), + module_dependencies: ModuleDependencyRepo::new(&db), releases: ReleaseRepo::new(&db), workflows: WorkflowRepo::new(&db), node_executions: NodeExecutionRepo::new(&db), diff --git a/src/api/module.ts b/src/api/module.ts index 48c72a7..a5b9caa 100644 --- a/src/api/module.ts +++ b/src/api/module.ts @@ -59,6 +59,17 @@ export interface GitChangedFile { path: string } +/** 工程依赖关系记录(对齐后端 ModuleDependencyRecord)。 */ +export interface ModuleDependencyRecord { + id: string + project_id: string + from_module_id: string + to_module_id: string + dep_type: string + label: string | null + created_at: string +} + /** 最近提交项(对齐后端 GitRecentCommit)。 */ export interface GitRecentCommit { hash: string @@ -157,6 +168,21 @@ export const moduleApi = { return invoke('list_branches', { moduleId }) }, + /** 列出项目全部工程依赖。 */ + listModuleDependencies(projectId: string): Promise { + return invoke('list_module_dependencies', { projectId }) + }, + + /** 新增工程依赖。 */ + addModuleDependency(projectId: string, fromModuleId: string, toModuleId: string, depType?: string, label?: string): Promise { + return invoke('add_module_dependency', { projectId, fromModuleId, toModuleId, depType: depType ?? null, label: label ?? null }) + }, + + /** 删除工程依赖。 */ + removeModuleDependency(id: string): Promise { + return invoke('remove_module_dependency', { id }) + }, + getModuleCommits(moduleId: string, skip?: number, limit?: number): Promise<{ commits: { hash: string; subject: string; timestamp: number; author: string }[] has_more: boolean diff --git a/src/components/project/DependencyGraph.vue b/src/components/project/DependencyGraph.vue index af15477..2749bba 100644 --- a/src/components/project/DependencyGraph.vue +++ b/src/components/project/DependencyGraph.vue @@ -23,30 +23,40 @@ * 节点用 Vue 组件(ModuleNode.vue)渲染,通过 @antv/x6-vue-shape 注册。 */ import { onMounted, onBeforeUnmount, ref, watch, markRaw } from 'vue' -import { Graph, Selection, Snapline, History, Scroller } from '@antv/x6' +import { useRouter } from 'vue-router' +import { Graph, Selection, Snapline, History, Scroller, MiniMap } from '@antv/x6' import '@antv/x6-vue-shape' -import { moduleApi, type ProjectModuleRecord } from '@/api/module' +import { moduleApi, type ProjectModuleRecord, type ModuleDependencyRecord } from '@/api/module' import ModuleNode from './ModuleNode.vue' const props = defineProps<{ projectId: string }>() +const router = useRouter() + const emit = defineEmits<{ (e: 'select-module', moduleId: string): void }>() const containerRef = ref(null) const modules = ref([]) +const dependencies = ref([]) const loading = ref(true) let graph: Graph | null = null async function loadModules() { loading.value = true try { - modules.value = await moduleApi.listProjectModules(props.projectId) + const [mods, deps] = await Promise.all([ + moduleApi.listProjectModules(props.projectId), + moduleApi.listModuleDependencies(props.projectId), + ]) + modules.value = mods + dependencies.value = deps } catch { modules.value = [] + dependencies.value = [] } finally { loading.value = false } @@ -81,8 +91,21 @@ function renderGraph() { } }) - // 边暂为空(module_dependencies 表未建) - graph.fromJSON({ nodes, edges: [] }) + // 边数据(module_dependencies 表) + const edges = dependencies.value.map(d => ({ + source: d.from_module_id, + target: d.to_module_id, + labels: d.label ? [{ text: d.label }] : [{ text: d.dep_type }], + attrs: { + line: { + stroke: depTypeColor(d.dep_type), + strokeWidth: 1.5, + targetMarker: { name: 'block', width: 8, height: 6 }, + }, + }, + })) + + graph.fromJSON({ nodes, edges }) graph.centerContent() } @@ -101,9 +124,11 @@ function buildGraph() { graph.use(new Snapline({ enabled: true })) graph.use(new History({ enabled: true })) graph.use(new Scroller({ enabled: true, pannable: true })) + graph.use(new MiniMap({ width: 200, height: 120, padding: 10 })) graph.on('node:click', ({ node }) => { emit('select-module', String(node.id)) + router.push(`/projects/${props.projectId}`) }) renderGraph() @@ -121,6 +146,17 @@ function zoomOut() { graph?.zoom(-0.1) } +/** 依赖类型 → 边颜色 */ +function depTypeColor(depType: string): string { + switch (depType) { + case 'api': return '#5378e8' + case 'mq': return '#e8a053' + case 'shared': return '#53e8a0' + case 'custom': return '#e853a0' + default: return '#5c6b8a' // library + } +} + onMounted(async () => { await loadModules() buildGraph()