Compare commits

...

18 Commits

Author SHA1 Message Date
李佳航
777f7b3758 Merge pull request #52 from dromara/dev
Dev
2024-08-08 10:52:09 +08:00
lijiahang
947fa0fea3 🐳 添加 push 脚本. 2024-08-08 10:17:31 +08:00
lijiahang
7109e89fb4 🔖 升级版本. 2024-08-08 10:14:39 +08:00
lijiahang
70e7b1d544 修改终端标签颜色显示. 2024-08-07 10:18:39 +08:00
lijiahangmax
613f86155c 💄 修改终端样式. 2024-08-06 00:01:27 +08:00
lijiahang
8d0b58e48f ⬆️ 升级 orion kit 版本. 2024-08-05 13:49:52 +08:00
lijiahang
8cea9dc977 优化终端代码. 2024-08-05 09:11:54 +08:00
lijiahangmax
471acfdf00 优化自动聚焦逻辑. 2024-08-04 17:01:44 +08:00
lijiahangmax
8ed42131d0 优化命令片段处理逻辑. 2024-08-02 01:52:29 +08:00
lijiahangmax
18c605354a 🔨 修改终端逻辑. 2024-07-31 00:42:02 +08:00
李佳航
8c04411458 Merge pull request #48 from MemoryShadow/dev
perf: Use the.env file instead to modify docker-compose.yml
2024-07-30 17:53:31 +08:00
lijiahang
9a8d1d05cd 💄 修改终端样式. 2024-07-30 14:40:15 +08:00
MemoryShadow
1cbaf9c424 docs: Clarify behavior semantics and accelerate deployment efficiency 2024-07-29 07:02:32 +00:00
MemoryShadow
537c2fc108 perf: Use the.env file instead to modify docker-compose.yml 2024-07-29 07:02:21 +00:00
李佳航
122b568cf5 Merge pull request #46 from dromara/dev
Dev
2024-07-29 11:39:00 +08:00
lijiahang
8b97c02d15 🐳 修改 docker 配置. 2024-07-29 11:15:44 +08:00
lijiahang
dcfb016ce5 🔖 升级版本. 2024-07-29 10:33:59 +08:00
lijiahang
c842de9e23 修改 hook 位置. 2024-07-29 10:25:11 +08:00
135 changed files with 1057 additions and 863 deletions

15
.env.example Normal file
View 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
View File

@@ -33,3 +33,4 @@ build/
### VS Code ###
.vscode/
.env

View File

@@ -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;"

View File

@@ -1,17 +1,17 @@
version: '3.3'
services:
orion-visor-service:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.1
service:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.3
privileged: true
ports:
- 1081:80
environment:
- MYSQL_HOST=orion-visor-mysql
- MYSQL_HOST=mysql
- MYSQL_PORT=3306
- MYSQL_DATABASE=orion_visor
- MYSQL_USER=root
- MYSQL_PASSWORD=Data@123456
- REDIS_HOST=orion-visor-redis
- REDIS_HOST=redis
- REDIS_PASSWORD=Data@123456
- SECRET_KEY=uQeacXV8b3isvKLK
- DEMO_MODE=false
@@ -24,15 +24,15 @@ services:
retries: 200
start_period: 3s
depends_on:
orion-visor-mysql:
mysql:
condition: service_healthy
orion-visor-redis:
redis:
condition: service_healthy
links:
- orion-visor-mysql
- orion-visor-redis
orion-visor-mysql:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.1
- mysql
- redis
mysql:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.3
privileged: true
ports:
- 3307:3306
@@ -51,8 +51,8 @@ services:
timeout: 60s
retries: 10
start_period: 3s
orion-visor-redis:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.1
redis:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.3
privileged: true
ports:
- 6380:6379
@@ -71,9 +71,9 @@ services:
build:
context: ./docker/e2e
environment:
SERVER: http://orion-visor-service:80
SERVER: http://service:80
depends_on:
orion-visor-service:
service:
condition: service_healthy
links:
- orion-visor-service
- service

View File

@@ -1,22 +1,22 @@
version: '3.3'
services:
orion-visor-service:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-service:2.1.1
service:
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=orion-visor-mysql
- MYSQL_PORT=3306
- MYSQL_DATABASE=orion_visor
- MYSQL_USER=root
- MYSQL_PASSWORD=Data@123456
- REDIS_HOST=orion-visor-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
@@ -24,42 +24,42 @@ services:
retries: 200
start_period: 3s
depends_on:
orion-visor-mysql:
mysql:
condition: service_healthy
orion-visor-redis:
redis:
condition: service_healthy
links:
- orion-visor-mysql
- orion-visor-redis
orion-visor-mysql:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-mysql:2.1.1
- mysql
- redis
mysql:
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
timeout: 60s
retries: 10
start_period: 3s
orion-visor-redis:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-redis:2.1.1
redis:
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" ]
@@ -67,7 +67,12 @@ services:
timeout: 60s
retries: 10
start_period: 3s
orion-visor-adminer:
image: adminer
adminer:
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-visor-adminer:2.1.3
ports:
- 8081:8080
depends_on:
mysql:
condition: service_healthy
links:
- mysql

View File

@@ -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

View File

@@ -0,0 +1 @@
FROM adminer:latest

4
docker/adminer/build.sh Normal file
View File

@@ -0,0 +1,4 @@
#/bin/bash
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}

View File

@@ -1,7 +1,6 @@
#/bin/bash
version=2.1.1
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
View 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}

View File

@@ -1,5 +1,4 @@
#/bin/bash
version=2.1.1
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}

View File

@@ -1,9 +1,8 @@
#/bin/bash
version=2.1.1
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}

View File

@@ -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;"

View File

@@ -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

View File

@@ -14,11 +14,11 @@
<url>https://github.com/dromara/orion-visor</url>
<properties>
<revision>2.1.1</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>

View File

@@ -14,7 +14,7 @@ public interface AppConst extends OrionConst {
/**
* 同 ${orion.version} 迭代时候需要手动更改
*/
String VERSION = "2.1.1";
String VERSION = "2.1.3";
/**
* 同 ${spring.application.name}

View File

@@ -109,4 +109,6 @@ public interface ErrorMessage {
String CLIENT_ABORT = "手动中断";
String UNABLE_DOWNLOAD_FOLDER = "无法下载文件夹";
}

View File

@@ -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;

View File

@@ -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>
<!-- 修改 -->
@@ -118,7 +118,7 @@
<script lang="ts" setup>
import type { ${vue.featureEntity}QueryRequest, ${vue.featureEntity}QueryResponse } from '@/api/${vue.module}/${vue.feature}';
import { usePagination, useColLayout } from '@/types/card';
import { useCardPagination, useCardColLayout } from '@/hooks/card';
import { computed, reactive, ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { objectTruthKeyCount, resetObject } from '@/utils';
@@ -136,8 +136,8 @@
const emits = defineEmits(['openAdd', 'openUpdate']);
const cardColLayout = useColLayout();
const pagination = usePagination();
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
const { loading, setLoading } = useLoading();
#if($dictMap.entrySet().size() > 0)
const { toOptions, getDictValue } = useDictStore();

View File

@@ -151,9 +151,9 @@
import {} from '../types/const';
#end
#if($vue.enableRowSelection)
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
#else
import { usePagination } from '@/types/table';
import { useTablePagination } from '@/hooks/table';
#end
#if($dictMap.entrySet().size() > 0)
import { useDictStore } from '@/store';
@@ -161,7 +161,7 @@
const emits = defineEmits(['openAdd', 'openUpdate']);
const pagination = usePagination();
const pagination = useTablePagination();
#if($vue.enableRowSelection)
const rowSelection = useRowSelection();
#end

View File

@@ -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}")

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -33,9 +33,6 @@ public class CommandSnippetVO implements Serializable {
@Schema(description = "名称")
private String name;
@Schema(description = "触发前缀")
private String prefix;
@Schema(description = "代码片段")
private String command;

View File

@@ -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();

View File

@@ -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),
/**

View File

@@ -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;
}
// 返回

View File

@@ -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进制 权限

View File

@@ -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

View File

@@ -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) {

View File

@@ -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;
}
// 设置参数

View File

@@ -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) {
}
}

View File

@@ -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);
/**
* 获取连接信息
*

View File

@@ -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);

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -314,6 +314,11 @@ public class TerminalPreferenceModel implements GenericsDataModel {
*/
private Boolean openSftp;
/**
* 上传文件
*/
private Boolean uploadFile;
/**
* 清空
*/

View File

@@ -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))

View File

@@ -85,6 +85,8 @@ public class DataPermissionServiceImpl implements DataPermissionService {
.eq(DataPermissionDO::getRoleId, roleId)
.eq(DataPermissionDO::getType, type);
dataPermissionDAO.delete(wrapper);
// 删除缓存
this.deleteCache(type, userId, roleId);
if (Lists.isEmpty(request.getRelIdList())) {
return;
}
@@ -100,8 +102,6 @@ public class DataPermissionServiceImpl implements DataPermissionService {
.build())
.collect(Collectors.toList());
dataPermissionDAO.insertBatch(records);
// 删除缓存
this.deleteCache(type, userId, roleId);
}
@Override

View File

@@ -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);
}

View File

@@ -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.1'
VITE_APP_VERSION= '2.1.3'
VITE_APP_RELEASE= 'community'
VITE_SFTP_PREVIEW_MB= 2
VITE_DEMO_MODE= false

View File

@@ -1,6 +1,6 @@
VITE_API_BASE_URL= '/orion-visor/api'
VITE_WS_BASE_URL= '/orion-visor/keep-alive'
VITE_APP_VERSION= '2.1.1'
VITE_APP_VERSION= '2.1.3'
VITE_APP_RELEASE= 'community'
VITE_SFTP_PREVIEW_MB= 2
VITE_DEMO_MODE= false

View File

@@ -1,7 +1,7 @@
{
"name": "orion-visor-ui",
"description": "Orion Visor UI",
"version": "2.1.1",
"version": "2.1.3",
"private": true,
"author": "Jiahang Li",
"license": "Apache 2.0",

View File

@@ -29,7 +29,6 @@ export interface CommandSnippetQueryResponse extends CommandSnippetQueryResponse
export interface CommandSnippetQueryResponseExtra {
visible: boolean;
expand?: boolean;
}
/**

View File

@@ -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
*/

View File

@@ -31,7 +31,6 @@ export interface PathBookmarkQueryResponse extends PathBookmarkQueryResponseExtr
export interface PathBookmarkQueryResponseExtra {
visible: boolean;
expand?: boolean;
}
/**

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -54,7 +54,7 @@
import type { HostQueryResponse } from '@/api/asset/host';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import { useRowSelection } from '@/types/table';
import { useRowSelection } from '@/hooks/table';
import columns from '../types/table.columns';
import { computed } from 'vue';

View File

@@ -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();
}

View File

@@ -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<{

View File

@@ -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>
@@ -89,7 +89,7 @@
<script lang="ts" setup>
import type { ExecTemplateQueryRequest, ExecTemplateQueryResponse } from '@/api/exec/exec-template';
import { reactive, ref } from 'vue';
import { usePagination } from '@/types/table';
import { useTablePagination } from '@/hooks/table';
import useVisible from '@/hooks/visible';
import useLoading from '@/hooks/loading';
import { copy } from '@/hooks/copy';
@@ -100,7 +100,7 @@
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const pagination = usePagination();
const pagination = useTablePagination();
const tableRenderData = ref<ExecTemplateQueryResponse[]>([]);
const formModel = reactive<ExecTemplateQueryRequest>({

View File

@@ -9,7 +9,7 @@
<script lang="ts">
export default {
name: 'pathBookmarkGroupSelect'
name: 'pathBookmarkGroupSelector'
};
</script>

View File

@@ -9,7 +9,7 @@
<script lang="ts">
export default {
name: 'commandSnippetGroupSelect'
name: 'commandSnippetGroupSelector'
};
</script>

View File

@@ -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>

View File

@@ -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>
@@ -85,7 +85,7 @@
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { getHistoryValuePage } from '@/api/meta/history-value';
import { usePagination } from '@/types/table';
import { useTablePagination } from '@/hooks/table';
import { copy } from '@/hooks/copy';
import columns from './table.columns';
import { Message } from '@arco-design/web-vue';
@@ -96,7 +96,7 @@
}>();
const emits = defineEmits(['updated']);
const pagination = usePagination();
const pagination = useTablePagination();
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();

View File

@@ -129,6 +129,7 @@
: undefined;
// 查询数据
const { data } = await getSystemMessageList({
page: 1,
limit: messageLimit,
classify: currentClassify.value,
queryUnread: queryUnread.value,

View File

@@ -0,0 +1,35 @@
import type { PaginationProps } from '@arco-design/web-vue';
import type { ColResponsiveValue } from '@/types/card';
import { reactive } from 'vue';
import { isNumber } from '@/utils/is';
import { useAppStore } from '@/store';
import { CardPageSizeOptions } from '@/types/const';
/**
* 创建卡片列表列布局
*/
export const useCardColLayout = (): ColResponsiveValue => {
return reactive({
xs: 24,
sm: 12,
md: 8,
lg: 8,
xl: 8,
xxl: 6,
});
};
/**
* 创建创建卡片列表分页
*/
export const useCardPagination = (): PaginationProps => {
const appStore = useAppStore();
return reactive({
total: 0,
current: 1,
pageSize: isNumber(appStore.defaultCardPageSize) ? appStore.defaultCardPageSize : CardPageSizeOptions[0],
showTotal: true,
showPageSize: true,
pageSizeOptions: CardPageSizeOptions
});
};

View File

@@ -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) {

View File

@@ -8,7 +8,7 @@ import { TablePageSizeOptions } from '@/types/const';
/**
*
*/
export const usePagination = (ext?: PaginationProps): PaginationProps => {
export const useTablePagination = (ext?: PaginationProps): PaginationProps => {
const appStore = useAppStore();
return reactive({
total: 0,

View File

@@ -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);

View File

@@ -4,6 +4,7 @@ export type CacheType = 'users' | 'menus' | 'roles'
| 'dictKeys'
| 'authorizedHostKeys' | 'authorizedHostIdentities'
| 'commandSnippetGroups' | 'pathBookmarkGroups'
| 'commandSnippets' | 'pathBookmarks'
| 'execJob'
| string

View File

@@ -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);
}
},
},
});

View File

@@ -1,9 +1,5 @@
import type { PaginationProps, ResponsiveValue } from '@arco-design/web-vue';
import type { ResponsiveValue } from '@arco-design/web-vue';
import type { VNodeChild } from 'vue';
import { reactive } from 'vue';
import { useAppStore } from '@/store';
import { isNumber } from '@/utils/is';
import { CardPageSizeOptions } from '@/types/const';
/**
* 字段对齐方式
@@ -88,32 +84,3 @@ export interface HandleVisible {
disableSearch?: boolean;
disableReset?: boolean;
}
/**
* 创建卡片列表列布局
*/
export const useColLayout = (): ColResponsiveValue => {
return {
xs: 24,
sm: 12,
md: 8,
lg: 8,
xl: 8,
xxl: 6,
};
};
/**
* 创建创建卡片列表分页
*/
export const usePagination = (): PaginationProps => {
const appStore = useAppStore();
return reactive({
total: 0,
current: 1,
pageSize: isNumber(appStore.defaultCardPageSize) ? appStore.defaultCardPageSize : CardPageSizeOptions[0],
showTotal: true,
showPageSize: true,
pageSizeOptions: CardPageSizeOptions
});
};

View File

@@ -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

View File

@@ -203,7 +203,7 @@
import { reactive, ref, onMounted } from 'vue';
import { deleteHostConnectLog, getHostConnectLogPage, hostForceOffline } from '@/api/asset/host-connect-log';
import { connectStatusKey, connectTypeKey, HostConnectStatus } from '../types/const';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
import { Message } from '@arco-design/web-vue';
import columns from '../types/table.columns';
@@ -215,7 +215,7 @@
const emits = defineEmits(['openClear', 'openDetail']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore();

View File

@@ -188,7 +188,7 @@
import { reactive, ref, onMounted } from 'vue';
import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp';
import { sftpOperatorTypeKey, sftpOperatorResultKey, SftpOperatorType, showPathMaxCount } from '../types/const';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
import { Message } from '@arco-design/web-vue';
import columns from '../types/table.columns';
@@ -198,7 +198,7 @@
import UserSelector from '@/components/user/user/selector/index.vue';
import HostSelector from '@/components/asset/host/selector/index.vue';
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore();

View File

@@ -53,7 +53,7 @@
import type { HostKeyQueryResponse } from '@/api/asset/host-key';
import { ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { useRowSelection } from '@/types/table';
import { useRowSelection } from '@/hooks/table';
import { getAuthorizedHostIdentity, grantHostIdentity } from '@/api/asset/asset-data-grant';
import { useCacheStore, useDictStore } from '@/store';
import { hostIdentityColumns } from '../types/table.columns';

View File

@@ -33,7 +33,7 @@
import { ref, onMounted } from 'vue';
import { getAuthorizedHostKey, grantHostKey } from '@/api/asset/asset-data-grant';
import useLoading from '@/hooks/loading';
import { useRowSelection } from '@/types/table';
import { useRowSelection } from '@/hooks/table';
import { useCacheStore } from '@/store';
import { Message } from '@arco-design/web-vue';
import { hostKeyColumns } from '../types/table.columns';

View File

@@ -146,7 +146,7 @@
<script lang="ts" setup>
import type { HostIdentityQueryRequest, HostIdentityQueryResponse } from '@/api/asset/host-identity';
import { usePagination, useColLayout } from '@/types/card';
import { useCardPagination, useCardColLayout } from '@/hooks/card';
import { computed, reactive, ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { objectTruthKeyCount, resetObject } from '@/utils';
@@ -164,8 +164,8 @@
const list = ref<HostIdentityQueryResponse[]>([]);
const cardColLayout = useColLayout();
const pagination = usePagination();
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
const { toOptions, getDictValue } = useDictStore();
const { loading, setLoading } = useLoading();
const { hasAnyPermission } = usePermission();

View File

@@ -183,14 +183,14 @@
import usePermission from '@/hooks/permission';
import { copy } from '@/hooks/copy';
import { useDictStore } from '@/store';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
import { IdentityType, identityTypeKey } from '../types/const';
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
const emits = defineEmits(['openAdd', 'openUpdate', 'openKeyView']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { toOptions, getDictValue } = useDictStore();
const { loading, setLoading } = useLoading();

View File

@@ -81,7 +81,7 @@
<script lang="ts" setup>
import type { HostKeyQueryRequest, HostKeyQueryResponse } from '@/api/asset/host-key';
import { usePagination, useColLayout } from '@/types/card';
import { useCardPagination, useCardColLayout } from '@/hooks/card';
import { reactive, ref, onMounted } from 'vue';
import useLoading from '@/hooks/loading';
import { resetObject } from '@/utils';
@@ -94,8 +94,8 @@
const list = ref<HostKeyQueryResponse[]>([]);
const cardColLayout = useColLayout();
const pagination = usePagination();
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
const { loading, setLoading } = useLoading();
const formModel = reactive<HostKeyQueryRequest>({

View File

@@ -137,12 +137,12 @@
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
const emits = defineEmits(['openAdd', 'openUpdate', 'openView']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();

View File

@@ -196,7 +196,7 @@
<script lang="ts" setup>
import type { HostQueryRequest, HostQueryResponse } from '@/api/asset/host';
import { usePagination, useColLayout } from '@/types/card';
import { useCardPagination, useCardColLayout } from '@/hooks/card';
import { computed, reactive, ref, onMounted } from 'vue';
import { dataColor, objectTruthKeyCount, resetObject } from '@/utils';
import { deleteHost, getHostPage, updateHostStatus } from '@/api/asset/host';
@@ -211,8 +211,8 @@
const emits = defineEmits(['openAdd', 'openUpdate', 'openUpdateConfig', 'openHostGroup', 'openCopy']);
const cardColLayout = useColLayout();
const pagination = usePagination();
const cardColLayout = useCardColLayout();
const pagination = useCardPagination();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();

View File

@@ -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="主机地址">

View File

@@ -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,19 +255,20 @@
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 { usePagination, useRowSelection } from '@/types/table';
import { tagColor, hostTypeKey, hostStatusKey, hostType } from '../types/const';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
import { copy } from '@/hooks/copy';
import { dataColor } from '@/utils';
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']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue, toggleDictValue, toggleDict } = useDictStore();

View File

@@ -47,7 +47,7 @@ const columns = [
}, {
title: '操作',
slotName: 'handle',
width: 162,
width: 192,
align: 'center',
fixed: 'right',
},

View File

@@ -108,7 +108,7 @@
import columns from '../types/host-table.columns';
import { execHostStatusKey, execHostStatus } from '@/components/exec/log/const';
import { useDictStore } from '@/store';
import { useExpandable } from '@/types/table';
import { useExpandable } from '@/hooks/table';
import { dateFormat, formatDuration } from '@/utils';
import { downloadExecCommandLogFile, interruptHostExecCommand } from '@/api/exec/exec-command-log';
import { copy } from '@/hooks/copy';

View File

@@ -220,7 +220,7 @@
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { execStatus, execStatusKey } from '@/components/exec/log/const';
import { useExpandable, usePagination, useRowSelection } from '@/types/table';
import { useExpandable, useTablePagination, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
import { dateFormat, formatDuration } from '@/utils';
import { reExecCommand } from '@/api/exec/exec-command';
@@ -230,7 +230,7 @@
const emits = defineEmits(['viewCommand', 'viewParams', 'viewLog', 'openClear']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const expandable = useExpandable();
const { loading, setLoading } = useLoading();

View File

@@ -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>
@@ -133,12 +133,12 @@
import { Message } from '@arco-design/web-vue';
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { copy } from '@/hooks/copy';
const emits = defineEmits(['openAdd', 'openUpdate', 'openExec']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();

View File

@@ -187,14 +187,14 @@
import useLoading from '@/hooks/loading';
import columns from '../types/table.columns';
import { UploadTaskStatus, uploadTaskStatusKey } from '../types/const';
import { usePagination, useRowSelection } from '@/types/table';
import { useTablePagination, useRowSelection } from '@/hooks/table';
import { useDictStore } from '@/store';
import { copy } from '@/hooks/copy';
import UserSelector from '@/components/user/user/selector/index.vue';
const emits = defineEmits(['openClear']);
const pagination = usePagination();
const pagination = useTablePagination();
const rowSelection = useRowSelection();
const { loading, setLoading } = useLoading();
const { toOptions, getDictValue } = useDictStore();

View File

@@ -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 {

View File

@@ -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);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,5 +0,0 @@
// 打开 updateSnippet key
export const openUpdateSnippetKey = Symbol();
// 删除 snippet key
export const removeSnippetKey = Symbol();

View File

@@ -199,6 +199,7 @@
height: var(--header-height);
display: flex;
align-items: center;
transition: all .3s;
&::before {
display: none;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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);
}
};

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More