feat(deps): implement batch installation, manifest parsing/importing, and responsive mobile UI/UX improvements

This commit is contained in:
duorameng
2026-05-18 19:24:45 +08:00
parent 9b4826bbee
commit a31c4b9f03
9 changed files with 622 additions and 39 deletions
+27
View File
@@ -111,3 +111,30 @@ func (s *DependencyService) GetVerifyCommand(language, langVersion string) (stri
}
return m.GetVerifyCommand(langVersion)
}
// GetBatchInstallCommand 获取批量安装命令
func (s *DependencyService) GetBatchInstallCommand(depsList []models.Dependency) (string, error) {
if len(depsList) == 0 {
return "", errors.New("依赖包列表不能为空")
}
firstDep := depsList[0]
m := deps.GetManager(firstDep.Language)
if m == nil {
return "", errors.New("不支持的依赖类型: " + firstDep.Language)
}
return m.GetBatchInstallCommand(depsList)
}
// ImportDependencies 批量导入依赖并自动入库去重
func (s *DependencyService) ImportDependencies(depsList []models.Dependency) ([]models.Dependency, error) {
var imported []models.Dependency
for i := range depsList {
dep := &depsList[i]
if err := s.Create(dep); err == nil {
imported = append(imported, *dep)
}
}
return imported, nil
}
+28
View File
@@ -16,6 +16,7 @@ type Manager interface {
Uninstall(dep *models.Dependency) error
GetInstalledPackages(language, langVersion string) ([]models.Dependency, error)
GetInstallCommand(dep *models.Dependency) (string, error)
GetBatchInstallCommand(deps []models.Dependency) (string, error)
GetReinstallAllCommand(deps []models.Dependency) (string, error)
GetVerifyCommand(langVersion string) (string, error)
}
@@ -74,6 +75,33 @@ func (m *BaseManager) GetInstallCommand(dep *models.Dependency) (string, error)
return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil
}
func (m *BaseManager) GetBatchInstallCommand(deps []models.Dependency) (string, error) {
if len(deps) == 0 {
return "echo \"没有需要安装的依赖\"", nil
}
var packageSpecs []string
var langVersion string
var language string
for _, dep := range deps {
language = dep.Language
if dep.LangVersion != "" {
langVersion = dep.LangVersion
}
if dep.Version != "" {
packageSpecs = append(packageSpecs, dep.Name+m.Separator+dep.Version)
} else {
packageSpecs = append(packageSpecs, dep.Name)
}
}
args := append([]string{}, m.InstallCmd...)
args = append(args, packageSpecs...)
fullCmd := utils.BuildMiseCommandSimple(strings.Join(args, " "), language, langVersion)
return fullCmd + " && echo \"__INSTALL_SUCCESS__\" || echo \"__INSTALL_FAILED__\"", nil
}
func (m *BaseManager) GetReinstallAllCommand(deps []models.Dependency) (string, error) {
if len(deps) == 0 {
return "echo \"没有需要安装的依赖\"", nil
+105
View File
@@ -0,0 +1,105 @@
package deps
import (
"encoding/json"
"regexp"
"strings"
"github.com/engigu/baihu-panel/internal/models"
)
// ParseManifest 根据语言解析依赖清单文件内容
func ParseManifest(language, content string) ([]models.Dependency, error) {
lang := strings.ToLower(language)
if strings.Contains(lang, "python") {
return ParseRequirements(content), nil
}
if strings.Contains(lang, "node") {
return ParsePackageJson(content)
}
return []models.Dependency{}, nil
}
// ParseRequirements 解析 Python requirements.txt
func ParseRequirements(content string) []models.Dependency {
var deps []models.Dependency
// 使用正则表达式按行分割,兼容 Windows 和 Linux 的换行符
lines := regexp.MustCompile(`\r?\n`).Split(content, -1)
// 用于分割包名和版本的正则 (支持 ==, >=, <=, ~=, >, <, @)
versionRegex := regexp.MustCompile(`[=><~@]+`)
for _, line := range lines {
line = strings.TrimSpace(line)
// 忽略空行、注释行以及参数行 (以 - 开头的行如 -i, -r)
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "-") {
continue
}
// 分割名称与版本
parts := versionRegex.Split(line, 2)
name := strings.TrimSpace(parts[0])
version := ""
if len(parts) > 1 {
// 清除可能存在的后续参数,比如 requests==2.31.0 --hash=sha256:...
versionPart := strings.TrimSpace(parts[1])
// 如果有逗号分隔的多个范围限制,比如 >=1.20,<2.0,只取第一个范围作为参考版本号
if idx := strings.Index(versionPart, ","); idx != -1 {
versionPart = versionPart[:idx]
}
versionFields := strings.Fields(versionPart)
if len(versionFields) > 0 {
version = strings.TrimSpace(versionFields[0])
// 清除可能残留的首部版本符号
version = strings.TrimLeft(version, "=><~@ ")
}
}
if name != "" {
deps = append(deps, models.Dependency{
Name: name,
Version: version,
Language: "python3",
})
}
}
return deps
}
// PackageJson 代表 package.json 的结构定义
type PackageJson struct {
Dependencies map[string]string `json:"dependencies"`
DevDependencies map[string]string `json:"devDependencies"`
}
// ParsePackageJson 解析 Node.js package.json
func ParsePackageJson(content string) ([]models.Dependency, error) {
var pkg PackageJson
if err := json.Unmarshal([]byte(content), &pkg); err != nil {
return nil, err
}
var deps []models.Dependency
collect := func(m map[string]string, isDev bool) {
for name, versionRange := range m {
// 移除 npm 常见版本范围修饰符(如 ^1.2.3 或 ~2.3.0,保留底线版本号)
version := strings.TrimLeft(versionRange, "^~>=<* ")
remark := ""
if isDev {
remark = "devDependencies"
}
deps = append(deps, models.Dependency{
Name: name,
Version: version,
Language: "node",
Remark: remark,
})
}
}
collect(pkg.Dependencies, false)
collect(pkg.DevDependencies, true)
return deps, nil
}
+87
View File
@@ -0,0 +1,87 @@
package deps
import (
"testing"
)
func TestParseRequirements(t *testing.T) {
content := `
# This is a comment
requests==2.31.0
numpy>=1.20,<2.0
gunicorn
-r other-requirements.txt
pandas ~= 1.3.0
`
deps := ParseRequirements(content)
if len(deps) != 4 {
t.Fatalf("expected 4 dependencies, got %d", len(deps))
}
expected := []struct {
name string
version string
}{
{"requests", "2.31.0"},
{"numpy", "1.20"},
{"gunicorn", ""},
{"pandas", "1.3.0"},
}
for i, exp := range expected {
if deps[i].Name != exp.name {
t.Errorf("expected name %s, got %s", exp.name, deps[i].Name)
}
if deps[i].Version != exp.version {
t.Errorf("expected version %s, got %s", exp.version, deps[i].Version)
}
if deps[i].Language != "python3" {
t.Errorf("expected language python3, got %s", deps[i].Language)
}
}
}
func TestParsePackageJson(t *testing.T) {
content := `{
"dependencies": {
"lodash": "^4.17.21",
"express": "~4.18.2"
},
"devDependencies": {
"typescript": "^5.0.4"
}
}`
deps, err := ParsePackageJson(content)
if err != nil {
t.Fatalf("failed to parse package.json: %v", err)
}
if len(deps) != 3 {
t.Fatalf("expected 3 dependencies, got %d", len(deps))
}
expected := []struct {
name string
version string
remark string
}{
{"lodash", "4.17.21", ""},
{"express", "4.18.2", ""},
{"typescript", "5.0.4", "devDependencies"},
}
for i, exp := range expected {
if deps[i].Name != exp.name {
t.Errorf("expected name %s, got %s", exp.name, deps[i].Name)
}
if deps[i].Version != exp.version {
t.Errorf("expected version %s, got %s", exp.version, deps[i].Version)
}
if deps[i].Remark != exp.remark {
t.Errorf("expected remark %s, got %s", exp.remark, deps[i].Remark)
}
if deps[i].Language != "node" {
t.Errorf("expected language node, got %s", deps[i].Language)
}
}
}