feat: 应用安装页面 已安装应用列表

This commit is contained in:
zhengkunwang223 2022-09-26 16:32:40 +08:00 committed by zhengkunwang223
parent da85e416ea
commit f7263934f9
28 changed files with 708 additions and 169 deletions

View File

@ -3,7 +3,7 @@ version: '3'
services:
mysql5.7:
image: mysql:5.7.39
container_name: 1panel-mysql
container_name: ${CONTAINER_NAME}
restart: always
environment:
TZ: ${TZ}

View File

@ -1,52 +0,0 @@
{
"form_fields": [
{
"type": "text",
"label_zh": "时区",
"label_en": "TimeZone",
"required": true,
"default": "Asia/Shanghai",
"env_variable": "TZ"
},
{
"type": "text",
"label_zh": "数据库",
"label_en": "Database",
"required": true,
"default": "db",
"env_variable": "DATABASE"
},
{
"type": "text",
"label_zh": "普通用户",
"label_en": "User",
"required": true,
"default": "mysql",
"env_variable": "USER"
},
{
"type": "password",
"label_zh": "普通用户密码",
"label_en": "Password",
"required": true,
"default": "1qaz@WSX",
"env_variable": "PASSWORD"
},
{
"type": "password",
"label_zh": "Root用户密码",
"label_en": "RootPassword",
"required": true,
"default": "1panel@mysql",
"env_variable": "ROOT_PASSWORD"
},
{
"type": "number",
"label_zh": "端口",
"label_en": "Port",
"required": true,
"default": 3306,
"env_variable": "PORT"
}
]
}

View File

@ -0,0 +1,52 @@
{
"formFields": [
{
"type": "text",
"labelZh": "时区",
"labelEn": "TimeZone",
"required": true,
"default": "Asia/Shanghai",
"envKey": "TZ"
},
{
"type": "text",
"labelZh": "数据库",
"labelEn": "Database",
"required": true,
"default": "db",
"envKey": "DATABASE"
},
{
"type": "text",
"labelZh": "普通用户",
"labelEn": "User",
"required": true,
"default": "mysql",
"envKey": "USER"
},
{
"type": "text",
"labelZh": "普通用户密码",
"labelEn": "Password",
"required": true,
"default": "1qaz@WSX",
"envKey": "PASSWORD"
},
{
"type": "text",
"labelZh": "Root用户密码",
"labelEn": "RootPassword",
"required": true,
"default": "1panel@mysql",
"envKey": "ROOT_PASSWORD"
},
{
"type": "number",
"labelZh": "端口",
"labelEn": "Port",
"required": true,
"default": 3306,
"envKey": "PORT"
}
]
}

View File

@ -61,3 +61,34 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
}
helper.SuccessWithData(c, appDetailDTO)
}
func (b *BaseApi) InstallApp(c *gin.Context) {
var req dto.AppInstallRequest
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
if err := appService.Install(req.AppDetailId, req.Params); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, nil)
}
func (b *BaseApi) PageInstalled(c *gin.Context) {
var req dto.AppInstalledRequest
if err := c.ShouldBindJSON(&req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err)
return
}
total, list, err := appService.PageInstalled(req)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}

View File

@ -18,6 +18,7 @@ type AppDTO struct {
type AppDetailDTO struct {
model.AppDetail
Params interface{} `json:"params"`
}
type AppList struct {
@ -61,3 +62,20 @@ type AppRequest struct {
Name string `json:"name"`
Tags []string `json:"tags"`
}
type AppInstallRequest struct {
AppDetailId uint `json:"appDetailId" validate:"required"`
Params map[string]interface{} `json:"params"`
}
type AppInstalled struct {
model.AppInstall
Total int `json:"total"`
Ready int `json:"ready"`
AppName string `json:"appName"`
Icon string `json:"icon"`
}
type AppInstalledRequest struct {
PageInfo
}

View File

@ -4,7 +4,7 @@ type AppDetail struct {
BaseModel
AppId uint `json:"appId" gorm:"type:integer;not null"`
Version string `json:"version" gorm:"type:varchar(64);not null"`
FormFields string `json:"formFields" gorm:"type:longtext;"`
DockerCompose string `json:"dockerCompose" gorm:"type:longtext;not null"`
Params string `json:"-" gorm:"type:longtext;"`
DockerCompose string `json:"-" gorm:"type:longtext;not null"`
Readme string `json:"readme" gorm:"type:longtext;not null"`
}

View File

@ -0,0 +1,14 @@
package model
type AppInstall struct {
BaseModel
ContainerName string `json:"containerName" gorm:"type:varchar(256);not null"`
Version string `json:"version" gorm:"type:varchar(256);not null"`
AppId uint `json:"appId" gorm:"type:integer;not null"`
AppDetailId uint `json:"appDetailId" gorm:"type:integer;not null"`
Params string `json:"params" gorm:"type:longtext;not null"`
Status string `json:"status" gorm:"type:varchar(256);not null"`
Description string `json:"description" gorm:"type:varchar(256);not null"`
Message string `json:"message" gorm:"type:longtext;not null"`
App App `json:"-"`
}

View File

@ -0,0 +1,39 @@
package repo
import (
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/global"
)
type AppInstallRepo struct{}
func (a AppInstallRepo) GetBy(opts ...DBOption) ([]model.AppInstall, error) {
db := global.DB.Model(&model.AppInstall{})
for _, opt := range opts {
db = opt(db)
}
var install []model.AppInstall
err := db.Find(&install).Error
return install, err
}
func (a AppInstallRepo) Create(install model.AppInstall) error {
db := global.DB.Model(&model.AppInstall{})
return db.Create(&install).Error
}
func (a AppInstallRepo) Save(install model.AppInstall) error {
db := global.DB.Model(&model.AppInstall{})
return db.Save(&install).Error
}
func (a AppInstallRepo) Page(page, size int, opts ...DBOption) (int64, []model.AppInstall, error) {
var apps []model.AppInstall
db := global.DB.Model(&model.AppInstall{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Debug().Limit(size).Offset(size * (page - 1)).Preload("App").Find(&apps).Error
return count, apps, err
}

View File

@ -13,6 +13,7 @@ type RepoGroup struct {
AppTagRepo
TagRepo
AppDetailRepo
AppInstallRepo
}
var RepoGroupApp = new(RepoGroup)

View File

@ -6,13 +6,18 @@ import (
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/app/model"
"github.com/1Panel-dev/1Panel/app/repo"
"github.com/1Panel-dev/1Panel/constant"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/common"
"github.com/1Panel-dev/1Panel/utils/compose"
"github.com/1Panel-dev/1Panel/utils/files"
"github.com/joho/godotenv"
"golang.org/x/net/context"
"os"
"path"
"reflect"
"sort"
"strconv"
)
type AppService struct {
@ -105,6 +110,24 @@ func (a AppService) GetApp(id uint) (dto.AppDTO, error) {
return appDTO, nil
}
func (a AppService) PageInstalled(req dto.AppInstalledRequest) (int64, []dto.AppInstalled, error) {
total, installed, err := appInstallRepo.Page(req.Page, req.PageSize)
if err != nil {
return 0, nil, err
}
installDTOs := []dto.AppInstalled{}
for _, in := range installed {
installDto := dto.AppInstalled{
AppInstall: in,
AppName: in.App.Name,
Icon: in.App.Icon,
}
installDTOs = append(installDTOs, installDto)
}
return total, installDTOs, nil
}
func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO, error) {
var (
@ -117,11 +140,85 @@ func (a AppService) GetAppDetail(appId uint, version string) (dto.AppDetailDTO,
if err != nil {
return appDetailDTO, err
}
paramMap := make(map[string]interface{})
json.Unmarshal([]byte(detail.Params), &paramMap)
appDetailDTO.AppDetail = detail
appDetailDTO.Params = paramMap
return appDetailDTO, nil
}
func (a AppService) Install(appDetailId uint, params map[string]interface{}) error {
appDetail, err := appDetailRepo.GetAppDetail(commonRepo.WithByID(appDetailId))
if err != nil {
return err
}
app, err := appRepo.GetFirst(commonRepo.WithByID(appDetail.AppId))
paramByte, err := json.Marshal(params)
if err != nil {
return err
}
containerName := constant.ContainerPrefix + app.Key + "-" + common.RandStr(6)
appInstall := model.AppInstall{
AppId: appDetail.AppId,
AppDetailId: appDetail.ID,
Version: appDetail.Version,
Status: constant.Installing,
Params: string(paramByte),
ContainerName: containerName,
Message: "",
}
resourceDir := path.Join(global.CONF.System.ResourceDir, "apps", app.Key, appDetail.Version)
installDir := path.Join(global.CONF.System.AppDir, app.Key)
op := files.NewFileOp()
if err := op.CopyDir(resourceDir, installDir); err != nil {
return err
}
installAppDir := path.Join(installDir, appDetail.Version)
containerNameDir := path.Join(installDir, containerName)
if err := op.Rename(installAppDir, containerNameDir); err != nil {
return err
}
composeFilePath := path.Join(containerNameDir, "docker-compose.yml")
envPath := path.Join(containerNameDir, ".env")
envParams := make(map[string]string, len(params))
for k, v := range params {
switch t := v.(type) {
case string:
envParams[k] = t
case float64:
envParams[k] = strconv.FormatFloat(t, 'f', -1, 32)
default:
envParams[k] = t.(string)
}
}
envParams["CONTAINER_NAME"] = containerName
if err := godotenv.Write(envParams, envPath); err != nil {
return err
}
if err := appInstallRepo.Create(appInstall); err != nil {
return err
}
go upApp(composeFilePath, appInstall)
return nil
}
func upApp(composeFilePath string, appInstall model.AppInstall) {
out, err := compose.Up(composeFilePath)
if err != nil {
if out != "" {
appInstall.Message = out
} else {
appInstall.Message = err.Error()
}
appInstall.Status = constant.Error
_ = appInstallRepo.Save(appInstall)
} else {
appInstall.Status = constant.Running
_ = appInstallRepo.Save(appInstall)
}
}
func (a AppService) Sync() error {
//TODO 从 oss 拉取最新列表
var appConfig model.AppConfig
@ -204,11 +301,11 @@ func (a AppService) Sync() error {
continue
}
detail.DockerCompose = string(dockerComposeStr)
formStr, err := os.ReadFile(path.Join(detailPath, "form.json"))
paramStr, err := os.ReadFile(path.Join(detailPath, "params.json"))
if err != nil {
global.LOG.Errorf("get [%s] form.json error: %s", detailPath, err.Error())
}
detail.FormFields = string(formStr)
detail.Params = string(paramStr)
app.Details = append(app.Details, detail)
}
}

View File

@ -30,4 +30,5 @@ var (
appTagRepo = repo.RepoGroupApp.AppTagRepo
appDetailRepo = repo.RepoGroupApp.AppDetailRepo
tagRepo = repo.RepoGroupApp.TagRepo
appInstallRepo = repo.AppInstallRepo{}
)

View File

@ -1,14 +1,13 @@
package service
import (
"crypto/rand"
"fmt"
"github.com/1Panel-dev/1Panel/app/dto"
"github.com/1Panel-dev/1Panel/global"
"github.com/1Panel-dev/1Panel/utils/common"
"github.com/1Panel-dev/1Panel/utils/files"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"io"
"io/fs"
"os"
"path/filepath"
@ -36,14 +35,14 @@ func (f FileService) GetFileTree(op dto.FileOption) ([]dto.FileTree, error) {
return nil, err
}
node := dto.FileTree{
ID: getUuid(),
ID: common.GetUuid(),
Name: info.Name,
Path: info.Path,
}
for _, v := range info.Items {
if v.IsDir {
node.Children = append(node.Children, dto.FileTree{
ID: getUuid(),
ID: common.GetUuid(),
Name: v.Name,
Path: v.Path,
})
@ -177,11 +176,3 @@ func (f FileService) DirSize(req dto.DirSizeReq) (dto.DirSizeRes, error) {
}
return dto.DirSizeRes{Size: size}, nil
}
func getUuid() string {
b := make([]byte, 16)
_, _ = io.ReadFull(rand.Reader, b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

13
backend/constant/app.go Normal file
View File

@ -0,0 +1,13 @@
package constant
const (
Running = "Running"
Warning = "Warning"
Error = "Error"
Stopped = "Stopped"
Installing = "Installing"
)
const (
ContainerPrefix = "1Panel-"
)

View File

@ -276,6 +276,7 @@ github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHW
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=

View File

@ -150,6 +150,6 @@ var AddTableCronjob = &gormigrate.Migration{
var AddTableApp = &gormigrate.Migration{
ID: "20200921-add-table-app",
Migrate: func(tx *gorm.DB) error {
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{})
return tx.AutoMigrate(&model.App{}, &model.AppDetail{}, &model.Tag{}, &model.AppTag{}, &model.AppConfig{}, &model.AppInstall{})
},
}

View File

@ -19,5 +19,7 @@ func (a *AppRouter) InitAppRouter(Router *gin.RouterGroup) {
appRouter.POST("/search", baseApi.AppSearch)
appRouter.GET("/:id", baseApi.GetApp)
appRouter.GET("/detail/:appid/:version", baseApi.GetAppDetail)
appRouter.POST("/install", baseApi.InstallApp)
appRouter.POST("/installed", baseApi.PageInstalled)
}
}

View File

@ -1,6 +1,10 @@
package common
import (
"crypto/rand"
"fmt"
"io"
mathRand "math/rand"
"regexp"
"strconv"
"strings"
@ -38,3 +42,21 @@ func min(a, b int) int {
}
return b
}
func GetUuid() string {
b := make([]byte, 16)
_, _ = io.ReadFull(rand.Reader, b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
func RandStr(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = letters[mathRand.Intn(len(letters))]
}
return string(b)
}

View File

@ -6,7 +6,7 @@ func Up(filePath string) (string, error) {
cmd := exec.Command("docker-compose", "-f", filePath, "up", "-d")
stdout, err := cmd.CombinedOutput()
if err != nil {
return "", err
return string(stdout), err
}
return string(stdout), nil
}

View File

@ -34,7 +34,7 @@ export namespace App {
icon: string;
version: string;
readme: string;
formFields: string;
params: AppParams;
dockerCompose: string;
}
@ -42,4 +42,37 @@ export namespace App {
name: string;
tags: string[];
}
export interface AppParams {
formFields: FromField[];
}
export interface FromField {
type: string;
labelZh: string;
labelEn: string;
required: boolean;
default: any;
envKey: string;
}
export interface AppInstall {
appDetailId: number;
params: any;
}
export interface AppInstalled extends CommonModel {
containerName: string;
version: string;
appId: string;
appDetailId: string;
params: string;
status: string;
description: string;
message: string;
appName: string;
total: number;
ready: number;
icon: string;
}
}

View File

@ -1,4 +1,5 @@
import http from '@/api';
import { ReqPage, ResPage } from '../interface';
import { App } from '../interface/app';
export const SyncApp = () => {
@ -16,3 +17,11 @@ export const GetApp = (id: number) => {
export const GetAppDetail = (id: number, version: string) => {
return http.get<App.AppDetail>('apps/detail/' + id + '/' + version);
};
export const InstallApp = (install: App.AppInstall) => {
return http.post<any>('apps/install', install);
};
export const GetAppInstalled = (info: ReqPage) => {
return http.post<ResPage<App.AppInstalled>>('apps/installed', info);
};

View File

@ -22,7 +22,7 @@
</template>
<div class="complex-table__body">
<fu-table v-bind="$attrs" @selection-change="handleSelectionChange" height="67vh">
<fu-table v-bind="$attrs" @selection-change="handleSelectionChange" max-height="67vh">
<slot></slot>
</fu-table>
</div>

View File

@ -394,5 +394,14 @@ export default {
version: '版本',
detail: '详情',
install: '安装',
author: '作者',
source: '来源',
sync: '同步',
appName: '应用名称',
status: '状态',
container: '容器',
restart: '重启',
up: '启动',
down: '停止',
},
};

View File

@ -0,0 +1,137 @@
<template>
<el-row :gutter="20">
<el-col :span="12">
<el-input v-model="req.name" @blur="searchByName"></el-input>
</el-col>
<el-col :span="12">
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-col>
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
<div @click="getAppDetail(app.id)">
<el-card :body-style="{ padding: '0px' }" class="a-card">
<el-row :gutter="24">
<el-col :span="8">
<div class="icon">
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
</div>
</el-col>
<el-col :span="16">
<div class="a-detail">
<div class="d-name">
<font size="3" style="font-weight: 700">{{ app.name }}</font>
</div>
<div class="d-description">
<font size="1">
<span>
{{ app.shortDesc }}
</span>
</font>
</div>
<div class="d-tag">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
{{ tag.name }}
</el-tag>
</div>
</div>
</el-col>
</el-row>
</el-card>
</div>
</el-col>
</el-row>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import { onMounted, reactive, ref } from 'vue';
import router from '@/routers';
import { SearchApp } from '@/api/modules/app';
let req = reactive({
name: '',
tags: [],
page: 1,
pageSize: 50,
});
let apps = ref<App.App[]>([]);
let tags = ref<App.Tag[]>([]);
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
const getColor = (index: number) => {
return colorArr[index];
};
const search = async (req: App.AppReq) => {
await SearchApp(req).then((res) => {
apps.value = res.data.items;
tags.value = res.data.tags;
});
};
const getAppDetail = (id: number) => {
let params: { [key: string]: any } = {
id: id,
};
router.push({ name: 'AppDetail', params });
};
const changeTag = () => {
search(req);
};
const searchByName = () => {
search(req);
};
onMounted(() => {
search(req);
});
</script>
<style lang="scss">
.header {
padding-bottom: 10px;
}
.a-card {
height: 100px;
margin-top: 10px;
cursor: pointer;
padding: 1px;
.icon {
width: 100%;
height: 80%;
padding: 10%;
margin-top: 5px;
.image {
width: auto;
height: auto;
}
}
.a-detail {
margin-top: 10px;
height: 100%;
width: 100%;
.d-name {
height: 20%;
}
.d-description {
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
}
}
.a-card:hover {
transform: scale(1.1);
}
</style>

View File

@ -28,15 +28,15 @@
</el-option>
</el-select>
</el-descriptions-item>
<el-descriptions-item :label="'来源'">
<el-descriptions-item :label="$t('app.source')">
<el-link @click="toLink(app.source)">
<el-icon><Link /></el-icon>
</el-link>
</el-descriptions-item>
<el-descriptions-item :label="'作者'">{{ app.author }}</el-descriptions-item>
<el-descriptions-item :label="$t('app.author')">{{ app.author }}</el-descriptions-item>
</el-descriptions>
<div>
<el-button type="primary">{{ $t('app.install') }}</el-button>
<el-button @click="openInstall" type="primary">{{ $t('app.install') }}</el-button>
</div>
</div>
</el-col>
@ -46,6 +46,7 @@
<div class="detail" v-loading="loadingDetail">
<v-md-preview :text="appDetail.readme"></v-md-preview>
</div>
<Install ref="installRef"></Install>
</LayoutContent>
</template>
@ -53,6 +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';
interface OperateProps {
id: number;
@ -64,6 +66,7 @@ let app = ref<any>({});
let appDetail = ref<any>({});
let version = ref('');
let loadingDetail = ref(false);
const installRef = ref();
const getApp = () => {
GetApp(props.id).then((res) => {
@ -88,6 +91,14 @@ const toLink = (link: string) => {
window.open(link, '_blank');
};
const openInstall = () => {
let params = {
params: appDetail.value.params,
appDetailId: appDetail.value.id,
};
installRef.value.acceptParams(params);
};
onMounted(() => {
getApp();
});

View File

@ -0,0 +1,94 @@
<template>
<el-dialog v-model="open" :title="$t('app.install')" width="30%">
<el-form ref="paramForm" label-position="left" :model="form" label-width="150px" :rules="rules">
<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' || 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-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose" :disabled="loading">{{ $t('commons.button.cancel') }}</el-button>
<el-button type="primary" @click="submit(paramForm)" :disabled="loading">
{{ $t('commons.button.confirm') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup name="appInstall">
import { App } from '@/api/interface/app';
import { InstallApp } from '@/api/modules/app';
import { Rules } from '@/global/form-rues';
import { FormInstance, FormRules } from 'element-plus';
import { reactive, ref } from 'vue';
interface InstallRrops {
appDetailId: number;
params?: App.AppParams;
}
const installData = ref<InstallRrops>({
appDetailId: 0,
});
let open = ref(false);
let form = reactive<{ [key: string]: any }>({});
let rules = reactive<FormRules>({});
let loading = false;
const paramForm = ref<FormInstance>();
const req = reactive({
appDetailId: 0,
params: {},
});
const em = defineEmits(['close']);
const handleClose = () => {
if (paramForm.value) {
paramForm.value.resetFields();
}
open.value = false;
em('close', open);
};
const acceptParams = (props: InstallRrops): void => {
installData.value = props;
const params = installData.value.params;
if (params?.formFields != undefined) {
for (const p of params?.formFields) {
form[p.envKey] = p.default;
if (p.required) {
rules[p.envKey] = [Rules.requiredInput];
}
}
}
open.value = true;
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
req.appDetailId = installData.value.appDetailId;
req.params = form;
InstallApp(req).then((res) => {
console.log(res);
});
});
};
defineExpose({
acceptParams,
});
</script>

View File

@ -1,108 +1,40 @@
<template>
<LayoutContent>
<el-row :gutter="20">
<el-col :span="12">
<el-input v-model="req.name" @blur="searchByName"></el-input>
</el-col>
<el-col :span="12">
<el-select v-model="req.tags" multiple style="width: 100%" @change="changeTag">
<el-option v-for="item in tags" :key="item.key" :label="item.name" :value="item.key"></el-option>
</el-select>
</el-col>
<!-- <el-button @click="sync">同步</el-button> -->
</el-row>
<el-row :gutter="20">
<el-col v-for="(app, index) in apps" :key="index" :xs="8" :sm="8" :lg="4">
<div @click="getAppDetail(app.id)">
<el-card :body-style="{ padding: '0px' }" class="a-card">
<el-row :gutter="24">
<el-col :span="8">
<div class="icon">
<el-image class="image" :src="'data:image/png;base64,' + app.icon"></el-image>
</div>
</el-col>
<el-col :span="16">
<div class="a-detail">
<div class="d-name">
<font size="3" style="font-weight: 700">{{ app.name }}</font>
</div>
<div class="d-description">
<font size="1">
<span>
{{ app.shortDesc }}
</span>
</font>
</div>
<div class="d-tag">
<el-tag v-for="(tag, ind) in app.tags" :key="ind" round :colr="getColor(ind)">
{{ tag.name }}
</el-tag>
</div>
</div>
</el-col>
</el-row>
</el-card>
<el-col :span="24">
<div style="margin-bottom: 10px">
<el-radio-group v-model="activeName">
<el-radio-button label="all">
{{ $t('app.all') }}
</el-radio-button>
<el-radio-button label="installed">
{{ $t('app.installed') }}
</el-radio-button>
</el-radio-group>
<div style="float: right">
<el-button @click="sync">{{ $t('app.sync') }}</el-button>
</div>
</div>
</el-col>
</el-row>
<Apps v-if="activeName === 'all'"></Apps>
<Installed v-if="activeName === 'installed'"></Installed>
</LayoutContent>
</template>
<script lang="ts" setup>
import { App } from '@/api/interface/app';
import LayoutContent from '@/layout/layout-content.vue';
import { onMounted, reactive, ref } from 'vue';
import router from '@/routers';
// import { SyncApp } from '@/api/modules/app';
import { SearchApp } from '@/api/modules/app';
import { ref } from 'vue';
import { SyncApp } from '@/api/modules/app';
import Apps from './apps/index.vue';
import Installed from './installed/index.vue';
const activeName = ref('all');
let req = reactive({
name: '',
tags: [],
page: 1,
pageSize: 50,
});
let apps = ref<App.App[]>([]);
let tags = ref<App.Tag[]>([]);
const colorArr = ['#6495ED', '#54FF9F', '#BEBEBE', '#FFF68F', '#FFFF00', '#8B0000'];
const getColor = (index: number) => {
return colorArr[index];
};
// const sync = () => {
// SyncApp().then((res) => {
// console.log(res);
// });
// };
const search = async (req: App.AppReq) => {
await SearchApp(req).then((res) => {
apps.value = res.data.items;
tags.value = res.data.tags;
const sync = () => {
SyncApp().then((res) => {
console.log(res);
});
};
const getAppDetail = (id: number) => {
console.log(id);
let params: { [key: string]: any } = {
id: id,
};
router.push({ name: 'AppDetail', params });
};
const changeTag = () => {
search(req);
};
const searchByName = () => {
search(req);
};
onMounted(() => {
search(req);
});
</script>
<style lang="scss">

View File

@ -0,0 +1,85 @@
<template>
<ComplexTable :pagination-config="paginationConfig" :data="data" @search="search">
<el-table-column :label="$t('app.appName')" prop="appName"></el-table-column>
<el-table-column :label="$t('app.version')" prop="version"></el-table-column>
<el-table-column :label="$t('app.container')">
<template #default="{ row }">
{{ row.ready / row.total }}
</template>
</el-table-column>
<el-table-column :label="$t('app.status')">
<template #default="{ row }">
<el-tag>{{ row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="createdAt"
:label="$t('commons.table.date')"
:formatter="dateFromat"
show-overflow-tooltip
/>
<fu-table-operations :ellipsis="10" :buttons="buttons" :label="$t('commons.table.operate')" fixed="right" fix />
</ComplexTable>
</template>
<script lang="ts" setup>
import { GetAppInstalled } from '@/api/modules/app';
import { onMounted, reactive, ref } from 'vue';
import ComplexTable from '@/components/complex-table/index.vue';
import { dateFromat } from '@/utils/util';
import i18n from '@/lang';
let data = ref<any>();
const paginationConfig = reactive({
currentPage: 1,
pageSize: 20,
total: 0,
});
const search = () => {
const req = {
page: paginationConfig.currentPage,
pageSize: paginationConfig.pageSize,
};
GetAppInstalled(req).then((res) => {
data.value = res.data.items;
paginationConfig.total = res.data.total;
});
};
const buttons = [
{
label: i18n.global.t('app.restart'),
},
{
label: i18n.global.t('app.up'),
},
{
label: i18n.global.t('app.down'),
},
];
onMounted(() => {
search();
});
</script>
<style lang="scss">
.i-card {
height: 60px;
cursor: pointer;
.content {
.image {
width: auto;
height: auto;
}
}
}
.i-card:hover {
border: 1px solid;
border-color: $primary-color;
z-index: 1;
}
</style>

View File

@ -64,7 +64,6 @@ const initProcess = () => {
const getKeys = () => {
FileKeys().then((res) => {
console.log(res);
if (res.data.keys.length > 0) {
keys.value = res.data.keys;
initProcess();