add nginx manage

This commit is contained in:
mc0814 2023-05-22 14:38:23 +08:00
parent e987fad091
commit 50b8f75971
11 changed files with 1109 additions and 4 deletions

View File

@ -21,6 +21,7 @@ import (
"net/http"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
@ -59,6 +60,11 @@ func (s Server) Handler() []server.Route {
server.NewRoute("/server/deleteProcess", http.MethodDelete, s.DeleteProcess).Permissions(config.DeleteServerProcess).LogFunc(middleware.AddOPLog),
server.NewRoute("/server/execProcess", http.MethodPost, s.ExecProcess).Permissions(config.ShowServerProcessPage).LogFunc(middleware.AddOPLog),
server.NewRoute("/server/execScript", http.MethodPost, s.ExecScript).Permissions(config.ShowServerScriptPage).LogFunc(middleware.AddOPLog),
server.NewRoute("/server/getNginxConfigList", http.MethodGet, s.GetNginxConfigList).Permissions(config.ShowServerNginxPage),
server.NewRoute("/server/manageNginx", http.MethodPost, s.ManageNginx).Permissions(config.ManageServerNginx).LogFunc(middleware.AddOPLog),
server.NewRoute("/server/getNginxConfigContent", http.MethodPost, s.GetNginxConfContent).Permissions(config.ShowServerNginxPage),
server.NewRoute("/server/editNginxConfig", http.MethodPut, s.EditNginxConfig).Permissions(config.EditNginxConfig).LogFunc(middleware.AddOPLog),
server.NewRoute("/server/copyNginxConfig", http.MethodPut, s.CopyNginxConfig).Permissions(config.CopyNginxConfig).LogFunc(middleware.AddOPLog),
}
}
@ -1242,3 +1248,300 @@ func (Server) ExecScript(gp *server.Goploy) server.Response {
}
return response.JSON{Data: respData}
}
func (Server) GetNginxConfigList(gp *server.Goploy) server.Response {
serverID, err := strconv.ParseInt(gp.URLQuery.Get("serverId"), 10, 64)
srv, err := (model.Server{ID: serverID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
sftpClient, err := sftp.NewClient(client)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer sftpClient.Close()
session, err := client.NewSession()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer session.Close()
nginxPath := gp.URLQuery.Get("dir")
output, err := session.CombinedOutput(path.Join(nginxPath, "sbin", "nginx") + " -t")
if err != nil {
return response.JSON{Code: response.Error, Message: fmt.Sprintf("output: %s", output)}
}
configPath := ""
lines := strings.Split(string(output), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "nginx: the configuration file ") && strings.Contains(line, "syntax is ok") {
configPath = strings.TrimPrefix(line, "nginx: the configuration file ")
configPath = strings.TrimSuffix(configPath, " syntax is ok")
}
}
if configPath == "" {
return response.JSON{Code: response.Error, Message: "can not find nginx config path or config error"}
}
configFile, err := sftpClient.Open(configPath)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer configFile.Close()
configContent, err := io.ReadAll(configFile)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
configFileDir := path.Dir(configPath)
var includeConfigPaths []string
// match the file path in the include directive
re := regexp.MustCompile("(?i)include\\s+([\"']?)(.*?)([\"']?);")
matches := re.FindAllSubmatch(configContent, -1)
for _, match := range matches {
tmpPath := match[2]
includeConfigPaths = append(includeConfigPaths, path.Join(configFileDir, string(tmpPath)))
}
type fileInfo struct {
Name string `json:"name"`
Size int64 `json:"size"`
Mode string `json:"mode"`
ModTime string `json:"modTime"`
Dir string `json:"dir"`
}
var fileList []fileInfo
for _, includeConfigPath := range includeConfigPaths {
fileInfos, err := sftpClient.Glob(includeConfigPath)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
} else {
for _, f := range fileInfos {
fileStat, err := sftpClient.Stat(f)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
if fileStat.Mode()&os.ModeSymlink != 0 {
continue
}
if !fileStat.IsDir() {
fileList = append(fileList, fileInfo{
Name: fileStat.Name(),
Size: fileStat.Size(),
Mode: fileStat.Mode().String(),
ModTime: fileStat.ModTime().Format("2006-01-02 15:04:05"),
Dir: path.Dir(includeConfigPath),
})
}
}
}
}
return response.JSON{
Data: struct {
List []fileInfo `json:"list"`
}{List: fileList},
}
}
func (Server) ManageNginx(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
Path string `json:"path" validate:"required"`
Command string `json:"command" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
nginxPath := path.Join(reqData.Path, "sbin", "nginx")
script := ""
switch reqData.Command {
case "reload":
script = nginxPath + " -s reload"
case "stop":
script = nginxPath + " -s stop"
case "check":
script = nginxPath + " -t"
default:
script = reqData.Command
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer session.Close()
output, err := session.CombinedOutput(script)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
log.Trace(fmt.Sprintf("%s exec nginx cmd %s, result %t, output: %s", gp.UserInfo.Name, script, err == nil, string(output)))
return response.JSON{
Data: struct {
ExecRes bool `json:"execRes"`
Output string `json:"output"`
}{ExecRes: err == nil, Output: string(output)},
}
}
func (Server) GetNginxConfContent(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
Dir string `json:"dir" validate:"required"`
Filename string `json:"filename" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
sftpClient, err := sftp.NewClient(client)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer sftpClient.Close()
configFile, err := sftpClient.Open(path.Join(reqData.Dir, reqData.Filename))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer configFile.Close()
configContent, err := io.ReadAll(configFile)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{
Data: struct {
Content string `json:"content"`
}{Content: string(configContent)},
}
}
func (Server) EditNginxConfig(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
Dir string `json:"dir" validate:"required"`
Filename string `json:"filename" validate:"required"`
Content string `json:"content" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
sftpClient, err := sftp.NewClient(client)
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer sftpClient.Close()
file, err := sftpClient.Create(path.Join(reqData.Dir, reqData.Filename))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer file.Close()
_, err = file.Write([]byte(reqData.Content))
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
return response.JSON{}
}
func (Server) CopyNginxConfig(gp *server.Goploy) server.Response {
type ReqData struct {
ServerID int64 `json:"serverId" validate:"gt=0"`
Dir string `json:"dir" validate:"required"`
SrcName string `json:"srcName" validate:"required"`
DstName string `json:"dstName" validate:"required"`
}
var reqData ReqData
if err := decodeJson(gp.Body, &reqData); err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
srv, err := (model.Server{ID: reqData.ServerID}).GetData()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
client, err := srv.ToSSHConfig().Dial()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer client.Close()
session, err := client.NewSession()
if err != nil {
return response.JSON{Code: response.Error, Message: err.Error()}
}
defer session.Close()
var sshOutbuf, sshErrbuf bytes.Buffer
session.Stdout = &sshOutbuf
session.Stderr = &sshErrbuf
if err = session.Run(fmt.Sprintf("cp %s %s", path.Join(reqData.Dir, reqData.SrcName), path.Join(reqData.Dir, reqData.DstName))); err != nil {
return response.JSON{Code: response.Error, Message: "err: " + err.Error() + ", detail: " + sshErrbuf.String()}
}
return response.JSON{}
}

View File

@ -84,4 +84,8 @@ const (
ShowServerScriptPage = 77
SFTPRenameFile = 78
SFTPEditFile = 79
ShowServerNginxPage = 80
ManageServerNginx = 81
EditNginxConfig = 82
CopyNginxConfig = 83
)

View File

@ -475,6 +475,10 @@ INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALU
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (77, 23, 'ShowServerScriptPage', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (78, 23, 'SFTPRenameFile', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (79, 23, 'SFTPEditFile', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (80, 23, 'ShowServerNginxPage', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (81, 23, 'ManageServerNginx', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (82, 23, 'EditNginxConfig', 0, '');
INSERT IGNORE INTO `permission`(`id`, `pid`, `name`, `sort`, `description`) VALUES (83, 23, 'CopyNginxConfig', 0, '');
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 13);
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 14);
INSERT IGNORE INTO `role_permission`(`role_id`, `permission_id`) VALUES (1, 15);

View File

@ -505,3 +505,98 @@ export class ServerRemoteCrontabList extends Request {
this.param = param
}
}
export class ManageNginx extends Request {
readonly url = '/server/manageNginx'
readonly method = 'post'
readonly timeout = 0
public param: {
serverId: number
path: string
command: string
}
public declare datagram: {
execRes: boolean
output: string
}
constructor(param: ManageNginx['param']) {
super()
this.param = param
}
}
export interface ServerNginxData {
[key: string]: any
modTime: string
mode: string
name: string
size: number
dir: string
}
export class ServerNginxConfigList extends Request {
readonly url = '/server/getNginxConfigList'
readonly method = 'get'
public declare datagram: {
list: ServerNginxData[]
}
public param: {
serverId: number
dir: string
}
constructor(param: ServerNginxConfigList['param']) {
super()
this.param = param
}
}
export class NginxConfigContent extends Request {
readonly url = '/server/getNginxConfigContent'
readonly method = 'post'
public param: {
serverId: number
dir: string
filename: string
}
public declare datagram: {
content: string
}
constructor(param: NginxConfigContent['param']) {
super()
this.param = param
}
}
export class NginxConfigEdit extends Request {
readonly url = '/server/editNginxConfig'
readonly method = 'put'
public param: {
serverId: number
dir: string
filename: string
content: string
}
constructor(param: NginxConfigEdit['param']) {
super()
this.param = param
}
}
export class NginxConfigCopy extends Request {
readonly url = '/server/copyNginxConfig'
readonly method = 'put'
readonly timeout = 0
public param: {
serverId: number
dir: string
srcName: string
dstName: string
}
constructor(param: NginxConfigCopy['param']) {
super()
this.param = param
}
}

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1684729012224" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="983" width="360" height="360" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512 0L68.48 256v512L512 1024l443.52-256V256L512 0z m256 707.84c0 30.08-27.562667 55.04-65.237333 55.04-26.922667 0-57.642667-10.88-76.842667-34.56l-256-304.682667v284.16c0 30.762667-24.32 55.04-54.357333 55.04H312.32c-30.762667 0-55.04-25.6-55.04-55.04V316.16c0-30.08 26.88-55.04 64-55.04 27.562667 0 58.88 10.88 78.08 34.56l254.72 304.682667V316.16c0-30.762667 25.6-55.04 55.04-55.04h3.2c30.72 0 55.04 25.6 55.04 55.04v391.68H768z" fill="" p-id="984"></path></svg>

After

Width:  |  Height:  |  Size: 798 B

View File

@ -166,6 +166,7 @@
"serverProcess": "Process",
"serverCrontab": "Crontab UI",
"serverCron": "Cron",
"serverNginx": "Nginx",
"namespace": "Namespace",
"namespaceSetting": "NS setting",
"roleSetting": "Role setting",
@ -253,7 +254,11 @@
"DeleteServerProcess": "Delete the server process",
"SFTPTransferFile": "Can transfer file via SFTP",
"SFTPDeleteFile": "Can delete file via SFTP",
"ShowServerScriptPage": "Can execute script"
"ShowServerScriptPage": "Can execute script",
"ShowServerNginxPage": "See the server nginx page",
"ManageServerNginx": "Manage nginx process",
"EditNginxConfig": "Edit the nginx config",
"CopyNginxConfig": "Copy the nginx config"
},
"tagsView": {
"refresh": "Refresh",
@ -306,7 +311,9 @@
"advance": "Advanced setting",
"transferFile": "Transfer file",
"sftpFileCount": "items",
"saveTemplate": "Save template"
"saveTemplate": "Save template",
"execTips": "Exec command {command}?",
"nginxStartTips": "Input your nginx start command"
},
"monitorPage": {
"defaultServer": "Follow Host",

View File

@ -148,6 +148,7 @@
"serverProcess": "进程管理",
"serverCrontab": "Crontab UI",
"serverCron": "定时任务",
"serverNginx": "Nginx管理",
"namespace": "空间",
"namespaceSetting": "空间设置",
"roleSetting": "角色设置",
@ -236,7 +237,11 @@
"ShowOperationLogPage": "查看操作日志",
"SFTPTransferFile": "SFTP传输文件",
"SFTPDeleteFile": "SFTP删除文件",
"ShowServerScriptPage": "执行脚本"
"ShowServerScriptPage": "执行脚本",
"ShowServerNginxPage": "查看Nginx管理",
"ManageServerNginx": "Nginx进程管理",
"EditNginxConfig": "编辑Nginx配置文件",
"CopyNginxConfig": "复制Nginx配置文件"
},
"tagsView": {
"refresh": "刷新",
@ -289,7 +294,9 @@
"advance": "高级选项",
"transferFile": "传输文件",
"sftpFileCount": "个项目",
"saveTemplate": "保存模板"
"saveTemplate": "保存模板",
"execTips": "执行{command}命令?",
"nginxStartTips": "请输入nginx的启动命令"
},
"monitorPage": {
"scriptMode": "脚本类型",

View File

@ -78,4 +78,8 @@ export default Object.freeze({
ShowServerScriptPage: 77,
SFTPRenameFile: 78,
SFTPEditFile: 79,
ShowServerNginxPage: 80,
ManageServerNginx: 81,
EditNginxConfig: 82,
CopyNginxConfig: 83,
})

View File

@ -156,6 +156,16 @@ export default <RouteRecordRaw[]>[
permissions: [permission.ShowServerMonitorPage],
},
},
{
path: 'nginx',
name: 'ServerNginx',
component: () => import('@/views/server/nginx/index.vue'),
meta: {
title: 'serverNginx',
icon: 'nginx',
permissions: [permission.ShowServerNginxPage],
},
},
],
},
{

View File

@ -0,0 +1,479 @@
<template>
<el-row class="nginx-container">
<el-row class="nav" align="middle">
<el-row class="nav-path" style="flex: 1">
<el-input
v-model="dir"
placeholder="Please input nginx absolute path"
class="input-with-select"
@keyup.enter="dirOpen(dir)"
>
<template #append>
<el-button :icon="RefreshRight" @click="dirOpen(dir)" />
</template>
</el-input>
</el-row>
<el-row style="margin-left: 10px">
<el-button
:loading="commandLoading"
type="warning"
@click="handleNginxCmd(dir, 'check')"
>
check
</el-button>
<el-button
:loading="commandLoading"
type="primary"
@click="handleNginxCmd(dir, 'reload')"
>
reload
</el-button>
<el-button
:loading="commandLoading"
type="success"
@click="handleNginxCmd(dir, 'start')"
>
start
</el-button>
<el-button
:loading="commandLoading"
type="danger"
@click="handleNginxCmd(dir, 'stop')"
>
stop
</el-button>
</el-row>
</el-row>
<el-row class="operator">
<el-row class="nav-search" align="middle">
<el-dropdown
:disabled="fileFilteredList.length === 0"
@command="handleSort"
>
<el-button text :icon="Sort">
{{ $t('sort') }}
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="default">
{{ $t('default') }}
</el-dropdown-item>
<el-dropdown-item command="nameAsc">
{{ $t('name') }} {{ $t('asc') }}
</el-dropdown-item>
<el-dropdown-item command="nameDesc">
{{ $t('name') }} {{ $t('desc') }}
</el-dropdown-item>
<el-dropdown-item command="sizeAsc">
{{ $t('size') }} {{ $t('asc') }}
</el-dropdown-item>
<el-dropdown-item command="sizeDesc">
{{ $t('size') }} {{ $t('desc') }}
</el-dropdown-item>
<el-dropdown-item command="modTimeAsc">
{{ $t('modifiedTime') }} {{ $t('asc') }}
</el-dropdown-item>
<el-dropdown-item command="modTimeDesc">
{{ $t('modifiedTime') }} {{ $t('desc') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-input
v-model="input"
placeholder="Filter file"
style="flex: 1"
@input="filterFile"
/>
</el-row>
</el-row>
<el-row v-loading="fileListLoading" class="files">
<div style="width: 100%">
<el-empty
v-show="fileFilteredList.length === 0"
description="No result"
></el-empty>
<el-table
v-show="fileFilteredList.length !== 0"
v-loading="fileListLoading"
height="100%"
highlight-current-row
:data="fileFilteredList"
>
<el-table-column prop="name" :label="$t('name')" min-width="120" />
<el-table-column
prop="modTime"
:label="$t('updateTime')"
min-width="100"
align="center"
/>
<el-table-column
prop="operation"
:label="$t('op')"
width="180"
align="center"
>
<template #default="scope">
<Button
type="primary"
:loading="EditProps.editButtonLoading"
:permissions="[permission.EditNginxConfig]"
@click="handleEdit(scope.row)"
>
{{ $t('edit') }}
</Button>
<Button
type="primary"
:permissions="[permission.CopyNginxConfig]"
@click="handleCopy(scope.row)"
>
{{ $t('copy') }}
</Button>
</template>
</el-table-column>
</el-table>
</div>
</el-row>
<el-row class="footer" justify="space-between">
<div>
{{ fileFilteredList.length }} {{ $t('serverPage.sftpFileCount') }}
</div>
</el-row>
<el-dialog
v-model="editDialogVisible"
:title="$t('edit')"
:close-on-click-modal="false"
:fullscreen="$store.state.app.device === 'mobile'"
@close="editDialogVisible = false"
>
<v-ace-editor
v-model:value="fileContent"
lang="nginx"
:theme="isDark ? 'one_dark' : 'github'"
style="height: 500px; width: 100%"
/>
<template #footer>
<el-button
:disabled="EditProps.disabled"
@click="editDialogVisible = false"
>
{{ $t('cancel') }}
</el-button>
<el-button
:loading="EditProps.disabled"
type="primary"
@click="editConfig"
>
{{ $t('confirm') }}
</el-button>
</template>
</el-dialog>
</el-row>
</template>
<script lang="ts">
export default { name: 'NginxExplorer' }
</script>
<script lang="ts" setup>
import { RefreshRight, Sort } from '@element-plus/icons-vue'
import {
ServerData,
ServerNginxConfigList,
ServerNginxData,
NginxConfigContent,
NginxConfigEdit,
NginxConfigCopy,
} from '@/api/server'
import permission from '@/permission'
import { Button } from '@/components/Permission'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, PropType } from 'vue'
import { useI18n } from 'vue-i18n'
import { ManageNginx } from '@/api/server'
import { useDark } from '@vueuse/core'
import { VAceEditor } from 'vue3-ace-editor'
import * as ace from 'ace-builds/src-noconflict/ace'
ace.config.set(
'basePath',
'https://cdn.jsdelivr.net/npm/ace-builds@' + ace.version + '/src-noconflict/'
)
ace.config.set(
'themePath',
'https://cdn.jsdelivr.net/npm/ace-builds@' + ace.version + '/src-noconflict/'
)
const { t } = useI18n()
const emit = defineEmits(['dir-change'])
const props = defineProps({
uuid: {
type: Number,
default: 0,
},
server: {
type: Object as PropType<ServerData>,
required: true,
},
})
const serverId = ref(props.server.id)
const dir = ref('')
const lastDir = ref('')
const fileListLoading = ref(false)
const fileList = ref<ServerNginxData[]>([])
const fileFilteredList = ref<ServerNginxData[]>([])
const input = ref('')
const fileContent = ref('')
const editDialogVisible = ref(false)
const isDark = useDark()
const EditProps = ref({
editButtonLoading: false,
disabled: false,
selectedConfig: {} as ServerNginxData,
})
const commandLoading = ref(false)
function handleSort(command: string) {
let compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return 0
}
switch (command) {
case 'nameAsc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return fileA.name.localeCompare(fileB.name)
}
break
case 'nameDesc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return fileB.name.localeCompare(fileA.name)
}
break
case 'sizeAsc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return fileA.size - fileB.size
}
break
case 'sizeDesc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return fileB.size - fileA.size
}
break
case 'modTimeAsc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return (
new Date(fileA.modTime).getTime() - new Date(fileB.modTime).getTime()
)
}
break
case 'modTimeDesc':
compareFunc = (
fileA: ServerNginxData,
fileB: ServerNginxData
): number => {
return (
new Date(fileB.modTime).getTime() - new Date(fileA.modTime).getTime()
)
}
break
}
fileFilteredList.value.sort(compareFunc)
}
function dirOpen(dir: string) {
lastDir.value = dir
emit('dir-change', dir)
getNginxConfigList(dir)
}
function filterFile(value: string) {
fileFilteredList.value = fileList.value.filter((file) =>
file.name.includes(value)
)
}
function handleCopy(data: ServerNginxData) {
ElMessageBox.prompt('', t('copy') + ' ' + data.name, {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
inputPattern: /.+/,
inputErrorMessage: 'Name required',
})
.then(({ value }) => {
fileListLoading.value = true
new NginxConfigCopy({
serverId: serverId.value,
dir: data.dir,
srcName: data.name,
dstName: value,
})
.request()
.then(() => {
getNginxConfigList(lastDir.value)
})
.finally(() => {
fileListLoading.value = false
})
})
.catch(() => {
ElMessage.info('Cancel')
})
}
function handleEdit(data: any) {
EditProps.value.editButtonLoading = true
EditProps.value.selectedConfig = data
new NginxConfigContent({
serverId: serverId.value,
dir: data.dir,
filename: data.name,
})
.request()
.then((response) => {
editDialogVisible.value = true
fileContent.value = response.data.content
})
.finally(() => {
EditProps.value.editButtonLoading = false
})
}
function getNginxConfigList(dir: string) {
fileListLoading.value = true
fileList.value = []
new ServerNginxConfigList({ serverId: serverId.value, dir: dir })
.request()
.then((response) => {
fileFilteredList.value = fileList.value = response.data.list
})
.finally(() => {
fileListLoading.value = false
})
}
async function handleNginxCmd(dir: string, command: string) {
if (command === 'start') {
await ElMessageBox.prompt(t('serverPage.nginxStartTips'), 'start', {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
inputPattern: /.+/,
inputErrorMessage: 'command required',
})
.then(({ value }) => {
command = value
})
.catch(() => {
ElMessage.info('Cancel')
command = ''
})
}
if (command !== '') {
ElMessageBox.confirm(t('serverPage.execTips', { command }), t('tips'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning',
})
.then(() => {
commandLoading.value = true
new ManageNginx({
serverId: serverId.value,
path: dir,
command,
})
.request()
.then((response) => {
ElMessage.success(
response.data.output === '' ? 'Success' : response.data.output
)
})
.finally(() => {
commandLoading.value = false
})
})
.catch(() => {
ElMessage.info('Cancel')
})
}
}
function editConfig() {
const file = EditProps.value.selectedConfig
EditProps.value.disabled = true
new NginxConfigEdit({
serverId: serverId.value,
dir: file.dir,
content: fileContent.value,
filename: file.name,
})
.request()
.then(() => {
editDialogVisible.value = false
ElMessage.success('Success')
})
.finally(() => {
EditProps.value.disabled = false
})
}
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
.nginx-container {
flex-direction: column;
width: 100%;
flex: 1;
min-height: 1px;
.nav {
padding: 10px;
border-bottom: 1px solid var(--el-border-color);
}
.operator {
padding: 5px 10px;
border-bottom: 1px solid var(--el-border-color);
.nav-search {
margin-left: auto;
}
}
.files {
flex: 1;
overflow-y: auto;
@include scrollBar();
}
.footer {
padding: 8px 15px;
font-size: 13px;
color: var(--el-text-color-regular);
}
}
@media only screen and (max-device-width: 400px) {
.nginx-container {
.nav {
flex-direction: column;
.el-row {
margin-top: 10px;
}
}
}
}
</style>
<style lang="scss">
.file-detail .el-dialog__body {
padding: 5px 20px;
}
</style>

View File

@ -0,0 +1,191 @@
<template>
<el-row class="app-container">
<el-row class="tabs-container">
<el-scrollbar style="width: 100%">
<el-row class="tabs" justify="start" align="middle">
<div
v-for="(item, index) in serverList"
:key="item.uuid"
class="tabs-item"
:class="item.uuid === currentUUID ? 'tabs-item-selected' : ''"
>
<el-row>
<div class="tabs-item-serial" @click="selectTab(item)">
{{ index + 1 }}
</div>
<div
class="tabs-item-name"
:title="`${item.server.name} : ${item.dir}`"
@click="selectTab(item)"
>
{{ item.server.name }} : {{ item.dir }}
</div>
<el-button
link
style="font-size: 14px; padding-left: 8px"
@click="deleteTab(item, index)"
>
x
</el-button>
</el-row>
{{ item }}
</div>
<div class="tabs-plus">
<el-select
v-model="serverId"
style="width: 200px"
filterable
clearable
@change="selectServer"
>
<el-option
v-for="item in serverOption"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</div>
<div class="tabs-placeholder"></div>
</el-row>
</el-scrollbar>
</el-row>
<explorer
v-for="item in serverList"
v-show="item.uuid === currentUUID"
:key="item.uuid"
:server="item.server"
@dir-change="handleDirChange"
></explorer>
</el-row>
</template>
<script lang="ts">
export default { name: 'ServerNginx' }
</script>
<script lang="ts" setup>
import explorer from './explorer.vue'
import { ServerOption, ServerData } from '@/api/server'
import { ref } from 'vue'
interface nginx {
uuid: number
server: ServerData
dir: string
}
const currentUUID = ref(0)
const serverOption = ref<ServerOption['datagram']['list']>([])
const serverId = ref('')
const serverList = ref<nginx[]>([])
const selectedNginx = ref<nginx>({} as nginx)
function handleDirChange(dir: string) {
selectedNginx.value.dir = dir
}
getServerOption()
function getServerOption() {
new ServerOption().request().then((response) => {
serverOption.value = response.data.list
})
}
function selectTab(nginx: nginx) {
currentUUID.value = nginx.uuid
selectedNginx.value = nginx
}
function deleteTab(nginx: nginx, index: number) {
serverList.value.splice(index, 1)
if (currentUUID.value === nginx.uuid) {
currentUUID.value =
serverList.value.length === 0
? 0
: serverList.value[serverList.value.length - 1].uuid
}
}
function selectServer(value: number) {
const server =
serverOption.value.find((_) => _.id === value) || ({} as ServerData)
if (serverList.value.length === 0) {
currentUUID.value = 0
} else {
currentUUID.value = serverList.value[serverList.value.length - 1].uuid + 1
}
const serverTab = { uuid: currentUUID.value, server, dir: '' }
serverList.value.push(serverTab)
selectTab(serverTab)
serverId.value = ''
}
</script>
<style lang="scss" scoped>
@import '@/styles/mixin.scss';
.app-container {
background-color: var(--el-bg-color);
border: 1px solid var(--el-border-color);
}
.tabs-container {
width: 100%;
height: 46px;
.tabs {
height: auto;
flex-wrap: nowrap;
&-item {
font-size: 14px;
flex-shrink: 0;
width: 195px;
height: 46px;
line-height: 46px;
padding: 0 10px;
border-bottom: 1px solid var(--el-border-color);
background-color: var(--el-disabled-bg-color);
&:not(:last-child) {
border-right: 1px solid var(--el-border-color);
}
&-serial {
cursor: pointer;
color: var(--el-text-color-secondary);
text-align: center;
font-weight: 600;
width: 20px;
}
&-name {
flex: 1;
cursor: pointer;
text-overflow: ellipsis;
overflow: hidden;
padding-left: 5px;
white-space: nowrap;
display: inline-block;
}
&-selected {
border-bottom: none;
background-color: var(--el-bg-color);
}
}
&-placeholder {
height: 46px;
flex: 1;
border-bottom: 1px solid var(--el-border-color);
}
&-plus {
text-align: center;
border-bottom: 1px solid var(--el-border-color);
border-right: 1px solid var(--el-border-color);
}
}
}
</style>
<style lang="scss">
.tabs-plus {
.el-input__wrapper {
border-radius: 0px !important;
box-shadow: none;
}
.el-input__inner {
height: 43px !important;
}
}
</style>