feat: 增加一键部署网站功能

This commit is contained in:
zhengkunwang223 2022-11-02 15:19:14 +08:00 committed by zhengkunwang223
parent ef789cdfea
commit 9621d6ea1f
30 changed files with 670 additions and 141 deletions

View File

@ -7,7 +7,7 @@ services:
- 1panel
ports:
- ${PANEL_APP_PORT_HTTP}:6379
command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
command: redis-server --save 20 1 --loglevel warning --requirepass ${PANEL_DB_ROOT_PASSWORD}
volumes:
- ./data:/data
networks:

View File

@ -7,7 +7,7 @@ services:
- 1panel
ports:
- ${PANEL_APP_PORT_HTTP}:6379
command: redis-server --save 20 1 --loglevel warning --requirepass eYVX7EwVmmxKPCDmwMtyKVge8oLd2t81
command: redis-server --save 20 1 --loglevel warning --requirepass ${PANEL_DB_ROOT_PASSWORD}
volumes:
- ./data:/data
networks:

View File

@ -70,12 +70,13 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := appService.Install(req.Name, req.AppDetailId, req.Params); err != nil {
install, err := appService.Install(req.Name, req.AppDetailId, req.Params)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
helper.SuccessWithData(c, install)
}
func (b *BaseApi) DeleteAppBackup(c *gin.Context) {

View File

@ -7,14 +7,29 @@ import (
"github.com/gin-gonic/gin"
)
func (b *BaseApi) CreateWebsite(c *gin.Context) {
func (b *BaseApi) PageWebsite(c *gin.Context) {
var req dto.WebSiteReq
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, websites, err := websiteService.PageWebSite(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: websites,
})
}
func (b *BaseApi) CreateWebsite(c *gin.Context) {
var req dto.WebSiteCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := websiteService.CreateWebsite(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
@ -22,3 +37,17 @@ func (b *BaseApi) CreateWebsite(c *gin.Context) {
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) DeleteWebSite(c *gin.Context) {
var req dto.WebSiteDel
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
err := websiteService.DeleteWebSite(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -2,11 +2,12 @@ package v1
import (
"github.com/1Panel-dev/1Panel/backend/app/api/v1/helper"
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/gin-gonic/gin"
)
func (b *BaseApi) GetGroups(c *gin.Context) {
func (b *BaseApi) GetWebGroups(c *gin.Context) {
list, err := websiteGroupService.GetGroups()
if err != nil {
@ -15,3 +16,18 @@ func (b *BaseApi) GetGroups(c *gin.Context) {
}
helper.SuccessWithData(c, list)
}
func (b *BaseApi) CreateWebGroup(c *gin.Context) {
var req dto.WebSiteGroupCreate
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := websiteGroupService.CreateGroup(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}

View File

@ -29,6 +29,7 @@ type AppRequest struct {
PageInfo
Name string `json:"name"`
Tags []string `json:"tags"`
Type string `json:"type"`
}
type AppInstallRequest struct {

View File

@ -1,16 +1,43 @@
package dto
type WebPage struct {
import "github.com/1Panel-dev/1Panel/backend/app/model"
type WebSiteReq struct {
PageInfo
}
type AppType string
const (
NewApp AppType = "new"
InstalledApp AppType = "installed"
)
type WebSiteCreate struct {
PrimaryDomain string `json:"primaryDomain"`
Type string `json:"type"`
Alias string `json:"alias"`
Remark string `json:"remark"`
Domains []string `json:"domains"`
AppType string `json:"appType"`
AppInstallID uint `json:"appInstallID"`
WebSiteGroupID uint `json:"webSiteGroupID"`
PrimaryDomain string `json:"primaryDomain" validate:"required"`
Type string `json:"type" validate:"required"`
Alias string `json:"alias" validate:"required"`
Remark string `json:"remark"`
Domains []string `json:"domains"`
AppType AppType `json:"appType" validate:"required"`
AppInstall NewAppInstall `json:"appInstall"`
AppID uint `json:"appID"`
AppInstallID uint `json:"appInstallID"`
WebSiteGroupID uint `json:"webSiteGroupID" validate:"required"`
}
type NewAppInstall struct {
Name string `json:"name"`
AppDetailId uint `json:"appDetailID"`
Params map[string]interface{} `json:"params"`
}
type WebSiteDel struct {
ID uint `json:"id"`
DeleteApp bool `json:"deleteApp"`
DeleteBackup bool `json:"deleteBackup"`
}
type WebSiteDTO struct {
model.WebSite
}

View File

@ -1,10 +1,10 @@
package dto
type WebSiteGroupCreateReq struct {
type WebSiteGroupCreate struct {
Name string
}
type WebSiteGroupUpdateReq struct {
type WebSiteGroupUpdate struct {
ID uint
Name string
}

View File

@ -2,6 +2,7 @@ package repo
import (
"context"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
@ -70,10 +71,10 @@ func (c *CommonRepo) WithIdsNotIn(ids []uint) DBOption {
}
func getTx(ctx context.Context, opts ...DBOption) *gorm.DB {
if ctx == nil || ctx.Value("db") == nil {
if ctx == nil || ctx.Value(constant.DB) == nil {
return getDb()
}
tx := ctx.Value("db").(*gorm.DB)
tx := ctx.Value(constant.DB).(*gorm.DB)
for _, opt := range opts {
tx = opt(tx)
}

View File

@ -43,3 +43,7 @@ func (w WebSiteRepo) Create(ctx context.Context, app *model.WebSite) error {
func (w WebSiteRepo) Save(ctx context.Context, app *model.WebSite) error {
return getTx(ctx).Omit(clause.Associations).Save(app).Error
}
func (w WebSiteRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.WebSite{}).Error
}

View File

@ -3,12 +3,18 @@ package repo
import (
"context"
"github.com/1Panel-dev/1Panel/backend/app/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type WebSiteDomainRepo struct {
}
func (w WebSiteDomainRepo) WithWebSiteId(websiteId uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("web_site_id = ?", websiteId)
}
}
func (w WebSiteDomainRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebSiteDomain, error) {
var domains []model.WebSiteDomain
db := getDb(opts...).Model(&model.WebSiteDomain{})
@ -47,3 +53,7 @@ func (w WebSiteDomainRepo) Create(ctx context.Context, app *model.WebSiteDomain)
func (w WebSiteDomainRepo) Save(ctx context.Context, app *model.WebSiteDomain) error {
return getTx(ctx).Omit(clause.Associations).Save(app).Error
}
func (w WebSiteDomainRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.WebSiteDomain{}).Error
}

View File

@ -34,6 +34,9 @@ func (a AppService) PageApp(req dto.AppRequest) (interface{}, error) {
if req.Name != "" {
opts = append(opts, commonRepo.WithLikeName(req.Name))
}
if req.Type != "" {
opts = append(opts, appRepo.WithType(req.Type))
}
if len(req.Tags) != 0 {
tags, err := tagRepo.GetByKeys(req.Tags)
if err != nil {
@ -212,7 +215,13 @@ func (a AppService) OperateInstall(req dto.AppInstallOperate) error {
}
install.Status = constant.Running
case dto.Delete:
return deleteAppInstall(install)
tx, ctx := getTxAndContext()
if err := deleteAppInstall(ctx, install); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
case dto.Sync:
return a.SyncInstalled(install.ID)
case dto.Backup:
@ -286,36 +295,38 @@ func (a AppService) ChangeAppPort(req dto.PortUpdate) error {
return nil
}
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) error {
func (a AppService) Install(name string, appDetailId uint, params map[string]interface{}) (*model.AppInstall, error) {
httpPort, err := checkPort("PANEL_APP_PORT_HTTP", params)
if err != nil {
return fmt.Errorf("%d port is in used", httpPort)
return nil, fmt.Errorf("%d port is in used", httpPort)
}
httpsPort, err := checkPort("PANEL_APP_PORT_HTTPS", params)
if err != nil {
return fmt.Errorf("%d port is in used", httpPort)
return nil, fmt.Errorf("%d port is in used", httpPort)
}
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(appDetailId))
if err != nil {
return err
return nil, err
}
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
if err != nil {
return err
return nil, err
}
if err := checkRequiredAndLimit(app); err != nil {
return err
return nil, err
}
if err := copyAppData(app.Key, appDetail.Version, name, params); err != nil {
return err
return nil, err
}
paramByte, err := json.Marshal(params)
if err != nil {
return err
return nil, err
}
appInstall := model.AppInstall{
Name: name,
@ -331,7 +342,7 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
composeMap := make(map[string]interface{})
if err := yaml.Unmarshal([]byte(appDetail.DockerCompose), &composeMap); err != nil {
return err
return nil, err
}
servicesMap := composeMap["services"].(map[string]interface{})
changeKeys := make(map[string]string, len(servicesMap))
@ -350,27 +361,27 @@ func (a AppService) Install(name string, appDetailId uint, params map[string]int
}
composeByte, err := yaml.Marshal(composeMap)
if err != nil {
return err
return nil, err
}
appInstall.DockerCompose = string(composeByte)
fileOp := files.NewFileOp()
if err := fileOp.WriteFile(appInstall.GetComposePath(), strings.NewReader(string(composeByte)), 0775); err != nil {
return err
return nil, err
}
tx, ctx := getTxAndContext()
if err := appInstallRepo.Create(ctx, &appInstall); err != nil {
tx.Rollback()
return err
return nil, err
}
if err := createLink(ctx, app, &appInstall, params); err != nil {
tx.Rollback()
return err
return nil, err
}
tx.Commit()
go upApp(appInstall.GetComposePath(), appInstall)
return nil
return &appInstall, nil
}
func (a AppService) SyncAllInstalled() error {

View File

@ -150,7 +150,7 @@ func createLink(ctx context.Context, app model.App, appInstall *model.AppInstall
return nil
}
func deleteAppInstall(install model.AppInstall) error {
func deleteAppInstall(ctx context.Context, install model.AppInstall) error {
op := files.NewFileOp()
appDir := install.GetPath()
dir, _ := os.Stat(appDir)
@ -164,20 +164,15 @@ func deleteAppInstall(install model.AppInstall) error {
}
}
tx, ctx := getTxAndContext()
if err := appInstallRepo.Delete(ctx, install); err != nil {
tx.Rollback()
return err
}
if err := deleteLink(ctx, &install); err != nil {
tx.Rollback()
return err
}
if err := appInstallBackupRepo.Delete(ctx, appInstallBackupRepo.WithAppInstallID(install.ID)); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}

View File

@ -2,18 +2,13 @@ package service
import (
"context"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/global"
"gorm.io/gorm"
)
type DBContext string
const (
DB DBContext = "db"
)
func getTxAndContext() (tx *gorm.DB, ctx context.Context) {
tx = global.DB.Begin()
ctx = context.WithValue(context.Background(), DB, tx)
ctx = context.WithValue(context.Background(), constant.DB, tx)
return
}

View File

@ -4,6 +4,8 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/dto"
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/pkg/errors"
"gorm.io/gorm"
"reflect"
"time"
)
@ -11,10 +13,22 @@ import (
type WebsiteService struct {
}
func (w WebsiteService) PageWebSite(req dto.WebSiteReq) (int64, []dto.WebSiteDTO, error) {
var websiteDTOs []dto.WebSiteDTO
total, websites, err := websiteRepo.Page(req.Page, req.PageSize)
if err != nil {
return 0, nil, err
}
for _, web := range websites {
websiteDTOs = append(websiteDTOs, dto.WebSiteDTO{
WebSite: web,
})
}
return total, websiteDTOs, nil
}
func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error {
defaultDate, _ := time.Parse(constant.DateLayout, constant.DefaultDate)
website := &model.WebSite{
PrimaryDomain: create.PrimaryDomain,
Type: create.Type,
@ -26,6 +40,14 @@ func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error {
WebSiteGroupID: create.WebSiteGroupID,
}
if create.AppType == dto.NewApp {
install, err := ServiceGroupApp.Install(create.AppInstall.Name, create.AppInstall.AppDetailId, create.AppInstall.Params)
if err != nil {
return err
}
website.AppInstallID = install.ID
}
tx, ctx := getTxAndContext()
if err := websiteRepo.Create(ctx, website); err != nil {
return err
@ -50,7 +72,7 @@ func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error {
}
}
if err := configDefaultNginx(*website, domains); err != nil {
if err := configDefaultNginx(website, domains); err != nil {
tx.Rollback()
return err
}
@ -58,3 +80,38 @@ func (w WebsiteService) CreateWebsite(create dto.WebSiteCreate) error {
tx.Commit()
return nil
}
func (w WebsiteService) DeleteWebSite(req dto.WebSiteDel) error {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.ID))
if err != nil {
return err
}
if err := delNginxConfig(website); err != nil {
return err
}
tx, ctx := getTxAndContext()
if req.DeleteApp {
appInstall, err := appInstallRepo.GetFirst(commonRepo.WithByID(website.AppInstallID))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
if !reflect.DeepEqual(model.AppInstall{}, appInstall) {
if err := deleteAppInstall(ctx, appInstall); err != nil {
return err
}
}
}
//TODO 删除备份
if err := websiteRepo.DeleteBy(ctx, commonRepo.WithByID(req.ID)); err != nil {
tx.Rollback()
return err
}
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebSiteId(req.ID)); err != nil {
tx.Rollback()
return err
}
tx.Commit()
return nil
}

View File

@ -8,7 +8,7 @@ import (
type WebsiteGroupService struct {
}
func (w WebsiteGroupService) CreateGroup(create dto.WebSiteGroupCreateReq) error {
func (w WebsiteGroupService) CreateGroup(create dto.WebSiteGroupCreate) error {
return websiteGroupRepo.Create(&model.WebSiteGroup{
Name: create.Name,
})
@ -18,7 +18,7 @@ func (w WebsiteGroupService) GetGroups() ([]model.WebSiteGroup, error) {
return websiteGroupRepo.GetBy()
}
func (w WebsiteGroupService) UpdateGroup(update dto.WebSiteGroupUpdateReq) error {
func (w WebsiteGroupService) UpdateGroup(update dto.WebSiteGroupUpdate) error {
return websiteGroupRepo.Save(&model.WebSiteGroup{
BaseModel: model.BaseModel{
ID: update.ID,

View File

@ -5,10 +5,12 @@ import (
"github.com/1Panel-dev/1Panel/backend/app/model"
"github.com/1Panel-dev/1Panel/backend/constant"
"github.com/1Panel-dev/1Panel/backend/utils/cmd"
"github.com/1Panel-dev/1Panel/backend/utils/files"
"github.com/1Panel-dev/1Panel/backend/utils/nginx"
"github.com/1Panel-dev/1Panel/backend/utils/nginx/parser"
"github.com/1Panel-dev/1Panel/cmd/server/nginx_conf"
"github.com/pkg/errors"
"gorm.io/gorm"
"path"
"strconv"
"strings"
@ -21,6 +23,7 @@ func getDomain(domainStr string, websiteID uint) (model.WebSiteDomain, error) {
domainArray := strings.Split(domainStr, ":")
if len(domainArray) == 1 {
domain.Domain = domainArray[0]
domain.Port = 80
return domain, nil
}
if len(domainArray) > 1 {
@ -36,7 +39,7 @@ func getDomain(domainStr string, websiteID uint) (model.WebSiteDomain, error) {
return model.WebSiteDomain{}, nil
}
func configDefaultNginx(website model.WebSite, domains []model.WebSiteDomain) error {
func configDefaultNginx(website *model.WebSite, domains []model.WebSiteDomain) error {
nginxApp, err := appRepo.GetFirst(appRepo.WithKey("nginx"))
if err != nil {
@ -67,7 +70,7 @@ func configDefaultNginx(website model.WebSite, domains []model.WebSiteDomain) er
server.UpdateListen(string(rune(domain.Port)), false)
}
server.UpdateServerName(serverNames)
proxy := fmt.Sprintf("%s:%d", appInstall.ServiceName, appInstall.HttpPort)
proxy := fmt.Sprintf("http://%s:%d", appInstall.ServiceName, appInstall.HttpPort)
server.UpdateRootProxy([]string{proxy})
config.FilePath = configPath
@ -90,3 +93,34 @@ func opNginx(containerName, operate string) error {
}
return nil
}
func delNginxConfig(website model.WebSite) error {
nginxApp, err := appRepo.GetFirst(appRepo.WithKey("nginx"))
if err != nil {
return err
}
nginxInstall, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(nginxApp.ID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
return err
}
nginxFileName := website.PrimaryDomain + ".conf"
configPath := path.Join(constant.AppInstallDir, "nginx", nginxInstall.Name, "conf", "conf.d", nginxFileName)
fileOp := files.NewFileOp()
if !fileOp.Stat(configPath) {
return nil
}
if err := fileOp.DeleteFile(configPath); err != nil {
return err
}
return opNginx(nginxInstall.ContainerName, "reload")
}
func delApp() error {
return nil
}

View File

@ -0,0 +1,7 @@
package constant
type DBContext string
const (
DB DBContext = "db"
)

View File

@ -16,5 +16,7 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
groupRouter.POST("", baseApi.CreateWebsite)
groupRouter.POST("/search", baseApi.PageWebsite)
groupRouter.POST("/del", baseApi.DeleteWebSite)
}
}

View File

@ -15,6 +15,7 @@ func (a *WebsiteGroupRouter) InitWebsiteGroupRouter(Router *gin.RouterGroup) {
baseApi := v1.ApiGroupApp.BaseApi
{
groupRouter.GET("", baseApi.GetGroups)
groupRouter.GET("", baseApi.GetWebGroups)
//groupRouter.GET("", baseApi.GetGroups)
}
}

View File

@ -39,8 +39,9 @@ export namespace App {
}
export interface AppReq extends ReqPage {
name: string;
tags: string[];
name?: string;
tags?: string[];
type?: string;
}
export interface AppParams {

View File

@ -1,6 +1,35 @@
import { CommonModel } from '.';
import { CommonModel, ReqPage } from '.';
export namespace WebSite {
export interface WebSite extends CommonModel {
primaryDomain: string;
type: string;
alias: string;
remark: string;
domains: string[];
appType: string;
appInstallID?: number;
webSiteGroupID: number;
otherDomains: string;
appinstall?: NewAppInstall;
}
export interface NewAppInstall {
name: string;
appDetailID: number;
params: any;
}
export interface WebSiteSearch extends ReqPage {
name: string;
}
export interface WebSiteDel {
id: number;
deleteApp: boolean;
deleteBackup: boolean;
}
export interface WebSiteCreateReq {
primaryDomain: string;
type: string;
@ -15,5 +44,6 @@ export namespace WebSite {
export interface Group extends CommonModel {
name: string;
default: boolean;
}
}

View File

@ -1,10 +1,19 @@
import http from '@/api';
import { ResPage } from '../interface';
import { WebSite } from '../interface/website';
export const listGroups = () => {
export const SearchWebSites = (req: WebSite.WebSiteSearch) => {
return http.post<ResPage<WebSite.WebSite>>(`/websites/search`, req);
};
export const ListGroups = () => {
return http.get<WebSite.Group[]>(`/websites/groups`);
};
export const CreateWebsite = (req: WebSite.WebSiteCreateReq) => {
return http.post<any>(`/websites`, req);
};
export const DeleteWebsite = (req: WebSite.WebSiteDel) => {
return http.post<any>(`/websites/del`, req);
};

View File

@ -633,6 +633,7 @@ export default {
description: '一个现代化的 Linux 面板工具',
},
app: {
app: '应用',
installed: '已安装',
all: '全部',
version: '版本',
@ -676,5 +677,8 @@ export default {
app_new: '新装应用',
app_installed: '已装应用',
create: '创建网站',
delete: '删除网站',
deleteApp: '删除应用',
deleteBackup: '删除备份',
},
};

View File

@ -54,7 +54,7 @@
import { GetApp, GetAppDetail } from '@/api/modules/app';
import LayoutContent from '@/layout/layout-content.vue';
import { onMounted, ref } from 'vue';
import Install from './install.vue';
import Install from './install/index.vue';
interface OperateProps {
id: number;

View File

@ -4,26 +4,7 @@
<el-form-item :label="$t('app.name')" prop="NAME">
<el-input v-model="form['NAME']"></el-input>
</el-form-item>
<div v-for="(f, index) in installData.params?.formFields" :key="index">
<el-form-item :label="f.labelZh" :prop="f.envKey">
<el-input v-model="form[f.envKey]" v-if="f.type == 'text'" :type="f.type"></el-input>
<el-input v-model.number="form[f.envKey]" v-if="f.type == 'number'" :type="f.type"></el-input>
<el-input
v-model="form[f.envKey]"
v-if="f.type == 'password'"
:type="f.type"
show-password
></el-input>
<el-select v-model="form[f.envKey]" v-if="f.type == 'service'">
<el-option
v-for="service in services"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
</el-form-item>
</div>
<Params v-model:form="form" v-model:params="installData.params" v-model:rules="rules"></Params>
</el-form>
<template #footer>
<span class="dialog-footer">
@ -38,12 +19,12 @@
<script lang="ts" setup name="appInstall">
import { App } from '@/api/interface/app';
import { InstallApp, GetAppService } from '@/api/modules/app';
import { InstallApp } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import { getRandomStr } from '@/utils/util';
import { FormInstance, FormRules } from 'element-plus';
import { nextTick, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
import Params from '../params/index.vue';
const router = useRouter();
interface InstallRrops {
@ -55,8 +36,8 @@ const installData = ref<InstallRrops>({
appDetailId: 0,
});
let open = ref(false);
let form = reactive<{ [key: string]: any }>({});
let rules = reactive<FormRules>({
let form = ref<{ [key: string]: any }>({});
let rules = ref<FormRules>({
NAME: [Rules.requiredInput],
});
let loading = false;
@ -66,7 +47,6 @@ const req = reactive({
params: {},
name: '',
});
let services = ref();
const handleClose = () => {
open.value = false;
@ -89,35 +69,9 @@ const resetForm = () => {
const acceptParams = (props: InstallRrops): void => {
installData.value = props;
const params = installData.value.params;
if (params?.formFields != undefined) {
for (const p of params?.formFields) {
if (p.default == 'random') {
form[p.envKey] = getRandomStr(6);
} else {
form[p.envKey] = p.default;
}
if (p.required) {
rules[p.envKey] = [Rules.requiredInput];
}
if (p.key) {
form[p.envKey] = '';
getServices(p.envKey, p.key);
}
}
}
open.value = true;
};
const getServices = async (envKey: string, key: string | undefined) => {
await GetAppService(key).then((res) => {
services.value = res.data;
if (services.value != null) {
form[envKey] = services.value[0].value;
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
@ -125,8 +79,8 @@ const submit = async (formEl: FormInstance | undefined) => {
return;
}
req.appDetailId = installData.value.appDetailId;
req.params = form;
req.name = form['NAME'];
req.params = form.value;
req.name = form.value['NAME'];
InstallApp(req).then(() => {
handleClose();
router.push({ path: '/apps/installed' });

View File

@ -0,0 +1,133 @@
<template>
<div v-for="(p, index) in paramObjs" :key="index">
<el-form-item :label="p.labelZh" :prop="p.prop">
<el-input v-model="form[p.envKey]" v-if="p.type == 'text'" :type="p.type" @change="updateParam"></el-input>
<el-input
v-model.number="form[p.envKey]"
v-if="p.type == 'number'"
:type="p.type"
@change="updateParam"
></el-input>
<el-input
v-model="form[p.envKey]"
v-if="p.type == 'password'"
:type="p.type"
show-password
@change="updateParam"
></el-input>
<el-select v-model="form[p.envKey]" v-if="p.type == 'service'" @change="updateParam">
<el-option
v-for="service in p.services"
:key="service.label"
:value="service.value"
:label="service.label"
></el-option>
</el-select>
</el-form-item>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, reactive, ref } from 'vue';
import { getRandomStr } from '@/utils/util';
import { GetAppService } from '@/api/modules/app';
import { Rules } from '@/global/form-rules';
import { App } from '@/api/interface/app';
interface ParamObj extends App.FromField {
services: App.AppService[];
prop: string;
}
const emit = defineEmits(['update:form', 'update:rules']);
const props = defineProps({
form: {
type: Object,
default: function () {
return {};
},
},
params: {
type: Object,
default: function () {
return {};
},
},
rules: {
type: Object,
default: function () {
return {};
},
},
propStart: {
type: String,
default: '',
},
});
const form = reactive({});
let rules = reactive({});
const params = computed({
get() {
return props.params;
},
set() {},
});
const propStart = computed({
get() {
return props.propStart;
},
set() {},
});
const paramObjs = ref<ParamObj[]>([]);
const updateParam = () => {
emit('update:form', form);
};
const handleParams = () => {
rules = props.rules;
if (params.value != undefined && params.value.formFields != undefined) {
for (const p of params.value.formFields) {
const pObj = p;
pObj.prop = propStart.value + p.envKey;
paramObjs.value.push(pObj);
if (p.default == 'random') {
form[p.envKey] = getRandomStr(6);
} else {
form[p.envKey] = p.default;
}
if (p.required) {
if (p.type === 'service') {
rules[p.envKey] = [Rules.requiredSelect];
} else {
rules[p.envKey] = [Rules.requiredInput];
}
}
if (p.key) {
form[p.envKey] = '';
getServices(p.envKey, p.key, pObj);
}
emit('update:rules', rules);
updateParam();
}
console.log(rules);
console.log(paramObjs);
}
};
const getServices = async (envKey: string, key: string | undefined, pObj: ParamObj) => {
await GetAppService(key).then((res) => {
pObj.services = res.data;
if (res.data.length > 0) {
form[envKey] = res.data[0].value;
updateParam();
}
});
};
onMounted(() => {
handleParams();
});
</script>

View File

@ -1,6 +1,6 @@
<template>
<el-dialog v-model="open" :title="$t('website.create')" width="40%" :before-close="handleClose">
<el-form ref="websiteForm" label-position="right" :model="website" label-width="100px" :rules="rules">
<el-form ref="websiteForm" label-position="right" :model="website" label-width="130px" :rules="rules">
<el-form-item :label="$t('website.type')" prop="type">
<el-select v-model="website.type">
<el-option :label="$t('website.deployment')" value="deployment"></el-option>
@ -19,7 +19,7 @@
</el-form-item>
<div v-if="website.type === 'deployment'">
<el-form-item prop="appType">
<el-radio-group v-model="website.appType">
<el-radio-group v-model="website.appType" @change="changeAppType(website.appType)">
<el-radio :label="'installed'" :value="'installed'">
{{ $t('website.app_installed') }}
</el-radio>
@ -42,6 +42,45 @@
></el-option>
</el-select>
</el-form-item>
<div v-if="website.appType == 'new'">
<el-form-item :label="$t('app.app')" prop="appinstall.appID">
<el-row :gutter="20">
<el-col :span="12">
<el-select v-model="website.appinstall.appID" @change="getApp()">
<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="website.appinstall.version"
@change="getAppDetail(website.appinstall.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>
<el-form-item :label="$t('app.name')" prop="appinstall.name">
<el-input v-model="website.appinstall.name"></el-input>
</el-form-item>
<Params
:key="paramKey"
v-model:form="website.appinstall.params"
v-model:rules="rules.appinstall.params"
:params="appParams"
:propStart="'appinstall.params.'"
></Params>
</div>
</div>
<el-form-item :label="$t('website.primaryDomain')" prop="primaryDomain">
<el-input v-model="website.primaryDomain"></el-input>
@ -59,7 +98,7 @@
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(websiteForm)" :disabled="loading">
<el-button type="primary" @click="submit(websiteForm)" :loading="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
@ -70,53 +109,125 @@
<script lang="ts" setup name="CreateWebSite">
import { App } from '@/api/interface/app';
import { WebSite } from '@/api/interface/website';
import { SearchAppInstalled } from '@/api/modules/app';
import { CreateWebsite, listGroups } from '@/api/modules/website';
import { GetApp, GetAppDetail, SearchApp, SearchAppInstalled } from '@/api/modules/app';
import { CreateWebsite, ListGroups } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import { FormRules, ElDialog, ElForm, FormInstance } from 'element-plus';
import i18n from '@/lang';
import { ElDialog, ElForm, FormInstance, ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
import Params from '@/views/app-store/detail/params/index.vue';
const websiteForm = ref<FormInstance>();
const website = reactive({
const website = ref({
primaryDomain: '',
type: 'deployment',
alias: '',
remark: '',
domains: [],
appType: 'installed',
appInstallID: 0,
appInstallID: null,
webSiteGroupID: 1,
otherDomains: '',
appinstall: {
appID: null,
name: '',
appDetailID: null,
params: {},
version: '',
},
});
let rules = reactive<FormRules>({
let rules = ref({
primaryDomain: [Rules.requiredInput],
alias: [Rules.requiredInput],
type: [Rules.requiredInput],
webSiteGroupID: [Rules.requiredInput],
appInstallID: [Rules.requiredInput],
appType: [Rules.requiredInput],
appinstall: {
name: [Rules.requiredInput],
appID: [Rules.requiredInput],
params: {},
},
});
let open = ref(false);
let loading = ref(false);
let groups = ref<WebSite.Group[]>([]);
let appInstalles = ref<App.AppInstalled[]>([]);
let appReq = reactive({
type: 'website',
page: 1,
pageSize: 20,
});
let apps = ref<App.App[]>([]);
let appVersions = ref<string[]>([]);
let appDetail = ref<App.AppDetail>();
let appParams = ref<App.AppParams>();
let paramKey = ref(1);
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
em('close', false);
};
const searchAppInstalled = () => {
SearchAppInstalled({ type: 'website' }).then((res) => {
appInstalles.value = res.data;
if (res.data.length > 0) {
website.value.appInstallID = res.data[0].id;
}
});
};
const searchApp = () => {
SearchApp(appReq).then((res) => {
apps.value = res.data.items;
if (res.data.items.length > 0) {
website.value.appinstall.appID = res.data.items[0].id;
getApp();
}
});
};
const getApp = () => {
GetApp(website.value.appinstall.appID).then((res) => {
appVersions.value = res.data.versions;
if (res.data.versions.length > 0) {
website.value.appinstall.version = res.data.versions[0];
getAppDetail(res.data.versions[0]);
}
});
};
const getAppDetail = (version: string) => {
GetAppDetail(website.value.appinstall.appID, version).then((res) => {
website.value.appinstall.appDetailID = res.data.id;
appDetail.value = res.data;
appParams.value = res.data.params;
paramKey.value++;
});
};
const acceptParams = async () => {
await listGroups().then((res) => {
if (websiteForm.value) {
websiteForm.value.resetFields();
}
await ListGroups().then((res) => {
groups.value = res.data;
open.value = true;
});
await SearchAppInstalled({ type: 'website' }).then((res) => {
appInstalles.value = res.data;
if (res.data.length > 0) {
website.appInstallID = res.data[0].id;
}
});
searchAppInstalled();
};
const changeAppType = (type: string) => {
if (type === 'installed') {
searchAppInstalled();
} else {
searchApp();
}
};
const submit = async (formEl: FormInstance | undefined) => {
@ -125,7 +236,15 @@ const submit = async (formEl: FormInstance | undefined) => {
if (!valid) {
return;
}
CreateWebsite(website).then(() => {});
loading.value = true;
CreateWebsite(website.value)
.then(() => {
ElMessage.success(i18n.global.t('commons.msg.createSuccess'));
handleClose();
})
.finally(() => {
loading.value = false;
});
});
};

View File

@ -0,0 +1,65 @@
<template>
<el-dialog v-model="open" :title="$t('website.delete')" width="40%" :before-close="handleClose">
<div style="text-align: center">
<el-checkbox v-model="deleteReq.deleteApp" :label="$t('website.deleteApp')" />
<el-checkbox v-model="deleteReq.deleteBackup" :label="$t('website.deleteBackup')" />
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit()" :disabled="loading" :loading="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { DeleteWebsite } from '@/api/modules/website';
import i18n from '@/lang';
import { ElMessage } from 'element-plus';
import { reactive, ref } from 'vue';
// interface DeleteProps {
// id: number;
// }
// const deleteData = ref<DeleteProps>({
// id: 0,
// });
let open = ref(false);
let loading = ref(false);
let deleteReq = reactive({
id: 0,
deleteApp: false,
deleteBackup: false,
});
const em = defineEmits(['close']);
const handleClose = () => {
open.value = false;
em('close', false);
};
const acceptParams = async (id: number) => {
deleteReq.id = id;
open.value = true;
};
const submit = () => {
loading.value = true;
DeleteWebsite(deleteReq)
.then(() => {
handleClose();
ElMessage.success(i18n.global.t('commons.msg.deleteSuccess'));
})
.finally(() => {
loading.value = false;
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -23,29 +23,42 @@
fix
/>
</ComplexTable>
<CreateWebSite ref="createRef"></CreateWebSite>
<CreateWebSite ref="createRef" @close="search"></CreateWebSite>
<DeleteWebsite ref="deleteRef" @close="search"></DeleteWebsite>
</LayoutContent>
</template>
<script lang="ts" setup>
import LayoutContent from '@/layout/layout-content.vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { onMounted, ref } from '@vue/runtime-core';
import { onMounted, reactive, ref } from '@vue/runtime-core';
import router from '@/routers';
import CreateWebSite from './create/index.vue';
import DeleteWebsite from './delete/index.vue';
import { SearchWebSites } from '@/api/modules/website';
import i18n from '@/lang';
import { WebSite } from '@/api/interface/website';
const createRef = ref();
const deleteRef = ref();
const paginationConfig = reactive({
currentPage: 1,
pageSize: 20,
total: 0,
});
const data = ref();
const search = async () => {
data.value = [
{
primaryDomain: 'www.baicu.com',
status: 'Running',
backup: '1',
remark: '主网站',
},
];
const req = {
name: '',
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
SearchWebSites(req).then((res) => {
data.value = res.data.items;
});
};
const openConfig = () => {
@ -57,8 +70,18 @@ const buttons = [
label: '设置',
click: open,
},
{
label: i18n.global.t('app.delete'),
click: function (row: WebSite.WebSite) {
openDelete(row.id);
},
},
];
const openDelete = (id: number) => {
deleteRef.value.acceptParams(id);
};
const openCreate = () => {
createRef.value.acceptParams();
};