2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 07:57:21 +08:00

refactor: 应用渠道机制

This commit is contained in:
耗子
2024-10-10 02:26:32 +08:00
parent 7e18d75203
commit 95f0e0d536
19 changed files with 266 additions and 150 deletions

View File

@@ -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"

View File

@@ -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)

View File

@@ -1,4 +1,4 @@
package percona
package mysql
type UpdateConfig struct {
Config string `form:"config" json:"config"`

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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"`
}

View File

@@ -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"`
}

View File

@@ -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')
}

View File

@@ -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"
}

View File

@@ -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": "操作"
}

View File

@@ -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>

View 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>

View File

@@ -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
}