mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2024-11-27 20:49:03 +08:00
feat: PHP 运行环境增加扩展管理 (#6352)
This commit is contained in:
parent
2538944701
commit
ba9feb0941
@ -233,3 +233,46 @@ func (b *BaseApi) SyncStatus(c *gin.Context) {
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
||||
// @Tags Runtime
|
||||
// @Summary Get php runtime extension
|
||||
// @Description 获取 PHP 运行环境扩展
|
||||
// @Accept json
|
||||
// @Param id path string true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /runtimes/php/:id/extensions [get]
|
||||
func (b *BaseApi) GetRuntimeExtension(c *gin.Context) {
|
||||
id, err := helper.GetIntParamByKey(c, "id")
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
|
||||
return
|
||||
}
|
||||
res, err := runtimeService.GetPHPExtensions(id)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags Runtime
|
||||
// @Summary Install php extension
|
||||
// @Description 安装 PHP 扩展
|
||||
// @Accept json
|
||||
// @Param request body request.PHPExtensionsCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Router /runtimes/php/extensions/install [post]
|
||||
func (b *BaseApi) InstallPHPExtension(c *gin.Context) {
|
||||
var req request.PHPExtensionInstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := runtimeService.InstallPHPExtension(req)
|
||||
if err != nil {
|
||||
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithOutData(c)
|
||||
}
|
||||
|
@ -70,3 +70,9 @@ type NodeModuleOperateReq struct {
|
||||
type NodeModuleReq struct {
|
||||
ID uint `json:"ID" validate:"required"`
|
||||
}
|
||||
|
||||
type PHPExtensionInstallReq struct {
|
||||
ID uint `json:"ID" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
@ -57,3 +57,16 @@ type NodeModule struct {
|
||||
License string `json:"license"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type SupportExtension struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
Check string `json:"check"`
|
||||
Versions []string `json:"versions"`
|
||||
}
|
||||
|
||||
type PHPExtensionRes struct {
|
||||
Extensions []string `json:"extensions"`
|
||||
SupportExtensions []SupportExtension `json:"supportExtensions"`
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ type Runtime struct {
|
||||
Port int `json:"port"`
|
||||
Message string `json:"message"`
|
||||
CodeDir string `json:"codeDir"`
|
||||
ContainerName string `json:"containerName"`
|
||||
}
|
||||
|
||||
func (r *Runtime) GetComposePath() string {
|
||||
|
@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/cmd/server/nginx_conf"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@ -45,6 +47,9 @@ type IRuntimeService interface {
|
||||
SyncForRestart() error
|
||||
SyncRuntimeStatus() error
|
||||
DeleteCheck(installID uint) ([]dto.AppResource, error)
|
||||
|
||||
GetPHPExtensions(runtimeID uint) (response.PHPExtensionRes, error)
|
||||
InstallPHPExtension(req request.PHPExtensionInstallReq) error
|
||||
}
|
||||
|
||||
func NewRuntimeService() IRuntimeService {
|
||||
@ -101,10 +106,12 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
|
||||
}
|
||||
}
|
||||
}
|
||||
if containerName, ok := create.Params["CONTAINER_NAME"]; ok {
|
||||
if err := checkContainerName(containerName.(string)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
containerName, ok := create.Params["CONTAINER_NAME"]
|
||||
if !ok {
|
||||
return nil, buserr.New("ErrContainerNameIsNull")
|
||||
}
|
||||
if err := checkContainerName(containerName.(string)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
appDetail, err := appDetailRepo.GetFirst(commonRepo.WithByID(create.AppDetailID))
|
||||
@ -124,12 +131,13 @@ func (r *RuntimeService) Create(create request.RuntimeCreate) (*model.Runtime, e
|
||||
}
|
||||
|
||||
runtime := &model.Runtime{
|
||||
Name: create.Name,
|
||||
AppDetailID: create.AppDetailID,
|
||||
Type: create.Type,
|
||||
Image: create.Image,
|
||||
Resource: create.Resource,
|
||||
Version: create.Version,
|
||||
Name: create.Name,
|
||||
AppDetailID: create.AppDetailID,
|
||||
Type: create.Type,
|
||||
Image: create.Image,
|
||||
Resource: create.Resource,
|
||||
Version: create.Version,
|
||||
ContainerName: containerName.(string),
|
||||
}
|
||||
|
||||
switch create.Type {
|
||||
@ -204,35 +212,34 @@ func (r *RuntimeService) Delete(runtimeDelete request.RuntimeDelete) error {
|
||||
if website.ID > 0 {
|
||||
return buserr.New(constant.ErrDelWithWebsite)
|
||||
}
|
||||
if runtime.Resource == constant.ResourceAppstore {
|
||||
projectDir := runtime.GetPath()
|
||||
switch runtime.Type {
|
||||
case constant.RuntimePHP:
|
||||
client, err := docker.NewClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
imageID, err := client.GetImageIDByName(runtime.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if imageID != "" {
|
||||
if err := client.DeleteImage(imageID); err != nil {
|
||||
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
|
||||
}
|
||||
}
|
||||
case constant.RuntimeNode, constant.RuntimeJava, constant.RuntimeGo:
|
||||
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
|
||||
if out != "" {
|
||||
return errors.New(out)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if runtime.Resource != constant.ResourceAppstore {
|
||||
return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
|
||||
}
|
||||
projectDir := runtime.GetPath()
|
||||
if out, err := compose.Down(runtime.GetComposePath()); err != nil && !runtimeDelete.ForceDelete {
|
||||
if out != "" {
|
||||
return errors.New(out)
|
||||
}
|
||||
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
|
||||
return err
|
||||
}
|
||||
if runtime.Type == constant.RuntimePHP {
|
||||
client, err := docker.NewClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Close()
|
||||
imageID, err := client.GetImageIDByName(runtime.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if imageID != "" {
|
||||
if err := client.DeleteImage(imageID); err != nil {
|
||||
global.LOG.Errorf("delete image id [%s] error %v", imageID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := files.NewFileOp().DeleteDir(projectDir); err != nil && !runtimeDelete.ForceDelete {
|
||||
return err
|
||||
}
|
||||
return runtimeRepo.DeleteBy(commonRepo.WithByID(runtimeDelete.ID))
|
||||
}
|
||||
@ -635,3 +642,100 @@ func (r *RuntimeService) SyncRuntimeStatus() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RuntimeService) GetPHPExtensions(runtimeID uint) (response.PHPExtensionRes, error) {
|
||||
var res response.PHPExtensionRes
|
||||
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(runtimeID))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
phpCmd := fmt.Sprintf("docker exec -i %s %s", runtime.ContainerName, "php -m")
|
||||
out, err := cmd2.ExecWithTimeOut(phpCmd, 20*time.Second)
|
||||
if err != nil {
|
||||
if out != "" {
|
||||
return res, errors.New(out)
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
extensions := strings.Split(out, "\n")
|
||||
var cleanExtensions []string
|
||||
for _, ext := range extensions {
|
||||
extStr := strings.TrimSpace(ext)
|
||||
if extStr != "" && extStr != "[Zend Modules]" && extStr != "[PHP Modules]" {
|
||||
cleanExtensions = append(cleanExtensions, extStr)
|
||||
}
|
||||
}
|
||||
res.Extensions = cleanExtensions
|
||||
var phpExtensions []response.SupportExtension
|
||||
if err = json.Unmarshal(nginx_conf.PHPExtensionsJson, &phpExtensions); err != nil {
|
||||
return res, err
|
||||
}
|
||||
for _, ext := range phpExtensions {
|
||||
for _, cExt := range cleanExtensions {
|
||||
if ext.Check == cExt {
|
||||
ext.Installed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
res.SupportExtensions = append(res.SupportExtensions, ext)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (r *RuntimeService) InstallPHPExtension(req request.PHPExtensionInstallReq) error {
|
||||
runtime, err := runtimeRepo.GetFirst(commonRepo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installTask, err := task.NewTaskWithOps(req.Name, task.TaskInstall, task.TaskScopeRuntime, req.TaskID, runtime.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
installTask.AddSubTask("", func(t *task.Task) error {
|
||||
installCmd := fmt.Sprintf("docker exec -i %s %s %s", runtime.ContainerName, "install-ext", req.Name)
|
||||
err = cmd2.ExecWithLogFile(installCmd, 15*time.Minute, t.Task.LogFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
go func() {
|
||||
err = installTask.Execute()
|
||||
if err == nil {
|
||||
envs, err := gotenv.Unmarshal(runtime.Env)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("get runtime env error %v", err)
|
||||
return
|
||||
}
|
||||
extensions, ok := envs["PHP_EXTENSIONS"]
|
||||
exist := false
|
||||
var extensionArray []string
|
||||
if ok {
|
||||
extensionArray = strings.Split(extensions, ",")
|
||||
for _, ext := range extensionArray {
|
||||
if ext == req.Name {
|
||||
exist = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !exist {
|
||||
extensionArray = append(extensionArray, req.Name)
|
||||
envs["PHP_EXTENSIONS"] = strings.Join(extensionArray, ",")
|
||||
if err = gotenv.Write(envs, runtime.GetEnvPath()); err != nil {
|
||||
global.LOG.Errorf("write runtime env error %v", err)
|
||||
return
|
||||
}
|
||||
envStr, err := gotenv.Marshal(envs)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("marshal runtime env error %v", err)
|
||||
return
|
||||
}
|
||||
runtime.Env = envStr
|
||||
_ = runtimeRepo.Save(runtime)
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
@ -43,3 +43,6 @@ var PathAuth []byte
|
||||
|
||||
//go:embed upstream.conf
|
||||
var Upstream []byte
|
||||
|
||||
//go:embed php_extensions.json
|
||||
var PHPExtensionsJson []byte
|
||||
|
135
agent/cmd/server/nginx_conf/php_extensions.json
Normal file
135
agent/cmd/server/nginx_conf/php_extensions.json
Normal file
@ -0,0 +1,135 @@
|
||||
[
|
||||
{
|
||||
"name": "ZendGuardLoader",
|
||||
"description": "用于解密ZendGuard加密脚本!",
|
||||
"check": "ZendGuardLoader",
|
||||
"versions": ["53", "54", "55", "56"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "ionCube",
|
||||
"description": "用于解密ionCube Encoder加密脚本!",
|
||||
"check": "ionCube",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "81", "82"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "fileinfo",
|
||||
"description": "",
|
||||
"check": "fileinfo",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "opcache",
|
||||
"description": "用于加速PHP脚本!",
|
||||
"check": "opcache",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "xcache",
|
||||
"description": "支持脚本缓存和变量缓存!",
|
||||
"check": "xcache",
|
||||
"versions": ["56"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "memcache",
|
||||
"description": "强大的内容缓存器",
|
||||
"check": "memcache",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "memcached",
|
||||
"description": "比memcache支持更多高级功能",
|
||||
"check": "memcached",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "redis",
|
||||
"description": "基于内存亦可持久化的Key-Value数据库",
|
||||
"check": "redis",
|
||||
"versions": [ "56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "mcrypt",
|
||||
"description": "mcrypt加密/解密",
|
||||
"check": "mcrypt",
|
||||
"versions": ["70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "apcu",
|
||||
"description": "脚本缓存器",
|
||||
"check": "apcu",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "imagemagick",
|
||||
"description": "Imagick高性能图形库",
|
||||
"check": "imagick",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "xdebug",
|
||||
"description": "开源的PHP程序调试器",
|
||||
"check": "xdebug",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "imap",
|
||||
"description": "邮件服务器必备",
|
||||
"check": "imap",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "exif",
|
||||
"description": "用于读取图片EXIF信息",
|
||||
"check": "exif",
|
||||
"versions": [ "56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "intl",
|
||||
"description": "提供国际化支持",
|
||||
"check": "intl",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82", "83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "xsl",
|
||||
"description": "xsl解析扩展",
|
||||
"check": "xsl",
|
||||
"versions": ["56", "70", "71", "72", "73", "74", "80", "81", "82"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "mbstring",
|
||||
"description": "mbstring扩展",
|
||||
"check": "mbstring",
|
||||
"versions": ["83"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "Swoole",
|
||||
"description": "异步、协程高性能网络通信引擎",
|
||||
"check": "swoole",
|
||||
"versions": ["70", "71", "72"],
|
||||
"installed": false
|
||||
},
|
||||
{
|
||||
"name": "zstd",
|
||||
"description": "使用 Zstandard 库进行压缩和解压缩的 PHP 扩展",
|
||||
"check": "zstd",
|
||||
"versions": ["70", "71", "72"],
|
||||
"installed": false
|
||||
}
|
||||
]
|
@ -153,6 +153,7 @@ ErrPackageJsonNotFound: "package.json file does not exist"
|
||||
ErrScriptsNotFound: "No scripts configuration item was found in package.json"
|
||||
ErrContainerNameNotFound: "Unable to get container name, please check .env file"
|
||||
ErrNodeModulesNotFound: "The node_modules folder does not exist! Please edit the running environment or wait for the running environment to start successfully"
|
||||
ErrContainerNameIsNull: "Container name cannot be empty"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "The backup account is already being used in a cronjob and cannot be deleted."
|
||||
|
@ -154,6 +154,7 @@ ErrPackageJsonNotFound: "package.json 文件不存在"
|
||||
ErrScriptsNotFound: "沒有在 package.json 中找到 scripts 配置項"
|
||||
ErrContainerNameNotFound: "無法取得容器名稱,請檢查 .env 文件"
|
||||
ErrNodeModulesNotFound: "node_modules 文件夾不存在!請編輯運行環境或者等待運行環境啟動成功"
|
||||
ErrContainerNameIsNull: "容器名稱不能為空"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "該備份帳號已在計劃任務中使用,無法刪除"
|
||||
|
@ -156,6 +156,7 @@ ErrPackageJsonNotFound: "package.json 文件不存在"
|
||||
ErrScriptsNotFound: "没有在 package.json 中找到 scripts 配置项"
|
||||
ErrContainerNameNotFound: "无法获取容器名称,请检查 .env 文件"
|
||||
ErrNodeModulesNotFound: "node_modules 文件夹不存在!请编辑运行环境或者等待运行环境启动成功"
|
||||
ErrContainerNameIsNull: "容器名称不存在"
|
||||
|
||||
#setting
|
||||
ErrBackupInUsed: "该备份账号已在计划任务中使用,无法删除"
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var AddTable = &gormigrate.Migration{
|
||||
ID: "20240902-add-table",
|
||||
ID: "20240903-add-table",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
return tx.AutoMigrate(
|
||||
&model.AppDetail{},
|
||||
|
@ -30,6 +30,9 @@ func (r *RuntimeRouter) InitRouter(Router *gin.RouterGroup) {
|
||||
groupRouter.POST("/php/extensions", baseApi.CreatePHPExtensions)
|
||||
groupRouter.POST("/php/extensions/update", baseApi.UpdatePHPExtensions)
|
||||
groupRouter.POST("/php/extensions/del", baseApi.DeletePHPExtensions)
|
||||
|
||||
groupRouter.GET("/php/:id/extensions", baseApi.GetRuntimeExtension)
|
||||
groupRouter.POST("/php/extensions/install", baseApi.InstallPHPExtension)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -60,6 +60,41 @@ func ExecWithTimeOut(cmdStr string, timeout time.Duration) (string, error) {
|
||||
return stdout.String(), nil
|
||||
}
|
||||
|
||||
func ExecWithLogFile(cmdStr string, timeout time.Duration, outputFile string) error {
|
||||
cmd := exec.Command("bash", "-c", cmdStr)
|
||||
|
||||
outFile, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
cmd.Stdout = outFile
|
||||
cmd.Stderr = outFile
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
after := time.After(timeout)
|
||||
select {
|
||||
case <-after:
|
||||
_ = cmd.Process.Kill()
|
||||
return buserr.New(constant.ErrCmdTimeout)
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExecContainerScript(containerName, cmdStr string, timeout time.Duration) error {
|
||||
cmdStr = fmt.Sprintf("docker exec -i %s bash -c '%s'", containerName, cmdStr)
|
||||
out, err := ExecWithTimeOut(cmdStr, timeout)
|
||||
|
@ -122,4 +122,23 @@ export namespace Runtime {
|
||||
export interface PHPExtensionsDelete {
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface PHPExtensionsRes {
|
||||
extensions: string[];
|
||||
supportExtensions: SupportExtension[];
|
||||
}
|
||||
|
||||
export interface SupportExtension {
|
||||
name: string;
|
||||
description: string;
|
||||
installed: boolean;
|
||||
check: string;
|
||||
versions: string[];
|
||||
}
|
||||
|
||||
export interface PHPExtensionInstall {
|
||||
name: string;
|
||||
id: number;
|
||||
taskID: string;
|
||||
}
|
||||
}
|
||||
|
@ -67,3 +67,11 @@ export const DeletePHPExtensions = (req: Runtime.PHPExtensionsDelete) => {
|
||||
export const SyncRuntime = () => {
|
||||
return http.post(`/runtimes/sync`, {});
|
||||
};
|
||||
|
||||
export const GetPHPExtensions = (id: number) => {
|
||||
return http.get<Runtime.PHPExtensionsRes>(`/runtimes/php/${id}/extensions`);
|
||||
};
|
||||
|
||||
export const InstallPHPExtension = (req: Runtime.PHPExtensionInstall) => {
|
||||
return http.post(`/runtimes/php/extensions/install`, req);
|
||||
};
|
||||
|
@ -195,6 +195,7 @@ const initCodemirror = () => {
|
||||
});
|
||||
let hljsDom = scrollerElement.value.querySelector('.hljs') as HTMLElement;
|
||||
hljsDom.style['min-height'] = '100px';
|
||||
hljsDom.style['max-height'] = '400px';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -152,6 +152,7 @@ const message = {
|
||||
rootInfoErr: "It's already the root directory",
|
||||
resetSuccess: 'Reset successful',
|
||||
creatingInfo: 'Creating, no need for this operation',
|
||||
installSuccess: 'Install successful',
|
||||
},
|
||||
login: {
|
||||
username: 'Username',
|
||||
@ -2434,6 +2435,10 @@ const message = {
|
||||
javaDirHelper: 'The directory must contain jar files, subdirectories are also acceptable',
|
||||
goHelper: 'Please provide a complete start command, for example: go run main.go or ./main',
|
||||
goDirHelper: 'The directory must contain go files or binary files, subdirectories are also acceptable',
|
||||
extension: 'Extension',
|
||||
installExtension: 'Do you confirm to install the extension {0}',
|
||||
loadedExtension: 'Loaded Extension',
|
||||
popularExtension: 'Popular Extension',
|
||||
},
|
||||
process: {
|
||||
pid: 'Process ID',
|
||||
|
@ -152,6 +152,7 @@ const message = {
|
||||
rootInfoErr: '已經是根目錄了',
|
||||
resetSuccess: '重置成功',
|
||||
creatingInfo: '正在創建,無需此操作',
|
||||
installSuccess: '安裝成功',
|
||||
},
|
||||
login: {
|
||||
username: '用戶名',
|
||||
@ -2258,6 +2259,10 @@ const message = {
|
||||
javaDirHelper: '目錄中要包含 jar 包,子目錄中包含也可',
|
||||
goHelper: '請填寫完整啟動命令,例如:go run main.go 或 ./main',
|
||||
goDirHelper: '目錄中要包含 go 文件或者二進制文件,子目錄中包含也可',
|
||||
extension: '擴充',
|
||||
installExtension: '是否確認安裝擴充功能 {0}',
|
||||
loadedExtension: '已載入擴充功能',
|
||||
popularExtension: '常用擴充',
|
||||
},
|
||||
process: {
|
||||
pid: '進程ID',
|
||||
|
@ -152,6 +152,7 @@ const message = {
|
||||
rootInfoErr: '已经是根目录了',
|
||||
resetSuccess: '重置成功',
|
||||
creatingInfo: '正在创建,无需此操作',
|
||||
installSuccess: '安装成功',
|
||||
},
|
||||
login: {
|
||||
username: '用户名',
|
||||
@ -2260,6 +2261,10 @@ const message = {
|
||||
javaDirHelper: '目录中要包含 jar 包,子目录中包含也可',
|
||||
goHelper: '请填写完整启动命令,例如:go run main.go 或 ./main',
|
||||
goDirHelper: '目录中要包含 go 文件或者二进制文件,子目录中包含也可',
|
||||
extension: '扩展',
|
||||
installExtension: '是否确认安装扩展 {0}',
|
||||
loadedExtension: '已加载扩展',
|
||||
popularExtension: '常用扩展',
|
||||
},
|
||||
process: {
|
||||
pid: '进程ID',
|
||||
|
@ -1,112 +0,0 @@
|
||||
<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.key]"
|
||||
v-if="p.type == 'select'"
|
||||
:multiple="p.multiple"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
@change="updateParam"
|
||||
clearable
|
||||
>
|
||||
<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 { getLanguage } from '@/utils/util';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
interface ParamObj extends App.InstallParams {
|
||||
prop: string;
|
||||
}
|
||||
const props = defineProps({
|
||||
form: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: Array<App.InstallParams>,
|
||||
default: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
type: Object,
|
||||
default: function () {
|
||||
return {};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let form = reactive({});
|
||||
let rules = reactive({});
|
||||
const params = computed({
|
||||
get() {
|
||||
return props.params;
|
||||
},
|
||||
set() {},
|
||||
});
|
||||
const emit = defineEmits(['update:form', 'update:rules']);
|
||||
const updateParam = () => {
|
||||
emit('update:form', form);
|
||||
};
|
||||
const paramObjs = ref<ParamObj[]>([]);
|
||||
|
||||
const handleParams = () => {
|
||||
rules = props.rules;
|
||||
if (params.value != undefined) {
|
||||
for (const p of params.value) {
|
||||
form[p.key] = p.value;
|
||||
paramObjs.value.push({
|
||||
prop: p.key,
|
||||
labelEn: p.labelEn,
|
||||
labelZh: p.labelZh,
|
||||
values: p.values,
|
||||
value: p.value,
|
||||
required: p.required,
|
||||
edit: p.edit,
|
||||
key: p.key,
|
||||
rule: p.rule,
|
||||
type: p.type,
|
||||
multiple: p.multiple,
|
||||
});
|
||||
if (p.required) {
|
||||
if (p.type === 'select') {
|
||||
rules[p.key] = [Rules.requiredSelect];
|
||||
} else {
|
||||
rules[p.key] = [Rules.requiredInput];
|
||||
}
|
||||
}
|
||||
}
|
||||
emit('update:rules', rules);
|
||||
updateParam();
|
||||
}
|
||||
};
|
||||
|
||||
const getLabel = (row: ParamObj): string => {
|
||||
const language = getLanguage();
|
||||
if (language == 'zh' || language == 'tw') {
|
||||
return row.labelZh;
|
||||
} else {
|
||||
return row.labelEn;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleParams();
|
||||
});
|
||||
</script>
|
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<DrawerPro v-model="open" :header="$t('runtime.extension')" size="large" :back="handleClose">
|
||||
<el-descriptions title="" border>
|
||||
<el-descriptions-item :label="$t('runtime.loadedExtension')" width="100px">
|
||||
<el-tag v-for="(ext, index) in extensions" :key="index" type="info" class="mr-1 mt-1">{{ ext }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<div class="mt-5">
|
||||
<el-text>{{ $t('runtime.popularExtension') }}</el-text>
|
||||
</div>
|
||||
<ComplexTable :data="supportExtensions" @search="search()" :heightDiff="350" :loading="loading">
|
||||
<el-table-column prop="name" :label="$t('commons.table.name')" width="150" />
|
||||
<el-table-column prop="description" :label="$t('commons.table.description')" />
|
||||
<el-table-column prop="installed" :label="$t('commons.table.status')" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-icon v-if="row.installed" color="green"><Select /></el-icon>
|
||||
<el-icon v-else><CloseBold /></el-icon>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<fu-table-operations
|
||||
:ellipsis="10"
|
||||
width="100px"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
fix
|
||||
/>
|
||||
</ComplexTable>
|
||||
</DrawerPro>
|
||||
<TaskLog ref="taskLogRef" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Runtime } from '@/api/interface/runtime';
|
||||
import { GetPHPExtensions, InstallPHPExtension } from '@/api/modules/runtime';
|
||||
import i18n from '@/lang';
|
||||
import { ref } from 'vue';
|
||||
import { newUUID } from '@/utils/util';
|
||||
|
||||
const open = ref(false);
|
||||
const runtime = ref();
|
||||
const extensions = ref([]);
|
||||
const supportExtensions = ref([]);
|
||||
const loading = ref(false);
|
||||
const taskLogRef = ref();
|
||||
|
||||
const handleClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('commons.operate.install'),
|
||||
click: function (row: Runtime.SupportExtension) {
|
||||
installExtension(row);
|
||||
},
|
||||
show: function (row: Runtime.SupportExtension) {
|
||||
return !row.installed;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const installExtension = async (row: Runtime.SupportExtension) => {
|
||||
ElMessageBox.confirm(i18n.global.t('runtime.installExtension', [row.name]), i18n.global.t('runtime.extension'), {
|
||||
confirmButtonText: i18n.global.t('commons.button.confirm'),
|
||||
cancelButtonText: i18n.global.t('commons.button.cancel'),
|
||||
}).then(async () => {
|
||||
const req = {
|
||||
id: runtime.value.id,
|
||||
name: row.check,
|
||||
taskID: newUUID(),
|
||||
};
|
||||
loading.value = true;
|
||||
try {
|
||||
await InstallPHPExtension(req);
|
||||
taskLogRef.value.openWithTaskID(req.taskID);
|
||||
|
||||
loading.value = false;
|
||||
} catch (error) {}
|
||||
});
|
||||
};
|
||||
|
||||
const search = async () => {
|
||||
try {
|
||||
const res = await GetPHPExtensions(runtime.value.id);
|
||||
extensions.value = res.data.extensions;
|
||||
supportExtensions.value = res.data.supportExtensions;
|
||||
} catch (error) {}
|
||||
};
|
||||
|
||||
const acceptParams = (req: Runtime.Runtime): void => {
|
||||
open.value = true;
|
||||
runtime.value = req;
|
||||
search();
|
||||
};
|
||||
|
||||
defineExpose({ acceptParams });
|
||||
</script>
|
@ -14,7 +14,7 @@
|
||||
{{ $t('runtime.create') }}
|
||||
</el-button>
|
||||
|
||||
<el-button @click="openExtensions">
|
||||
<el-button type="primary" plain @click="openExtensions">
|
||||
{{ $t('php.extensions') }}
|
||||
</el-button>
|
||||
|
||||
@ -94,7 +94,7 @@
|
||||
/>
|
||||
<fu-table-operations
|
||||
:ellipsis="10"
|
||||
width="120px"
|
||||
width="200px"
|
||||
:buttons="buttons"
|
||||
:label="$t('commons.table.operate')"
|
||||
fixed="right"
|
||||
@ -109,6 +109,7 @@
|
||||
<Log ref="logRef" @close="search" />
|
||||
<Extensions ref="extensionsRef" @close="search" />
|
||||
<AppResources ref="checkRef" @close="search" />
|
||||
<ExtManagement ref="extManagementRef" @close="search" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -117,16 +118,17 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue';
|
||||
import { Runtime } from '@/api/interface/runtime';
|
||||
import { DeleteRuntime, RuntimeDeleteCheck, SearchRuntimes } from '@/api/modules/runtime';
|
||||
import { dateFormat, toLowerCase } from '@/utils/util';
|
||||
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import i18n from '@/lang';
|
||||
import RouterMenu from '../index.vue';
|
||||
import Log from '@/components/log-dialog/index.vue';
|
||||
import Extensions from './extensions/index.vue';
|
||||
import AppResources from '@/views/website/runtime/php/check/index.vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { containerPrune } from '@/api/modules/container';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
import ExtManagement from './extension-management/index.vue';
|
||||
import Extensions from './extension-template/index.vue';
|
||||
import AppResources from '@/views/website/runtime/php/check/index.vue';
|
||||
import CreateRuntime from '@/views/website/runtime/php/create/index.vue';
|
||||
import Status from '@/components/status/index.vue';
|
||||
import RouterMenu from '../index.vue';
|
||||
import Log from '@/components/log-dialog/index.vue';
|
||||
|
||||
const paginationConfig = reactive({
|
||||
cacheSizeKey: 'runtime-page-size',
|
||||
@ -144,10 +146,22 @@ let timer: NodeJS.Timer | null = null;
|
||||
const opRef = ref();
|
||||
const logRef = ref();
|
||||
const extensionsRef = ref();
|
||||
|
||||
const extManagementRef = ref();
|
||||
const checkRef = ref();
|
||||
const createRef = ref();
|
||||
const loading = ref(false);
|
||||
const items = ref<Runtime.RuntimeDTO[]>([]);
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
label: i18n.global.t('runtime.extension'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
openExtensionsManagement(row);
|
||||
},
|
||||
disabled: function (row: Runtime.Runtime) {
|
||||
return row.status != 'running';
|
||||
},
|
||||
},
|
||||
{
|
||||
label: i18n.global.t('commons.button.edit'),
|
||||
click: function (row: Runtime.Runtime) {
|
||||
@ -167,9 +181,6 @@ const buttons = [
|
||||
},
|
||||
},
|
||||
];
|
||||
const loading = ref(false);
|
||||
const items = ref<Runtime.RuntimeDTO[]>([]);
|
||||
const createRef = ref();
|
||||
|
||||
const search = async () => {
|
||||
req.page = paginationConfig.currentPage;
|
||||
@ -205,6 +216,10 @@ const openExtensions = () => {
|
||||
extensionsRef.value.acceptParams();
|
||||
};
|
||||
|
||||
const openExtensionsManagement = (row: Runtime.Runtime) => {
|
||||
extManagementRef.value.acceptParams(row);
|
||||
};
|
||||
|
||||
const openDelete = async (row: Runtime.Runtime) => {
|
||||
RuntimeDeleteCheck(row.id).then(async (res) => {
|
||||
const items = res.data;
|
||||
|
@ -1,271 +0,0 @@
|
||||
<template>
|
||||
<div v-for="(p, index) in paramObjs" :key="index">
|
||||
<el-form-item :label="getLabel(p)" :prop="p.prop">
|
||||
<el-input
|
||||
v-model.trim="form[p.envKey]"
|
||||
v-if="p.type == 'text'"
|
||||
:type="p.type"
|
||||
@change="updateParam"
|
||||
:disabled="p.disabled"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model.number="form[p.envKey]"
|
||||
@blur="form[p.envKey] = Number(form[p.envKey])"
|
||||
v-if="p.type == 'number'"
|
||||
maxlength="15"
|
||||
@change="updateParam"
|
||||
:disabled="p.disabled"
|
||||
></el-input>
|
||||
<el-input
|
||||
v-model.trim="form[p.envKey]"
|
||||
v-if="p.type == 'password'"
|
||||
:type="p.type"
|
||||
show-password
|
||||
clearable
|
||||
@change="updateParam"
|
||||
></el-input>
|
||||
<el-select
|
||||
class="p-w-200"
|
||||
v-model="form[p.envKey]"
|
||||
v-if="p.type == 'service'"
|
||||
@change="changeService(form[p.envKey], p.services)"
|
||||
>
|
||||
<el-option
|
||||
v-for="service in p.services"
|
||||
:key="service.label"
|
||||
:value="service.value"
|
||||
:label="service.label"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<span v-if="p.type === 'service' && p.services.length === 0" class="ml-1.5">
|
||||
<el-link type="primary" :underline="false" @click="toPage(p.key)">
|
||||
{{ $t('app.toInstall') }}
|
||||
</el-link>
|
||||
</span>
|
||||
<el-select
|
||||
v-model="form[p.envKey]"
|
||||
v-if="p.type == 'select'"
|
||||
:multiple="p.multiple"
|
||||
:allowCreate="p.allowCreate"
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="service in p.values"
|
||||
:key="service.label"
|
||||
:value="service.value"
|
||||
:label="service.label"
|
||||
></el-option>
|
||||
</el-select>
|
||||
<el-row :gutter="10" v-if="p.type == 'apps'">
|
||||
<el-col :span="12">
|
||||
<el-form-item :prop="p.prop">
|
||||
<el-select
|
||||
v-model="form[p.envKey]"
|
||||
@change="getServices(p.child.envKey, form[p.envKey], p)"
|
||||
class="p-w-200"
|
||||
>
|
||||
<el-option
|
||||
v-for="service in p.values"
|
||||
:label="service.label"
|
||||
:key="service.value"
|
||||
:value="service.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :prop="p.childProp">
|
||||
<el-select
|
||||
v-model="form[p.child.envKey]"
|
||||
v-if="p.child.type == 'service'"
|
||||
@change="changeService(form[p.child.envKey], p.services)"
|
||||
class="p-w-200"
|
||||
>
|
||||
<el-option
|
||||
v-for="service in p.services"
|
||||
:key="service.label"
|
||||
:value="service.value"
|
||||
:label="service.label"
|
||||
>
|
||||
<span>{{ service.label }}</span>
|
||||
<span class="float-right" v-if="service.from != ''">
|
||||
<el-tag v-if="service.from === 'local'">{{ $t('database.local') }}</el-tag>
|
||||
<el-tag v-else type="success">{{ $t('database.remote') }}</el-tag>
|
||||
</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<span v-if="p.child.type === 'service' && p.services.length === 0">
|
||||
<el-link type="primary" :underline="false" @click="toPage(form[p.envKey])">
|
||||
{{ $t('app.toInstall') }}
|
||||
</el-link>
|
||||
</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</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';
|
||||
import { getDBName } from '@/utils/util';
|
||||
|
||||
interface ParamObj extends App.FromField {
|
||||
services: App.AppService[];
|
||||
prop: string;
|
||||
disabled: false;
|
||||
childProp: 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;
|
||||
pObj.disabled = p.disabled;
|
||||
paramObjs.value.push(pObj);
|
||||
if (p.random) {
|
||||
if (p.envKey === 'PANEL_DB_NAME') {
|
||||
form[p.envKey] = p.default + '_' + getDBName(6);
|
||||
} else {
|
||||
form[p.envKey] = p.default + '_' + getRandomStr(6);
|
||||
}
|
||||
} else {
|
||||
form[p.envKey] = p.default;
|
||||
}
|
||||
if (p.required) {
|
||||
if (p.type === 'service' || p.type === 'apps') {
|
||||
rules[p.envKey] = [Rules.requiredSelect];
|
||||
if (p.child) {
|
||||
p.childProp = propStart.value + p.child.envKey;
|
||||
if (p.child.type === 'service') {
|
||||
rules[p.child.envKey] = [Rules.requiredSelect];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rules[p.envKey] = [Rules.requiredInput];
|
||||
}
|
||||
if (p.rule && p.rule != '') {
|
||||
rules[p.envKey].push(Rules[p.rule]);
|
||||
}
|
||||
} else {
|
||||
delete rules[p.envKey];
|
||||
}
|
||||
if (p.type === 'apps') {
|
||||
getServices(p.child.envKey, p.default, p);
|
||||
p.child.services = [];
|
||||
form[p.child.envKey] = '';
|
||||
}
|
||||
if (p.type === 'service') {
|
||||
getServices(p.envKey, p.key, p);
|
||||
p.services = [];
|
||||
form[p.envKey] = '';
|
||||
}
|
||||
emit('update:rules', rules);
|
||||
updateParam();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getServices = async (childKey: string, key: string | undefined, pObj: ParamObj | undefined) => {
|
||||
pObj.services = [];
|
||||
await GetAppService(key).then((res) => {
|
||||
pObj.services = res.data || [];
|
||||
form[childKey] = '';
|
||||
if (res.data && res.data.length > 0) {
|
||||
form[childKey] = res.data[0].value;
|
||||
if (pObj.params) {
|
||||
pObj.params.forEach((param: App.FromParam) => {
|
||||
if (param.key === key) {
|
||||
form[param.envKey] = param.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
changeService(form[childKey], pObj.services);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const changeService = (value: string, services: App.AppService[]) => {
|
||||
services.forEach((item) => {
|
||||
if (item.value === value && item.config) {
|
||||
Object.entries(item.config).forEach(([k, v]) => {
|
||||
if (form.hasOwnProperty(k)) {
|
||||
form[k] = v;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
updateParam();
|
||||
};
|
||||
|
||||
const getLabel = (row: ParamObj): string => {
|
||||
const language = localStorage.getItem('lang') || 'zh';
|
||||
if (language == 'zh' || language == 'tw') {
|
||||
return row.labelZh;
|
||||
} else {
|
||||
return row.labelEn;
|
||||
}
|
||||
};
|
||||
|
||||
const toPage = (key: string) => {
|
||||
window.location.href = '/apps/all?install=' + key;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
handleParams();
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user