diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 65180d4..2846404 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -2,9 +2,9 @@ name: Lint
on:
push:
- branches: [master]
+ branches: [ master, v2 ]
pull_request:
- branches: [master]
+ branches: [ master, v2 ]
jobs:
lint:
@@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
- go-version: ">=1.22.0"
+ go-version-file: 'go.mod'
- name: golangci-lint
uses: golangci/golangci-lint-action@v8
diff --git a/.golangci.yml b/.golangci.yml
index d6395ac..b479b30 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,4 +1,6 @@
version: "2"
+issues:
+ fix: true
linters:
default: none
enable:
@@ -10,8 +12,7 @@ linters:
- legacy
- std-error-handling
paths:
- - internal/example
- - cmds
+ - internal/examples
- vendor
- third_party$
- builtin$
@@ -20,11 +21,24 @@ formatters:
enable:
- goimports
- gofmt
+ - gofumpt
+ settings:
+ gofumpt:
+ extra-rules: true
+ goimports:
+ local-prefixes:
+ - github.com/pubgo/dix
+ gofmt:
+ simplify: false
+ rewrite-rules:
+ - pattern: 'interface{}'
+ replacement: 'any'
+ - pattern: 'a[b:len(a)]'
+ replacement: 'a[b:]'
+
exclusions:
paths:
- - internal/example
- - cmds
+ - internal/examples
- vendor
- - third_party$
- - builtin$
- examples$
+ - proto
diff --git a/.version b/.version/VERSION
similarity index 100%
rename from .version
rename to .version/VERSION
diff --git a/dix.go b/dix.go
index 447236d..095d0b4 100644
--- a/dix.go
+++ b/dix.go
@@ -9,10 +9,10 @@ import (
"github.com/pubgo/dix/v2/dixrender"
)
-//go:embed .version
+//go:embed .version/VERSION
var version string
-func ReleaseVersion() string { return version }
+func Version() string { return version }
type (
Option = dixinternal.Option
diff --git a/dixhttp/server.go b/dixhttp/server.go
index 6c79646..c508dfd 100644
--- a/dixhttp/server.go
+++ b/dixhttp/server.go
@@ -30,8 +30,8 @@ func NewServer(dix *dixinternal.Dix) *Server {
// setupRoutes configures all HTTP routes
func (s *Server) setupRoutes() {
- s.mux.HandleFunc("/", s.handleIndex)
- s.mux.HandleFunc("/api/dependencies", s.handleDependencies)
+ s.mux.HandleFunc("/", s.HandleIndex)
+ s.mux.HandleFunc("/api/dependencies", s.HandleDependencies)
}
// ServeHTTP implements http.Handler interface
@@ -44,15 +44,15 @@ func (s *Server) ListenAndServe(addr string) error {
return http.ListenAndServe(addr, s)
}
-// handleIndex serves the HTML visualization page
-func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
+// HandleIndex serves the HTML visualization page
+func (s *Server) HandleIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, htmlTemplate)
}
-// handleDependencies returns JSON data about providers and objects relationships
-func (s *Server) handleDependencies(w http.ResponseWriter, r *http.Request) {
+// HandleDependencies returns JSON data about providers and objects relationships
+func (s *Server) HandleDependencies(w http.ResponseWriter, r *http.Request) {
data := s.extractDependencyData()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
diff --git a/dixhttp/template.html b/dixhttp/template.html
index 0dec641..6d0b388 100644
--- a/dixhttp/template.html
+++ b/dixhttp/template.html
@@ -85,6 +85,75 @@
border-color: #667eea;
}
+ .search-container {
+ position: relative;
+ display: inline-block;
+ }
+
+ .controls input[type="text"] {
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ background: white;
+ border-radius: 4px;
+ font-size: 14px;
+ width: 200px;
+ }
+
+ .controls input[type="text"]:focus {
+ outline: none;
+ border-color: #667eea;
+ }
+
+ .controls input[type="text"]::placeholder {
+ color: #999;
+ }
+
+ .search-suggestions {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: white;
+ border: 1px solid #ddd;
+ border-top: none;
+ border-radius: 0 0 4px 4px;
+ max-height: 300px;
+ overflow-y: auto;
+ z-index: 1000;
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
+ display: none;
+ }
+
+ .search-suggestion-item {
+ padding: 10px 12px;
+ cursor: pointer;
+ border-bottom: 1px solid #f0f0f0;
+ font-size: 13px;
+ }
+
+ .search-suggestion-item:hover {
+ background: #f5f5f5;
+ }
+
+ .search-suggestion-item:last-child {
+ border-bottom: none;
+ }
+
+ .search-suggestion-label {
+ font-weight: 500;
+ color: #333;
+ }
+
+ .search-suggestion-id {
+ font-size: 11px;
+ color: #999;
+ margin-top: 2px;
+ }
+
+ .search-suggestion-item.highlighted {
+ background: #e3f2fd;
+ }
+
.container {
display: flex;
height: calc(100vh - 140px);
@@ -221,6 +290,15 @@
🔗 Dix 依赖关系可视化
力导向布局
圆形布局
+
+ 重置过滤
加载中...
@@ -284,6 +362,17 @@ 图例
• 圆形布局:节点呈圆形排列
+
+
+ 搜索过滤:
+
+
+ • 双击节点:自动过滤该节点及其依赖关系
+ • 搜索框:输入节点名称,从下拉列表中选择
+ • 按 Enter:直接过滤第一个匹配的节点
+ • 重置按钮:清除过滤,显示所有节点
+
+
@@ -293,6 +382,9 @@ 图例
let data = null;
let currentView = 'providers';
let currentLayout = 'hierarchical';
+ let allNodes = []; // 存储所有节点(用于过滤)
+ let allEdges = []; // 存储所有边(用于过滤)
+ let filteredNodeId = null; // 当前过滤的节点ID
// HTML 转义函数
function escapeHtml(text) {
@@ -302,6 +394,137 @@ 图例
return div.innerHTML;
}
+ // 格式化类型名称,包含包名但保持合理长度
+ function formatTypeLabel(typeStr, maxLength) {
+ if (!typeStr || typeof typeStr !== 'string') return typeStr;
+
+ // 如果不包含点号,直接返回
+ if (!typeStr.includes('.')) return typeStr;
+
+ // 如果指定了最大长度且类型名不超过,直接返回完整名称
+ if (!maxLength || typeStr.length <= maxLength) return typeStr;
+
+ // 拆分包名和类型名
+ const parts = typeStr.split('.');
+ const typeName = parts[parts.length - 1];
+ const packageParts = parts.slice(0, -1);
+
+ // 如果类型名本身就超过了限制,直接截断类型名
+ if (typeName.length > maxLength) {
+ return typeName.substring(0, maxLength - 3) + '...';
+ }
+
+ // 尝试显示包名的最后一部分 + 类型名
+ // 例如:github.com/pubgo/dix/v2/pkg.Type -> pkg.Type
+ if (packageParts.length === 0) {
+ return typeName;
+ }
+
+ // 从后往前尝试添加包名部分,最多显示最后两个包名部分
+ let result = typeName;
+ const partsToTry = Math.min(2, packageParts.length); // 最多显示最后2个包名部分
+
+ for (let i = packageParts.length - partsToTry; i < packageParts.length; i++) {
+ if (i < 0) continue;
+ const part = packageParts[i];
+ const candidate = part + '.' + result;
+
+ if (candidate.length <= maxLength) {
+ result = candidate;
+ } else {
+ // 如果加上当前包名部分超过限制,添加省略号并返回
+ return '...' + result;
+ }
+ }
+
+ // 如果还有更多包名部分,添加省略号
+ if (packageParts.length > partsToTry) {
+ return '...' + result;
+ }
+
+ return result;
+ }
+
+ // 格式化函数名,包含包名但保持合理长度
+ function formatFunctionLabel(fnName, maxLength) {
+ if (!fnName || typeof fnName !== 'string' || fnName === 'unknown') return 'unknown';
+
+ // 如果不包含点号,直接返回
+ if (!fnName.includes('.')) return fnName;
+
+ // 如果指定了最大长度且函数名不超过,直接返回完整名称
+ if (!maxLength || fnName.length <= maxLength) return fnName;
+
+ // 处理 (*Type).method 格式
+ // 例如:github.com/pubgo/dix/pkg.(*Service).Method
+ let methodName = null;
+ let typeName = null;
+ let functionName = fnName;
+
+ // 检查是否是方法格式 (*Type).method 或 (*pkg.Type).method
+ const methodPattern = /\(([^)]+)\)\.(\w+)$/;
+ const match = fnName.match(methodPattern);
+ if (match) {
+ methodName = match[2]; // 方法名,例如 "Method"
+ const typePart = match[1]; // 类型部分,例如 "*Service" 或 "*pkg.Service"
+ // 从类型部分提取类型名(去掉 * 和包名)
+ typeName = typePart.replace(/^\*/, ''); // 去掉 *
+ if (typeName.includes('.')) {
+ typeName = typeName.split('.').pop(); // 只保留最后的类型名
+ }
+ // 提取方法名前的部分(包名)
+ functionName = fnName.substring(0, match.index);
+ }
+
+ // 拆分包名和函数名
+ const parts = functionName.split('.');
+ const packageParts = parts.length > 0 ? parts : [];
+
+ // 构建最终的函数名
+ let finalFuncName;
+ if (methodName && typeName) {
+ // 如果是方法,格式为:Type.Method
+ finalFuncName = typeName + '.' + methodName;
+ } else {
+ // 如果是普通函数,使用最后一个部分作为函数名
+ finalFuncName = packageParts.length > 0 ? packageParts[packageParts.length - 1] : fnName;
+ }
+
+ // 如果函数名本身就超过了限制,直接截断
+ if (finalFuncName.length > maxLength) {
+ return finalFuncName.substring(0, maxLength - 3) + '...';
+ }
+
+ // 尝试显示包名的最后一部分 + 函数名
+ if (packageParts.length === 0) {
+ return finalFuncName;
+ }
+
+ // 从后往前尝试添加包名部分,最多显示最后两个包名部分
+ let result = finalFuncName;
+ const partsToTry = Math.min(2, packageParts.length);
+
+ for (let i = packageParts.length - partsToTry; i < packageParts.length; i++) {
+ if (i < 0) continue;
+ const part = packageParts[i];
+ const candidate = part + '.' + result;
+
+ if (candidate.length <= maxLength) {
+ result = candidate;
+ } else {
+ // 如果加上当前包名部分超过限制,添加省略号并返回
+ return '...' + result;
+ }
+ }
+
+ // 如果还有更多包名部分,添加省略号
+ if (packageParts.length > partsToTry) {
+ return '...' + result;
+ }
+
+ return result;
+ }
+
// 计算节点的层级(基于依赖深度)
function calculateLevels(nodes, edges) {
const nodeMap = new Map();
@@ -492,7 +715,19 @@ 图例
layout: layoutConfig,
physics: physicsConfig,
edges: {
- smooth: smoothConfig
+ smooth: smoothConfig,
+ font: {
+ size: 12,
+ align: 'middle',
+ strokeWidth: 3,
+ strokeColor: '#ffffff',
+ color: '#333333',
+ face: 'Arial',
+ bold: true,
+ vadjust: 0
+ },
+ labelHighlightBold: true,
+ selectionWidth: 3
}
});
@@ -631,9 +866,17 @@ 图例
},
width: 2,
font: {
- size: 10,
- align: 'middle'
- }
+ size: 12,
+ align: 'middle',
+ strokeWidth: 3,
+ strokeColor: '#ffffff',
+ color: '#333333',
+ face: 'Arial',
+ bold: true,
+ vadjust: 0
+ },
+ labelHighlightBold: true,
+ selectionWidth: 3
},
physics: physicsConfig,
interaction: {
@@ -661,6 +904,34 @@ 图例
}
}
});
+
+ // 双击节点进行过滤
+ network.on("doubleClick", function(params) {
+ if (params.nodes.length > 0) {
+ const nodeId = params.nodes[0];
+ const node = data.nodes.get(nodeId);
+ if (node) {
+ // 从 allNodes 中找到对应的节点(包含完整信息)
+ const fullNode = allNodes.find(n => n.id === nodeId);
+ if (fullNode) {
+ // 更新搜索框
+ const searchInput = document.getElementById('node-search');
+ if (searchInput) {
+ searchInput.value = fullNode.label || fullNode.id;
+ }
+ // 应用过滤
+ applyNodeFilter(fullNode);
+ } else {
+ // 如果没有找到完整节点,使用当前节点信息
+ const searchInput = document.getElementById('node-search');
+ if (searchInput) {
+ searchInput.value = node.label || node.id;
+ }
+ applyNodeFilter(node);
+ }
+ }
+ }
+ });
}
// 加载数据
@@ -669,7 +940,7 @@ 图例
document.getElementById('loading').style.display = 'block';
document.getElementById('error').style.display = 'none';
- const response = await fetch('/api/dependencies');
+ const response = await fetch('api/dependencies');
if (!response.ok) {
throw new Error('加载数据失败: ' + response.statusText);
}
@@ -698,6 +969,13 @@ 图例
return;
}
+ // 切换视图时清除过滤状态
+ if (currentView !== view) {
+ filteredNodeId = null;
+ document.getElementById('node-search').value = '';
+ document.getElementById('btn-reset-filter').style.display = 'none';
+ }
+
currentView = view;
// 更新按钮状态
@@ -740,45 +1018,21 @@ 图例
const inputTypes = provider.input_types || provider.inputTypes || [];
const outputType = provider.output_type || provider.outputType || 'unknown';
- // 提取函数名
- let fnLabel = 'unknown';
- if (typeof fnName === 'string' && fnName.length > 0 && fnName !== 'unknown') {
- const parts = fnName.split('.');
- if (parts.length > 0) {
- let lastPart = parts[parts.length - 1];
- if (lastPart.includes('(') && lastPart.includes(')')) {
- const methodMatch = lastPart.match(/\)\.(\w+)$/);
- if (methodMatch) {
- lastPart = methodMatch[1];
- }
- }
- fnLabel = lastPart;
- }
- }
-
- // 简化类型名称
- const simplifyType = (typeStr) => {
- if (!typeStr || typeof typeStr !== 'string') return typeStr;
- if (typeStr.includes('.')) {
- return typeStr.split('.').pop();
- }
- return typeStr;
- };
+ // 格式化函数名(包含包名)
+ const fnLabel = formatFunctionLabel(fnName, 35);
- // 构建简洁的标签
- const outputLabel = simplifyType(outputType);
+ // 构建标签,使用格式化类型名称(包含包名)
+ const outputLabel = formatTypeLabel(outputType, 30);
let label = fnLabel;
- const maxFnNameLength = 25;
- if (label.length > maxFnNameLength) {
- label = label.substring(0, maxFnNameLength) + '...';
- }
+
if (outputType !== 'unknown' && outputLabel) {
- let shortOutput = outputLabel;
- const maxTypeLength = 20;
- if (shortOutput.length > maxTypeLength) {
- shortOutput = shortOutput.substring(0, maxTypeLength) + '...';
- }
- label = label + ' → ' + shortOutput;
+ label = label + ' → ' + outputLabel;
+ }
+
+ // 如果标签太长,截断
+ const maxLabelLength = 60;
+ if (label.length > maxLabelLength) {
+ label = label.substring(0, maxLabelLength - 3) + '...';
}
// 构建标题
@@ -838,7 +1092,6 @@ 图例
to: providerB.id,
label: '提供',
color: { color: '#4CAF50' },
- font: { align: 'middle' },
title: 'Provider A 的输出类型被 Provider B 使用'
});
}
@@ -858,60 +1111,22 @@ 图例
const inputTypes = provider.input_types || provider.inputTypes || [];
const outputType = provider.output_type || 'unknown';
- // 提取函数名(去掉包名,只保留函数名)
- let fnLabel = 'unknown';
- if (typeof fnName === 'string' && fnName.length > 0 && fnName !== 'unknown') {
- // 处理格式:package.function 或 package.(*Type).method
- const parts = fnName.split('.');
- if (parts.length > 0) {
- // 取最后一部分,如果是方法则去掉类型部分
- let lastPart = parts[parts.length - 1];
- // 处理 (*Type).method 格式
- if (lastPart.includes('(') && lastPart.includes(')')) {
- const methodMatch = lastPart.match(/\)\.(\w+)$/);
- if (methodMatch) {
- lastPart = methodMatch[1];
- }
- }
- fnLabel = lastPart;
- }
- }
-
- // 简化类型名称(去掉包名)
- const simplifyType = (typeStr) => {
- if (!typeStr || typeof typeStr !== 'string') return typeStr;
- if (typeStr.includes('.')) {
- return typeStr.split('.').pop();
- }
- return typeStr;
- };
+ // 格式化函数名(包含包名)
+ const fnLabel = formatFunctionLabel(fnName, 35);
- // 构建节点标签:只显示简洁的函数名和输出类型
- // 不使用换行符,保持标签简洁
- const outputLabel = simplifyType(outputType);
+ // 构建节点标签:显示函数名和输出类型(包含包名)
+ const outputLabel = formatTypeLabel(outputType, 30);
let label = fnLabel;
- // 如果函数名太长,截断
- const maxFnNameLength = 20;
- if (label.length > maxFnNameLength) {
- label = label.substring(0, maxFnNameLength) + '...';
- }
-
- // 添加输出类型(简洁形式)
+ // 添加输出类型
if (outputType !== 'unknown' && outputLabel) {
- // 如果输出类型名太长,也截断
- let shortOutput = outputLabel;
- const maxTypeLength = 15;
- if (shortOutput.length > maxTypeLength) {
- shortOutput = shortOutput.substring(0, maxTypeLength) + '...';
- }
- label = label + ' → ' + shortOutput;
+ label = label + ' → ' + outputLabel;
}
- // 如果标签还是太长,进一步截断
- const maxLabelLength = 40;
+ // 如果标签太长,截断
+ const maxLabelLength = 60;
if (label.length > maxLabelLength) {
- label = label.substring(0, maxLabelLength) + '...';
+ label = label.substring(0, maxLabelLength - 3) + '...';
}
// 构建完整的标题信息(使用格式化文本)
@@ -949,9 +1164,7 @@ 图例
// 添加输出类型节点
const outputType = provider.output_type || 'unknown';
if (outputType && !nodeMap.has(outputType)) {
- const label = (typeof outputType === 'string' && outputType.includes('.'))
- ? outputType.split('.').pop()
- : outputType;
+ const label = formatTypeLabel(outputType, 40);
let outputTitle = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
outputTitle += '输出类型\n';
outputTitle += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
@@ -972,15 +1185,12 @@ 图例
// 添加从 Provider 到输出类型的边(显示输出类型信息)
if (provider.id && outputType) {
- const outputLabel = outputType.includes('.')
- ? outputType.split('.').pop()
- : outputType;
+ const outputLabel = formatTypeLabel(outputType, 25);
edges.push({
from: provider.id,
to: outputType,
label: '输出: ' + outputLabel,
- color: { color: '#4CAF50' },
- font: { align: 'middle' }
+ color: { color: '#4CAF50' }
});
}
@@ -990,9 +1200,7 @@ 图例
if (!inputType || typeof inputType !== 'string') return;
if (!nodeMap.has(inputType)) {
- const label = inputType.includes('.')
- ? inputType.split('.').pop()
- : inputType;
+ const label = formatTypeLabel(inputType, 40);
let inputTitle = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
inputTitle += '输入类型\n';
inputTitle += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
@@ -1013,15 +1221,12 @@ 图例
// 添加从输入类型到输出类型的边(显示依赖关系)
if (inputType && outputType) {
- const inputLabel = inputType.includes('.')
- ? inputType.split('.').pop()
- : inputType;
+ const inputLabel = formatTypeLabel(inputType, 25);
edges.push({
from: inputType,
to: outputType,
label: '依赖',
color: { color: '#9E9E9E' },
- font: { align: 'middle' },
// 如果是多个输入,可以添加序号
title: '输入 ' + (index + 1) + ': ' + inputType
});
@@ -1029,15 +1234,12 @@ 图例
// 也可以添加从输入类型到 Provider 的边,更清晰地显示依赖
if (inputType && provider.id) {
- const inputLabel = inputType.includes('.')
- ? inputType.split('.').pop()
- : inputType;
+ const inputLabel = formatTypeLabel(inputType, 25);
edges.push({
from: inputType,
to: provider.id,
label: '输入',
color: { color: '#FF9800' },
- font: { align: 'middle' },
dashes: true, // 使用虚线表示输入依赖
title: '输入参数: ' + inputType
});
@@ -1089,9 +1291,7 @@ 图例
const objGroup = object.group || 'default';
if (!nodeMap.has(object.id)) {
- const typeLabel = (typeof objType === 'string' && objType.includes('.'))
- ? objType.split('.').pop()
- : objType;
+ const typeLabel = formatTypeLabel(objType, 40);
let objectTitle = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
objectTitle += 'Object 实例\n';
objectTitle += '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n';
@@ -1138,19 +1338,14 @@ 图例
nodeMap.set('edge_' + edgeKey, true);
// 添加从 objectB 到 objectA 的边(B 被 A 依赖)
- const typeBLabel = (typeof objectB.type === 'string' && objectB.type.includes('.'))
- ? objectB.type.split('.').pop()
- : objectB.type;
- const typeALabel = (typeof typeA === 'string' && typeA.includes('.'))
- ? typeA.split('.').pop()
- : typeA;
+ const typeBLabel = formatTypeLabel(objectB.type, 25);
+ const typeALabel = formatTypeLabel(typeA, 25);
edges.push({
from: objectB.id,
to: objectA.id,
label: '依赖',
color: { color: '#9E9E9E' },
- font: { align: 'middle' },
dashes: false,
title: typeBLabel + ' → ' + typeALabel + '\\n类型依赖关系'
});
@@ -1159,6 +1354,24 @@ 图例
});
}
+ // 保存所有节点和边的深拷贝(用于过滤功能)
+ allNodes = JSON.parse(JSON.stringify(nodes));
+ allEdges = JSON.parse(JSON.stringify(edges));
+
+ // 如果当前有过滤,重新应用过滤
+ if (filteredNodeId) {
+ const targetNode = allNodes.find(node => node.id === filteredNodeId);
+ if (targetNode) {
+ applyNodeFilter(targetNode);
+ return;
+ } else {
+ // 如果在新视图中找不到目标节点,清除过滤
+ filteredNodeId = null;
+ document.getElementById('node-search').value = '';
+ document.getElementById('btn-reset-filter').style.display = 'none';
+ }
+ }
+
// 更新信息
document.getElementById('info').textContent =
'节点: ' + nodes.length + ' | 边: ' + edges.length;
@@ -1190,6 +1403,260 @@ 图例
}
}
+ // 搜索节点
+ function handleSearchKeyup(event) {
+ if (event.key === 'Enter') {
+ const searchValue = event.target.value.trim();
+ if (searchValue) {
+ filterByNode(searchValue);
+ hideSearchSuggestions();
+ } else {
+ resetFilter();
+ }
+ } else if (event.key === 'Escape') {
+ hideSearchSuggestions();
+ }
+ }
+
+ // 处理搜索输入(实时显示建议)
+ function handleSearchInput(event) {
+ const searchValue = event.target.value.trim();
+ if (searchValue.length === 0) {
+ hideSearchSuggestions();
+ return;
+ }
+
+ if (!allNodes || allNodes.length === 0) {
+ return;
+ }
+
+ // 查找匹配的节点(最多显示10个)
+ const matchedNodes = allNodes.filter(node => {
+ const nodeId = node.id.toLowerCase();
+ const nodeLabel = (node.label || '').toLowerCase();
+ const searchLower = searchValue.toLowerCase();
+ return nodeId.includes(searchLower) || nodeLabel.includes(searchLower);
+ }).slice(0, 10);
+
+ showSearchSuggestions(matchedNodes, searchValue);
+ }
+
+ // 处理搜索框获得焦点
+ function handleSearchFocus(event) {
+ const searchValue = event.target.value.trim();
+ if (searchValue.length > 0 && allNodes && allNodes.length > 0) {
+ const matchedNodes = allNodes.filter(node => {
+ const nodeId = node.id.toLowerCase();
+ const nodeLabel = (node.label || '').toLowerCase();
+ const searchLower = searchValue.toLowerCase();
+ return nodeId.includes(searchLower) || nodeLabel.includes(searchLower);
+ }).slice(0, 10);
+ showSearchSuggestions(matchedNodes, searchValue);
+ }
+ }
+
+ // 处理搜索框失去焦点(延迟隐藏,以便点击建议项)
+ function handleSearchBlur(event) {
+ setTimeout(() => {
+ hideSearchSuggestions();
+ }, 200);
+ }
+
+ // 显示搜索建议
+ function showSearchSuggestions(nodes, searchValue) {
+ const suggestionsDiv = document.getElementById('search-suggestions');
+ if (!suggestionsDiv) return;
+
+ if (nodes.length === 0) {
+ suggestionsDiv.innerHTML = '未找到匹配的节点
';
+ suggestionsDiv.style.display = 'block';
+ return;
+ }
+
+ const searchLower = searchValue.toLowerCase();
+ // 清空之前的建议
+ suggestionsDiv.innerHTML = '';
+
+ nodes.forEach(node => {
+ const label = node.label || node.id;
+ const id = node.id;
+
+ // 创建建议项元素
+ const itemDiv = document.createElement('div');
+ itemDiv.className = 'search-suggestion-item';
+ itemDiv.onclick = () => selectSearchSuggestion(id);
+
+ // 高亮匹配的文本
+ const labelDiv = document.createElement('div');
+ labelDiv.className = 'search-suggestion-label';
+ if (label.toLowerCase().includes(searchLower)) {
+ const index = label.toLowerCase().indexOf(searchLower);
+ const before = escapeHtml(label.substring(0, index));
+ const match = escapeHtml(label.substring(index, index + searchValue.length));
+ const after = escapeHtml(label.substring(index + searchValue.length));
+ labelDiv.innerHTML = before + '' + match + ' ' + after;
+ } else {
+ labelDiv.textContent = label;
+ }
+
+ const idDiv = document.createElement('div');
+ idDiv.className = 'search-suggestion-id';
+ idDiv.textContent = id;
+
+ itemDiv.appendChild(labelDiv);
+ itemDiv.appendChild(idDiv);
+ suggestionsDiv.appendChild(itemDiv);
+ });
+
+ suggestionsDiv.style.display = 'block';
+ }
+
+ // 隐藏搜索建议
+ function hideSearchSuggestions() {
+ const suggestionsDiv = document.getElementById('search-suggestions');
+ if (suggestionsDiv) {
+ suggestionsDiv.style.display = 'none';
+ }
+ }
+
+ // 选择搜索建议
+ function selectSearchSuggestion(nodeId) {
+ if (!allNodes || allNodes.length === 0) {
+ return;
+ }
+
+ const node = allNodes.find(n => n.id === nodeId);
+ if (node) {
+ const searchInput = document.getElementById('node-search');
+ if (searchInput) {
+ searchInput.value = node.label || node.id;
+ }
+ applyNodeFilter(node);
+ hideSearchSuggestions();
+ }
+ }
+
+ // 根据节点名称或ID过滤
+ function filterByNode(searchValue) {
+ if (!allNodes || allNodes.length === 0) {
+ return;
+ }
+
+ // 查找匹配的节点(支持按ID或标签匹配)
+ const matchedNodes = allNodes.filter(node => {
+ const nodeId = node.id.toLowerCase();
+ const nodeLabel = (node.label || '').toLowerCase();
+ const searchLower = searchValue.toLowerCase();
+ return nodeId.includes(searchLower) || nodeLabel.includes(searchLower);
+ });
+
+ if (matchedNodes.length === 0) {
+ alert('未找到匹配的节点: ' + searchValue);
+ hideSearchSuggestions();
+ return;
+ }
+
+ // 使用第一个匹配的节点
+ const targetNode = matchedNodes[0];
+ applyNodeFilter(targetNode);
+ hideSearchSuggestions();
+ }
+
+ // 应用节点过滤
+ function applyNodeFilter(targetNode) {
+ if (!targetNode || !allNodes || allNodes.length === 0) {
+ return;
+ }
+
+ filteredNodeId = targetNode.id;
+
+ // 更新搜索框的值(显示节点标签)
+ const searchInput = document.getElementById('node-search');
+ if (searchInput) {
+ searchInput.value = targetNode.label || targetNode.id;
+ }
+
+ // 显示重置按钮
+ document.getElementById('btn-reset-filter').style.display = 'inline-block';
+
+ // 找到所有直接相关的节点和边
+ const relatedNodeIds = new Set([targetNode.id]);
+
+ // 找到所有相关的边(以目标节点为起点或终点的边)
+ allEdges.forEach(edge => {
+ const isFromTarget = edge.from === targetNode.id;
+ const isToTarget = edge.to === targetNode.id;
+
+ if (isFromTarget || isToTarget) {
+ relatedNodeIds.add(edge.from);
+ relatedNodeIds.add(edge.to);
+ }
+ });
+
+ // 过滤节点和边
+ const filteredNodes = allNodes.filter(node => relatedNodeIds.has(node.id));
+
+ // 过滤边,只保留两个端点都在过滤节点中的边
+ const filteredEdges = allEdges.filter(edge =>
+ relatedNodeIds.has(edge.from) && relatedNodeIds.has(edge.to)
+ );
+
+ // 更新网络图
+ if (network) {
+ // 创建节点副本以避免修改原始数据
+ const nodesToDisplay = filteredNodes.map(node => {
+ const nodeCopy = JSON.parse(JSON.stringify(node));
+ // 高亮目标节点
+ if (nodeCopy.id === targetNode.id) {
+ nodeCopy.color = {
+ background: '#FFD54F',
+ border: '#F57F17',
+ highlight: {
+ background: '#FFD54F',
+ border: '#F57F17'
+ }
+ };
+ }
+ return nodeCopy;
+ });
+
+ // 重新计算层级
+ const levels = calculateLevels(nodesToDisplay, filteredEdges);
+ nodesToDisplay.forEach(node => {
+ const level = levels.get(node.id) || 0;
+ if (node.id !== targetNode.id) {
+ const colorInfo = getColorByLevel(level, node.group);
+ node.color = {
+ background: colorInfo.bg,
+ border: colorInfo.border,
+ highlight: {
+ background: '#FFD54F',
+ border: '#F57F17'
+ }
+ };
+ }
+ node.level = level;
+ });
+
+ network.setData({ nodes: nodesToDisplay, edges: filteredEdges });
+ applyLayout(network, currentLayout);
+
+ // 更新信息
+ document.getElementById('info').textContent =
+ '节点: ' + filteredNodes.length + ' | 边: ' + filteredEdges.length + ' (已过滤)';
+ }
+ }
+
+ // 重置过滤
+ function resetFilter() {
+ filteredNodeId = null;
+ document.getElementById('node-search').value = '';
+ document.getElementById('btn-reset-filter').style.display = 'none';
+
+ // 重新加载视图以显示所有节点
+ loadView(currentView);
+ }
+
// 页面加载时初始化
window.addEventListener('DOMContentLoaded', function() {
loadData();
diff --git a/dixinternal/dix.go b/dixinternal/dix.go
index ba41370..b7fc054 100644
--- a/dixinternal/dix.go
+++ b/dixinternal/dix.go
@@ -495,7 +495,7 @@ func (dix *Dix) getProvideInput(typ reflect.Type) []*providerInputType {
}
// provide registers a constructor function
-func (dix *Dix) provide(param interface{}) {
+func (dix *Dix) provide(param any) {
defer func() {
if r := recover(); r != nil {
debug.PrintStack()
diff --git a/dixinternal/dix_test.go b/dixinternal/dix_test.go
index d63f0b1..d3596b3 100644
--- a/dixinternal/dix_test.go
+++ b/dixinternal/dix_test.go
@@ -23,10 +23,12 @@ type testStruct2 struct{}
func (s *testStruct2) Do() {}
// Types for TestMethodInjection
-type methodInjectDependency struct{}
-type methodInjectTarget struct {
- injected bool
-}
+type (
+ methodInjectDependency struct{}
+ methodInjectTarget struct {
+ injected bool
+ }
+)
func (t *methodInjectTarget) DixInject(d *methodInjectDependency) {
if d == nil {
@@ -1601,7 +1603,6 @@ func TestOptionsMerge(t *testing.T) {
func TestOptionsValidate(t *testing.T) {
opts := Options{}
err := opts.Validate()
-
if err != nil {
t.Fatalf("Validate returned unexpected error: %v", err)
}
@@ -1686,7 +1687,7 @@ func TestProviderInputTypeValidate(t *testing.T) {
// TestProviderFnCall tests the providerFn.call method
func TestProviderFnCall(t *testing.T) {
// Test normal function call
- fn := func(x int, y int) int {
+ fn := func(x, y int) int {
return x + y
}
diff --git a/dixinternal/logger.go b/dixinternal/logger.go
index 0e04889..735dc0e 100644
--- a/dixinternal/logger.go
+++ b/dixinternal/logger.go
@@ -14,7 +14,7 @@ func getLogPackage() slog.Attr {
}
func createDefaultLogger() *slog.Logger {
- var logOpt = &tint.Options{Level: slog.LevelInfo, AddSource: true}
+ logOpt := &tint.Options{Level: slog.LevelInfo, AddSource: true}
return slog.New(tint.NewHandler(os.Stderr, logOpt)).With(getLogPackage())
}
diff --git a/dixrender/adapter.go b/dixrender/adapter.go
index e6a71e5..77503cc 100644
--- a/dixrender/adapter.go
+++ b/dixrender/adapter.go
@@ -18,7 +18,7 @@ func NewDixAdapter(dix *dixinternal.Dix) DixAccessor {
// providerFnWrapper wraps dixinternal's providerFn to implement dixrender.ProviderFnAccessor
type providerFnWrapper struct {
- providerFn interface{} // *dixinternal.providerFn (unexported type)
+ providerFn any // *dixinternal.providerFn (unexported type)
}
func (w *providerFnWrapper) GetFn() reflect.Value {
diff --git a/dixrender/renderer.go b/dixrender/renderer.go
index d1b79b0..95a756f 100644
--- a/dixrender/renderer.go
+++ b/dixrender/renderer.go
@@ -23,11 +23,11 @@ func NewDotRenderer() *DotRenderer {
}
// Writef writes a formatted string to the renderer buffer
-func (d *DotRenderer) Writef(format string, args ...interface{}) {
+func (d *DotRenderer) Writef(format string, args ...any) {
_, _ = fmt.Fprintf(d.Buf, d.indent+format+"\n", args...)
}
-func (d *DotRenderer) writef(format string, args ...interface{}) {
+func (d *DotRenderer) writef(format string, args ...any) {
d.Writef(format, args...)
}