Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
777f7b3758 | ||
|
|
947fa0fea3 | ||
|
|
7109e89fb4 | ||
|
|
70e7b1d544 | ||
|
|
613f86155c | ||
|
|
8d0b58e48f | ||
|
|
8cea9dc977 | ||
|
|
471acfdf00 | ||
|
|
8ed42131d0 | ||
|
|
18c605354a | ||
|
|
8c04411458 | ||
|
|
9a8d1d05cd | ||
|
|
1cbaf9c424 | ||
|
|
537c2fc108 |
15
.env.example
Normal file
15
.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
SERVICE_PORT=1081
|
||||
VOLUME_BASE=/data/orion-visor-space/docker-volumes
|
||||
|
||||
MYSQL_HOST=mysql
|
||||
MYSQL_PORT=3306
|
||||
MYSQL_DATABASE=orion_visor
|
||||
MYSQL_USER=orion
|
||||
MYSQL_PASSWORD=Data@123456
|
||||
MYSQL_ROOT_PASSWORD=Data@123456
|
||||
|
||||
REDIS_HOST=redis
|
||||
REDIS_PASSWORD=Data@123456
|
||||
|
||||
SECRET_KEY=uQeacXV8b3isvKLK
|
||||
DEMO_MODE=false
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,3 +33,4 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
.env
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
href="https://gitee.com/dromara/orion-visor/stargazers">
|
||||
<img src="https://gitee.com/dromara/orion-visor/badge/star.svg?theme=dark" alt="star" />
|
||||
<img src="https://gitee.com/dromara/orion-visor/badge/star.svg?theme=gvp" alt="star" />
|
||||
</a>
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
href="https://gitee.com/dromara/orion-visor/members">
|
||||
<img src="https://gitee.com/dromara/orion-visor/badge/fork.svg?theme=dark" alt="fork" />
|
||||
<img src="https://gitee.com/dromara/orion-visor/badge/fork.svg?theme=gvp" alt="fork" />
|
||||
</a>
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
service:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 1081:80
|
||||
@@ -32,7 +32,7 @@ services:
|
||||
- mysql
|
||||
- redis
|
||||
mysql:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 3307:3306
|
||||
@@ -52,7 +52,7 @@ services:
|
||||
retries: 10
|
||||
start_period: 3s
|
||||
redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 6380:6379
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
service:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 1081:80
|
||||
- ${SERVICE_PORT:-1081}:80
|
||||
environment:
|
||||
- MYSQL_HOST=mysql
|
||||
- MYSQL_PORT=3306
|
||||
- MYSQL_DATABASE=orion_visor
|
||||
- MYSQL_USER=root
|
||||
- MYSQL_PASSWORD=Data@123456
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PASSWORD=Data@123456
|
||||
- SECRET_KEY=uQeacXV8b3isvKLK
|
||||
- DEMO_MODE=false
|
||||
- MYSQL_HOST=${MYSQL_HOST:-mysql}
|
||||
- MYSQL_PORT=${MYSQL_PORT:-3306}
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-orion_visor}
|
||||
- MYSQL_USER=${MYSQL_USER:-root}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-Data@123456}
|
||||
- REDIS_HOST=${REDIS_HOST:-redis}
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-Data@123456}
|
||||
- SECRET_KEY=${SECRET_KEY:-uQeacXV8b3isvKLK}
|
||||
- DEMO_MODE=${DEMO_MODE:-false}
|
||||
volumes:
|
||||
- /data/orion-visor-space/docker-volumes/service/root-orion:/root/orion
|
||||
- ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/service/root-orion:/root/orion
|
||||
healthcheck:
|
||||
test: [ "CMD", "curl", "http://127.0.0.1:9200/orion-visor/api/server/bootstrap/health" ]
|
||||
interval: 3s
|
||||
@@ -32,19 +32,19 @@ services:
|
||||
- mysql
|
||||
- redis
|
||||
mysql:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 3307:3306
|
||||
environment:
|
||||
- MYSQL_DATABASE=orion_visor
|
||||
- MYSQL_USER=orion
|
||||
- MYSQL_PASSWORD=Data@123456
|
||||
- MYSQL_ROOT_PASSWORD=Data@123456
|
||||
- MYSQL_DATABASE=${MYSQL_DATABASE:-orion_visor}
|
||||
- MYSQL_USER=${MYSQL_USER:-orion}
|
||||
- MYSQL_PASSWORD=${MYSQL_PASSWORD:-Data@123456}
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-Data@123456}
|
||||
volumes:
|
||||
- /data/orion-visor-space/docker-volumes/mysql/var-lib-mysql:/var/lib/mysql
|
||||
- /data/orion-visor-space/docker-volumes/mysql/var-lib-mysql-files:/var/lib/mysql-files
|
||||
- /data/orion-visor-space/docker-volumes/mysql/etc-mysql:/etc/mysql
|
||||
- ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/mysql/var-lib-mysql:/var/lib/mysql
|
||||
- ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/mysql/var-lib-mysql-files:/var/lib/mysql-files
|
||||
- ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/mysql/etc-mysql:/etc/mysql
|
||||
healthcheck:
|
||||
test: [ "CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/3306" ]
|
||||
interval: 3s
|
||||
@@ -52,14 +52,14 @@ services:
|
||||
retries: 10
|
||||
start_period: 3s
|
||||
redis:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.3
|
||||
privileged: true
|
||||
ports:
|
||||
- 6380:6379
|
||||
environment:
|
||||
- REDIS_PASSWORD=Data@123456
|
||||
- REDIS_PASSWORD=${REDIS_PASSWORD:-Data@123456}
|
||||
volumes:
|
||||
- /data/orion-visor-space/docker-volumes/redis/data:/data
|
||||
- ${VOLUME_BASE:-/data/orion-visor-space/docker-volumes}/redis/data:/data
|
||||
command: sh -c "redis-server /usr/local/redis.conf --requirepass $${REDIS_PASSWORD}"
|
||||
healthcheck:
|
||||
test: [ "CMD", "redis-cli", "--raw", "incr", "ping" ]
|
||||
@@ -68,7 +68,7 @@ services:
|
||||
retries: 10
|
||||
start_period: 3s
|
||||
adminer:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:2.1.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:2.1.3
|
||||
ports:
|
||||
- 8081:8080
|
||||
depends_on:
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
docker compose down
|
||||
# demo 启动
|
||||
if [ "$1" == "demo" ]; then
|
||||
sed -i 's/DEMO_MODE=false/DEMO_MODE=true/g' docker-compose.yml
|
||||
sed -i 's/\${DEMO_MODE:-false}/true/g' docker-compose.yml
|
||||
fi
|
||||
docker compose up -d --remove-orphans
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#/bin/bash
|
||||
version=2.1.2
|
||||
version=2.1.3
|
||||
docker build -t orion-visor-adminer:${version} .
|
||||
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:${version}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#/bin/bash
|
||||
version=2.1.2
|
||||
version=2.1.3
|
||||
cp -r ../../sql ./sql
|
||||
docker build -t orion-visor-mysql:${version} .
|
||||
rm -rf ./sql
|
||||
docker tag orion-visor-mysql:${version} registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:${version}
|
||||
|
||||
6
docker/push.sh
Normal file
6
docker/push.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#/bin/bash
|
||||
version=2.1.3
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:${version}
|
||||
@@ -1,5 +1,4 @@
|
||||
#/bin/bash
|
||||
version=2.1.2
|
||||
version=2.1.3
|
||||
docker build -t orion-visor-redis:${version} .
|
||||
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:${version}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#/bin/bash
|
||||
version=2.1.2
|
||||
version=2.1.3
|
||||
mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar
|
||||
mv ../../orion-visor-ui/dist ./dist
|
||||
docker build -t orion-visor-service:${version} .
|
||||
rm -rf ./orion-visor-launch.jar
|
||||
rm -rf ./dist
|
||||
docker tag orion-visor-service:${version} registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:${version}
|
||||
docker push registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:${version}
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
href="https://gitee.com/lijiahangmax/orion-visor/stargazers">
|
||||
<img src="https://gitee.com/lijiahangmax/orion-visor/badge/star.svg?theme=dark" alt="star" />
|
||||
<img src="https://gitee.com/lijiahangmax/orion-visor/badge/star.svg?theme=gvp" alt="star" />
|
||||
</a>
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
href="https://gitee.com/lijiahangmax/orion-visor/members">
|
||||
<img src="https://gitee.com/lijiahangmax/orion-visor/badge/fork.svg?theme=dark" alt="fork" />
|
||||
<img src="https://gitee.com/lijiahangmax/orion-visor/badge/fork.svg?theme=gvp" alt="fork" />
|
||||
</a>
|
||||
<a target="_blank"
|
||||
style="text-decoration: none !important;"
|
||||
|
||||
@@ -21,22 +21,35 @@ Dashboard 修改)
|
||||
|
||||
```shell
|
||||
# github
|
||||
git clone https://github.com/lijiahangmax/orion-visor
|
||||
git clone --depth=1 https://github.com/lijiahangmax/orion-visor
|
||||
# gitee
|
||||
git clone https://gitee.com/lijiahangmax/orion-visor
|
||||
git clone --depth=1 https://gitee.com/lijiahangmax/orion-visor
|
||||
```
|
||||
|
||||
### 构建镜像
|
||||
### 拉取镜像
|
||||
|
||||
```
|
||||
# 进入仓库目录
|
||||
cd orion-visor
|
||||
# 修改 docker-compose.yml (建议修改)
|
||||
# MYSQL_USER mysql 用户名
|
||||
# MYSQL_PASSWORD mysql 用户密码
|
||||
# MYSQL_ROOT_PASSWORD mysql root 密码
|
||||
# REDIS_PASSWORD redis 密码
|
||||
# SECRET_KEY 加密密钥
|
||||
# 创建名为 .env 的 .env.example 副本
|
||||
cp .env.example .env
|
||||
# 将其中的值删除以保持默认或将其修改为你喜欢的值
|
||||
# SERVICE_PORT 你希望服务监听的端口
|
||||
# VOLUME_BASE 你希望数据持久化保存的目录, 如果不提前创建将以 docker 进程宿主身份创建(通常是 root)
|
||||
|
||||
# MYSQL_HOST mysql 服务所在的主机, 如果你没有现有的 MySQL 请保持值为 mysql, 如果你有自部署的请在 docker-compose.yml 中移除 services.mysql 以节约性能
|
||||
# MYSQL_PORT mysql 监听的端口
|
||||
# MYSQL_DATABASE mysql 数据库
|
||||
# MYSQL_USER mysql 用户名
|
||||
# MYSQL_PASSWORD mysql 用户密码
|
||||
# MYSQL_ROOT_PASSWORD mysql root 密码
|
||||
|
||||
# REDIS_HOST redis 服务所在的主机, 如果你没有现有的 Redis 请保持值为 redis, 如果你有自部署的请在 docker-compose.yml 中移除 services.redis 以节约性能
|
||||
# REDIS_PASSWORD redis 密码
|
||||
|
||||
# SECRET_KEY 加密密钥
|
||||
# 拉取远程镜像
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
### 启动
|
||||
@@ -45,7 +58,7 @@ cd orion-visor
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### 修改加密方式
|
||||
### 修改 MySQL 账户的加密方式
|
||||
|
||||
```
|
||||
访问 adminer: http://localhost:8081
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
<url>https://github.com/dromara/orion-visor</url>
|
||||
|
||||
<properties>
|
||||
<revision>2.1.2</revision>
|
||||
<revision>2.1.3</revision>
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
|
||||
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>
|
||||
<orion.kit.version>1.0.7</orion.kit.version>
|
||||
<orion.kit.version>1.0.8</orion.kit.version>
|
||||
<aspectj.version>1.9.7</aspectj.version>
|
||||
<lombok.version>1.18.26</lombok.version>
|
||||
<springdoc.version>1.6.15</springdoc.version>
|
||||
|
||||
@@ -14,7 +14,7 @@ public interface AppConst extends OrionConst {
|
||||
/**
|
||||
* 同 ${orion.version} 迭代时候需要手动更改
|
||||
*/
|
||||
String VERSION = "2.1.2";
|
||||
String VERSION = "2.1.3";
|
||||
|
||||
/**
|
||||
* 同 ${spring.application.name}
|
||||
|
||||
@@ -109,4 +109,6 @@ public interface ErrorMessage {
|
||||
|
||||
String CLIENT_ABORT = "手动中断";
|
||||
|
||||
String UNABLE_DOWNLOAD_FOLDER = "无法下载文件夹";
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public class PageRequest implements IPageRequest {
|
||||
@Schema(description = "页码")
|
||||
private int page;
|
||||
|
||||
@Range(min = 1, max = 100, groups = Page.class)
|
||||
@Range(min = 1, max = 200, groups = Page.class)
|
||||
@Schema(description = "大小")
|
||||
private int limit;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<template #extra="{ record }">
|
||||
<a-space>
|
||||
<!-- 更多操作 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<icon-more class="card-extra-icon" />
|
||||
<template #content>
|
||||
<!-- 修改 -->
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.orion.visor.module.asset.handler.host.terminal.TerminalMessageDispatc
|
||||
import com.orion.visor.module.asset.handler.host.transfer.TransferMessageDispatcher;
|
||||
import com.orion.visor.module.asset.interceptor.ExecLogTailInterceptor;
|
||||
import com.orion.visor.module.asset.interceptor.TerminalAccessInterceptor;
|
||||
import com.orion.visor.module.asset.interceptor.TerminalTransferInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
@@ -28,6 +29,9 @@ public class AssetWebSocketConfiguration implements WebSocketConfigurer {
|
||||
@Resource
|
||||
private TerminalAccessInterceptor terminalAccessInterceptor;
|
||||
|
||||
@Resource
|
||||
private TerminalTransferInterceptor terminalTransferInterceptor;
|
||||
|
||||
@Resource
|
||||
private ExecLogTailInterceptor execLogTailInterceptor;
|
||||
|
||||
@@ -42,13 +46,13 @@ public class AssetWebSocketConfiguration implements WebSocketConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
// 终端
|
||||
// 终端会话
|
||||
registry.addHandler(terminalMessageDispatcher, prefix + "/host/terminal/{accessToken}")
|
||||
.addInterceptors(terminalAccessInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
// 文件传输
|
||||
registry.addHandler(transferMessageDispatcher, prefix + "/host/transfer/{accessToken}")
|
||||
.addInterceptors(terminalAccessInterceptor)
|
||||
registry.addHandler(transferMessageDispatcher, prefix + "/host/transfer/{transferToken}")
|
||||
.addInterceptors(terminalTransferInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
// 执行日志
|
||||
registry.addHandler(execLogTailHandler, prefix + "/exec/log/{token}")
|
||||
|
||||
@@ -49,5 +49,12 @@ public class HostTerminalController {
|
||||
return hostTerminalService.getTerminalAccessToken();
|
||||
}
|
||||
|
||||
@GetMapping("/transfer")
|
||||
@Operation(summary = "获取主机终端 transferToken")
|
||||
@PreAuthorize("@ss.hasPermission('asset:host-terminal:access')")
|
||||
public String getTerminalTransferToken() {
|
||||
return hostTerminalService.getTerminalTransferToken();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.orion.lang.define.cache.key.CacheKeyBuilder;
|
||||
import com.orion.lang.define.cache.key.CacheKeyDefine;
|
||||
import com.orion.lang.define.cache.key.struct.RedisCacheStruct;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalAccessDTO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalTransferDTO;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -24,4 +25,12 @@ public interface HostTerminalCacheKeyDefine {
|
||||
.timeout(3, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
CacheKeyDefine HOST_TERMINAL_TRANSFER = new CacheKeyBuilder()
|
||||
.key("host:terminal:transfer:{}")
|
||||
.desc("主机终端传输token ${token}")
|
||||
.type(HostTerminalTransferDTO.class)
|
||||
.struct(RedisCacheStruct.STRING)
|
||||
.timeout(3, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@@ -42,10 +42,6 @@ public class CommandSnippetDO extends BaseDO {
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "触发前缀")
|
||||
@TableField("prefix")
|
||||
private String prefix;
|
||||
|
||||
@Schema(description = "代码片段")
|
||||
@TableField("command")
|
||||
private String command;
|
||||
|
||||
@@ -34,9 +34,6 @@ public class CommandSnippetCacheDTO implements LongCacheIdModel, Serializable {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "触发前缀")
|
||||
private String prefix;
|
||||
|
||||
@Schema(description = "代码片段")
|
||||
private String command;
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.orion.visor.module.asset.entity.dto;
|
||||
|
||||
import com.orion.visor.framework.desensitize.core.annotation.DesensitizeObject;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 主机终端传输参数
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/12/26 15:47
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@DesensitizeObject
|
||||
@Schema(name = "HostTerminalTransferDTO", description = "主机终端传输参数")
|
||||
public class HostTerminalTransferDTO {
|
||||
|
||||
@Schema(description = "userId")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "username")
|
||||
private String username;
|
||||
|
||||
}
|
||||
@@ -33,9 +33,6 @@ public class CommandSnippetVO implements Serializable {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "触发前缀")
|
||||
private String prefix;
|
||||
|
||||
@Schema(description = "代码片段")
|
||||
private String command;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.orion.lang.exception.argument.InvalidArgumentException;
|
||||
import com.orion.lang.utils.Exceptions;
|
||||
import com.orion.lang.utils.Strings;
|
||||
import com.orion.net.host.SessionHolder;
|
||||
import com.orion.net.host.SessionLogger;
|
||||
import com.orion.net.host.SessionStore;
|
||||
import com.orion.visor.framework.common.constant.Const;
|
||||
import com.orion.visor.framework.common.utils.CryptoUtils;
|
||||
@@ -40,6 +41,7 @@ public class SessionStores {
|
||||
CURRENT_ADDRESS.set(address);
|
||||
// 创建会话
|
||||
SessionHolder sessionHolder = SessionHolder.create();
|
||||
sessionHolder.setLogger(SessionLogger.INFO);
|
||||
SessionStore session = createSessionStore(conn, sessionHolder);
|
||||
// 连接
|
||||
session.connect();
|
||||
|
||||
@@ -131,9 +131,9 @@ public enum InputTypeEnum {
|
||||
* SFTP 修改文件权限
|
||||
*/
|
||||
SFTP_CHMOD("cm",
|
||||
SftpChangeModHandler.class,
|
||||
SftpChangeModeHandler.class,
|
||||
new String[]{"type", "sessionId", "path", "mod"},
|
||||
SftpChangeModRequest.class,
|
||||
SftpChangeModeRequest.class,
|
||||
true),
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,7 +5,7 @@ import com.orion.visor.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
import com.orion.visor.framework.common.enums.BooleanBit;
|
||||
import com.orion.visor.module.asset.define.operator.HostTerminalOperatorType;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.enums.OutputTypeEnum;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.model.request.SftpChangeModRequest;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.model.request.SftpChangeModeRequest;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.model.response.SftpBaseResponse;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.session.ISftpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -23,24 +23,24 @@ import java.util.Map;
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SftpChangeModHandler extends AbstractTerminalHandler<SftpChangeModRequest> {
|
||||
public class SftpChangeModeHandler extends AbstractTerminalHandler<SftpChangeModeRequest> {
|
||||
|
||||
@Override
|
||||
public void handle(WebSocketSession channel, SftpChangeModRequest payload) {
|
||||
public void handle(WebSocketSession channel, SftpChangeModeRequest payload) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
// 获取会话
|
||||
String sessionId = payload.getSessionId();
|
||||
ISftpSession session = hostTerminalManager.getSession(channel.getId(), sessionId);
|
||||
String path = payload.getPath();
|
||||
Integer mod = payload.getMod();
|
||||
log.info("SftpChangeModHandler-handle start sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
|
||||
log.info("SftpChangeModeHandler-handle start sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
|
||||
Exception ex = null;
|
||||
// 修改权限
|
||||
try {
|
||||
session.chmod(path, mod);
|
||||
log.info("SftpChangeModHandler-handle success sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
|
||||
log.info("SftpChangeModeHandler-handle success sessionId: {}, path: {}, mod: {}", sessionId, path, mod);
|
||||
} catch (Exception e) {
|
||||
log.error("SftpChangeModHandler-handle error sessionId: {}", sessionId, e);
|
||||
log.error("SftpChangeModeHandler-handle error sessionId: {}", sessionId, e);
|
||||
ex = e;
|
||||
}
|
||||
// 返回
|
||||
@@ -20,7 +20,7 @@ import lombok.experimental.SuperBuilder;
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class SftpChangeModRequest extends SftpBaseRequest {
|
||||
public class SftpChangeModeRequest extends SftpBaseRequest {
|
||||
|
||||
/**
|
||||
* 10进制的8进制 权限
|
||||
@@ -12,7 +12,6 @@ import com.orion.visor.framework.common.constant.Const;
|
||||
import com.orion.visor.framework.common.utils.Valid;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.model.TerminalConfig;
|
||||
import com.orion.visor.module.asset.handler.host.terminal.model.response.SftpFileVO;
|
||||
import com.orion.visor.module.asset.utils.SftpUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
@@ -84,9 +83,7 @@ public class SftpSession extends TerminalSession implements ISftpSession {
|
||||
public void move(String source, String target) {
|
||||
source = Valid.checkNormalize(source);
|
||||
// 移动
|
||||
SftpUtils.move(executor, source, target);
|
||||
// FIXME kit
|
||||
// executor.move(source, target);
|
||||
executor.move(source, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,8 +37,11 @@ public class DownloadSession extends TransferSession implements StreamingRespons
|
||||
|
||||
protected InputStream inputStream;
|
||||
|
||||
private Long fileSize;
|
||||
|
||||
public DownloadSession(HostTerminalConnectDTO connectInfo, SessionStore sessionStore, WebSocketSession channel) {
|
||||
super(connectInfo, sessionStore, channel);
|
||||
this.fileSize = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,7 +56,9 @@ public class DownloadSession extends TransferSession implements StreamingRespons
|
||||
// 检查文件是否存在
|
||||
SftpFile file = executor.getFile(path);
|
||||
Valid.notNull(file, ErrorMessage.FILE_ABSENT);
|
||||
if (file.getSize() == 0L) {
|
||||
// 验证非文件夹
|
||||
Valid.isTrue(!file.isDirectory(), ErrorMessage.UNABLE_DOWNLOAD_FOLDER);
|
||||
if ((this.fileSize = file.getSize()) == 0L) {
|
||||
// 文件为空
|
||||
log.info("DownloadSession.startDownload file empty channelId: {}, path: {}", channelId, path);
|
||||
TransferUtils.sendMessage(channel, TransferReceiver.FINISH, null);
|
||||
@@ -101,14 +106,14 @@ public class DownloadSession extends TransferSession implements StreamingRespons
|
||||
// 首次触发
|
||||
if (i == 0) {
|
||||
outputStream.flush();
|
||||
this.sendProgress(size, null);
|
||||
this.sendProgress(size, fileSize);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
// 最后一次也要 flush
|
||||
if (i != 0) {
|
||||
outputStream.flush();
|
||||
this.sendProgress(size, null);
|
||||
this.sendProgress(size, fileSize);
|
||||
}
|
||||
log.info("DownloadSession.download finish channelId: {}, path: {}", channelId, path);
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 终端拦截器
|
||||
* 终端访问拦截器
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
@@ -34,11 +34,11 @@ public class TerminalAccessInterceptor implements HandshakeInterceptor {
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
// 获取 accessToken
|
||||
String accessToken = Urls.getUrlSource(request.getURI().getPath());
|
||||
log.info("TerminalInterceptor-beforeHandshake start accessToken: {}", accessToken);
|
||||
log.info("TerminalAccessInterceptor-beforeHandshake start accessToken: {}", accessToken);
|
||||
// 获取连接数据
|
||||
HostTerminalAccessDTO access = hostTerminalService.getAccessInfoByToken(accessToken);
|
||||
if (access == null) {
|
||||
log.error("TerminalInterceptor-beforeHandshake absent accessToken: {}", accessToken);
|
||||
log.error("TerminalAccessInterceptor-beforeHandshake absent accessToken: {}", accessToken);
|
||||
return false;
|
||||
}
|
||||
// 设置参数
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.orion.visor.module.asset.interceptor;
|
||||
|
||||
import com.orion.lang.utils.Urls;
|
||||
import com.orion.visor.framework.common.constant.ExtraFieldConst;
|
||||
import com.orion.visor.framework.common.meta.TraceIdHolder;
|
||||
import com.orion.visor.framework.common.utils.Requests;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalTransferDTO;
|
||||
import com.orion.visor.module.asset.service.HostTerminalService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 终端传输拦截器
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/12/27 23:53
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TerminalTransferInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Resource
|
||||
private HostTerminalService hostTerminalService;
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
||||
// 获取 transferToken
|
||||
String transferToken = Urls.getUrlSource(request.getURI().getPath());
|
||||
log.info("TerminalTransferInterceptor-beforeHandshake start transferToken: {}", transferToken);
|
||||
// 获取连接数据
|
||||
HostTerminalTransferDTO transfer = hostTerminalService.getTransferInfoByToken(transferToken);
|
||||
if (transfer == null) {
|
||||
log.error("TerminalTransferInterceptor-beforeHandshake absent transferToken: {}", transferToken);
|
||||
return false;
|
||||
}
|
||||
// 设置参数
|
||||
attributes.put(ExtraFieldConst.USER_ID, transfer.getUserId());
|
||||
attributes.put(ExtraFieldConst.USERNAME, transfer.getUsername());
|
||||
attributes.put(ExtraFieldConst.TRACE_ID, TraceIdHolder.get());
|
||||
attributes.put(ExtraFieldConst.IDENTITY, Requests.getIdentity());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.orion.visor.module.asset.service;
|
||||
import com.orion.visor.module.asset.entity.domain.HostDO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalAccessDTO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalConnectDTO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalTransferDTO;
|
||||
import com.orion.visor.module.asset.entity.vo.HostTerminalThemeVO;
|
||||
|
||||
import java.util.List;
|
||||
@@ -30,6 +31,13 @@ public interface HostTerminalService {
|
||||
*/
|
||||
String getTerminalAccessToken();
|
||||
|
||||
/**
|
||||
* 获取主机终端传输 transferToken
|
||||
*
|
||||
* @return transferToken
|
||||
*/
|
||||
String getTerminalTransferToken();
|
||||
|
||||
/**
|
||||
* 通过 accessToken 获取主机终端访问信息
|
||||
*
|
||||
@@ -38,6 +46,14 @@ public interface HostTerminalService {
|
||||
*/
|
||||
HostTerminalAccessDTO getAccessInfoByToken(String token);
|
||||
|
||||
/**
|
||||
* 通过 transferToken 获取主机终端传输信息
|
||||
*
|
||||
* @param token token
|
||||
* @return config
|
||||
*/
|
||||
HostTerminalTransferDTO getTransferInfoByToken(String token);
|
||||
|
||||
/**
|
||||
* 获取连接信息
|
||||
*
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.orion.visor.module.asset.entity.domain.HostIdentityDO;
|
||||
import com.orion.visor.module.asset.entity.domain.HostKeyDO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalAccessDTO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalConnectDTO;
|
||||
import com.orion.visor.module.asset.entity.dto.HostTerminalTransferDTO;
|
||||
import com.orion.visor.module.asset.entity.vo.HostTerminalThemeVO;
|
||||
import com.orion.visor.module.asset.enums.*;
|
||||
import com.orion.visor.module.asset.handler.host.config.model.HostSshConfigModel;
|
||||
@@ -99,6 +100,21 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTerminalTransferToken() {
|
||||
LoginUser user = Valid.notNull(SecurityUtils.getLoginUser());
|
||||
log.info("HostConnectService.getTerminalTransferToken userId: {}", user.getId());
|
||||
String transferToken = UUIds.random19();
|
||||
HostTerminalTransferDTO transfer = HostTerminalTransferDTO.builder()
|
||||
.userId(user.getId())
|
||||
.username(user.getUsername())
|
||||
.build();
|
||||
// 设置 transfer 缓存
|
||||
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_TRANSFER.format(transferToken);
|
||||
RedisStrings.setJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_TRANSFER, transfer);
|
||||
return transferToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostTerminalAccessDTO getAccessInfoByToken(String token) {
|
||||
// 获取缓存
|
||||
@@ -111,6 +127,18 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
||||
return access;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostTerminalTransferDTO getTransferInfoByToken(String token) {
|
||||
// 获取缓存
|
||||
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_TRANSFER.format(token);
|
||||
HostTerminalTransferDTO transfer = RedisStrings.getJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_TRANSFER);
|
||||
// 删除缓存
|
||||
if (transfer != null) {
|
||||
RedisStrings.delete(key);
|
||||
}
|
||||
return transfer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HostTerminalConnectDTO getTerminalConnectInfo(Long hostId) {
|
||||
log.info("HostConnectService.getTerminalConnectInfo-withHost hostId: {}", hostId);
|
||||
|
||||
@@ -2,9 +2,7 @@ package com.orion.visor.module.asset.utils;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.orion.lang.utils.Booleans;
|
||||
import com.orion.lang.utils.Exceptions;
|
||||
import com.orion.lang.utils.Strings;
|
||||
import com.orion.lang.utils.io.Files1;
|
||||
import com.orion.net.host.sftp.SftpExecutor;
|
||||
import com.orion.net.host.sftp.SftpFile;
|
||||
import com.orion.visor.module.asset.define.config.AppSftpConfig;
|
||||
@@ -41,49 +39,8 @@ public class SftpUtils {
|
||||
SftpFileBackupParams backupParams = new SftpFileBackupParams(file.getName(), System.currentTimeMillis());
|
||||
String target = Strings.format(config.getBackupFileName(), JSON.parseObject(JSON.toJSONString(backupParams)));
|
||||
// 移动
|
||||
// FIXME kit
|
||||
move(executor, path, target);
|
||||
// executor.move(path, target);
|
||||
executor.move(path, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动文件
|
||||
* FIXME kit DELETE
|
||||
*
|
||||
* @param executor executor
|
||||
* @param source source
|
||||
* @param target target
|
||||
*/
|
||||
public static void move(SftpExecutor executor, String source, String target) {
|
||||
try {
|
||||
source = Files1.getPath(source);
|
||||
target = Files1.getPath(target);
|
||||
if (target.charAt(0) == '/') {
|
||||
// 检查是否需要创建目标文件目录
|
||||
if (!isSameParentPath(source, target)) {
|
||||
executor.makeDirectories(Files1.getParentPath(target));
|
||||
}
|
||||
// 绝对路径
|
||||
executor.getChannel().rename(source, Files1.getPath(Files1.normalize(target)));
|
||||
} else {
|
||||
// 相对路径
|
||||
executor.getChannel().rename(source, Files1.getPath(Files1.normalize(Files1.getPath(source + "/../" + target))));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw Exceptions.sftp(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME kit DELETE
|
||||
*
|
||||
* @param source source
|
||||
* @param target target
|
||||
* @return res
|
||||
*/
|
||||
private static boolean isSameParentPath(String source, String target) {
|
||||
return Files1.getParentPath(source).equals(Files1.getParentPath(target));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<result column="user_id" property="userId"/>
|
||||
<result column="group_id" property="groupId"/>
|
||||
<result column="name" property="name"/>
|
||||
<result column="prefix" property="prefix"/>
|
||||
<result column="command" property="command"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
@@ -19,7 +18,7 @@
|
||||
|
||||
<!-- 通用查询结果列 -->
|
||||
<sql id="Base_Column_List">
|
||||
id, user_id, group_id, name, prefix, command, create_time, update_time, creator, updater, deleted
|
||||
id, user_id, group_id, name, command, create_time, update_time, creator, updater, deleted
|
||||
</sql>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.orion.visor.module.infra.controller;
|
||||
|
||||
import com.orion.visor.framework.common.validator.group.Page;
|
||||
import com.orion.visor.framework.log.core.annotation.IgnoreLog;
|
||||
import com.orion.visor.framework.log.core.enums.IgnoreLogMode;
|
||||
import com.orion.visor.framework.web.core.annotation.RestWrapper;
|
||||
@@ -38,7 +39,7 @@ public class SystemMessageController {
|
||||
@IgnoreLog(IgnoreLogMode.ALL)
|
||||
@PostMapping("/list")
|
||||
@Operation(summary = "查询系统消息列表")
|
||||
public List<SystemMessageVO> getSystemMessageList(@RequestBody SystemMessageQueryRequest request) {
|
||||
public List<SystemMessageVO> getSystemMessageList(@Validated(Page.class) @RequestBody SystemMessageQueryRequest request) {
|
||||
return systemMessageService.getSystemMessageList(request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
package com.orion.visor.module.infra.entity.request.message;
|
||||
|
||||
import com.orion.visor.framework.common.entity.PageRequest;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 系统消息 查询请求对象
|
||||
@@ -17,11 +15,9 @@ import lombok.NoArgsConstructor;
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "SystemMessageQueryRequest", description = "系统消息 查询请求对象")
|
||||
public class SystemMessageQueryRequest {
|
||||
|
||||
@Schema(description = "大小")
|
||||
private Integer limit;
|
||||
public class SystemMessageQueryRequest extends PageRequest {
|
||||
|
||||
@Schema(description = "maxId")
|
||||
private Long maxId;
|
||||
|
||||
@@ -314,6 +314,11 @@ public class TerminalPreferenceModel implements GenericsDataModel {
|
||||
*/
|
||||
private Boolean openSftp;
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*/
|
||||
private Boolean uploadFile;
|
||||
|
||||
/**
|
||||
* 清空
|
||||
*/
|
||||
|
||||
@@ -91,6 +91,7 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("toBottom", true, true, false, "ArrowDown", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("selectAll", true, true, false, "KeyA", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("search", true, true, false, "KeyF", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("uploadFile", true, true, false, "KeyU", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("commandEditor", true, false, true, "KeyE", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("fontSizePlus", true, false, true, "Equal", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("fontSizeSubtract", true, false, true, "Minus", true)
|
||||
@@ -101,8 +102,8 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
String actionBarSetting = TerminalPreferenceModel.ActionBarSettingModel.builder()
|
||||
.commandInput(false)
|
||||
.connectStatus(true)
|
||||
.toTop(true)
|
||||
.toBottom(true)
|
||||
.toTop(false)
|
||||
.toBottom(false)
|
||||
.selectAll(false)
|
||||
.search(true)
|
||||
.copy(true)
|
||||
@@ -112,7 +113,10 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
.fontSizePlus(false)
|
||||
.fontSizeSubtract(false)
|
||||
.commandEditor(true)
|
||||
.fontSizePlus(false)
|
||||
.fontSizeSubtract(false)
|
||||
.openSftp(true)
|
||||
.uploadFile(true)
|
||||
.clear(true)
|
||||
.disconnect(false)
|
||||
.build()
|
||||
@@ -123,7 +127,7 @@ public class TerminalPreferenceStrategy extends AbstractGenericsDataStrategy<Ter
|
||||
.theme(new JSONObject())
|
||||
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
|
||||
.actionBarSetting(JSONObject.parseObject(actionBarSetting))
|
||||
.rightMenuSetting(Lists.of("selectAll", "copy", "paste", "fontSizePlus", "fontSizeSubtract", "search", "clear"))
|
||||
.rightMenuSetting(Lists.of("selectAll", "copy", "paste", "search", "clear"))
|
||||
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
|
||||
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
|
||||
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
|
||||
|
||||
@@ -74,9 +74,9 @@ public class SystemMessageServiceImpl implements SystemMessageService {
|
||||
.eq(SystemMessageDO::getClassify, request.getClassify())
|
||||
.lt(SystemMessageDO::getId, request.getMaxId())
|
||||
.eq(SystemMessageDO::getStatus, status)
|
||||
.last(Const.LIMIT + Const.SPACE + request.getLimit())
|
||||
.orderByDesc(SystemMessageDO::getId)
|
||||
.then()
|
||||
.limit(request.getLimit())
|
||||
.list(SystemMessageConvert.MAPPER::to);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-visor/api'
|
||||
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion-visor/keep-alive'
|
||||
VITE_APP_VERSION= '2.1.2'
|
||||
VITE_APP_VERSION= '2.1.3'
|
||||
VITE_APP_RELEASE= 'community'
|
||||
VITE_SFTP_PREVIEW_MB= 2
|
||||
VITE_DEMO_MODE= false
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
VITE_API_BASE_URL= '/orion-visor/api'
|
||||
VITE_WS_BASE_URL= '/orion-visor/keep-alive'
|
||||
VITE_APP_VERSION= '2.1.2'
|
||||
VITE_APP_VERSION= '2.1.3'
|
||||
VITE_APP_RELEASE= 'community'
|
||||
VITE_SFTP_PREVIEW_MB= 2
|
||||
VITE_DEMO_MODE= false
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "orion-visor-ui",
|
||||
"description": "Orion Visor UI",
|
||||
"version": "2.1.2",
|
||||
"version": "2.1.3",
|
||||
"private": true,
|
||||
"author": "Jiahang Li",
|
||||
"license": "Apache 2.0",
|
||||
|
||||
@@ -29,7 +29,6 @@ export interface CommandSnippetQueryResponse extends CommandSnippetQueryResponse
|
||||
|
||||
export interface CommandSnippetQueryResponseExtra {
|
||||
visible: boolean;
|
||||
expand?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,6 +51,13 @@ export function getTerminalAccessToken() {
|
||||
return axios.get<string>('/asset/host-terminal/access');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主机终端 transferToken
|
||||
*/
|
||||
export function getTerminalTransferToken() {
|
||||
return axios.get<string>('/asset/host-terminal/transfer');
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开主机终端 websocket
|
||||
*/
|
||||
|
||||
@@ -31,7 +31,6 @@ export interface PathBookmarkQueryResponse extends PathBookmarkQueryResponseExtr
|
||||
|
||||
export interface PathBookmarkQueryResponseExtra {
|
||||
visible: boolean;
|
||||
expand?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Pagination } from '@/types/global';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
* 系统消息查询请求
|
||||
*/
|
||||
export interface MessageQueryRequest {
|
||||
limit?: number;
|
||||
export interface MessageQueryRequest extends Pagination {
|
||||
maxId?: number;
|
||||
classify?: string;
|
||||
queryUnread?: boolean;
|
||||
|
||||
@@ -82,7 +82,7 @@ body {
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: background-color 0.1s cubic-bezier(0, 0, 1, 1);
|
||||
transition: background-color 0.15s cubic-bezier(0, 0, 1, 1);
|
||||
|
||||
&:hover {
|
||||
background: var(--color-fill-3);
|
||||
@@ -230,6 +230,26 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.fs12 {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fs13 {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fs14 {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.fs15 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.fs16 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -301,13 +321,13 @@ body {
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: var(--color-fill-1);
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border: 1px solid transparent;
|
||||
background-clip: padding-box;
|
||||
border-radius: 8px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-fill-4);
|
||||
|
||||
//&:hover {
|
||||
|
||||
@@ -15,11 +15,13 @@ body {
|
||||
--color-bg-panel-tabs: var(--color-bg-panel);
|
||||
--color-bg-panel-tabs-active: #F9F9F9;
|
||||
--color-bg-panel-icon-1: #F5F5F5;
|
||||
--color-bg-panel-bar: #F3F4F5;
|
||||
--color-bg-panel-bar: #F0F0F0;
|
||||
--color-panel-text-1: var(--color-content-text-1);
|
||||
--color-panel-text-2: var(--color-content-text-3);
|
||||
--color-panel-gradient-start: rgba(218, 218, 218, 1);
|
||||
--color-panel-gradient-end: rgba(218, 218, 218, 0);
|
||||
--color-button-bg: #E3E3E3;
|
||||
--color-button-bg-active: var(--color-sidebar-icon-checked);
|
||||
--search-bg-focus: rgba(234, 234, 234, .75);
|
||||
--search-bg: rgba(234, 234, 234, .95);
|
||||
--search-color-text: #0E0E0E;
|
||||
@@ -52,6 +54,8 @@ body[terminal-theme='dark'] {
|
||||
--color-panel-text-2: var(--color-content-text-3);
|
||||
--color-panel-gradient-start: rgba(38, 38, 38, 1);
|
||||
--color-panel-gradient-end: rgba(38, 38, 38, 0);
|
||||
--color-button-bg: var(--color-sidebar-icon-bg);
|
||||
--color-button-bg-active: #484848;
|
||||
--search-bg: rgba(12, 12, 12, .75);
|
||||
--search-bg-focus: rgba(12, 12, 12, .95);
|
||||
--search-color-text: #E0E0E0;
|
||||
@@ -508,7 +512,7 @@ body[terminal-theme='dark'] .arco-modal-container {
|
||||
color: var(--color-sidebar-icon);
|
||||
border-radius: 4px;
|
||||
border: 1px solid transparent;
|
||||
transition: 0.1s cubic-bezier(0, 0, 1, 1);
|
||||
transition: 0.15s cubic-bezier(0, 0, 1, 1);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -227,7 +227,7 @@ export default class LogAppender implements ILogAppender {
|
||||
|
||||
// 复制
|
||||
copy(): void {
|
||||
copyText(this.current.terminal.getSelection(), '已复制');
|
||||
copyText(this.current.terminal.getSelection(), true);
|
||||
this.focus();
|
||||
}
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
import { downloadExecCommandLogFile } from '@/api/exec/exec-command-log';
|
||||
import { downloadExecJobLogFile } from '@/api/job/exec-job-log';
|
||||
import { downloadFile } from '@/utils/file';
|
||||
import XtermSearchModal from '@/components/xtrem/search-modal/index.vue';
|
||||
import XtermSearchModal from '@/components/xterm/search-modal/index.vue';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
</template>
|
||||
<!-- 模板命令 -->
|
||||
<template #command="{ record }">
|
||||
<span class="copy-left" @click="copy(record.command, '已复制')">
|
||||
<span class="copy-left" @click="copy(record.command, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span :title="record.command">{{ record.command }}</span>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'pathBookmarkGroupSelect'
|
||||
name: 'pathBookmarkGroupSelector'
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'commandSnippetGroupSelect'
|
||||
name: 'commandSnippetGroupSelector'
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
<template #append>
|
||||
<span class="allow-click span-blue"
|
||||
title="点击复制"
|
||||
@click="copy(inputValues.cron,'已复制')">
|
||||
@click="copy(inputValues.cron, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<template #beforeValue="{ record }">
|
||||
<span class="copy-left"
|
||||
title="复制"
|
||||
@click="copy(record.beforeValue, '已复制')">
|
||||
@click="copy(record.beforeValue, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span>{{ record.beforeValue }}</span>
|
||||
@@ -49,7 +49,7 @@
|
||||
<template #afterValue="{ record }">
|
||||
<span class="copy-left"
|
||||
title="复制"
|
||||
@click="copy(record.afterValue, '已复制')">
|
||||
@click="copy(record.afterValue, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span>{{ record.afterValue }}</span>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
: undefined;
|
||||
// 查询数据
|
||||
const { data } = await getSystemMessageList({
|
||||
page: 1,
|
||||
limit: messageLimit,
|
||||
classify: currentClassify.value,
|
||||
queryUnread: queryUnread.value,
|
||||
|
||||
@@ -7,7 +7,9 @@ export const copy = async (value: string | undefined, tips: string | boolean = `
|
||||
return;
|
||||
}
|
||||
await copyToClipboard(value);
|
||||
if (tips) {
|
||||
if (tips === true) {
|
||||
Message.success('已复制');
|
||||
} else if (tips) {
|
||||
Message.success(tips as string);
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
12
orion-visor-ui/src/store/modules/cache/index.ts
vendored
12
orion-visor-ui/src/store/modules/cache/index.ts
vendored
@@ -18,6 +18,8 @@ import { getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@
|
||||
import { getCommandSnippetGroupList } from '@/api/asset/command-snippet-group';
|
||||
import { getExecJobList } from '@/api/job/exec-job';
|
||||
import { getPathBookmarkGroupList } from '@/api/asset/path-bookmark-group';
|
||||
import { getCommandSnippetList } from '@/api/asset/command-snippet';
|
||||
import { getPathBookmarkList } from '@/api/asset/path-bookmark';
|
||||
|
||||
export default defineStore('cache', {
|
||||
state: (): CacheState => ({}),
|
||||
@@ -121,6 +123,16 @@ export default defineStore('cache', {
|
||||
return await this.load('pathBookmarkGroups', getPathBookmarkGroupList, force);
|
||||
},
|
||||
|
||||
// 获取命令片段列表
|
||||
async loadCommandSnippets(force = false) {
|
||||
return await this.load('commandSnippets', getCommandSnippetList, force);
|
||||
},
|
||||
|
||||
// 获取路径书签列表
|
||||
async loadPathBookmarks(force = false) {
|
||||
return await this.load('pathBookmarks', getPathBookmarkList, force);
|
||||
},
|
||||
|
||||
// 获取执行计划列表
|
||||
async loadExecJobs(force = false) {
|
||||
return await this.load('execJob', getExecJobList, force);
|
||||
|
||||
@@ -4,6 +4,7 @@ export type CacheType = 'users' | 'menus' | 'roles'
|
||||
| 'dictKeys'
|
||||
| 'authorizedHostKeys' | 'authorizedHostIdentities'
|
||||
| 'commandSnippetGroups' | 'pathBookmarkGroups'
|
||||
| 'commandSnippets' | 'pathBookmarks'
|
||||
| 'execJob'
|
||||
| string
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
TerminalShortcutSetting,
|
||||
TerminalState
|
||||
} from './types';
|
||||
import type { ITerminalSession, PanelSessionTabType, TerminalPanelTabItem } from '@/views/host/terminal/types/define';
|
||||
import type { ISshSession, ITerminalSession, PanelSessionTabType, TerminalPanelTabItem } from '@/views/host/terminal/types/define';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
@@ -18,7 +18,7 @@ import { defineStore } from 'pinia';
|
||||
import { getPreference, updatePreference } from '@/api/user/preference';
|
||||
import { nextId } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { TerminalTabs } from '@/views/host/terminal/types/const';
|
||||
import { PanelSessionType, TerminalTabs } from '@/views/host/terminal/types/const';
|
||||
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
|
||||
import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager';
|
||||
import TerminalPanelManager from '@/views/host/terminal/handler/terminal-panel-manager';
|
||||
@@ -65,7 +65,7 @@ export default defineStore('terminal', {
|
||||
} as TerminalShortcutSetting,
|
||||
},
|
||||
hosts: {} as AuthorizedHostQueryResponse,
|
||||
tabManager: new TerminalTabManager(TerminalTabs.NEW_CONNECTION),
|
||||
tabManager: new TerminalTabManager(),
|
||||
panelManager: new TerminalPanelManager(),
|
||||
sessionManager: new TerminalSessionManager(),
|
||||
transferManager: new SftpTransferManager(),
|
||||
@@ -248,6 +248,22 @@ export default defineStore('terminal', {
|
||||
return this.sessionManager.getSession<T>(sessionTab.sessionId);
|
||||
},
|
||||
|
||||
// 拼接命令到当前会话
|
||||
appendCommandToCurrentSession(command: string, newLine: boolean = false) {
|
||||
this.appendCommandToSession(this.getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true), command, newLine);
|
||||
},
|
||||
|
||||
// 拼接命令到会话
|
||||
appendCommandToSession(session: ISshSession | undefined, command: string, newLine: boolean = false) {
|
||||
const handler = session?.handler;
|
||||
if (handler && handler.enabledStatus('checkAppendMissing')) {
|
||||
if (newLine) {
|
||||
command = `${command}\r\n`;
|
||||
}
|
||||
handler.checkAppendMissing(command);
|
||||
}
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ImageAddon } from '@xterm/addon-image';
|
||||
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
||||
|
||||
// 默认字体
|
||||
export const defaultFontFamily = 'Courier New, Monaco, courier, monospace';
|
||||
export const defaultFontFamily = 'Consolas, Courier New, Monaco, courier, monospace';
|
||||
|
||||
// 默认主题
|
||||
export const defaultTheme = {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -33,7 +33,7 @@
|
||||
</a-form-item>
|
||||
<!-- 主机编码 -->
|
||||
<a-form-item field="code" label="主机编码">
|
||||
<a-input v-model="formModel.code" placeholder="请输入主机编码" />
|
||||
<a-input v-model="formModel.code" placeholder="请输入主机编码 (定义主机唯一值)" />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="address" label="主机地址">
|
||||
|
||||
@@ -180,6 +180,13 @@
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
<!-- 配置 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
配置
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
@@ -193,18 +200,11 @@
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 更多 -->
|
||||
<a-dropdown trigger="hover">
|
||||
<a-dropdown trigger="hover" :popup-max-height="false">
|
||||
<a-button type="text" size="mini">
|
||||
更多
|
||||
</a-button>
|
||||
<template #content>
|
||||
<!-- 配置 -->
|
||||
<a-doption v-permission="['asset:host:update-config']"
|
||||
@click="emits('openUpdateConfig', record)">
|
||||
<span class="more-doption normal">
|
||||
配置
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- 修改状态 -->
|
||||
<a-doption v-permission="['asset:host:update-status']"
|
||||
@click="updateStatus(record)">
|
||||
@@ -220,6 +220,22 @@
|
||||
复制
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- SSH -->
|
||||
<a-doption v-if="record.type === hostType.SSH.type"
|
||||
v-permission="['asset:host-terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SSH' } })">
|
||||
<span class="more-doption normal">
|
||||
SSH
|
||||
</span>
|
||||
</a-doption>
|
||||
<!-- SFTP -->
|
||||
<a-doption v-if="record.type === hostType.SSH.type"
|
||||
v-permission="['asset:host-terminal:access']"
|
||||
@click="openNewRoute({ name: 'terminal', query: { connect: record.id, type: 'SFTP' } })">
|
||||
<span class="more-doption normal">
|
||||
SFTP
|
||||
</span>
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
@@ -239,7 +255,7 @@
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHost, batchDeleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { tagColor, hostTypeKey, hostStatusKey } from '../types/const';
|
||||
import { tagColor, hostTypeKey, hostStatusKey, hostType } from '../types/const';
|
||||
import { useTablePagination, useRowSelection } from '@/hooks/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
@@ -247,6 +263,7 @@
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { openNewRoute } from '@/router';
|
||||
import TagMultiSelector from '@/components/meta/tag/multi-selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openCopy', 'openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup']);
|
||||
|
||||
@@ -47,7 +47,7 @@ const columns = [
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 162,
|
||||
width: 192,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
@page-size-change="(size: number) => fetchTableData(1, size)">
|
||||
<!-- 模板命令 -->
|
||||
<template #command="{ record }">
|
||||
<span class="copy-left" @click="copy(record.command, '已复制')">
|
||||
<span class="copy-left" @click="copy(record.command, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span :title="record.command">{{ record.command }}</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a-drawer v-model:visible="visible"
|
||||
:width="388"
|
||||
:footer="false"
|
||||
@close="onClose">
|
||||
@close="emits('closed')">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="snippet-drawer-title usn">
|
||||
@@ -13,26 +13,26 @@
|
||||
<div class="snippet-container">
|
||||
<!-- 命令头部 -->
|
||||
<div class="snippet-header">
|
||||
<!-- 左侧按钮 -->
|
||||
<!-- 搜索框 -->
|
||||
<a-input-search class="snippet-header-input"
|
||||
v-model="filterValue"
|
||||
placeholder="请输入名称/命令"
|
||||
allow-clear />
|
||||
<!-- 右侧按钮 -->
|
||||
<a-space size="small">
|
||||
<!-- 创建命令 -->
|
||||
<span class="click-icon-wrapper snippet-header-icon"
|
||||
title="创建命令"
|
||||
@click="openAdd">
|
||||
<icon-plus />
|
||||
</span>
|
||||
<icon-plus />
|
||||
</span>
|
||||
<!-- 刷新 -->
|
||||
<span class="click-icon-wrapper snippet-header-icon"
|
||||
title="刷新"
|
||||
@click="fetchData(true)">
|
||||
<icon-refresh />
|
||||
</span>
|
||||
@click="fetchData">
|
||||
<icon-refresh />
|
||||
</span>
|
||||
</a-space>
|
||||
<!-- 搜索框 -->
|
||||
<a-input-search class="snippet-header-input"
|
||||
v-model="filterValue"
|
||||
placeholder="名称/命令"
|
||||
allow-clear />
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading"
|
||||
@@ -43,7 +43,7 @@
|
||||
:line-spacing="12" />
|
||||
</a-skeleton>
|
||||
<!-- 无数据 -->
|
||||
<a-empty v-else-if="!snippetValue || (snippetValue.groups.length === 0 && snippetValue.ungroupedItems.length === 0)"
|
||||
<a-empty v-else-if="snippetGroups.length === 0 && ungroupedItems.length === 0"
|
||||
style="padding: 28px 0">
|
||||
<span>暂无数据</span><br>
|
||||
<span>点击上方 '<icon-plus />' 添加一条数据吧~</span>
|
||||
@@ -51,13 +51,40 @@
|
||||
<!-- 命令片段 -->
|
||||
<div v-else class="snippet-list-container">
|
||||
<!-- 命令片段组 -->
|
||||
<command-snippet-list-group :value="snippetValue" />
|
||||
<a-collapse :bordered="false">
|
||||
<template v-for="group in snippetGroups">
|
||||
<a-collapse-item v-if="calcGroupTotal(group) > 0"
|
||||
:key="group.id"
|
||||
:header="group.name">
|
||||
<!-- 总量 -->
|
||||
<template #extra>
|
||||
{{ calcGroupTotal(group) }} 条
|
||||
</template>
|
||||
<!-- 代码片段 -->
|
||||
<template v-for="item in group.items">
|
||||
<command-snippet-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@copy="(s: string) => copy(s, true)"
|
||||
@paste="(s: string) => appendCommandToCurrentSession(s)"
|
||||
@exec="(s: string) => appendCommandToCurrentSession(s, true)"
|
||||
@remove="remoteSnippet"
|
||||
@update="(s: CommandSnippetQueryResponse) => formDrawer.openUpdate(s)" />
|
||||
</template>
|
||||
</a-collapse-item>
|
||||
</template>
|
||||
</a-collapse>
|
||||
<!-- 未分组命令片段 -->
|
||||
<div class="ungrouped-snippet-container">
|
||||
<template v-for="item in snippetValue.ungroupedItems">
|
||||
<command-snippet-list-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item" />
|
||||
<template v-for="item in ungroupedItems">
|
||||
<command-snippet-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@copy="(s: string) => copy(s, true)"
|
||||
@paste="(s: string) => appendCommandToCurrentSession(s)"
|
||||
@exec="(s: string) => appendCommandToCurrentSession(s, true)"
|
||||
@remove="remoteSnippet"
|
||||
@update="(s: CommandSnippetQueryResponse) => formDrawer.openUpdate(s)" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,27 +103,30 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession } from '../../types/define';
|
||||
import type { CommandSnippetWrapperResponse, CommandSnippetQueryResponse } from '@/api/asset/command-snippet';
|
||||
import { ref, watch, provide } from 'vue';
|
||||
import type { CommandSnippetQueryResponse } from '@/api/asset/command-snippet';
|
||||
import type { CommandSnippetGroupQueryResponse } from '@/api/asset/command-snippet-group';
|
||||
import { ref, watch } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { deleteCommandSnippet, getCommandSnippetList } from '@/api/asset/command-snippet';
|
||||
import { deleteCommandSnippet } from '@/api/asset/command-snippet';
|
||||
import { useCacheStore, useTerminalStore } from '@/store';
|
||||
import { openUpdateSnippetKey, removeSnippetKey } from './types/const';
|
||||
import { PanelSessionType } from '../../types/const';
|
||||
import CommandSnippetListItem from './command-snippet-list-item.vue';
|
||||
import CommandSnippetListGroup from './command-snippet-list-group.vue';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import CommandSnippetItem from './command-snippet-item.vue';
|
||||
import CommandSnippetFormDrawer from './command-snippet-form-drawer.vue';
|
||||
|
||||
const emits = defineEmits(['closed']);
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { getCurrentSession } = useTerminalStore();
|
||||
const { appendCommandToCurrentSession } = useTerminalStore();
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const formDrawer = ref();
|
||||
const filterValue = ref<string>();
|
||||
const snippetValue = ref<CommandSnippetWrapperResponse>();
|
||||
const filterValue = ref<string>('');
|
||||
|
||||
const snippetGroups = ref<Array<CommandSnippetGroupQueryResponse>>([]);
|
||||
const ungroupedItems = ref<Array<CommandSnippetQueryResponse>>([]);
|
||||
|
||||
// 打开
|
||||
const open = async () => {
|
||||
@@ -108,15 +138,13 @@
|
||||
defineExpose({ open });
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async (force: boolean = false) => {
|
||||
if (snippetValue.value && !force) {
|
||||
return;
|
||||
}
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询
|
||||
const { data } = await getCommandSnippetList();
|
||||
snippetValue.value = data;
|
||||
const data = await cacheStore.loadCommandSnippets(true);
|
||||
snippetGroups.value = data.groups;
|
||||
ungroupedItems.value = data.ungroupedItems;
|
||||
// 设置状态
|
||||
filterSnippet();
|
||||
} catch (e) {
|
||||
@@ -125,16 +153,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 计算总量
|
||||
const calcGroupTotal = (group: CommandSnippetGroupQueryResponse) => {
|
||||
return group.items.filter(s => s.visible).length;
|
||||
};
|
||||
|
||||
// 过滤
|
||||
const filterSnippet = () => {
|
||||
snippetValue.value?.groups.forEach(g => {
|
||||
snippetGroups.value.forEach(g => {
|
||||
g.items?.forEach(s => {
|
||||
s.visible = !filterValue.value
|
||||
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
|| s.command.toLowerCase().includes(filterValue.value.toLowerCase());
|
||||
});
|
||||
});
|
||||
snippetValue.value?.ungroupedItems.forEach(s => {
|
||||
ungroupedItems.value.forEach(s => {
|
||||
s.visible = !filterValue.value
|
||||
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
|| s.command.toLowerCase().includes(filterValue.value.toLowerCase());
|
||||
@@ -161,41 +194,33 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露 修改抽屉
|
||||
provide(openUpdateSnippetKey, (e: CommandSnippetQueryResponse) => {
|
||||
formDrawer.value.openUpdate(e);
|
||||
});
|
||||
|
||||
// 暴露 删除
|
||||
provide(removeSnippetKey, async (id: number) => {
|
||||
if (!snippetValue.value) {
|
||||
return;
|
||||
}
|
||||
// 删除代码片段
|
||||
const remoteSnippet = async (id: number) => {
|
||||
// 删除
|
||||
await deleteCommandSnippet(id);
|
||||
// 查找并且删除未分组的数据
|
||||
if (findAndSplice(id, snippetValue.value.ungroupedItems)) {
|
||||
if (findAndSplice(id, ungroupedItems.value)) {
|
||||
return;
|
||||
}
|
||||
// 查找并且删除分组内数据
|
||||
for (let group of snippetValue.value.groups) {
|
||||
for (let group of snippetGroups.value) {
|
||||
if (findAndSplice(id, group.items)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加回调
|
||||
const onAdded = async (item: CommandSnippetQueryResponse) => {
|
||||
if (item.groupId) {
|
||||
let group = snippetValue.value?.groups.find(g => g.id === item.groupId);
|
||||
let group = snippetGroups.value.find(g => g.id === item.groupId);
|
||||
if (group) {
|
||||
group?.items.push(item);
|
||||
} else {
|
||||
const cacheGroups = await cacheStore.loadCommandSnippetGroups();
|
||||
const findGroup = cacheGroups.find(s => s.id === item.groupId);
|
||||
if (findGroup) {
|
||||
snippetValue.value?.groups.push({
|
||||
snippetGroups.value.push({
|
||||
id: item.groupId,
|
||||
name: findGroup.name,
|
||||
items: [item]
|
||||
@@ -203,7 +228,7 @@
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snippetValue.value?.ungroupedItems.push(item);
|
||||
ungroupedItems.value.push(item);
|
||||
}
|
||||
// 重置过滤
|
||||
filterSnippet();
|
||||
@@ -211,16 +236,13 @@
|
||||
|
||||
// 修改回调
|
||||
const onUpdated = async (item: CommandSnippetQueryResponse) => {
|
||||
if (!snippetValue.value) {
|
||||
return;
|
||||
}
|
||||
// 查找原始数据
|
||||
let originItem;
|
||||
const findInUngrouped = snippetValue.value.ungroupedItems.find(s => s.id === item.id);
|
||||
const findInUngrouped = ungroupedItems.value.find(s => s.id === item.id);
|
||||
if (findInUngrouped) {
|
||||
originItem = findInUngrouped;
|
||||
} else {
|
||||
for (let group of snippetValue.value.groups) {
|
||||
for (let group of snippetGroups.value) {
|
||||
const find = group.items.find(s => s.id === item.id);
|
||||
if (find) {
|
||||
originItem = find;
|
||||
@@ -232,12 +254,12 @@
|
||||
return;
|
||||
}
|
||||
// 检查分组是否存在
|
||||
const findGroup = snippetValue.value.groups.find(s => s.id === item.groupId);
|
||||
const findGroup = snippetGroups.value.find(s => s.id === item.groupId);
|
||||
if (!findGroup) {
|
||||
const cacheGroups = await cacheStore.loadCommandSnippetGroups();
|
||||
const cacheGroup = cacheGroups.find(s => s.id === item.groupId);
|
||||
if (cacheGroup) {
|
||||
snippetValue.value.groups.push({
|
||||
snippetGroups.value.push({
|
||||
id: item.groupId,
|
||||
name: cacheGroup.name,
|
||||
items: []
|
||||
@@ -253,34 +275,28 @@
|
||||
if (item.groupId !== originGroupId) {
|
||||
// 从原始分组移除
|
||||
if (originGroupId) {
|
||||
const findGroup = snippetValue.value.groups.find(s => s.id === originGroupId);
|
||||
const findGroup = snippetGroups.value.find(s => s.id === originGroupId);
|
||||
if (findGroup) {
|
||||
findAndSplice(item.id, findGroup.items);
|
||||
}
|
||||
} else {
|
||||
// 从未分组数据中移除
|
||||
findAndSplice(item.id, snippetValue.value.ungroupedItems);
|
||||
findAndSplice(item.id, ungroupedItems.value);
|
||||
}
|
||||
// 添加到新分组
|
||||
if (item.groupId) {
|
||||
const findGroup = snippetValue.value.groups.find(s => s.id === item.groupId);
|
||||
const findGroup = snippetGroups.value.find(s => s.id === item.groupId);
|
||||
if (findGroup) {
|
||||
findGroup.items.push(item);
|
||||
}
|
||||
} else {
|
||||
snippetValue.value.ungroupedItems.push(originItem);
|
||||
ungroupedItems.value.push(originItem);
|
||||
}
|
||||
}
|
||||
// 重置过滤
|
||||
filterSnippet();
|
||||
};
|
||||
|
||||
// 关闭回调
|
||||
const onClose = () => {
|
||||
// 关闭时候如果打开的是终端 则聚焦终端
|
||||
getCurrentSession<ISshSession>(PanelSessionType.SSH.type)?.focus();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -332,6 +348,32 @@
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--color-fill-4);
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item) {
|
||||
border: none;
|
||||
|
||||
&-header {
|
||||
border: none;
|
||||
|
||||
&-title {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</a-form-item>
|
||||
<!-- 分组 -->
|
||||
<a-form-item field="groupId" label="分组">
|
||||
<command-snippet-group-select v-model="formModel.groupId" />
|
||||
<command-snippet-group-selector v-model="formModel.groupId" />
|
||||
</a-form-item>
|
||||
<!-- 代码片段 -->
|
||||
<a-form-item field="command"
|
||||
@@ -49,12 +49,12 @@
|
||||
<script lang="ts" setup>
|
||||
import type { CommandSnippetUpdateRequest } from '@/api/asset/command-snippet';
|
||||
import { ref } from 'vue';
|
||||
import { createCommandSnippet, updateCommandSnippet } from '@/api/asset/command-snippet';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { createCommandSnippet, updateCommandSnippet } from '@/api/asset/command-snippet';
|
||||
import formRules from './types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import CommandSnippetGroupSelect from './command-snippet-group-select.vue';
|
||||
import CommandSnippetGroupSelector from '@/components/host/command-snippte/gruop/selector/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
@@ -77,10 +77,10 @@
|
||||
const emits = defineEmits(['added', 'updated']);
|
||||
|
||||
// 打开新增
|
||||
const openAdd = () => {
|
||||
const openAdd = (command: string = '') => {
|
||||
title.value = '添加命令片段';
|
||||
isAddHandle.value = true;
|
||||
renderForm({ ...defaultForm() });
|
||||
renderForm({ ...defaultForm(), command });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
alignPoint>
|
||||
<!-- 命令 -->
|
||||
<div class="snippet-item-wrapper"
|
||||
:class="[!!item.expand ? 'snippet-item-wrapper-expand' : '']"
|
||||
:class="[expand ? 'snippet-item-wrapper-expand' : '']"
|
||||
@click="clickItem">
|
||||
<div class="snippet-item">
|
||||
<div class="snippet-item-title">
|
||||
@@ -22,7 +22,7 @@
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop.prevent="paste">
|
||||
@click.stop.prevent="emits('paste', item.command)">
|
||||
<template #icon>
|
||||
<icon-paste />
|
||||
</template>
|
||||
@@ -33,7 +33,7 @@
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop="exec">
|
||||
@click.stop="emits('exec', item.command)">
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
@@ -52,43 +52,43 @@
|
||||
<!-- 右键菜单 -->
|
||||
<template #content>
|
||||
<!-- 复制 -->
|
||||
<a-doption @click="copyCommand">
|
||||
<a-doption @click="emits('copy', item.command)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-copy />
|
||||
</div>
|
||||
<div>复制</div>
|
||||
</a-doption>
|
||||
<!-- 粘贴 -->
|
||||
<a-doption @click="paste">
|
||||
<a-doption @click="emits('paste', item.command)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-paste />
|
||||
</div>
|
||||
<div>粘贴</div>
|
||||
</a-doption>
|
||||
<!-- 执行 -->
|
||||
<a-doption @click="exec">
|
||||
<a-doption @click="emits('exec', item.command)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
<div>执行</div>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption @click="openUpdateSnippet(item)">
|
||||
<a-doption @click="emits('update', item)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-edit />
|
||||
</div>
|
||||
<div>修改</div>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption @click="removeSnippet(item.id)">
|
||||
<a-doption @click="emits('remove', item.id)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-delete />
|
||||
</div>
|
||||
<div>删除</div>
|
||||
</a-doption>
|
||||
<!-- 展开 -->
|
||||
<a-doption v-if="!item.expand"
|
||||
@click="() => item.expand = true">
|
||||
<a-doption v-if="!expand"
|
||||
@click="() => expand = true">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-expand />
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@
|
||||
</a-doption>
|
||||
<!-- 收起 -->
|
||||
<a-doption v-else
|
||||
@click="() => item.expand = false">
|
||||
@click="() => expand = false">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-shrink />
|
||||
</div>
|
||||
@@ -108,40 +108,31 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'commandSnippetListItem'
|
||||
name: 'commandSnippetItem'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession } from '../../types/define';
|
||||
import type { CommandSnippetQueryResponse } from '@/api/asset/command-snippet';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { ref } from 'vue';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { inject } from 'vue';
|
||||
import { openUpdateSnippetKey, removeSnippetKey } from './types/const';
|
||||
import { PanelSessionType } from '../../types/const';
|
||||
|
||||
const props = defineProps<{
|
||||
item: CommandSnippetQueryResponse;
|
||||
}>();
|
||||
|
||||
const { getCurrentSession } = useTerminalStore();
|
||||
const emits = defineEmits(['remove', 'update', 'copy', 'exec', 'paste']);
|
||||
|
||||
const expand = ref(false);
|
||||
|
||||
let clickCount = 0;
|
||||
|
||||
// 修改
|
||||
const openUpdateSnippet = inject(openUpdateSnippetKey) as (item: CommandSnippetQueryResponse) => void;
|
||||
|
||||
// 删除
|
||||
const removeSnippet = inject(removeSnippetKey) as (id: number) => void;
|
||||
|
||||
// 点击命令
|
||||
const clickItem = () => {
|
||||
if (++clickCount == 2) {
|
||||
// 双击执行
|
||||
clickCount = 0;
|
||||
exec();
|
||||
emits('exec', props.item.command);
|
||||
} else {
|
||||
// 单击展开
|
||||
expandItem();
|
||||
@@ -153,7 +144,7 @@
|
||||
setTimeout(() => {
|
||||
// 为 0 则代表为双击
|
||||
if (clickCount !== 0) {
|
||||
props.item.expand = !props.item.expand;
|
||||
expand.value = !expand.value;
|
||||
clickCount = 0;
|
||||
}
|
||||
}, 50);
|
||||
@@ -161,7 +152,7 @@
|
||||
|
||||
// 点击命令
|
||||
const clickCommand = (e: Event) => {
|
||||
if (props.item.expand) {
|
||||
if (expand.value) {
|
||||
// 获取选中的文本
|
||||
const selectedText = window.getSelection()?.toString();
|
||||
if (selectedText) {
|
||||
@@ -170,29 +161,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 复制命令
|
||||
const copyCommand = () => {
|
||||
copy(props.item.command, '已复制');
|
||||
};
|
||||
|
||||
// 粘贴
|
||||
const paste = () => {
|
||||
write(props.item.command);
|
||||
};
|
||||
|
||||
// 执行
|
||||
const exec = () => {
|
||||
write(props.item.command + '\r\n');
|
||||
};
|
||||
|
||||
// 写入命令
|
||||
const write = (command: string) => {
|
||||
const handler = getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true)?.handler;
|
||||
if (handler && handler.enabledStatus('checkAppendMissing')) {
|
||||
handler.checkAppendMissing(command);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<a-collapse :bordered="false">
|
||||
<template v-for="group in value.groups">
|
||||
<a-collapse-item v-if="calcTotal(group) > 0"
|
||||
:key="group.id"
|
||||
:header="group.name">
|
||||
<!-- 总量 -->
|
||||
<template #extra>
|
||||
{{ calcTotal(group) }} 条
|
||||
</template>
|
||||
<!-- snippet -->
|
||||
<template v-for="item in group.items">
|
||||
<command-snippet-list-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item" />
|
||||
</template>
|
||||
</a-collapse-item>
|
||||
</template>
|
||||
</a-collapse>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'commandSnippetListGroup'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CommandSnippetGroupQueryResponse } from '@/api/asset/command-snippet-group';
|
||||
import type { CommandSnippetWrapperResponse } from '@/api/asset/command-snippet';
|
||||
import CommandSnippetListItem from './command-snippet-list-item.vue';
|
||||
|
||||
defineProps<{
|
||||
value: CommandSnippetWrapperResponse;
|
||||
}>();
|
||||
|
||||
// 计算总量
|
||||
const calcTotal = (group: CommandSnippetGroupQueryResponse) => {
|
||||
return group.items.filter(s => s.visible).length;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-collapse-item) {
|
||||
border: none;
|
||||
|
||||
&-header {
|
||||
border: none;
|
||||
|
||||
&-title {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
// 打开 updateSnippet key
|
||||
export const openUpdateSnippetKey = Symbol();
|
||||
|
||||
// 删除 snippet key
|
||||
export const removeSnippetKey = Symbol();
|
||||
@@ -199,6 +199,7 @@
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: all .3s;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- 终端 tab -->
|
||||
<a-tabs v-model:active-key="panel.active"
|
||||
:editable="true"
|
||||
:auto-switch="true"
|
||||
:auto-switch="false"
|
||||
:show-add-button="true"
|
||||
@add="openNewConnect"
|
||||
@tab-click="(k: string) => panel.clickTab(k)"
|
||||
@@ -11,7 +11,10 @@
|
||||
<!-- 右侧按钮 -->
|
||||
<template #extra>
|
||||
<a-space class="panel-extra">
|
||||
<span class="extra-icon" @click="close">
|
||||
<!-- 关闭 -->
|
||||
<span class="extra-icon"
|
||||
title="关闭面板"
|
||||
@click="close">
|
||||
<icon-close />
|
||||
</span>
|
||||
</a-space>
|
||||
@@ -21,7 +24,10 @@
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="tab-title-wrapper usn"
|
||||
:style="{ 'border-bottom': `2px ${tab.color || 'transparent'} solid` }"
|
||||
:style="{
|
||||
'--color': getDictValue(tabColorKey, tab.color, 'color', 'transparent'),
|
||||
'--bg': panel.active === tab.key ? getDictValue(tabColorKey, tab.color, 'bg', 'transparent') : 'transparent',
|
||||
}"
|
||||
@dblclick="copySession(tab, index)">
|
||||
<span class="tab-title-icon">
|
||||
<component :is="tab.icon" />
|
||||
@@ -47,8 +53,8 @@
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession, ITerminalTabManager, TerminalPanelTabItem } from '../../types/define';
|
||||
import { watch } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { PanelSessionType } from '../../types/const';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { tabColorKey, PanelSessionType } from '../../types/const';
|
||||
import SshView from '../ssh/ssh-view.vue';
|
||||
import SftpView from '../sftp/sftp-view.vue';
|
||||
|
||||
@@ -60,6 +66,7 @@
|
||||
const emits = defineEmits(['close', 'openNewConnect']);
|
||||
|
||||
const { sessionManager, copySession } = useTerminalStore();
|
||||
const { getDictValue } = useDictStore();
|
||||
|
||||
// 监听 tab 切换
|
||||
watch(() => props.panel.active, (active, before) => {
|
||||
@@ -107,12 +114,28 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 11px 18px 9px 14px;
|
||||
margin: 0 2px;
|
||||
background: var(--bg);
|
||||
position: relative;
|
||||
transition: all .3s;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: calc(100% - 3px);
|
||||
height: 2px;
|
||||
background: var(--color);
|
||||
position: absolute;
|
||||
left: 1px;
|
||||
bottom: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-extra {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a-drawer v-model:visible="visible"
|
||||
:width="388"
|
||||
:footer="false"
|
||||
@close="onClose">
|
||||
@close="emits('closed')">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="path-drawer-title usn">
|
||||
@@ -13,26 +13,26 @@
|
||||
<div class="path-container">
|
||||
<!-- 路径头部 -->
|
||||
<div class="path-header">
|
||||
<!-- 左侧按钮 -->
|
||||
<!-- 搜索框 -->
|
||||
<a-input-search class="path-header-input"
|
||||
v-model="filterValue"
|
||||
placeholder="请输入名称/路径"
|
||||
allow-clear />
|
||||
<!-- 右侧侧按钮 -->
|
||||
<a-space size="small">
|
||||
<!-- 创建路径 -->
|
||||
<span class="click-icon-wrapper path-header-icon"
|
||||
title="创建路径"
|
||||
@click="openAdd">
|
||||
<icon-plus />
|
||||
</span>
|
||||
<icon-plus />
|
||||
</span>
|
||||
<!-- 刷新 -->
|
||||
<span class="click-icon-wrapper path-header-icon"
|
||||
title="刷新"
|
||||
@click="fetchData(true)">
|
||||
<icon-refresh />
|
||||
</span>
|
||||
@click="fetchData">
|
||||
<icon-refresh />
|
||||
</span>
|
||||
</a-space>
|
||||
<!-- 搜索框 -->
|
||||
<a-input-search class="path-header-input"
|
||||
v-model="filterValue"
|
||||
placeholder="名称/路径"
|
||||
allow-clear />
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading"
|
||||
@@ -43,7 +43,7 @@
|
||||
:line-spacing="12" />
|
||||
</a-skeleton>
|
||||
<!-- 无数据 -->
|
||||
<a-empty v-else-if="!pathBookmarkData || (pathBookmarkData.groups.length === 0 && pathBookmarkData.ungroupedItems.length === 0)"
|
||||
<a-empty v-else-if="bookmarkGroups.length === 0 && ungroupedItems.length === 0"
|
||||
style="padding: 28px 0">
|
||||
<span>暂无数据</span><br>
|
||||
<span>点击上方 '<icon-plus />' 添加一条数据吧~</span>
|
||||
@@ -51,13 +51,42 @@
|
||||
<!-- 路径书签 -->
|
||||
<div v-else class="path-list-container">
|
||||
<!-- 路径书签组 -->
|
||||
<path-bookmark-list-group :value="pathBookmarkData" />
|
||||
<a-collapse :bordered="false">
|
||||
<template v-for="group in bookmarkGroups">
|
||||
<a-collapse-item v-if="calcGroupTotal(group) > 0"
|
||||
:key="group.id"
|
||||
:header="group.name">
|
||||
<!-- 总量 -->
|
||||
<template #extra>
|
||||
{{ calcGroupTotal(group) }} 条
|
||||
</template>
|
||||
<!-- 路径 -->
|
||||
<template v-for="item in group.items">
|
||||
<path-bookmark-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@copy="(s: string) => copy(s, true)"
|
||||
@paste="(s: string) => appendCommandToCurrentSession(s)"
|
||||
@exec="(s: string) => appendCommandToCurrentSession(s, true)"
|
||||
@change="(s: string) => changePath(s)"
|
||||
@update="(s: PathBookmarkQueryResponse) => formDrawer.openUpdate(s)"
|
||||
@remove="removeBookmark" />
|
||||
</template>
|
||||
</a-collapse-item>
|
||||
</template>
|
||||
</a-collapse>
|
||||
<!-- 未分组路径书签 -->
|
||||
<div class="ungrouped-path-container">
|
||||
<template v-for="item in pathBookmarkData.ungroupedItems">
|
||||
<path-bookmark-list-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item" />
|
||||
<template v-for="item in ungroupedItems">
|
||||
<path-bookmark-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item"
|
||||
@copy="(s: string) => copy(s, true)"
|
||||
@paste="(s: string) => appendCommandToCurrentSession(s)"
|
||||
@exec="(s: string) => appendCommandToCurrentSession(s, true)"
|
||||
@change="(s: string) => changePath(s)"
|
||||
@update="(s: PathBookmarkQueryResponse) => formDrawer.openUpdate(s)"
|
||||
@remove="removeBookmark" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,27 +105,30 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession } from '../../types/define';
|
||||
import type { PathBookmarkWrapperResponse, PathBookmarkQueryResponse } from '@/api/asset/path-bookmark';
|
||||
import { ref, provide, watch } from 'vue';
|
||||
import type { ISftpSession } from '../../types/define';
|
||||
import type { PathBookmarkQueryResponse } from '@/api/asset/path-bookmark';
|
||||
import type { PathBookmarkGroupQueryResponse } from '@/api/asset/path-bookmark-group';
|
||||
import { ref, watch } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { deletePathBookmark, getPathBookmarkList } from '@/api/asset/path-bookmark';
|
||||
import { deletePathBookmark } from '@/api/asset/path-bookmark';
|
||||
import { useCacheStore, useTerminalStore } from '@/store';
|
||||
import { PanelSessionType } from '../../types/const';
|
||||
import { openUpdatePathKey, removePathKey } from './types/const';
|
||||
import PathBookmarkListItem from './path-bookmark-list-item.vue';
|
||||
import PathBookmarkListGroup from './path-bookmark-list-group.vue';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import PathBookmarkItem from './path-bookmark-item.vue';
|
||||
import PathBookmarkFormDrawer from './path-bookmark-form-drawer.vue';
|
||||
|
||||
const emits = defineEmits(['closed']);
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { getCurrentSession } = useTerminalStore();
|
||||
const { getCurrentSession, appendCommandToCurrentSession } = useTerminalStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const formDrawer = ref();
|
||||
const filterValue = ref<string>();
|
||||
const pathBookmarkData = ref<PathBookmarkWrapperResponse>();
|
||||
const bookmarkGroups = ref<Array<PathBookmarkGroupQueryResponse>>([]);
|
||||
const ungroupedItems = ref<Array<PathBookmarkQueryResponse>>([]);
|
||||
|
||||
// 打开
|
||||
const open = async () => {
|
||||
@@ -108,15 +140,13 @@
|
||||
defineExpose({ open });
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async (force: boolean = false) => {
|
||||
if (pathBookmarkData.value && !force) {
|
||||
return;
|
||||
}
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询
|
||||
const { data } = await getPathBookmarkList();
|
||||
pathBookmarkData.value = data;
|
||||
const data = await cacheStore.loadPathBookmarks(true);
|
||||
bookmarkGroups.value = data.groups;
|
||||
ungroupedItems.value = data.ungroupedItems;
|
||||
// 设置状态
|
||||
filterPath();
|
||||
} catch (e) {
|
||||
@@ -127,14 +157,14 @@
|
||||
|
||||
// 过滤
|
||||
const filterPath = () => {
|
||||
pathBookmarkData.value?.groups.forEach(g => {
|
||||
bookmarkGroups.value.forEach(g => {
|
||||
g.items?.forEach(s => {
|
||||
s.visible = !filterValue.value
|
||||
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
|| s.path.toLowerCase().includes(filterValue.value.toLowerCase());
|
||||
});
|
||||
});
|
||||
pathBookmarkData.value?.ungroupedItems.forEach(s => {
|
||||
ungroupedItems.value.forEach(s => {
|
||||
s.visible = !filterValue.value
|
||||
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
|| s.path.toLowerCase().includes(filterValue.value.toLowerCase());
|
||||
@@ -149,6 +179,11 @@
|
||||
formDrawer.value.openAdd();
|
||||
};
|
||||
|
||||
// 计算总量
|
||||
const calcGroupTotal = (group: PathBookmarkGroupQueryResponse) => {
|
||||
return group.items.filter(s => s.visible).length;
|
||||
};
|
||||
|
||||
// 查找并且删除
|
||||
const findAndSplice = (id: number, items: Array<PathBookmarkQueryResponse>) => {
|
||||
if (items) {
|
||||
@@ -161,41 +196,38 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露 修改抽屉
|
||||
provide(openUpdatePathKey, (e: PathBookmarkQueryResponse) => {
|
||||
formDrawer.value.openUpdate(e);
|
||||
});
|
||||
// 查询 sftp 文件列表
|
||||
const changePath = (path: string) => {
|
||||
getCurrentSession<ISftpSession>(PanelSessionType.SFTP.type, true)?.list(path);
|
||||
};
|
||||
|
||||
// 暴露 删除
|
||||
provide(removePathKey, async (id: number) => {
|
||||
if (!pathBookmarkData.value) {
|
||||
return;
|
||||
}
|
||||
// 删除书签路径
|
||||
const removeBookmark = async (id: number) => {
|
||||
// 删除
|
||||
await deletePathBookmark(id);
|
||||
// 查找并且删除未分组的数据
|
||||
if (findAndSplice(id, pathBookmarkData.value.ungroupedItems)) {
|
||||
if (findAndSplice(id, ungroupedItems.value)) {
|
||||
return;
|
||||
}
|
||||
// 查找并且删除分组内数据
|
||||
for (let group of pathBookmarkData.value.groups) {
|
||||
for (let group of bookmarkGroups.value) {
|
||||
if (findAndSplice(id, group.items)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 添加回调
|
||||
const onAdded = async (item: PathBookmarkQueryResponse) => {
|
||||
if (item.groupId) {
|
||||
let group = pathBookmarkData.value?.groups.find(g => g.id === item.groupId);
|
||||
let group = bookmarkGroups.value.find(g => g.id === item.groupId);
|
||||
if (group) {
|
||||
group?.items.push(item);
|
||||
} else {
|
||||
const cacheGroups = await cacheStore.loadPathBookmarkGroups();
|
||||
const findGroup = cacheGroups.find(s => s.id === item.groupId);
|
||||
if (findGroup) {
|
||||
pathBookmarkData.value?.groups.push({
|
||||
bookmarkGroups.value.push({
|
||||
id: item.groupId,
|
||||
name: findGroup.name,
|
||||
items: [item]
|
||||
@@ -203,7 +235,7 @@
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pathBookmarkData.value?.ungroupedItems.push(item);
|
||||
ungroupedItems.value.push(item);
|
||||
}
|
||||
// 重置过滤
|
||||
filterPath();
|
||||
@@ -211,16 +243,13 @@
|
||||
|
||||
// 修改回调
|
||||
const onUpdated = async (item: PathBookmarkQueryResponse) => {
|
||||
if (!pathBookmarkData.value) {
|
||||
return;
|
||||
}
|
||||
// 查找原始数据
|
||||
let originItem;
|
||||
const findInUngrouped = pathBookmarkData.value.ungroupedItems.find(s => s.id === item.id);
|
||||
const findInUngrouped = ungroupedItems.value.find(s => s.id === item.id);
|
||||
if (findInUngrouped) {
|
||||
originItem = findInUngrouped;
|
||||
} else {
|
||||
for (let group of pathBookmarkData.value.groups) {
|
||||
for (let group of bookmarkGroups.value) {
|
||||
const find = group.items.find(s => s.id === item.id);
|
||||
if (find) {
|
||||
originItem = find;
|
||||
@@ -232,12 +261,12 @@
|
||||
return;
|
||||
}
|
||||
// 检查分组是否存在
|
||||
const findGroup = pathBookmarkData.value.groups.find(s => s.id === item.groupId);
|
||||
const findGroup = bookmarkGroups.value.find(s => s.id === item.groupId);
|
||||
if (!findGroup) {
|
||||
const cacheGroups = await cacheStore.loadPathBookmarkGroups();
|
||||
const cacheGroup = cacheGroups.find(s => s.id === item.groupId);
|
||||
if (cacheGroup) {
|
||||
pathBookmarkData.value.groups.push({
|
||||
bookmarkGroups.value.push({
|
||||
id: item.groupId,
|
||||
name: cacheGroup.name,
|
||||
items: []
|
||||
@@ -253,34 +282,28 @@
|
||||
if (item.groupId !== originGroupId) {
|
||||
// 从原始分组移除
|
||||
if (originGroupId) {
|
||||
const findGroup = pathBookmarkData.value.groups.find(s => s.id === originGroupId);
|
||||
const findGroup = bookmarkGroups.value.find(s => s.id === originGroupId);
|
||||
if (findGroup) {
|
||||
findAndSplice(item.id, findGroup.items);
|
||||
}
|
||||
} else {
|
||||
// 从未分组数据中移除
|
||||
findAndSplice(item.id, pathBookmarkData.value.ungroupedItems);
|
||||
findAndSplice(item.id, ungroupedItems.value);
|
||||
}
|
||||
// 添加到新分组
|
||||
if (item.groupId) {
|
||||
const findGroup = pathBookmarkData.value.groups.find(s => s.id === item.groupId);
|
||||
const findGroup = bookmarkGroups.value.find(s => s.id === item.groupId);
|
||||
if (findGroup) {
|
||||
findGroup.items.push(item);
|
||||
}
|
||||
} else {
|
||||
pathBookmarkData.value.ungroupedItems.push(originItem);
|
||||
ungroupedItems.value.push(originItem);
|
||||
}
|
||||
}
|
||||
// 重置过滤
|
||||
filterPath();
|
||||
};
|
||||
|
||||
// 关闭回调
|
||||
const onClose = () => {
|
||||
// 关闭时候如果打开的是终端 则聚焦终端
|
||||
getCurrentSession<ISshSession>(PanelSessionType.SSH.type)?.focus();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -332,6 +355,31 @@
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--color-fill-4);
|
||||
}
|
||||
|
||||
:deep(.arco-collapse-item) {
|
||||
border: none;
|
||||
|
||||
&-header {
|
||||
border: none;
|
||||
|
||||
&-title {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-skeleton {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
</a-form-item>
|
||||
<!-- 分组 -->
|
||||
<a-form-item field="groupId" label="分组">
|
||||
<path-bookmark-group-select v-model="formModel.groupId" />
|
||||
<path-bookmark-group-selector v-model="formModel.groupId" />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
@@ -54,13 +54,13 @@
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { createPathBookmark, updatePathBookmark } from '@/api/asset/path-bookmark';
|
||||
import formRules from './types/form.rules';
|
||||
import { createPathBookmark, updatePathBookmark } from '@/api/asset/path-bookmark';
|
||||
import { PathBookmarkType } from './types/const';
|
||||
import { pathBookmarkTypeKey } from '../../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import PathBookmarkGroupSelect from './path-bookmark-group-select.vue';
|
||||
import { pathBookmarkTypeKey } from '../../types/const';
|
||||
import PathBookmarkGroupSelector from '@/components/host/bookmark-path/group/selector/index.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
alignPoint>
|
||||
<!-- 路径 -->
|
||||
<div class="path-item-wrapper"
|
||||
:class="[!!item.expand ? 'path-item-wrapper-expand' : '']"
|
||||
:class="[expand ? 'path-item-wrapper-expand' : '']"
|
||||
@click="clickItem">
|
||||
<div class="path-item">
|
||||
<div class="path-item-title">
|
||||
@@ -33,7 +33,7 @@
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop.prevent="copyPath">
|
||||
@click.stop.prevent="emits('copy', item.path)">
|
||||
<template #icon>
|
||||
<icon-copy />
|
||||
</template>
|
||||
@@ -58,36 +58,36 @@
|
||||
<div>进入</div>
|
||||
</a-doption>
|
||||
<!-- 复制 -->
|
||||
<a-doption @click="copyPath">
|
||||
<a-doption @click="emits('copy', item.path)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-copy />
|
||||
</div>
|
||||
<div>复制</div>
|
||||
</a-doption>
|
||||
<!-- 粘贴 -->
|
||||
<a-doption @click="paste">
|
||||
<a-doption @click="emits('paste', item.path)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-paste />
|
||||
</div>
|
||||
<div>粘贴</div>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption @click="openUpdatePath(item)">
|
||||
<a-doption @click="emits('update', item)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-edit />
|
||||
</div>
|
||||
<div>修改</div>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption @click="removePath(item.id)">
|
||||
<a-doption @click="emits('remove', item.id)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-delete />
|
||||
</div>
|
||||
<div>删除</div>
|
||||
</a-doption>
|
||||
<!-- 展开 -->
|
||||
<a-doption v-if="!item.expand"
|
||||
@click="() => item.expand = true">
|
||||
<a-doption v-if="!expand"
|
||||
@click="() => expand = true">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-expand />
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
</a-doption>
|
||||
<!-- 收起 -->
|
||||
<a-doption v-else
|
||||
@click="() => item.expand = false">
|
||||
@click="() => expand = false">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-shrink />
|
||||
</div>
|
||||
@@ -107,48 +107,37 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'pathBookmarkListItem'
|
||||
name: 'pathBookmarkItem'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISftpSession, ISshSession } from '../../types/define';
|
||||
import type { PathBookmarkQueryResponse } from '@/api/asset/path-bookmark';
|
||||
import { ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { inject } from 'vue';
|
||||
import { getParentPath } from '@/utils/file';
|
||||
import { openUpdatePathKey, PathBookmarkType, removePathKey } from './types/const';
|
||||
import { PathBookmarkType } from './types/const';
|
||||
import { PanelSessionType } from '../../types/const';
|
||||
|
||||
const props = defineProps<{
|
||||
item: PathBookmarkQueryResponse;
|
||||
}>();
|
||||
|
||||
const { getCurrentSession, getCurrentSessionType } = useTerminalStore();
|
||||
const emits = defineEmits(['remove', 'update', 'copy', 'paste', 'exec', 'change']);
|
||||
|
||||
const { getCurrentSessionType } = useTerminalStore();
|
||||
|
||||
const expand = ref(false);
|
||||
|
||||
let clickCount = 0;
|
||||
|
||||
// 修改
|
||||
const openUpdatePath = inject(openUpdatePathKey) as (item: PathBookmarkQueryResponse) => void;
|
||||
|
||||
// 删除
|
||||
const removePath = inject(removePathKey) as (id: number) => void;
|
||||
|
||||
// 点击路径
|
||||
const clickItem = () => {
|
||||
if (++clickCount == 2) {
|
||||
clickCount = 0;
|
||||
// 双击
|
||||
const type = getCurrentSessionType(true);
|
||||
if (type === PanelSessionType.SSH.type) {
|
||||
// SSH 粘贴
|
||||
paste();
|
||||
} else if (type === PanelSessionType.SFTP.type) {
|
||||
// SFTP 切换目录
|
||||
listFiles();
|
||||
}
|
||||
changePath();
|
||||
} else {
|
||||
// 单击展开
|
||||
expandItem();
|
||||
@@ -160,7 +149,7 @@
|
||||
setTimeout(() => {
|
||||
// 为 0 则代表为双击
|
||||
if (clickCount !== 0) {
|
||||
props.item.expand = !props.item.expand;
|
||||
expand.value = !expand.value;
|
||||
clickCount = 0;
|
||||
}
|
||||
}, 50);
|
||||
@@ -168,7 +157,7 @@
|
||||
|
||||
// 点击路径
|
||||
const clickPath = (e: Event) => {
|
||||
if (props.item.expand) {
|
||||
if (expand.value) {
|
||||
// 获取选中的文本
|
||||
const selectedText = window.getSelection()?.toString();
|
||||
if (selectedText) {
|
||||
@@ -177,46 +166,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 复制路径
|
||||
const copyPath = () => {
|
||||
copy(props.item.path, '已复制');
|
||||
};
|
||||
|
||||
// 粘贴
|
||||
const paste = () => {
|
||||
writeCommand(props.item.path);
|
||||
};
|
||||
|
||||
// 切换目录
|
||||
const changePath = () => {
|
||||
const type = getCurrentSessionType(true);
|
||||
if (type === PanelSessionType.SSH.type) {
|
||||
// SSH cd
|
||||
const path = props.item.type === PathBookmarkType.DIR
|
||||
? props.item.path
|
||||
: getParentPath(props.item.path);
|
||||
// SSH cd
|
||||
writeCommand('cd ' + path + '\r\n');
|
||||
emits('exec', `cd ${path}`);
|
||||
} else if (type === PanelSessionType.SFTP.type) {
|
||||
// SFTP 切换目录
|
||||
listFiles();
|
||||
}
|
||||
};
|
||||
|
||||
// 查询 sftp 文件列表
|
||||
const listFiles = () => {
|
||||
// 如果非文件夹则查询父文件夹
|
||||
const path = props.item.type === PathBookmarkType.DIR
|
||||
? props.item.path
|
||||
: getParentPath(props.item.path);
|
||||
// 查询列表
|
||||
getCurrentSession<ISftpSession>(PanelSessionType.SFTP.type, true)?.list(path);
|
||||
};
|
||||
|
||||
// 写入 ssh 命令
|
||||
const writeCommand = (command: string) => {
|
||||
const handler = getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true)?.handler;
|
||||
if (handler && handler.enabledStatus('checkAppendMissing')) {
|
||||
handler.checkAppendMissing(command);
|
||||
const path = props.item.type === PathBookmarkType.DIR
|
||||
? props.item.path
|
||||
: getParentPath(props.item.path);
|
||||
emits('change', path);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<a-collapse :bordered="false">
|
||||
<template v-for="group in value.groups">
|
||||
<a-collapse-item v-if="calcTotal(group) > 0"
|
||||
:key="group.id"
|
||||
:header="group.name">
|
||||
<!-- 总量 -->
|
||||
<template #extra>
|
||||
{{ calcTotal(group) }} 条
|
||||
</template>
|
||||
<!-- 路径 -->
|
||||
<template v-for="item in group.items">
|
||||
<path-bookmark-list-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item" />
|
||||
</template>
|
||||
</a-collapse-item>
|
||||
</template>
|
||||
</a-collapse>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'pathBookmarkListGroup'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PathBookmarkGroupQueryResponse } from '@/api/asset/path-bookmark-group';
|
||||
import type { PathBookmarkWrapperResponse } from '@/api/asset/path-bookmark';
|
||||
import PathBookmarkListItem from './path-bookmark-list-item.vue';
|
||||
|
||||
defineProps<{
|
||||
value: PathBookmarkWrapperResponse;
|
||||
}>();
|
||||
|
||||
// 计算总量
|
||||
const calcTotal = (group: PathBookmarkGroupQueryResponse) => {
|
||||
return group.items.filter(s => s.visible).length;
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.arco-collapse-item) {
|
||||
border: none;
|
||||
|
||||
&-header {
|
||||
border: none;
|
||||
|
||||
&-title {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-extra {
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
background-color: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -3,9 +3,3 @@ export const PathBookmarkType = {
|
||||
FILE: 'FILE',
|
||||
DIR: 'DIR',
|
||||
};
|
||||
|
||||
// 打开 updatePath key
|
||||
export const openUpdatePathKey = Symbol();
|
||||
|
||||
// 删除 path key
|
||||
export const removePathKey = Symbol();
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
操作栏设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
@@ -19,7 +17,7 @@
|
||||
<a-form class="terminal-setting-form"
|
||||
:model="formModel"
|
||||
layout="vertical">
|
||||
<a-space>
|
||||
<a-space size="large">
|
||||
<!-- 顶部操作按钮 -->
|
||||
<a-form-item field="actions" label="顶部操作按钮">
|
||||
<icon-actions class="form-item-actions"
|
||||
@@ -29,7 +27,6 @@
|
||||
<!-- 命令输入框 -->
|
||||
<a-form-item field="commandInput" label="命令输入框">
|
||||
<a-switch v-model="formModel.commandInput"
|
||||
class="form-item-command-input"
|
||||
:default-checked="true"
|
||||
type="round" />
|
||||
</a-form-item>
|
||||
@@ -109,8 +106,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-actions, .form-item-command-input {
|
||||
margin-right: 48px;
|
||||
.form-item-actions {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
:deep(.arco-form) {
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
显示偏好
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body block-body setting-body">
|
||||
<a-form class="terminal-setting-form"
|
||||
@@ -24,11 +22,11 @@
|
||||
:filter-option="labelFilter">
|
||||
<!-- label -->
|
||||
<template #label="{ data }">
|
||||
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
|
||||
<span :style="{ fontFamily: data.value === '_' ? defaultFontFamily : data.value }">{{ data.label }}</span>
|
||||
</template>
|
||||
<!-- 选项 -->
|
||||
<template #option="{ data }">
|
||||
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
|
||||
<span :style="{ fontFamily: data.value === '_' ? defaultFontFamily : data.value }">{{ data.label }}</span>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
@@ -100,7 +98,7 @@
|
||||
<div class="terminal-example">
|
||||
<span class="vertical-form-label">预览效果</span>
|
||||
<div class="terminal-example-wrapper"
|
||||
:style="{ background: preference.theme.schema.background }">
|
||||
:style="{ background: background }">
|
||||
<terminal-example :schema="preference.theme.schema"
|
||||
ref="previewTerminal" />
|
||||
</div>
|
||||
@@ -117,22 +115,24 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalDisplaySetting } from '@/store/modules/terminal/types';
|
||||
import type { ISshSession } from '../../../types/define';
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import { fontFamilyKey, fontSizeKey, fontWeightKey, cursorStyleKey } from '../../../types/const';
|
||||
import { fontFamilyKey, fontSizeKey, fontWeightKey, cursorStyleKey, PanelSessionType } from '../../../types/const';
|
||||
import { labelFilter } from '@/types/form';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||
import { defaultFontFamily } from '@/types/xterm';
|
||||
import TerminalExample from '../terminal-example.vue';
|
||||
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||
const { preference, updateTerminalPreference, sessionManager } = useTerminalStore();
|
||||
|
||||
const background = preference.theme.schema.background;
|
||||
const previewTerminal = ref();
|
||||
const formModel = ref<TerminalDisplaySetting>({});
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
watch(formModel, (v, before) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
@@ -149,6 +149,26 @@
|
||||
options[key] = (formModel.value as any)[key];
|
||||
}
|
||||
});
|
||||
// 非初始化则修改终端样式
|
||||
if (before) {
|
||||
Object.values(sessionManager.sessions)
|
||||
.filter(s => s.type === PanelSessionType.SSH.type)
|
||||
.map(s => s as ISshSession)
|
||||
.forEach(s => {
|
||||
const options = s.inst.options;
|
||||
s.inst;
|
||||
// 修改样式
|
||||
Object.keys(v).forEach(k => {
|
||||
let value = v[k as keyof TerminalDisplaySetting];
|
||||
if (k === 'fontFamily') {
|
||||
value = value === '_' ? defaultFontFamily : `${value}, ${defaultFontFamily}`;
|
||||
}
|
||||
options[k as keyof typeof options] = value;
|
||||
});
|
||||
// 自适应
|
||||
s.fit();
|
||||
});
|
||||
}
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.DISPLAY_SETTING, formModel.value, true);
|
||||
// 聚焦
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<template v-for="color in toOptions(tabColorKey)">
|
||||
<div class="color-wrapper"
|
||||
:class="[formModel.color === color.value ? 'selected-color' : '']"
|
||||
:style="{ '--color': `${color.value}` }"
|
||||
:style="{ '--color': `${color.color}` }"
|
||||
@click="clickColor(color.value as string)">
|
||||
<div class="color-item">
|
||||
<div class="color-item-dot" />
|
||||
|
||||
@@ -19,52 +19,43 @@
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 快速滚动 -->
|
||||
<block-setting-item label="快速滚动" desc="alt + 鼠标滚轮快速滚动">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.fastScrollModifier" />
|
||||
<a-switch v-model="formModel.fastScrollModifier" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 点击移动光标 -->
|
||||
<block-setting-item label="点击移动光标" desc="alt + 鼠标左键可以切换光标位置">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.altClickMovesCursor" />
|
||||
<a-switch v-model="formModel.altClickMovesCursor" type="round" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键选中词条 -->
|
||||
<block-setting-item label="右键选中词条" desc="右键文本后会根据单词分隔符自动选中词条">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickSelectsWord" />
|
||||
<a-switch v-model="formModel.rightClickSelectsWord" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 选中自动复制 -->
|
||||
<block-setting-item label="选中自动复制" desc="自动将选中的文本复制到剪切板">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.selectionChangeCopy" />
|
||||
<a-switch v-model="formModel.selectionChangeCopy" type="round" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 复制去除空格 -->
|
||||
<block-setting-item label="复制去除空格" desc="复制文本后自动删除尾部空格">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.copyAutoTrim" />
|
||||
<a-switch v-model="formModel.copyAutoTrim" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 启用右键菜单 -->
|
||||
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableRightClickMenu" />
|
||||
<a-switch v-model="formModel.enableRightClickMenu" type="round" />
|
||||
</block-setting-item>
|
||||
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键粘贴 -->
|
||||
<block-setting-item label="右键粘贴"
|
||||
desc="启用右键自动粘贴需要关闭右键菜单. 如果启用右键选中词条且选中有文本时, 右键粘贴无效. 因浏览器安全策略限制, 此功能需要在 HTTPS 环境下使用">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickPaste" />
|
||||
<a-switch v-model="formModel.rightClickPaste" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 粘贴去除空格 -->
|
||||
<block-setting-item label="粘贴去除空格"
|
||||
desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键. 默认粘贴快捷键无法去除空格">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.pasteAutoTrim" />
|
||||
<a-switch v-model="formModel.pasteAutoTrim" type="round" />
|
||||
<template #desc>
|
||||
|
||||
</template>
|
||||
@@ -73,13 +64,13 @@
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 启用响铃 -->
|
||||
<block-setting-item label="启用响铃" desc="系统接收到 \a 时发出响铃 (一般不用开启)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableBell" />
|
||||
<a-switch v-model="formModel.enableBell" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 单词分隔符 -->
|
||||
<block-setting-item label="单词分隔符" desc="在终端中双击文本将使用该分隔符进行分割 (一般不用修改)">
|
||||
<a-input size="small"
|
||||
v-model="formModel.wordSeparator"
|
||||
<a-input v-model="formModel.wordSeparator"
|
||||
size="small"
|
||||
style="width: 188px"
|
||||
placeholder="单词分隔符"
|
||||
allow-clear />
|
||||
</block-setting-item>
|
||||
|
||||
@@ -11,25 +11,21 @@
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 超链接插件 -->
|
||||
<block-setting-item label="超链接插件" desc="自动检测 http(https) url 并可以点击">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableWeblinkPlugin" />
|
||||
<a-switch v-model="formModel.enableWeblinkPlugin" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- WebGL 渲染插件 -->
|
||||
<block-setting-item label="WebGL 渲染插件" desc="使用 WebGL 加速渲染终端 (建议开启, 若无法开启终端请关闭)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableWebglPlugin" />
|
||||
<a-switch v-model="formModel.enableWebglPlugin" type="round" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- unicode11 插件 -->
|
||||
<block-setting-item label="unicode11 插件" desc="支持 Unicode 11 字符集">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableUnicodePlugin" />
|
||||
<a-switch v-model="formModel.enableUnicodePlugin" type="round" />
|
||||
</block-setting-item>
|
||||
<!-- 图片渲染插件 -->
|
||||
<block-setting-item label="图片渲染插件" desc="支持使用 sixel 打开图片 (一般不需要开启)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableImagePlugin" />
|
||||
<a-switch v-model="formModel.enableImagePlugin" type="round" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
@@ -11,14 +11,15 @@
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 终端类型 -->
|
||||
<block-setting-item label="终端类型" desc="若显示异常请尝试切换此选项 兼容性 vt100 > xterm > 16color > 256color">
|
||||
<a-select style="width: 160px;"
|
||||
v-model="formModel.terminalEmulationType"
|
||||
<a-select v-model="formModel.terminalEmulationType"
|
||||
style="width: 188px;"
|
||||
size="small"
|
||||
:options="toOptions(emulationTypeKey)" />
|
||||
</block-setting-item>
|
||||
<!-- 缓冲区行数 -->
|
||||
<block-setting-item label="缓冲区行数" desc="保存在缓冲区的行数, 多出的行数会被忽略, 此值越大占用内存的内存会更多">
|
||||
<a-input-number v-model="formModel.scrollBackLine"
|
||||
style="width: 188px"
|
||||
size="small"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
</a-skeleton>
|
||||
<!-- 内容区域 -->
|
||||
<div v-else class="terminal-setting-body terminal-theme-container">
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">选择后会立刻保存, 刷新页面后生效</a-alert>
|
||||
<!-- 终端主题 -->
|
||||
<div class="theme-row"
|
||||
v-for="(themeArr, index) in themes"
|
||||
@@ -57,15 +55,17 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalTheme } from '@/api/asset/host-terminal';
|
||||
import type { ISshSession } from '../../../types/define';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||
import { PanelSessionType } from '../../../types/const';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { getTerminalThemes } from '@/api/asset/host-terminal';
|
||||
import { getPreference } from '@/api/user/preference';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import TerminalExample from '../terminal-example.vue';
|
||||
|
||||
const { updateTerminalPreference } = useTerminalStore();
|
||||
const { updateTerminalPreference, sessionManager } = useTerminalStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const currentThemeName = ref();
|
||||
@@ -73,8 +73,20 @@
|
||||
|
||||
// 选择主题
|
||||
const selectTheme = async (theme: TerminalTheme) => {
|
||||
// 修改主题色
|
||||
document.body.setAttribute('terminal-theme', theme.dark ? 'dark' : 'light');
|
||||
// 修改终端主题
|
||||
Object.values(sessionManager.sessions)
|
||||
.filter(s => s.type === PanelSessionType.SSH.type)
|
||||
.map(s => s as ISshSession)
|
||||
.forEach(s => {
|
||||
s.inst.options.theme = theme.schema;
|
||||
// 自适应
|
||||
s.blur();
|
||||
});
|
||||
// 同步
|
||||
currentThemeName.value = theme.name;
|
||||
await updateTerminalPreference(TerminalPreferenceItem.THEME, theme);
|
||||
await updateTerminalPreference(TerminalPreferenceItem.THEME, theme, true);
|
||||
};
|
||||
|
||||
// 加载用户主题
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
content="点击复制">
|
||||
<a-tag class="sftp-path-container pointer"
|
||||
color="arcoblue"
|
||||
@click="copy(path, '已复制')">
|
||||
@click="copy(path, true)">
|
||||
<span>{{ name }}</span>
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="复制路径">
|
||||
<span class="click-icon-wrapper row-action-icon"
|
||||
@click="copy(record.path, '已复制')">
|
||||
@click="copy(record.path, true)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
</a-tooltip>
|
||||
|
||||
@@ -86,6 +86,8 @@
|
||||
import { getFileSize } from '@/utils/file';
|
||||
import useVisible from '@/hooks/visible';
|
||||
|
||||
const emits = defineEmits(['closed']);
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { transferManager } = useTerminalStore();
|
||||
|
||||
@@ -130,6 +132,7 @@
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
fileList.value = [];
|
||||
emits('closed');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<div class="ssh-header-left">
|
||||
<!-- 主机地址 -->
|
||||
<span class="address-wrapper">
|
||||
{{ tab.address }}
|
||||
<span class="address-copy copy-right" title="复制" @click="copy(tab.address as string)">
|
||||
<icon-copy />
|
||||
<span class="text-copy"
|
||||
:title="tab.address"
|
||||
@click="copy(tab.address as string, true)">
|
||||
{{ tab.address }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -48,6 +49,8 @@
|
||||
class="search-modal"
|
||||
@find="findWords"
|
||||
@close="focus" />
|
||||
<!-- 上传文件模态框 -->
|
||||
<sftp-upload-modal ref="uploadModal" @closed="focus" />
|
||||
</div>
|
||||
</ssh-context-menu>
|
||||
<!-- 命令编辑器 -->
|
||||
@@ -76,7 +79,8 @@
|
||||
import ShellEditorModal from '@/components/view/shell-editor/modal/index.vue';
|
||||
import IconActions from '../layout/icon-actions.vue';
|
||||
import SshContextMenu from './ssh-context-menu.vue';
|
||||
import XtermSearchModal from '@/components/xtrem/search-modal/index.vue';
|
||||
import SftpUploadModal from '../sftp/sftp-upload-modal.vue';
|
||||
import XtermSearchModal from '@/components/xterm/search-modal/index.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
tab: TerminalPanelTabItem;
|
||||
@@ -87,6 +91,7 @@
|
||||
|
||||
const editorModal = ref();
|
||||
const searchModal = ref();
|
||||
const uploadModal = ref();
|
||||
const commandInput = ref();
|
||||
const terminalRef = ref();
|
||||
const session = ref<ISshSession>();
|
||||
@@ -141,7 +146,8 @@
|
||||
session.value = await sessionManager.openSsh(props.tab, {
|
||||
el: terminalRef.value,
|
||||
editorModal: editorModal.value,
|
||||
searchModal: searchModal.value
|
||||
searchModal: searchModal.value,
|
||||
uploadModal: uploadModal.value,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -177,7 +183,7 @@
|
||||
}
|
||||
|
||||
&-left {
|
||||
width: 34%;
|
||||
width: 25%;
|
||||
|
||||
.address-wrapper {
|
||||
height: 100%;
|
||||
@@ -185,16 +191,6 @@
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
.address-copy {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.address-copy {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: 'IP:';
|
||||
padding-right: 4px;
|
||||
@@ -203,7 +199,7 @@
|
||||
}
|
||||
|
||||
&-right {
|
||||
width: 66%;
|
||||
width: 75%;
|
||||
justify-content: flex-end;
|
||||
|
||||
.command-input {
|
||||
@@ -229,12 +225,23 @@
|
||||
}
|
||||
|
||||
.status-bridge {
|
||||
height: 100%;
|
||||
margin: 0 2px 0 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
user-select: none;
|
||||
|
||||
:deep(.arco-badge-status-text) {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
height: 56%;
|
||||
margin: 0 12px 0 6px;
|
||||
border-left: 2px solid var(--color-fill-4);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,7 +249,7 @@
|
||||
width: 100%;
|
||||
height: calc(100% - @ssh-header-height);
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
padding: 8px 2px 2px 8px;
|
||||
|
||||
.ssh-inst {
|
||||
width: 100%;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
class="transfer-drawer"
|
||||
:width="388"
|
||||
:unmount-on-close="false"
|
||||
:footer="false">
|
||||
:footer="false"
|
||||
@close="emits('closed')">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="path-drawer-title usn">
|
||||
@@ -76,6 +77,8 @@
|
||||
import { transferStatusKey } from '../../types/const';
|
||||
import TransferItem from './transfer-item.vue';
|
||||
|
||||
const emits = defineEmits(['closed']);
|
||||
|
||||
const { transferManager } = useTerminalStore();
|
||||
const { toOptions } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
@@ -110,16 +113,6 @@
|
||||
transferManager.cancelAllTransfer();
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -85,15 +85,21 @@ export default class SftpSession extends BaseSession implements ISftpSession {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '40vh',
|
||||
overflowY: 'auto'
|
||||
overflowY: 'auto',
|
||||
}
|
||||
},
|
||||
paths.map(s => {
|
||||
return h('span', { style: { marginTop: '4px' } }, s);
|
||||
return h('span', {
|
||||
style: {
|
||||
marginTop: '4px',
|
||||
wordBreak: 'break-all',
|
||||
}
|
||||
}, s);
|
||||
}));
|
||||
// 提示
|
||||
Modal.confirm({
|
||||
title: '确定后将立即删除这些文件且无法恢复!',
|
||||
title: `确定后将立即删除这 ${paths.length} 个文件且无法恢复!`,
|
||||
width: 426,
|
||||
modalStyle: { padding: '24px 32px' },
|
||||
bodyStyle: { marginTop: '-14px' },
|
||||
okButtonProps: { status: 'danger' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { ISftpTransferHandler, ISftpTransferManager, SftpFile, SftpTransferItem, TransferOperatorResponse } from '../types/define';
|
||||
import { sessionCloseMsg, TransferReceiver, TransferStatus, TransferType } from '../types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getTerminalAccessToken, openHostTransferChannel } from '@/api/asset/host-terminal';
|
||||
import { getTerminalTransferToken, openHostTransferChannel } from '@/api/asset/host-terminal';
|
||||
import { nextId } from '@/utils';
|
||||
import SftpTransferUploader from './sftp-transfer-uploader';
|
||||
import SftpTransferDownloader from './sftp-transfer-downloader';
|
||||
@@ -105,11 +105,11 @@ export default class SftpTransferManager implements ISftpTransferManager {
|
||||
// 打开会话
|
||||
private async openClient() {
|
||||
this.run = true;
|
||||
// 获取 access
|
||||
const { data: accessToken } = await getTerminalAccessToken();
|
||||
// 获取 transferToken
|
||||
const { data: transferToken } = await getTerminalTransferToken();
|
||||
// 打开会话
|
||||
try {
|
||||
this.client = await openHostTransferChannel(accessToken);
|
||||
this.client = await openHostTransferChannel(transferToken);
|
||||
} catch (e) {
|
||||
// 打开失败将传输列表置为失效
|
||||
Message.error('会话打开失败');
|
||||
|
||||
@@ -89,6 +89,7 @@ export default class SshSessionHandler implements ISshSessionHandler {
|
||||
case 'enter':
|
||||
case 'commandEditor':
|
||||
case 'openSftp':
|
||||
case 'uploadFile':
|
||||
case 'checkAppendMissing':
|
||||
return this.session.canWrite;
|
||||
case 'disconnect':
|
||||
@@ -217,6 +218,11 @@ export default class SshSessionHandler implements ISshSessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
uploadFile(): void {
|
||||
this.domRef.uploadModal.open(this.session.hostId, '/');
|
||||
}
|
||||
|
||||
// ctrl + c
|
||||
interrupt() {
|
||||
this.inst.paste(String.fromCharCode(3));
|
||||
|
||||
@@ -128,7 +128,7 @@ export default class SshSession extends BaseSession implements ISshSession {
|
||||
// 启用响铃
|
||||
if (preference.interactSetting.enableBell) {
|
||||
this.inst.onBell(() => {
|
||||
// 播放蜂鸣
|
||||
// 播放响铃
|
||||
playBell();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- 内容区域 -->
|
||||
<main class="host-terminal-layout-content">
|
||||
<!-- 主机加载中骨架 -->
|
||||
<loading-skeleton v-if="contentLoading" />
|
||||
<loading-skeleton v-if="loading" />
|
||||
<!-- 终端内容区域 -->
|
||||
<main-content v-else
|
||||
@open-command-snippet="() => snippetRef.open()"
|
||||
@@ -30,11 +30,11 @@
|
||||
</div>
|
||||
</main>
|
||||
<!-- 命令片段列表抽屉 -->
|
||||
<command-snippet-drawer ref="snippetRef" />
|
||||
<command-snippet-drawer ref="snippetRef" @closed="autoFocus" />
|
||||
<!-- 路径书签列表抽屉 -->
|
||||
<path-bookmark-drawer ref="pathRef" />
|
||||
<path-bookmark-drawer ref="pathRef" @closed="autoFocus" />
|
||||
<!-- 传输列表 -->
|
||||
<transfer-drawer ref="transferRef" />
|
||||
<transfer-drawer ref="transferRef" @closed="autoFocus" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,8 +49,10 @@
|
||||
import { ref, onBeforeMount, onUnmounted, onMounted } from 'vue';
|
||||
import { dictKeys, PanelSessionType, TerminalTabs } from './types/const';
|
||||
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
||||
import { useRoute } from 'vue-router';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import debug from '@/utils/env';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import LayoutHeader from './components/layout/layout-header.vue';
|
||||
import LeftSidebar from './components/layout/left-sidebar.vue';
|
||||
import RightSidebar from './components/layout/right-sidebar.vue';
|
||||
@@ -62,10 +64,9 @@
|
||||
import '@/assets/style/host-terminal-layout.less';
|
||||
import '@xterm/xterm/css/xterm.css';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const dictStore = useDictStore();
|
||||
const cacheStore = useCacheStore();
|
||||
const { loading: contentLoading, setLoading: setContentLoading } = useLoading(true);
|
||||
const { fetchPreference, getCurrentSession, openSession, preference, loadHosts, hosts, tabManager } = useTerminalStore();
|
||||
const { loading, setLoading } = useLoading(true);
|
||||
const route = useRoute();
|
||||
|
||||
const originTitle = document.title;
|
||||
const render = ref(false);
|
||||
@@ -75,7 +76,7 @@
|
||||
|
||||
// 终端截屏
|
||||
const screenshot = () => {
|
||||
const handler = terminalStore.getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true)?.handler;
|
||||
const handler = getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true)?.handler;
|
||||
if (handler && handler.enabledStatus('screenshot')) {
|
||||
handler.screenshot();
|
||||
}
|
||||
@@ -87,27 +88,58 @@
|
||||
event.returnValue = confirm('系统可能不会保存您所做的更改');
|
||||
};
|
||||
|
||||
// 自动聚焦
|
||||
const autoFocus = () => {
|
||||
getCurrentSession<ISshSession>(PanelSessionType.SSH.type)?.focus();
|
||||
};
|
||||
|
||||
// 打开默认打开页面
|
||||
onBeforeMount(() => {
|
||||
// 打开默认 tab
|
||||
let openTab;
|
||||
const tab = route.query.tab;
|
||||
if (tab) {
|
||||
openTab = Object.values(TerminalTabs).find(s => s.key === tab);
|
||||
}
|
||||
tabManager.openTab(openTab || TerminalTabs.NEW_CONNECTION);
|
||||
});
|
||||
|
||||
// 加载用户终端偏好
|
||||
onBeforeMount(async () => {
|
||||
await terminalStore.fetchPreference();
|
||||
// 加载偏好
|
||||
await fetchPreference();
|
||||
// 设置系统主题配色
|
||||
const dark = terminalStore.preference.theme.dark;
|
||||
const dark = preference.theme.dark;
|
||||
document.body.setAttribute('terminal-theme', dark ? 'dark' : 'light');
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 加载字典值
|
||||
onBeforeMount(async () => {
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
await useDictStore().loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 加载主机信息
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await terminalStore.loadHosts();
|
||||
// 加载主机
|
||||
await loadHosts();
|
||||
// 默认连接主机
|
||||
const connect = route.query.connect;
|
||||
if (connect) {
|
||||
const connectHostId = Number.parseInt(connect as string);
|
||||
const connectHost = hosts.hostList.find(s => s.id === connectHostId);
|
||||
// 打开连接
|
||||
if (connectHost) {
|
||||
const type = Object.values(PanelSessionType).find(s => s.type === route.query.type) || PanelSessionType.SSH;
|
||||
openSession(connectHost, type);
|
||||
} else {
|
||||
Message.error(`主机 ${connectHostId} 不存在/无权限`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setContentLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,13 +156,15 @@
|
||||
// 卸载处理
|
||||
onUnmounted(() => {
|
||||
// 卸载时清除 cache
|
||||
cacheStore.reset('authorizedHostKeys', 'authorizedHostIdentities', 'commandSnippetGroups', 'pathBookmarkGroups');
|
||||
// 移除关闭视口事件
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
useCacheStore().reset('authorizedHostKeys', 'authorizedHostIdentities', 'commandSnippetGroups', 'pathBookmarkGroups');
|
||||
// 去除 body style
|
||||
document.body.removeAttribute('terminal-theme');
|
||||
// 重置 title
|
||||
document.title = originTitle;
|
||||
// 移除关闭视口事件
|
||||
if (!debug) {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user