mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 07:57:21 +08:00
refactor: 应用渠道机制
This commit is contained in:
@@ -6,8 +6,8 @@ import (
|
||||
_ "github.com/TheTNB/panel/internal/apps/fail2ban"
|
||||
_ "github.com/TheTNB/panel/internal/apps/frp"
|
||||
_ "github.com/TheTNB/panel/internal/apps/gitea"
|
||||
_ "github.com/TheTNB/panel/internal/apps/mysql"
|
||||
_ "github.com/TheTNB/panel/internal/apps/openresty"
|
||||
_ "github.com/TheTNB/panel/internal/apps/percona"
|
||||
_ "github.com/TheTNB/panel/internal/apps/php"
|
||||
_ "github.com/TheTNB/panel/internal/apps/phpmyadmin"
|
||||
_ "github.com/TheTNB/panel/internal/apps/podman"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package percona
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func init() {
|
||||
apploader.Register(&types.App{
|
||||
Slug: "percona",
|
||||
Slug: "mysql",
|
||||
Route: func(r chi.Router) {
|
||||
service := NewService()
|
||||
r.Get("/load", service.Load)
|
||||
@@ -1,4 +1,4 @@
|
||||
package percona
|
||||
package mysql
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config"`
|
||||
@@ -1,4 +1,4 @@
|
||||
package percona
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -33,7 +33,7 @@ func NewService() *Service {
|
||||
func (s *Service) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
config, err := io.Read(app.Root + "/server/mysql/conf/my.cnf")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Percona 配置失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -49,12 +49,12 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if err := io.Write(app.Root+"/server/mysql/conf/my.cnf", req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "写入 Percona 配置失败")
|
||||
service.Error(w, http.StatusInternalServerError, "写入配置失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("mysqld"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "重载 Percona 失败")
|
||||
service.Error(w, http.StatusInternalServerError, "重载失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -63,14 +63,14 @@ func (s *Service) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Load 获取负载
|
||||
func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Percona root密码失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取root密码失败")
|
||||
return
|
||||
|
||||
}
|
||||
if len(rootPassword) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "Percona root密码为空")
|
||||
service.Error(w, http.StatusUnprocessableEntity, "root密码为空")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *Service) Load(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
raw, err := shell.Execf(`mysqladmin -u root -p "%s" extended-status`, rootPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Percona 负载失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取负载失败")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,13 +180,13 @@ func (s *Service) ClearSlowLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GetRootPassword 获取root密码
|
||||
func (s *Service) GetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
rootPassword, err := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "获取 Percona root密码失败")
|
||||
service.Error(w, http.StatusInternalServerError, "获取root密码失败")
|
||||
return
|
||||
}
|
||||
if len(rootPassword) == 0 {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "Percona root密码为空")
|
||||
service.Error(w, http.StatusUnprocessableEntity, "root密码为空")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func (s *Service) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
oldRootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
mysql, err := db.NewMySQL("root", oldRootPassword, s.getSock(), "unix")
|
||||
if err != nil {
|
||||
// 尝试安全模式直接改密
|
||||
@@ -215,7 +215,7 @@ func (s *Service) SetRootPassword(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = s.settingRepo.Set(biz.SettingKeyPerconaRootPassword, req.Password); err != nil {
|
||||
if err = s.settingRepo.Set(biz.SettingKeyMySQLRootPassword, req.Password); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, fmt.Sprintf("设置保存失败: %v", err))
|
||||
return
|
||||
}
|
||||
@@ -7,14 +7,14 @@ import (
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Slug string `gorm:"not null;unique" json:"slug"`
|
||||
VersionSlug string `gorm:"not null" json:"version_slug"`
|
||||
Version string `gorm:"not null" json:"version"`
|
||||
Show bool `gorm:"not null" json:"show"`
|
||||
ShowOrder int `gorm:"not null" json:"show_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Slug string `gorm:"not null;unique" json:"slug"`
|
||||
Channel string `gorm:"not null" json:"channel"`
|
||||
Version string `gorm:"not null" json:"version"`
|
||||
Show bool `gorm:"not null" json:"show"`
|
||||
ShowOrder int `gorm:"not null" json:"show_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AppRepo interface {
|
||||
@@ -26,9 +26,9 @@ type AppRepo interface {
|
||||
GetInstalledAll(query string, cond ...string) ([]*App, error)
|
||||
GetHomeShow() ([]map[string]string, error)
|
||||
IsInstalled(query string, cond ...string) (bool, error)
|
||||
Install(slug, versionSlug string) error
|
||||
Uninstall(slug, versionSlug string) error
|
||||
Update(slug, versionSlug string) error
|
||||
Install(channel, slug string) error
|
||||
Uninstall(slug string) error
|
||||
Update(slug string) error
|
||||
UpdateShow(slug string, show bool) error
|
||||
UpdateCache() error
|
||||
}
|
||||
|
||||
@@ -10,17 +10,17 @@ import (
|
||||
type SettingKey string
|
||||
|
||||
const (
|
||||
SettingKeyName SettingKey = "name"
|
||||
SettingKeyVersion SettingKey = "version"
|
||||
SettingKeyMonitor SettingKey = "monitor"
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyPerconaRootPassword SettingKey = "percona_root_password"
|
||||
SettingKeySshHost SettingKey = "ssh_host"
|
||||
SettingKeySshPort SettingKey = "ssh_port"
|
||||
SettingKeySshUser SettingKey = "ssh_user"
|
||||
SettingKeySshPassword SettingKey = "ssh_password"
|
||||
SettingKeyName SettingKey = "name"
|
||||
SettingKeyVersion SettingKey = "version"
|
||||
SettingKeyMonitor SettingKey = "monitor"
|
||||
SettingKeyMonitorDays SettingKey = "monitor_days"
|
||||
SettingKeyBackupPath SettingKey = "backup_path"
|
||||
SettingKeyWebsitePath SettingKey = "website_path"
|
||||
SettingKeyMySQLRootPassword SettingKey = "mysql_root_password"
|
||||
SettingKeySshHost SettingKey = "ssh_host"
|
||||
SettingKeySshPort SettingKey = "ssh_port"
|
||||
SettingKeySshUser SettingKey = "ssh_user"
|
||||
SettingKeySshPassword SettingKey = "ssh_password"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
|
||||
@@ -62,9 +62,9 @@ func (r *appRepo) UpdateExist(slug string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
for v := range slices.Values(item.Versions) {
|
||||
if v.Slug == installed.VersionSlug {
|
||||
current := str.FirstElement(v.Subs)
|
||||
for channel := range slices.Values(item.Channels) {
|
||||
if channel.Slug == installed.Channel {
|
||||
current := str.FirstElement(channel.Subs)
|
||||
if current != nil && current.Version != installed.Version {
|
||||
return true
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func (r *appRepo) IsInstalled(query string, cond ...string) (bool, error) {
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *appRepo) Install(slug, versionSlug string) error {
|
||||
func (r *appRepo) Install(channel, slug string) error {
|
||||
item, err := r.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -155,17 +155,19 @@ func (r *appRepo) Install(slug, versionSlug string) error {
|
||||
return errors.New("应用已安装")
|
||||
}
|
||||
|
||||
var shellUrl string
|
||||
for v := range slices.Values(item.Versions) {
|
||||
vs, err := version.NewVersion(v.Panel)
|
||||
shellUrl, shellChannel, shellVersion := "", "", ""
|
||||
for ch := range slices.Values(item.Channels) {
|
||||
vs, err := version.NewVersion(ch.Panel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v.Slug == versionSlug {
|
||||
if ch.Slug == channel {
|
||||
if vs.GreaterThan(panel) {
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, v.Panel, app.Version)
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
|
||||
}
|
||||
shellUrl = v.Install
|
||||
shellUrl = ch.Install
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -180,7 +182,7 @@ func (r *appRepo) Install(slug, versionSlug string) error {
|
||||
task := new(biz.Task)
|
||||
task.Name = "安装应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, item.Slug)
|
||||
task.Shell = fmt.Sprintf(`curl -s "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
@@ -189,7 +191,7 @@ func (r *appRepo) Install(slug, versionSlug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *appRepo) Uninstall(slug, versionSlug string) error {
|
||||
func (r *appRepo) Uninstall(slug string) error {
|
||||
item, err := r.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -202,18 +204,24 @@ func (r *appRepo) Uninstall(slug, versionSlug string) error {
|
||||
if installed, _ := r.IsInstalled(slug); !installed {
|
||||
return errors.New("应用未安装")
|
||||
}
|
||||
installed, err := r.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var shellUrl string
|
||||
for v := range slices.Values(item.Versions) {
|
||||
vs, err := version.NewVersion(v.Panel)
|
||||
shellUrl, shellChannel, shellVersion := "", "", ""
|
||||
for ch := range slices.Values(item.Channels) {
|
||||
vs, err := version.NewVersion(ch.Panel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v.Slug == versionSlug {
|
||||
if ch.Slug == installed.Channel {
|
||||
if vs.GreaterThan(panel) {
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, v.Panel, app.Version)
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
|
||||
}
|
||||
shellUrl = v.Uninstall
|
||||
shellUrl = ch.Uninstall
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = installed.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -228,7 +236,7 @@ func (r *appRepo) Uninstall(slug, versionSlug string) error {
|
||||
task := new(biz.Task)
|
||||
task.Name = "卸载应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, item.Slug)
|
||||
task.Shell = fmt.Sprintf(`curl -s "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
@@ -237,7 +245,7 @@ func (r *appRepo) Uninstall(slug, versionSlug string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *appRepo) Update(slug, versionSlug string) error {
|
||||
func (r *appRepo) Update(slug string) error {
|
||||
item, err := r.Get(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -250,18 +258,24 @@ func (r *appRepo) Update(slug, versionSlug string) error {
|
||||
if installed, _ := r.IsInstalled(slug); !installed {
|
||||
return errors.New("应用未安装")
|
||||
}
|
||||
installed, err := r.GetInstalled(slug)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var shellUrl string
|
||||
for v := range slices.Values(item.Versions) {
|
||||
vs, err := version.NewVersion(v.Panel)
|
||||
shellUrl, shellChannel, shellVersion := "", "", ""
|
||||
for ch := range slices.Values(item.Channels) {
|
||||
vs, err := version.NewVersion(ch.Panel)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v.Slug == versionSlug {
|
||||
if ch.Slug == installed.Channel {
|
||||
if vs.GreaterThan(panel) {
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, v.Panel, app.Version)
|
||||
return fmt.Errorf("应用 %s 需要面板版本 %s,当前版本 %s", item.Name, ch.Panel, app.Version)
|
||||
}
|
||||
shellUrl = v.Update
|
||||
shellUrl = ch.Update
|
||||
shellChannel = ch.Slug
|
||||
shellVersion = str.FirstElement(ch.Subs).Version
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -276,7 +290,7 @@ func (r *appRepo) Update(slug, versionSlug string) error {
|
||||
task := new(biz.Task)
|
||||
task.Name = "更新应用 " + item.Name
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = fmt.Sprintf("%s >> /tmp/%s.log 2>&1", shellUrl, item.Slug)
|
||||
task.Shell = fmt.Sprintf(`curl -s "%s" | bash -s -- "%s" "%s" >> /tmp/%s.log 2>&1`, shellUrl, shellChannel, shellVersion, item.Slug)
|
||||
task.Log = "/tmp/" + item.Slug + ".log"
|
||||
if err = r.taskRepo.Push(task); err != nil {
|
||||
return err
|
||||
|
||||
@@ -332,7 +332,7 @@ server
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err == nil && req.DB && req.DBType == "mysql" {
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock", "unix")
|
||||
if err != nil {
|
||||
@@ -610,7 +610,7 @@ func (r *websiteRepo) Delete(req *request.WebsiteDelete) error {
|
||||
_ = io.Remove(website.Path)
|
||||
}
|
||||
if req.DB {
|
||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
rootPassword, err := r.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package request
|
||||
|
||||
type App struct {
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
VersionSlug string `json:"version_slug" form:"version_slug"`
|
||||
Slug string `json:"slug" form:"slug"`
|
||||
Channel string `json:"channel" form:"channel"`
|
||||
}
|
||||
|
||||
type AppSlug struct {
|
||||
|
||||
@@ -36,24 +36,35 @@ func (s *AppService) List(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var apps []types.StoreApp
|
||||
for _, item := range all {
|
||||
installed, installedVersion, installedVersionSlug, updateExist, show := false, "", "", false, false
|
||||
installed, installedChannel, installedVersion, updateExist, show := false, "", "", false, false
|
||||
if _, ok := installedAppMap[item.Slug]; ok {
|
||||
installed = true
|
||||
installedChannel = installedAppMap[item.Slug].Channel
|
||||
installedVersion = installedAppMap[item.Slug].Version
|
||||
installedVersionSlug = installedAppMap[item.Slug].VersionSlug
|
||||
updateExist = s.appRepo.UpdateExist(item.Slug)
|
||||
show = installedAppMap[item.Slug].Show
|
||||
}
|
||||
apps = append(apps, types.StoreApp{
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
Versions: item.Versions,
|
||||
Installed: installed,
|
||||
InstalledVersion: installedVersion,
|
||||
InstalledVersionSlug: installedVersionSlug,
|
||||
UpdateExist: updateExist,
|
||||
Show: show,
|
||||
Name: item.Name,
|
||||
Description: item.Description,
|
||||
Slug: item.Slug,
|
||||
Channels: []struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Panel string `json:"panel"`
|
||||
Install string `json:"-"`
|
||||
Uninstall string `json:"-"`
|
||||
Update string `json:"-"`
|
||||
Subs []struct {
|
||||
Log string `json:"log"`
|
||||
Version string `json:"version"`
|
||||
} `json:"subs"`
|
||||
}(item.Channels),
|
||||
Installed: installed,
|
||||
InstalledChannel: installedChannel,
|
||||
InstalledVersion: installedVersion,
|
||||
UpdateExist: updateExist,
|
||||
Show: show,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +83,7 @@ func (s *AppService) Install(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.appRepo.Install(req.Slug, req.VersionSlug); err != nil {
|
||||
if err = s.appRepo.Install(req.Channel, req.Slug); err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -81,13 +92,13 @@ func (s *AppService) Install(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *AppService) Uninstall(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.App](r)
|
||||
req, err := Bind[request.AppSlug](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.appRepo.Uninstall(req.Slug, req.VersionSlug); err != nil {
|
||||
if err = s.appRepo.Uninstall(req.Slug); err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
@@ -96,13 +107,13 @@ func (s *AppService) Uninstall(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *AppService) Update(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := Bind[request.App](r)
|
||||
req, err := Bind[request.AppSlug](r)
|
||||
if err != nil {
|
||||
Error(w, http.StatusUnprocessableEntity, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.appRepo.Update(req.Slug, req.VersionSlug); err != nil {
|
||||
if err = s.appRepo.Update(req.Slug); err != nil {
|
||||
Error(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ func (s *InfoService) CountInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var databaseCount int64
|
||||
if mysqlInstalled {
|
||||
rootPassword, _ := s.settingRepo.Get(biz.SettingKeyPerconaRootPassword)
|
||||
rootPassword, _ := s.settingRepo.Get(biz.SettingKeyMySQLRootPassword)
|
||||
mysql, err := db.NewMySQL("root", rootPassword, "/tmp/mysql.sock")
|
||||
if err == nil {
|
||||
defer mysql.Close()
|
||||
|
||||
@@ -14,7 +14,7 @@ type App struct {
|
||||
Description string `json:"description"`
|
||||
Categories []string `json:"categories"`
|
||||
Depends string `json:"depends"`
|
||||
Versions []struct {
|
||||
Channels []struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Panel string `json:"panel"`
|
||||
@@ -24,8 +24,8 @@ type App struct {
|
||||
Subs []struct {
|
||||
Log string `json:"log"`
|
||||
Version string `json:"version"`
|
||||
} `json:"versions"`
|
||||
} `json:"versions"`
|
||||
} `json:"subs"`
|
||||
} `json:"channels"`
|
||||
Order int `json:"order"`
|
||||
}
|
||||
|
||||
|
||||
@@ -13,21 +13,21 @@ type StoreApp struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Slug string `json:"slug"`
|
||||
Versions []struct {
|
||||
Channels []struct {
|
||||
Slug string `json:"slug"`
|
||||
Name string `json:"name"`
|
||||
Panel string `json:"panel"`
|
||||
Install string `json:"install"`
|
||||
Uninstall string `json:"uninstall"`
|
||||
Update string `json:"update"`
|
||||
Install string `json:"-"`
|
||||
Uninstall string `json:"-"`
|
||||
Update string `json:"-"`
|
||||
Subs []struct {
|
||||
Log string `json:"log"`
|
||||
Version string `json:"version"`
|
||||
} `json:"versions"`
|
||||
} `json:"versions"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
InstalledVersionSlug string `json:"installed_version_slug"`
|
||||
UpdateExist bool `json:"update_exist"`
|
||||
Show bool `json:"show"`
|
||||
} `json:"subs"`
|
||||
} `json:"channels"`
|
||||
Installed bool `json:"installed"`
|
||||
InstalledChannel string `json:"installed_channel"`
|
||||
InstalledVersion string `json:"installed_version"`
|
||||
UpdateExist bool `json:"update_exist"`
|
||||
Show bool `json:"show"`
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ export default {
|
||||
list: (page: number, limit: number): Promise<AxiosResponse<any>> =>
|
||||
request.get('/app/list', { params: { page, limit } }),
|
||||
// 安装应用
|
||||
install: (slug: string): Promise<AxiosResponse<any>> => request.post('/app/install', { slug }),
|
||||
install: (slug: string, channel: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/app/install', { slug, channel }),
|
||||
// 卸载应用
|
||||
uninstall: (slug: string): Promise<AxiosResponse<any>> =>
|
||||
request.post('/app/uninstall', { slug }),
|
||||
@@ -18,5 +19,7 @@ export default {
|
||||
request.post('/app/updateShow', { slug, show }),
|
||||
// 应用是否已安装
|
||||
isInstalled: (slug: string): Promise<AxiosResponse<any>> =>
|
||||
request.get('/app/isInstalled', { params: { slug } })
|
||||
request.get('/app/isInstalled', { params: { slug } }),
|
||||
// 更新缓存
|
||||
updateCache: (): Promise<AxiosResponse<any>> => request.get('/app/updateCache')
|
||||
}
|
||||
|
||||
@@ -130,29 +130,28 @@
|
||||
"appIndex": {
|
||||
"title": "Apps",
|
||||
"alerts": {
|
||||
"info": "Click the button once, please do not click it repeatedly to avoid repeated execution!",
|
||||
"warning": "It is strongly recommended to take a backup/snapshot before upgrading the plug-in to avoid being unable to roll back if problems arise!",
|
||||
"cache": "Cache updated successfully",
|
||||
"warning": "It is strongly recommended to take a backup/snapshot before upgrading the app to avoid being unable to roll back if problems arise!",
|
||||
"setup": "Setup successful",
|
||||
"install": "The task has been submitted, please check the task progress later",
|
||||
"update": "The task has been submitted, please go to the task center to check the task progress",
|
||||
"uninstall": "The task has been submitted, please go to the task center to check the task progress"
|
||||
},
|
||||
"buttons": {
|
||||
"updateCache": "Update cache",
|
||||
"install": "Install",
|
||||
"manage": "Manage",
|
||||
"update": "Upgrade",
|
||||
"uninstall": "Uninstall"
|
||||
},
|
||||
"confirm": {
|
||||
"install": "Are you sure you want to install the app {app}?",
|
||||
"update": "Upgrading the {app} plug-in may reset related configurations to the default state. Are you sure you want to continue?",
|
||||
"update": "Upgrading the {app} app may reset related configurations to the default state. Are you sure you want to continue?",
|
||||
"uninstall": "Are you sure you want to uninstall the app {app}?"
|
||||
},
|
||||
"columns": {
|
||||
"name": "Name",
|
||||
"description": "Description",
|
||||
"installedVersion": "Installed Version",
|
||||
"version": "Latest Version",
|
||||
"show": "Homepage Display",
|
||||
"actions": "Actions"
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
"appIndex": {
|
||||
"title": "应用市场",
|
||||
"alerts": {
|
||||
"info": "按钮点击一次即可,请勿重复点击以免重复执行!",
|
||||
"cache": "缓存更新成功",
|
||||
"warning": "升级应用前强烈建议先备份/快照,以免出现问题时无法回滚!",
|
||||
"setup": "设置成功",
|
||||
"install": "任务已提交,请稍后查看任务进度",
|
||||
@@ -138,13 +138,13 @@
|
||||
"uninstall": "任务已提交,请前往任务中心查看任务进度"
|
||||
},
|
||||
"buttons": {
|
||||
"updateCache": "更新缓存",
|
||||
"install": "安装",
|
||||
"manage": "管理",
|
||||
"update": "升级",
|
||||
"uninstall": "卸载"
|
||||
},
|
||||
"confirm": {
|
||||
"install": "确定安装应用 {app} 吗?",
|
||||
"update": "升级 {app} 应用可能会重置相关配置到默认状态,确定继续吗?",
|
||||
"uninstall": "确定卸载应用 {app} 吗?"
|
||||
},
|
||||
@@ -152,7 +152,6 @@
|
||||
"name": "应用名",
|
||||
"description": "描述",
|
||||
"installedVersion": "已装版本",
|
||||
"version": "最新版本",
|
||||
"show": "首页显示",
|
||||
"actions": "操作"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import VersionModal from '@/views/app/VersionModal.vue'
|
||||
|
||||
import { NButton, NDataTable, NPopconfirm, NSwitch } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
@@ -9,6 +11,10 @@ import app from '../../api/panel/app'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const versionModalShow = ref(false)
|
||||
const versionModalOperation = ref('安装')
|
||||
const versionModalInfo = ref<App>({} as App)
|
||||
|
||||
const columns: any = [
|
||||
{ type: 'selection', fixed: 'left' },
|
||||
{
|
||||
@@ -30,12 +36,6 @@ const columns: any = [
|
||||
width: 100,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: t('appIndex.columns.version'),
|
||||
key: 'version',
|
||||
width: 100,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: t('appIndex.columns.show'),
|
||||
key: 'show',
|
||||
@@ -59,7 +59,7 @@ const columns: any = [
|
||||
hideInExcel: true,
|
||||
render(row: any) {
|
||||
return [
|
||||
row.installed && row.installed_version != row.version
|
||||
row.installed && row.update_exist
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
@@ -87,7 +87,7 @@ const columns: any = [
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed && row.installed_version == row.version
|
||||
row.installed
|
||||
? h(
|
||||
NButton,
|
||||
{
|
||||
@@ -101,7 +101,7 @@ const columns: any = [
|
||||
}
|
||||
)
|
||||
: null,
|
||||
row.installed && row.installed_version == row.version
|
||||
row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
{
|
||||
@@ -130,27 +130,19 @@ const columns: any = [
|
||||
: null,
|
||||
!row.installed
|
||||
? h(
|
||||
NPopconfirm,
|
||||
NButton,
|
||||
{
|
||||
onPositiveClick: () => handleInstall(row.slug)
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => {
|
||||
versionModalShow.value = true
|
||||
versionModalOperation.value = '安装'
|
||||
versionModalInfo.value = row
|
||||
}
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return t('appIndex.confirm.install', { app: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info'
|
||||
},
|
||||
{
|
||||
default: () => t('appIndex.buttons.install'),
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
}
|
||||
default: () => t('appIndex.buttons.install'),
|
||||
icon: renderIcon('material-symbols:download-rounded', { size: 14 })
|
||||
}
|
||||
)
|
||||
: null
|
||||
@@ -180,12 +172,6 @@ const handleShowChange = (row: any) => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleInstall = (slug: string) => {
|
||||
app.install(slug).then(() => {
|
||||
window.$message.success(t('appIndex.alerts.install'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdate = (slug: string) => {
|
||||
app.update(slug).then(() => {
|
||||
window.$message.success(t('appIndex.alerts.update'))
|
||||
@@ -202,6 +188,12 @@ const handleManage = (slug: string) => {
|
||||
router.push({ name: 'apps-' + slug + '-index' })
|
||||
}
|
||||
|
||||
const handleUpdateCache = () => {
|
||||
app.updateCache().then(() => {
|
||||
window.$message.success(t('appIndex.alerts.cache'))
|
||||
})
|
||||
}
|
||||
|
||||
const getAppList = async (page: number, limit: number) => {
|
||||
const { data } = await app.list(page, limit)
|
||||
return data
|
||||
@@ -232,8 +224,15 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-space vertical>
|
||||
<n-alert type="info">{{ $t('appIndex.alerts.info') }}</n-alert>
|
||||
<template #action>
|
||||
<div flex items-center>
|
||||
<n-button class="ml-16" type="primary" @click="handleUpdateCache">
|
||||
<TheIcon :size="18" class="mr-5" icon="material-symbols:refresh" />
|
||||
{{ $t('appIndex.buttons.updateCache') }}
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">{{ $t('appIndex.alerts.warning') }}</n-alert>
|
||||
<n-data-table
|
||||
striped
|
||||
@@ -247,6 +246,11 @@ onMounted(() => {
|
||||
@update:page="onPageChange"
|
||||
@update:page-size="onPageSizeChange"
|
||||
/>
|
||||
</n-space>
|
||||
<version-modal
|
||||
v-model:show="versionModalShow"
|
||||
v-model:operation="versionModalOperation"
|
||||
v-model:info="versionModalInfo"
|
||||
/>
|
||||
</n-flex>
|
||||
</common-page>
|
||||
</template>
|
||||
|
||||
74
web/src/views/app/VersionModal.vue
Normal file
74
web/src/views/app/VersionModal.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup lang="ts">
|
||||
import type { App } from '@/views/app/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import app from '../../api/panel/app'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const operation = defineModel<string>('operation', { type: String, required: true })
|
||||
const info = defineModel<App>('info', { type: Object, required: true })
|
||||
|
||||
const doSubmit = ref(false)
|
||||
|
||||
const model = reactive({
|
||||
channel: '',
|
||||
version: ''
|
||||
})
|
||||
|
||||
const options = computed(() => {
|
||||
return info.value.channels.map((channel) => {
|
||||
return {
|
||||
label: channel.name,
|
||||
value: channel.slug
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
app.install(info.value.slug, model.channel).then(() => {
|
||||
window.$message.success(t('appIndex.alerts.install'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleChannelUpdate = (value: string) => {
|
||||
const channel = info.value.channels.find((channel) => channel.slug === value)
|
||||
if (channel) {
|
||||
model.version = channel.subs[0].version
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="operation + ' ' + info.name"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-form :model="model">
|
||||
<n-form-item path="channel" label="渠道">
|
||||
<n-select
|
||||
v-model:value="model.channel"
|
||||
:options="options"
|
||||
@update-value="handleChannelUpdate"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="channel" label="版本号">
|
||||
<n-input v-model:value="model.version" placeholder="请选择渠道" readonly disabled />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-row :gutter="[0, 24]">
|
||||
<n-col :span="24">
|
||||
<n-button type="info" block :loading="doSubmit" :disabled="doSubmit" @click="handleSubmit">
|
||||
提交
|
||||
</n-button>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
@@ -2,10 +2,22 @@ export interface App {
|
||||
name: string
|
||||
description: string
|
||||
slug: string
|
||||
version: string
|
||||
requires: string
|
||||
excludes: string
|
||||
channels: Channel[]
|
||||
installed: boolean
|
||||
installed_channel: string
|
||||
installed_version: string
|
||||
update_exist: boolean
|
||||
show: boolean
|
||||
}
|
||||
|
||||
export interface Channel {
|
||||
slug: string
|
||||
name: string
|
||||
panel: string
|
||||
subs: Sub[]
|
||||
}
|
||||
|
||||
export interface Sub {
|
||||
log: string
|
||||
version: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user