feat: 增加创建 php 运行环境功能
This commit is contained in:

committed by
zhengkunwang223

parent
1949be2490
commit
64a954df53
@@ -76,9 +76,10 @@ func (b *BaseApi) GetApp(c *gin.Context) {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param appId path integer true "app id"
|
// @Param appId path integer true "app id"
|
||||||
// @Param version path string true "app 版本"
|
// @Param version path string true "app 版本"
|
||||||
|
// @Param version path string true "app 类型"
|
||||||
// @Success 200 {object} response.AppDetailDTO
|
// @Success 200 {object} response.AppDetailDTO
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Router /apps/detail/:appId/:version [get]
|
// @Router /apps/detail/:appId/:version/:type [get]
|
||||||
func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
||||||
appId, err := helper.GetIntParamByKey(c, "appId")
|
appId, err := helper.GetIntParamByKey(c, "appId")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -86,7 +87,8 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
version := c.Param("version")
|
version := c.Param("version")
|
||||||
appDetailDTO, err := appService.GetAppDetail(appId, version)
|
appType := c.Param("type")
|
||||||
|
appDetailDTO, err := appService.GetAppDetail(appId, version, appType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
return
|
return
|
||||||
|
@@ -32,3 +32,25 @@ func (b *BaseApi) SearchRuntimes(c *gin.Context) {
|
|||||||
Items: items,
|
Items: items,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Runtime
|
||||||
|
// @Summary Create runtime
|
||||||
|
// @Description 创建运行环境
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.RuntimeCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Router /runtimes [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFuntions":[],"formatZH":"创建运行环境 [name]","formatEN":"Create runtime [name]"}
|
||||||
|
func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
||||||
|
var req request.RuntimeCreate
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := runtimeService.Create(req); err != nil {
|
||||||
|
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, nil)
|
||||||
|
}
|
||||||
|
@@ -7,3 +7,13 @@ type RuntimeSearch struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RuntimeCreate struct {
|
||||||
|
AppDetailID uint `json:"appDetailId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Params map[string]interface{} `json:"params"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Image string `json:"image"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
@@ -4,10 +4,14 @@ type Runtime struct {
|
|||||||
BaseModel
|
BaseModel
|
||||||
Name string `gorm:"type:varchar;not null" json:"name"`
|
Name string `gorm:"type:varchar;not null" json:"name"`
|
||||||
AppDetailID uint `gorm:"type:integer" json:"appDetailId"`
|
AppDetailID uint `gorm:"type:integer" json:"appDetailId"`
|
||||||
Image string `gorm:"type:varchar;not null" json:"image"`
|
Image string `gorm:"type:varchar" json:"image"`
|
||||||
WorkDir string `gorm:"type:varchar;not null" json:"workDir"`
|
WorkDir string `gorm:"type:varchar" json:"workDir"`
|
||||||
DockerCompose string `gorm:"type:varchar;not null" json:"dockerCompose"`
|
DockerCompose string `gorm:"type:varchar" json:"dockerCompose"`
|
||||||
Env string `gorm:"type:varchar;not null" json:"env"`
|
Env string `gorm:"type:varchar" json:"env"`
|
||||||
Params string `gorm:"type:varchar;not null" json:"params"`
|
Params string `gorm:"type:varchar" json:"params"`
|
||||||
|
Version string `gorm:"type:varchar;not null" json:"version"`
|
||||||
Type string `gorm:"type:varchar;not null" json:"type"`
|
Type string `gorm:"type:varchar;not null" json:"type"`
|
||||||
|
Status string `gorm:"type:varchar;not null" json:"status"`
|
||||||
|
Resource string `gorm:"type:varchar;not null" json:"resource"`
|
||||||
|
Message string `gorm:"type:longtext;" json:"message"`
|
||||||
}
|
}
|
||||||
|
@@ -58,7 +58,7 @@ func (a AppRepo) Page(page, size int, opts ...DBOption) (int64, []model.App, err
|
|||||||
db := getDb(opts...).Model(&model.App{})
|
db := getDb(opts...).Model(&model.App{})
|
||||||
count := int64(0)
|
count := int64(0)
|
||||||
db = db.Count(&count)
|
db = db.Count(&count)
|
||||||
err := db.Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error
|
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("AppTags").Find(&apps).Error
|
||||||
return count, apps, err
|
return count, apps, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@ type IAppService interface {
|
|||||||
PageApp(req request.AppSearch) (interface{}, error)
|
PageApp(req request.AppSearch) (interface{}, error)
|
||||||
GetAppTags() ([]response.TagDTO, error)
|
GetAppTags() ([]response.TagDTO, error)
|
||||||
GetApp(key string) (*response.AppDTO, error)
|
GetApp(key string) (*response.AppDTO, error)
|
||||||
GetAppDetail(appId uint, version string) (response.AppDetailDTO, error)
|
GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error)
|
||||||
Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error)
|
Install(ctx context.Context, req request.AppInstallCreate) (*model.AppInstall, error)
|
||||||
SyncAppList() error
|
SyncAppList() error
|
||||||
GetAppUpdate() (*response.AppUpdateRes, error)
|
GetAppUpdate() (*response.AppUpdateRes, error)
|
||||||
@@ -138,7 +138,7 @@ func (a AppService) GetApp(key string) (*response.AppDTO, error) {
|
|||||||
return &appDTO, nil
|
return &appDTO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AppService) GetAppDetail(appId uint, version string) (response.AppDetailDTO, error) {
|
func (a AppService) GetAppDetail(appId uint, version, appType string) (response.AppDetailDTO, error) {
|
||||||
var (
|
var (
|
||||||
appDetailDTO response.AppDetailDTO
|
appDetailDTO response.AppDetailDTO
|
||||||
opts []repo.DBOption
|
opts []repo.DBOption
|
||||||
@@ -148,13 +148,35 @@ func (a AppService) GetAppDetail(appId uint, version string) (response.AppDetail
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
|
appDetailDTO.AppDetail = detail
|
||||||
|
appDetailDTO.Enable = true
|
||||||
|
|
||||||
|
if appType == "runtime" {
|
||||||
|
app, err := appRepo.GetFirst(commonRepo.WithByID(appId))
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
paramsPath := path.Join(constant.AppResourceDir, app.Key, "versions", detail.Version, "build", "config.json")
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if !fileOp.Stat(paramsPath) {
|
||||||
|
return appDetailDTO, buserr.New(constant.ErrFileNotExist)
|
||||||
|
}
|
||||||
|
param, err := fileOp.GetContent(paramsPath)
|
||||||
|
if err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
paramMap := make(map[string]interface{})
|
||||||
|
if err := json.Unmarshal(param, ¶mMap); err != nil {
|
||||||
|
return appDetailDTO, err
|
||||||
|
}
|
||||||
|
appDetailDTO.Params = paramMap
|
||||||
|
} else {
|
||||||
paramMap := make(map[string]interface{})
|
paramMap := make(map[string]interface{})
|
||||||
if err := json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
if err := json.Unmarshal([]byte(detail.Params), ¶mMap); err != nil {
|
||||||
return appDetailDTO, err
|
return appDetailDTO, err
|
||||||
}
|
}
|
||||||
appDetailDTO.AppDetail = detail
|
|
||||||
appDetailDTO.Params = paramMap
|
appDetailDTO.Params = paramMap
|
||||||
appDetailDTO.Enable = true
|
}
|
||||||
|
|
||||||
app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId))
|
app, err := appRepo.GetFirst(commonRepo.WithByID(detail.AppId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@@ -1,9 +1,17 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
"github.com/1Panel-dev/1Panel/backend/app/dto/request"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
|
"github.com/1Panel-dev/1Panel/backend/app/dto/response"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/backend/app/repo"
|
"github.com/1Panel-dev/1Panel/backend/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/backend/utils/files"
|
||||||
|
"github.com/subosito/gotenv"
|
||||||
|
"path"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RuntimeService struct {
|
type RuntimeService struct {
|
||||||
@@ -11,14 +19,96 @@ type RuntimeService struct {
|
|||||||
|
|
||||||
type IRuntimeService interface {
|
type IRuntimeService interface {
|
||||||
Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error)
|
Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error)
|
||||||
|
Create(create request.RuntimeCreate) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRuntimeService() IRuntimeService {
|
func NewRuntimeService() IRuntimeService {
|
||||||
return &RuntimeService{}
|
return &RuntimeService{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuntimeService) Create() {
|
func (r *RuntimeService) Create(create request.RuntimeCreate) error {
|
||||||
|
if create.Resource == constant.ResourceLocal {
|
||||||
|
runtime := &model.Runtime{
|
||||||
|
Name: create.Name,
|
||||||
|
Resource: create.Resource,
|
||||||
|
Type: create.Type,
|
||||||
|
Status: constant.RuntimeNormal,
|
||||||
|
}
|
||||||
|
return runtimeRepo.Create(context.Background(), runtime)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
buildDir := path.Join(constant.AppResourceDir, app.Key, "versions", appDetail.Version, "build")
|
||||||
|
if !fileOp.Stat(buildDir) {
|
||||||
|
return buserr.New(constant.ErrDirNotFound)
|
||||||
|
}
|
||||||
|
tempDir := path.Join(constant.RuntimeDir, app.Key)
|
||||||
|
if err := fileOp.CopyDir(buildDir, tempDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oldDir := path.Join(tempDir, "build")
|
||||||
|
newNameDir := path.Join(tempDir, create.Name)
|
||||||
|
defer func(defErr *error) {
|
||||||
|
if defErr != nil {
|
||||||
|
_ = fileOp.DeleteDir(newNameDir)
|
||||||
|
}
|
||||||
|
}(&err)
|
||||||
|
if oldDir != newNameDir {
|
||||||
|
if err := fileOp.Rename(oldDir, newNameDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
composeFile, err := fileOp.GetContent(path.Join(newNameDir, "docker-compose.yml"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
env, err := gotenv.Read(path.Join(newNameDir, ".env"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newMap := make(map[string]string)
|
||||||
|
handleMap(create.Params, newMap)
|
||||||
|
for k, v := range newMap {
|
||||||
|
env[k] = v
|
||||||
|
}
|
||||||
|
envStr, err := gotenv.Marshal(env)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := gotenv.Write(env, path.Join(newNameDir, ".env")); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
project, err := docker.GetComposeProject(create.Name, newNameDir, composeFile, []byte(envStr))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
composeService, err := docker.NewComposeService()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
composeService.SetProject(project)
|
||||||
|
if err := composeService.ComposeBuild(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
runtime := &model.Runtime{
|
||||||
|
Name: create.Name,
|
||||||
|
DockerCompose: string(composeFile),
|
||||||
|
Env: envStr,
|
||||||
|
AppDetailID: create.AppDetailID,
|
||||||
|
Type: create.Type,
|
||||||
|
Image: create.Image,
|
||||||
|
Resource: create.Resource,
|
||||||
|
Status: constant.RuntimeNormal,
|
||||||
|
}
|
||||||
|
return runtimeRepo.Create(context.Background(), runtime)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) {
|
func (r *RuntimeService) Page(req request.RuntimeSearch) (int64, []response.RuntimeRes, error) {
|
||||||
|
1
backend/app/service/runtime_utils.go
Normal file
1
backend/app/service/runtime_utils.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package service
|
@@ -62,7 +62,7 @@ var (
|
|||||||
Err1PanelNetworkFailed = "Err1PanelNetworkFailed"
|
Err1PanelNetworkFailed = "Err1PanelNetworkFailed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//website
|
// website
|
||||||
var (
|
var (
|
||||||
ErrDomainIsExist = "ErrDomainIsExist"
|
ErrDomainIsExist = "ErrDomainIsExist"
|
||||||
ErrAliasIsExist = "ErrAliasIsExist"
|
ErrAliasIsExist = "ErrAliasIsExist"
|
||||||
@@ -70,7 +70,7 @@ var (
|
|||||||
ErrGroupIsUsed = "ErrGroupIsUsed"
|
ErrGroupIsUsed = "ErrGroupIsUsed"
|
||||||
)
|
)
|
||||||
|
|
||||||
//ssl
|
// ssl
|
||||||
var (
|
var (
|
||||||
ErrSSLCannotDelete = "ErrSSLCannotDelete"
|
ErrSSLCannotDelete = "ErrSSLCannotDelete"
|
||||||
ErrAccountCannotDelete = "ErrAccountCannotDelete"
|
ErrAccountCannotDelete = "ErrAccountCannotDelete"
|
||||||
@@ -78,7 +78,7 @@ var (
|
|||||||
ErrEmailIsExist = "ErrEmailIsExist"
|
ErrEmailIsExist = "ErrEmailIsExist"
|
||||||
)
|
)
|
||||||
|
|
||||||
//file
|
// file
|
||||||
var (
|
var (
|
||||||
ErrPathNotFound = "ErrPathNotFound"
|
ErrPathNotFound = "ErrPathNotFound"
|
||||||
ErrMovePathFailed = "ErrMovePathFailed"
|
ErrMovePathFailed = "ErrMovePathFailed"
|
||||||
@@ -87,19 +87,26 @@ var (
|
|||||||
ErrFileUpload = "ErrFileUpload"
|
ErrFileUpload = "ErrFileUpload"
|
||||||
)
|
)
|
||||||
|
|
||||||
//mysql
|
// mysql
|
||||||
var (
|
var (
|
||||||
ErrUserIsExist = "ErrUserIsExist"
|
ErrUserIsExist = "ErrUserIsExist"
|
||||||
ErrDatabaseIsExist = "ErrDatabaseIsExist"
|
ErrDatabaseIsExist = "ErrDatabaseIsExist"
|
||||||
)
|
)
|
||||||
|
|
||||||
//redis
|
// redis
|
||||||
var (
|
var (
|
||||||
ErrTypeOfRedis = "ErrTypeOfRedis"
|
ErrTypeOfRedis = "ErrTypeOfRedis"
|
||||||
)
|
)
|
||||||
|
|
||||||
//container
|
// container
|
||||||
var (
|
var (
|
||||||
ErrInUsed = "ErrInUsed"
|
ErrInUsed = "ErrInUsed"
|
||||||
ErrObjectInUsed = "ErrObjectInUsed"
|
ErrObjectInUsed = "ErrObjectInUsed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//runtime
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrDirNotFound = "ErrDirNotFound"
|
||||||
|
ErrFileNotExist = "ErrFileNotExist"
|
||||||
|
)
|
||||||
|
10
backend/constant/runtime.go
Normal file
10
backend/constant/runtime.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const (
|
||||||
|
ResourceLocal = "Local"
|
||||||
|
ResourceAppstore = "Appstore"
|
||||||
|
|
||||||
|
RuntimeNormal = "Normal"
|
||||||
|
RuntimeBuildSuccess = "BuildSuccess"
|
||||||
|
RuntimeBuildFailed = "BuildFailed"
|
||||||
|
)
|
@@ -59,3 +59,7 @@ ErrTypeOfRedis: "The recovery file type does not match the current persistence m
|
|||||||
#container
|
#container
|
||||||
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
|
ErrInUsed: "{{ .detail }} is in use and cannot be deleted"
|
||||||
ErrObjectInUsed: "This object is in use and cannot be deleted"
|
ErrObjectInUsed: "This object is in use and cannot be deleted"
|
||||||
|
|
||||||
|
#runtime
|
||||||
|
ErrDirNotFound: "The build folder does not exist! Please check file integrity!"
|
||||||
|
ErrFileNotExist: "{{ .detail }} file does not exist! Please check source file integrity!"
|
@@ -59,3 +59,7 @@ ErrTypeOfRedis: "恢复文件类型与当前持久化方式不符,请修改后
|
|||||||
#container
|
#container
|
||||||
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
|
ErrInUsed: "{{ .detail }} 正被使用,无法删除"
|
||||||
ErrObjectInUsed: "该对象正被使用,无法删除"
|
ErrObjectInUsed: "该对象正被使用,无法删除"
|
||||||
|
|
||||||
|
#runtime
|
||||||
|
ErrDirNotFound: "build 文件夹不存在!请检查文件完整性!"
|
||||||
|
ErrFileNotExist: "{{ .detail }} 文件不存在!请检查源文件完整性!"
|
@@ -14,8 +14,9 @@ func Init() {
|
|||||||
constant.ResourceDir = path.Join(constant.DataDir, "resource")
|
constant.ResourceDir = path.Join(constant.DataDir, "resource")
|
||||||
constant.AppResourceDir = path.Join(constant.ResourceDir, "apps")
|
constant.AppResourceDir = path.Join(constant.ResourceDir, "apps")
|
||||||
constant.AppInstallDir = path.Join(constant.DataDir, "apps")
|
constant.AppInstallDir = path.Join(constant.DataDir, "apps")
|
||||||
|
constant.RuntimeDir = path.Join(constant.DataDir, "runtime")
|
||||||
|
|
||||||
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, global.CONF.System.Backup}
|
dirs := []string{constant.DataDir, constant.ResourceDir, constant.AppResourceDir, constant.AppInstallDir, global.CONF.System.Backup, constant.RuntimeDir}
|
||||||
|
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
|
@@ -249,7 +249,7 @@ var AddDefaultGroup = &gormigrate.Migration{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var AddTableRuntime = &gormigrate.Migration{
|
var AddTableRuntime = &gormigrate.Migration{
|
||||||
ID: "20230328-add-table-runtime",
|
ID: "20230330-add-table-runtime",
|
||||||
Migrate: func(tx *gorm.DB) error {
|
Migrate: func(tx *gorm.DB) error {
|
||||||
return tx.AutoMigrate(&model.Runtime{})
|
return tx.AutoMigrate(&model.Runtime{})
|
||||||
},
|
},
|
||||||
|
@@ -19,7 +19,7 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
|
|||||||
appRouter.GET("/checkupdate", baseApi.GetAppListUpdate)
|
appRouter.GET("/checkupdate", baseApi.GetAppListUpdate)
|
||||||
appRouter.POST("/search", baseApi.SearchApp)
|
appRouter.POST("/search", baseApi.SearchApp)
|
||||||
appRouter.GET("/:key", baseApi.GetApp)
|
appRouter.GET("/:key", baseApi.GetApp)
|
||||||
appRouter.GET("/detail/:appId/:version", baseApi.GetAppDetail)
|
appRouter.GET("/detail/:appId/:version/:type", baseApi.GetAppDetail)
|
||||||
appRouter.POST("/install", baseApi.InstallApp)
|
appRouter.POST("/install", baseApi.InstallApp)
|
||||||
appRouter.GET("/tags", baseApi.GetAppTags)
|
appRouter.GET("/tags", baseApi.GetAppTags)
|
||||||
appRouter.GET("/installed/:appInstallId/versions", baseApi.GetUpdateVersions)
|
appRouter.GET("/installed/:appInstallId/versions", baseApi.GetUpdateVersions)
|
||||||
|
@@ -16,5 +16,6 @@ func (r *RuntimeRouter) InitRuntimeRouter(Router *gin.RouterGroup) {
|
|||||||
baseApi := v1.ApiGroupApp.BaseApi
|
baseApi := v1.ApiGroupApp.BaseApi
|
||||||
{
|
{
|
||||||
groupRouter.POST("/search", baseApi.SearchRuntimes)
|
groupRouter.POST("/search", baseApi.SearchRuntimes)
|
||||||
|
groupRouter.POST("", baseApi.CreateRuntime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -83,6 +83,10 @@ func (s *ComposeService) ComposeCreate() error {
|
|||||||
return s.Create(context.Background(), s.project, api.CreateOptions{})
|
return s.Create(context.Background(), s.project, api.CreateOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ComposeService) ComposeBuild() error {
|
||||||
|
return s.Build(context.Background(), s.project, api.BuildOptions{})
|
||||||
|
}
|
||||||
|
|
||||||
func GetComposeProject(projectName, workDir string, yml []byte, env []byte) (*types.Project, error) {
|
func GetComposeProject(projectName, workDir string, yml []byte, env []byte) (*types.Project, error) {
|
||||||
var configFiles []types.ConfigFile
|
var configFiles []types.ConfigFile
|
||||||
configFiles = append(configFiles, types.ConfigFile{
|
configFiles = append(configFiles, types.ConfigFile{
|
||||||
|
@@ -37,6 +37,15 @@ func (f FileOp) OpenFile(dst string) (fs.File, error) {
|
|||||||
return f.Fs.Open(dst)
|
return f.Fs.Open(dst)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f FileOp) GetContent(dst string) ([]byte, error) {
|
||||||
|
afs := &afero.Afero{Fs: f.Fs}
|
||||||
|
cByte, err := afs.ReadFile(dst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cByte, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (f FileOp) CreateDir(dst string, mode fs.FileMode) error {
|
func (f FileOp) CreateDir(dst string, mode fs.FileMode) error {
|
||||||
return f.Fs.MkdirAll(dst, mode)
|
return f.Fs.MkdirAll(dst, mode)
|
||||||
}
|
}
|
||||||
|
@@ -64,6 +64,7 @@ export namespace App {
|
|||||||
values?: ServiceParam[];
|
values?: ServiceParam[];
|
||||||
child?: FromFieldChild;
|
child?: FromFieldChild;
|
||||||
params?: FromParam[];
|
params?: FromParam[];
|
||||||
|
multiple?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FromFieldChild extends FromField {
|
export interface FromFieldChild extends FromField {
|
||||||
|
@@ -18,4 +18,15 @@ export namespace Runtime {
|
|||||||
export interface RuntimeDTO extends Runtime {
|
export interface RuntimeDTO extends Runtime {
|
||||||
websites: string[];
|
websites: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuntimeCreate {
|
||||||
|
name: string;
|
||||||
|
appDetailId: number;
|
||||||
|
image: string;
|
||||||
|
params: object;
|
||||||
|
type: string;
|
||||||
|
resource: string;
|
||||||
|
appId?: number;
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -22,8 +22,8 @@ export const GetAppTags = () => {
|
|||||||
return http.get<App.Tag[]>('apps/tags');
|
return http.get<App.Tag[]>('apps/tags');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GetAppDetail = (id: number, version: string) => {
|
export const GetAppDetail = (id: number, version: string, type: string) => {
|
||||||
return http.get<App.AppDetail>(`apps/detail/${id}/${version}`);
|
return http.get<App.AppDetail>(`apps/detail/${id}/${version}/${type}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstallApp = (install: App.AppInstall) => {
|
export const InstallApp = (install: App.AppInstall) => {
|
||||||
|
@@ -5,3 +5,7 @@ import { Runtime } from '../interface/runtime';
|
|||||||
export const SearchRuntimes = (req: Runtime.RuntimeReq) => {
|
export const SearchRuntimes = (req: Runtime.RuntimeReq) => {
|
||||||
return http.post<ResPage<Runtime.RuntimeDTO>>(`/runtimes/search`, req);
|
return http.post<ResPage<Runtime.RuntimeDTO>>(`/runtimes/search`, req);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CreateRuntime = (req: Runtime.RuntimeCreate) => {
|
||||||
|
return http.post<any>(`/runtimes`, req);
|
||||||
|
};
|
||||||
|
@@ -1228,6 +1228,14 @@ const message = {
|
|||||||
runtime: '运行环境',
|
runtime: '运行环境',
|
||||||
image: '镜像',
|
image: '镜像',
|
||||||
workDir: '工作目录',
|
workDir: '工作目录',
|
||||||
|
create: '创建运行环境',
|
||||||
|
name: '名称',
|
||||||
|
resource: '来源',
|
||||||
|
appStore: '应用商店',
|
||||||
|
local: '本地',
|
||||||
|
app: '应用',
|
||||||
|
localHelper: '本地运行环境需要自行安装',
|
||||||
|
version: '版本',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export default {
|
export default {
|
||||||
|
@@ -40,7 +40,7 @@ const webSiteRouter = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/websites/runtime',
|
path: '/websites/runtime/php',
|
||||||
name: 'Runtime',
|
name: 'Runtime',
|
||||||
component: () => import('@/views/website/runtime/index.vue'),
|
component: () => import('@/views/website/runtime/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
|
@@ -131,7 +131,7 @@ const getApp = async () => {
|
|||||||
const getDetail = async (id: number, version: string) => {
|
const getDetail = async (id: number, version: string) => {
|
||||||
loadingDetail.value = true;
|
loadingDetail.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await GetAppDetail(id, version);
|
const res = await GetAppDetail(id, version, 'app');
|
||||||
appDetail.value = res.data;
|
appDetail.value = res.data;
|
||||||
} finally {
|
} finally {
|
||||||
loadingDetail.value = false;
|
loadingDetail.value = false;
|
||||||
|
190
frontend/src/views/website/runtime/create/index.vue
Normal file
190
frontend/src/views/website/runtime/create/index.vue
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<template>
|
||||||
|
<el-drawer :close-on-click-modal="false" v-model="open" size="50%">
|
||||||
|
<template #header>
|
||||||
|
<DrawerHeader :header="$t('runtime.create')" :back="handleClose" />
|
||||||
|
</template>
|
||||||
|
<el-row v-loading="loading">
|
||||||
|
<el-col :span="22" :offset="1">
|
||||||
|
<el-form
|
||||||
|
ref="runtimeForm"
|
||||||
|
label-position="top"
|
||||||
|
:model="runtimeCreate"
|
||||||
|
label-width="125px"
|
||||||
|
:rules="rules"
|
||||||
|
:validate-on-rule-change="false"
|
||||||
|
>
|
||||||
|
<el-form-item :label="$t('runtime.name')" prop="name">
|
||||||
|
<el-input v-model="runtimeCreate.name"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item :label="$t('runtime.resource')" prop="resource">
|
||||||
|
<el-radio-group
|
||||||
|
v-model="runtimeCreate.resource"
|
||||||
|
@change="changeResource(runtimeCreate.resource)"
|
||||||
|
>
|
||||||
|
<el-radio :label="'AppStore'" :value="'AppStore'">
|
||||||
|
{{ $t('runtime.appStore') }}
|
||||||
|
</el-radio>
|
||||||
|
<el-radio :label="'Local'" :value="'Local'">
|
||||||
|
{{ $t('runtime.local') }}
|
||||||
|
</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<div v-if="runtimeCreate.resource === 'AppStore'">
|
||||||
|
<el-form-item :label="$t('runtime.app')" prop="appId">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-select v-model="runtimeCreate.appId">
|
||||||
|
<el-option
|
||||||
|
v-for="(app, index) in apps"
|
||||||
|
:key="index"
|
||||||
|
:label="app.name"
|
||||||
|
:value="app.id"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-select v-model="runtimeCreate.version">
|
||||||
|
<el-option
|
||||||
|
v-for="(version, index) in appVersions"
|
||||||
|
:key="index"
|
||||||
|
:label="version"
|
||||||
|
:value="version"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form-item>
|
||||||
|
<Params
|
||||||
|
v-if="initParam"
|
||||||
|
v-model:form="runtimeCreate"
|
||||||
|
v-model:params="appParams"
|
||||||
|
v-model:rules="rules"
|
||||||
|
></Params>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<el-alert :title="$t('runtime.localHelper')" type="info" :closable="false" />
|
||||||
|
<el-form-item :label="$t('runtime.version')" prop="version">
|
||||||
|
<el-input v-model="runtimeCreate.version"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<template #footer>
|
||||||
|
<span>
|
||||||
|
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
|
||||||
|
<el-button type="primary" @click="submit(runtimeForm)" :disabled="loading">
|
||||||
|
{{ $t('commons.button.confirm') }}
|
||||||
|
</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { App } from '@/api/interface/app';
|
||||||
|
import { Runtime } from '@/api/interface/runtime';
|
||||||
|
import { GetApp, GetAppDetail, SearchApp } from '@/api/modules/app';
|
||||||
|
import { CreateRuntime } from '@/api/modules/runtime';
|
||||||
|
import { Rules } from '@/global/form-rules';
|
||||||
|
import i18n from '@/lang';
|
||||||
|
import { MsgSuccess } from '@/utils/message';
|
||||||
|
import { FormInstance } from 'element-plus';
|
||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import Params from '../param/index.vue';
|
||||||
|
|
||||||
|
const open = ref(false);
|
||||||
|
const apps = ref<App.App[]>([]);
|
||||||
|
const runtimeForm = ref<FormInstance>();
|
||||||
|
const loading = ref(false);
|
||||||
|
const initParam = ref(false);
|
||||||
|
let appParams = ref<App.AppParams>();
|
||||||
|
let appVersions = ref<string[]>([]);
|
||||||
|
let appReq = reactive({
|
||||||
|
type: 'php',
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
});
|
||||||
|
const runtimeCreate = ref<Runtime.RuntimeCreate>({
|
||||||
|
name: '',
|
||||||
|
appDetailId: undefined,
|
||||||
|
image: '',
|
||||||
|
params: {},
|
||||||
|
type: '',
|
||||||
|
resource: 'AppStore',
|
||||||
|
});
|
||||||
|
let rules = ref<any>({
|
||||||
|
name: [Rules.appName],
|
||||||
|
resource: [Rules.requiredInput],
|
||||||
|
appId: [Rules.requiredSelect],
|
||||||
|
version: [Rules.requiredInput],
|
||||||
|
});
|
||||||
|
|
||||||
|
const em = defineEmits(['close']);
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
open.value = false;
|
||||||
|
em('close', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeResource = (resource: string) => {
|
||||||
|
if (resource === 'Local') {
|
||||||
|
runtimeCreate.value.appDetailId = undefined;
|
||||||
|
runtimeCreate.value.version = '';
|
||||||
|
runtimeCreate.value.params = {};
|
||||||
|
runtimeCreate.value.image = '';
|
||||||
|
} else {
|
||||||
|
runtimeCreate.value.version = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchApp = () => {
|
||||||
|
SearchApp(appReq).then((res) => {
|
||||||
|
apps.value = res.data.items || [];
|
||||||
|
if (res.data && res.data.items && res.data.items.length > 0) {
|
||||||
|
runtimeCreate.value.appId = res.data.items[0].id;
|
||||||
|
getApp(res.data.items[0].key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const getApp = (appkey: string) => {
|
||||||
|
GetApp(appkey).then((res) => {
|
||||||
|
appVersions.value = res.data.versions || [];
|
||||||
|
if (res.data.versions.length > 0) {
|
||||||
|
runtimeCreate.value.version = res.data.versions[0];
|
||||||
|
GetAppDetail(runtimeCreate.value.appId, runtimeCreate.value.version, 'runtime').then((res) => {
|
||||||
|
runtimeCreate.value.appDetailId = res.data.id;
|
||||||
|
appParams.value = res.data.params;
|
||||||
|
initParam.value = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submit = async (formEl: FormInstance | undefined) => {
|
||||||
|
if (!formEl) return;
|
||||||
|
await formEl.validate((valid) => {
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
CreateRuntime(runtimeCreate.value)
|
||||||
|
.then(() => {
|
||||||
|
MsgSuccess(i18n.global.t('commons.msg.createSuccess'));
|
||||||
|
handleClose();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
loading.value = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const acceptParams = async () => {
|
||||||
|
searchApp();
|
||||||
|
open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
});
|
||||||
|
</script>
|
@@ -4,12 +4,16 @@
|
|||||||
:buttons="[
|
:buttons="[
|
||||||
{
|
{
|
||||||
label: 'PHP',
|
label: 'PHP',
|
||||||
path: '/runtimes',
|
path: '/runtimes/php',
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<LayoutContent :title="$t('runtime.runtime')" v-loading="loading">
|
<LayoutContent :title="$t('runtime.runtime')" v-loading="loading">
|
||||||
<template #toolbar></template>
|
<template #toolbar>
|
||||||
|
<el-button type="primary" @click="openCreate">
|
||||||
|
{{ $t('runtime.create') }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
<template #main>
|
<template #main>
|
||||||
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
|
<ComplexTable :pagination-config="paginationConfig" :data="items" @search="search()">
|
||||||
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
|
<el-table-column :label="$t('commons.table.name')" fix prop="name" min-width="120px">
|
||||||
@@ -28,6 +32,7 @@
|
|||||||
</ComplexTable>
|
</ComplexTable>
|
||||||
</template>
|
</template>
|
||||||
</LayoutContent>
|
</LayoutContent>
|
||||||
|
<CreateRuntime ref="createRef" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -35,10 +40,11 @@
|
|||||||
import { onMounted, reactive, ref } from 'vue';
|
import { onMounted, reactive, ref } from 'vue';
|
||||||
import { Runtime } from '@/api/interface/runtime';
|
import { Runtime } from '@/api/interface/runtime';
|
||||||
import { SearchRuntimes } from '@/api/modules/runtime';
|
import { SearchRuntimes } from '@/api/modules/runtime';
|
||||||
|
import { dateFormat } from '@/utils/util';
|
||||||
import RouterButton from '@/components/router-button/index.vue';
|
import RouterButton from '@/components/router-button/index.vue';
|
||||||
import ComplexTable from '@/components/complex-table/index.vue';
|
import ComplexTable from '@/components/complex-table/index.vue';
|
||||||
import LayoutContent from '@/layout/layout-content.vue';
|
import LayoutContent from '@/layout/layout-content.vue';
|
||||||
import { dateFormat } from '@/utils/util';
|
import CreateRuntime from '@/views/website/runtime/create/index.vue';
|
||||||
|
|
||||||
const paginationConfig = reactive({
|
const paginationConfig = reactive({
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
@@ -52,6 +58,7 @@ let req = reactive<Runtime.RuntimeReq>({
|
|||||||
});
|
});
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const items = ref<Runtime.RuntimeDTO[]>([]);
|
const items = ref<Runtime.RuntimeDTO[]>([]);
|
||||||
|
const createRef = ref();
|
||||||
|
|
||||||
const search = async () => {
|
const search = async () => {
|
||||||
req.page = paginationConfig.currentPage;
|
req.page = paginationConfig.currentPage;
|
||||||
@@ -67,6 +74,10 @@ const search = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openCreate = () => {
|
||||||
|
createRef.value.acceptParams();
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
search();
|
search();
|
||||||
});
|
});
|
||||||
|
94
frontend/src/views/website/runtime/param/index.vue
Normal file
94
frontend/src/views/website/runtime/param/index.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div v-for="(p, index) in paramObjs" :key="index">
|
||||||
|
<el-form-item :label="getLabel(p)" :prop="p.prop">
|
||||||
|
<el-select v-model="form[p.envKey]" v-if="p.type == 'select'" :multiple="p.multiple">
|
||||||
|
<el-option
|
||||||
|
v-for="service in p.values"
|
||||||
|
:key="service.label"
|
||||||
|
:value="service.value"
|
||||||
|
:label="service.label"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { App } from '@/api/interface/app';
|
||||||
|
// import { Rules } from '@/global/form-rules';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
interface ParamObj extends App.FromField {
|
||||||
|
services: App.AppService[];
|
||||||
|
prop: string;
|
||||||
|
disabled: false;
|
||||||
|
childProp: string;
|
||||||
|
}
|
||||||
|
const props = defineProps({
|
||||||
|
form: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
type: Object,
|
||||||
|
default: function () {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let form = reactive({});
|
||||||
|
// let rules = reactive({});
|
||||||
|
const params = computed({
|
||||||
|
get() {
|
||||||
|
return props.params;
|
||||||
|
},
|
||||||
|
set() {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const paramObjs = ref<ParamObj[]>([]);
|
||||||
|
|
||||||
|
const handleParams = () => {
|
||||||
|
// rules = props.rules;
|
||||||
|
form = props.form;
|
||||||
|
if (params.value != undefined && params.value.formFields != undefined) {
|
||||||
|
for (const p of params.value.formFields) {
|
||||||
|
const pObj = p;
|
||||||
|
pObj.prop = p.envKey;
|
||||||
|
pObj.disabled = p.disabled;
|
||||||
|
form[p.envKey] = p.default;
|
||||||
|
paramObjs.value.push(pObj);
|
||||||
|
// if (p.required) {
|
||||||
|
// if (p.type === 'select') {
|
||||||
|
// rules[p.envKey] = [Rules.requiredSelect];
|
||||||
|
// } else {
|
||||||
|
// rules[p.envKey] = [Rules.requiredInput];
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
console.log(form);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabel = (row: ParamObj): string => {
|
||||||
|
const language = useI18n().locale.value;
|
||||||
|
if (language == 'zh') {
|
||||||
|
return row.labelZh;
|
||||||
|
} else {
|
||||||
|
return row.labelEn;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
handleParams();
|
||||||
|
});
|
||||||
|
</script>
|
@@ -310,7 +310,7 @@ const getApp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getAppDetail = (version: string) => {
|
const getAppDetail = (version: string) => {
|
||||||
GetAppDetail(website.value.appinstall.appId, version).then((res) => {
|
GetAppDetail(website.value.appinstall.appId, version, 'app').then((res) => {
|
||||||
website.value.appinstall.appDetailId = res.data.id;
|
website.value.appinstall.appDetailId = res.data.id;
|
||||||
appDetail.value = res.data;
|
appDetail.value = res.data;
|
||||||
appParams.value = res.data.params;
|
appParams.value = res.data.params;
|
||||||
|
Reference in New Issue
Block a user