Compare commits

..

14 Commits

Author SHA1 Message Date
李佳航
032f1763f6 Merge pull request #95 from dromara/dev
Dev
2025-03-10 11:02:45 +08:00
lijiahangmax
d071ef64d8 🔨 编译问题. 2025-03-09 20:11:21 +08:00
lijiahangmax
c820443a3b 升级版本. 2025-03-09 20:00:10 +08:00
lijiahangmax
14c4e77445 修改终端提示. 2025-03-09 19:58:10 +08:00
lijiahangmax
79d9f69ed4 修改终端提示. 2025-03-08 12:44:33 +08:00
lijiahangmax
6c9065072d Merge remote-tracking branch 'origin/dev' into dev 2025-03-07 22:03:52 +08:00
lijiahang
05bc6c1fbb 修改路由规则. 2025-03-07 15:50:26 +08:00
lijiahang
a1dd9eec01 优化单元测试. 2025-03-07 14:57:26 +08:00
lijiahangmax
660df7c110 Merge remote-tracking branch 'origin/dev' into dev 2025-03-06 23:29:28 +08:00
lijiahang
093501a400 🐛 代码生成器未能读取到环境变量. 2025-03-06 10:17:48 +08:00
lijiahang
7943deb924 优化终端会话管理器. 2025-03-05 10:20:20 +08:00
lijiahangmax
490167e649 Merge remote-tracking branch 'origin/dev' into dev 2025-03-04 21:46:00 +08:00
lijiahang
8635f6bb05 🔨 修改静态资源匿名处理策略. 2025-03-04 10:27:55 +08:00
lijiahangmax
ac46dd6703 🎨 优化统计样式. 2025-03-02 20:16:09 +08:00
66 changed files with 367 additions and 291 deletions

View File

@@ -1,5 +1,5 @@
#/bin/bash #/bin/bash
version=2.3.4 version=2.3.5
docker build -t orion-visor-adminer:${version} . docker build -t orion-visor-adminer:${version} .
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version} docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:latest docker tag orion-visor-adminer:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:latest

View File

@@ -1,5 +1,5 @@
#/bin/bash #/bin/bash
version=2.3.4 version=2.3.5
cp -r ../../sql ./sql cp -r ../../sql ./sql
docker build -t orion-visor-mysql:${version} . docker build -t orion-visor-mysql:${version} .
rm -rf ./sql rm -rf ./sql

View File

@@ -1,5 +1,5 @@
#/bin/bash #/bin/bash
version=2.3.4 version=2.3.5
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version} docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-adminer:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version} docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-mysql:${version}
docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version} docker push registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}

View File

@@ -1,5 +1,5 @@
#/bin/bash #/bin/bash
version=2.3.4 version=2.3.5
docker build -t orion-visor-redis:${version} . docker build -t orion-visor-redis:${version} .
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version} docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:${version}
docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:latest docker tag orion-visor-redis:${version} registry.cn-hangzhou.aliyuncs.com/orionsec/orion-visor-redis:latest

View File

@@ -1,5 +1,5 @@
#/bin/bash #/bin/bash
version=2.3.4 version=2.3.5
mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar mv ../../orion-visor-launch/target/orion-visor-launch.jar ./orion-visor-launch.jar
mv ../../orion-visor-ui/dist ./dist mv ../../orion-visor-ui/dist ./dist
docker build -t orion-visor-service:${version} . docker build -t orion-visor-service:${version} .

View File

@@ -20,21 +20,21 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.dromara.visor.launch.configuration; package org.dromara.visor.common.configuration;
import cn.orionsec.kit.spring.SpringHolder; import cn.orionsec.kit.spring.SpringHolder;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
/** /**
* 应用配置类 * spring 配置类
* *
* @author Jiahang Li * @author Jiahang Li
* @version 1.0.0 * @version 1.0.0
* @since 2023/6/20 10:34 * @since 2023/6/20 10:34
*/ */
@Configuration @Configuration
public class LaunchApplicationConfiguration { public class SpringConfiguration {
/** /**
* @return spring 容器工具类 * @return spring 容器工具类

View File

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

View File

@@ -14,7 +14,7 @@
<url>https://github.com/dromara/orion-visor</url> <url>https://github.com/dromara/orion-visor</url>
<properties> <properties>
<revision>2.3.4</revision> <revision>2.3.5</revision>
<spring.boot.version>2.7.17</spring.boot.version> <spring.boot.version>2.7.17</spring.boot.version>
<spring.boot.admin.version>2.7.15</spring.boot.admin.version> <spring.boot.admin.version>2.7.15</spring.boot.admin.version>
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version> <flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>

View File

@@ -23,6 +23,8 @@
package org.dromara.visor.framework.mybatis.core.generator; package org.dromara.visor.framework.mybatis.core.generator;
import cn.orionsec.kit.lang.constant.Const; import cn.orionsec.kit.lang.constant.Const;
import cn.orionsec.kit.lang.utils.Strings;
import cn.orionsec.kit.lang.utils.Systems;
import cn.orionsec.kit.lang.utils.ansi.AnsiAppender; import cn.orionsec.kit.lang.utils.ansi.AnsiAppender;
import cn.orionsec.kit.lang.utils.ansi.style.AnsiFont; import cn.orionsec.kit.lang.utils.ansi.style.AnsiFont;
import cn.orionsec.kit.lang.utils.ansi.style.color.AnsiForeground; import cn.orionsec.kit.lang.utils.ansi.style.color.AnsiForeground;
@@ -32,6 +34,8 @@ import org.dromara.visor.framework.mybatis.core.generator.template.Table;
import org.dromara.visor.framework.mybatis.core.generator.template.Template; import org.dromara.visor.framework.mybatis.core.generator.template.Template;
import java.io.File; import java.io.File;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* 代码生成器 * 代码生成器
@@ -42,6 +46,8 @@ import java.io.File;
*/ */
public class CodeGenerators { public class CodeGenerators {
private static final Pattern ENV_VAR_PATTERN = Pattern.compile("\\$\\{([^:]+):([^}]+)\\}");
public static void main(String[] args) { public static void main(String[] args) {
// 输出路径 // 输出路径
String outputDir = "D:/MP/"; String outputDir = "D:/MP/";
@@ -76,11 +82,6 @@ public class CodeGenerators {
.disableUnitTest() .disableUnitTest()
.enableProviderApi() .enableProviderApi()
.vue("system", "message") .vue("system", "message")
.dict("messageClassify", "classify", "messageClassify")
.comment("消息分类")
.fields("NOTICE", "TODO")
.labels("通知", "待办")
.valueUseFields()
.dict("messageType", "type", "messageType") .dict("messageType", "type", "messageType")
.comment("消息类型") .comment("消息类型")
.fields("EXEC_FAILED", "UPLOAD_FAILED") .fields("EXEC_FAILED", "UPLOAD_FAILED")
@@ -94,9 +95,9 @@ public class CodeGenerators {
// jdbc 配置 - 使用配置文件 // jdbc 配置 - 使用配置文件
File yamlFile = new File("orion-visor-launch/src/main/resources/application-dev.yaml"); File yamlFile = new File("orion-visor-launch/src/main/resources/application-dev.yaml");
YmlExt yaml = YmlExt.load(yamlFile); YmlExt yaml = YmlExt.load(yamlFile);
String url = yaml.getValue("spring.datasource.druid.url"); String url = resolveConfigValue(yaml.getValue("spring.datasource.druid.url"));
String username = yaml.getValue("spring.datasource.druid.username"); String username = resolveConfigValue(yaml.getValue("spring.datasource.druid.username"));
String password = yaml.getValue("spring.datasource.druid.password"); String password = resolveConfigValue(yaml.getValue("spring.datasource.druid.password"));
// 执行 // 执行
runGenerator(outputDir, author, runGenerator(outputDir, author,
@@ -147,4 +148,31 @@ public class CodeGenerators {
System.out.print(line); System.out.print(line);
} }
/**
* 解析实际的配置
*
* @param value value
* @return value
*/
private static String resolveConfigValue(String value) {
if (Strings.isBlank(value)) {
return value;
}
Matcher matcher = ENV_VAR_PATTERN.matcher(value);
StringBuffer resultString = new StringBuffer();
while (matcher.find()) {
// 环境变量名
String envVar = matcher.group(1);
// 默认值
String defaultValue = matcher.group(2);
// 获取环境变量的值
String envValue = Systems.getEnv(envVar, defaultValue);
// 替换占位符
matcher.appendReplacement(resultString, Matcher.quoteReplacement(envValue));
}
// 处理结尾的剩余部分
matcher.appendTail(resultString);
return resultString.toString();
}
} }

View File

@@ -11,10 +11,10 @@ SELECT @TYPE_KEY_ID:= id FROM dict_key WHERE key_name = 'operatorLogType' AND de
INSERT INTO dict_value INSERT INTO dict_value
(`key_id`, `key_name`, `value`, `label`, `extra`, `sort`, `create_time`, `update_time`, `creator`, `updater`, `deleted`) (`key_id`, `key_name`, `value`, `label`, `extra`, `sort`, `create_time`, `update_time`, `creator`, `updater`, `deleted`)
VALUES VALUES
(@MODULE_KEY_ID, 'operatorLogModule', '${package.ModuleName}:${typeHyphen}', '$!{table.comment}', '{}', @MODULE_KEY_MAX_SORT + 10, now(), now(), '1', '1', 0), (@MODULE_KEY_ID, 'operatorLogModule', '${package.ModuleName}:${typeHyphen}', '$!{table.comment}', '{}', @MODULE_KEY_MAX_SORT + 10, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:create', '创建$!{table.comment}', '{}', 10, now(), now(), '1', '1', 0), (@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:create', '创建$!{table.comment}', '{}', 10, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:update', '更新$!{table.comment}', '{}', 20, now(), now(), '1', '1', 0), (@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:update', '更新$!{table.comment}', '{}', 20, now(), now(), 'admin', 'admin', 0),
(@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:delete', '删除$!{table.comment}', '{}', 30, now(), now(), '1', '1', 0); (@TYPE_KEY_ID, 'operatorLogType', '${typeHyphen}:delete', '删除$!{table.comment}', '{}', 30, now(), now(), 'admin', 'admin', 0);
#end #end
#if($dictMap.entrySet().size() > 0) #if($dictMap.entrySet().size() > 0)
@@ -23,7 +23,7 @@ VALUES
INSERT INTO dict_key INSERT INTO dict_key
(`key_name`, `value_type`, `extra_schema`, `description`, `create_time`, `update_time`, `creator`, `updater`, `deleted`) (`key_name`, `value_type`, `extra_schema`, `description`, `create_time`, `update_time`, `creator`, `updater`, `deleted`)
VALUES VALUES
('$enumEntity.value.keyName', 'STRING', '$enumEntity.value.extraSchema', '$enumEntity.value.comment', now(), now(), '1', '1', 0); ('$enumEntity.value.keyName', 'STRING', '$enumEntity.value.extraSchema', '$enumEntity.value.comment', now(), now(), 'admin', 'admin', 0);
-- 设置临时配置项id -- 设置临时配置项id
SELECT @TMP_KEY_ID:=LAST_INSERT_ID(); SELECT @TMP_KEY_ID:=LAST_INSERT_ID();
@@ -35,7 +35,7 @@ VALUES
#set($count = $enumEntity.value.fields.size() - 1) #set($count = $enumEntity.value.fields.size() - 1)
#foreach($index in [0..$count]) #foreach($index in [0..$count])
#set($sort = $index * 10 + 10) #set($sort = $index * 10 + 10)
(@TMP_KEY_ID, '$enumEntity.value.keyName', '$enumEntity.value.values.get($index)', '$enumEntity.value.labels.get($index)', #if($enumEntity.value.extraJson.size() > $index)'$enumEntity.value.extraJson.get($index)'#else'{}'#end, $sort, now(), now(), '1', '1', 0)#if($foreach.hasNext),#else;#end (@TMP_KEY_ID, '$enumEntity.value.keyName', '$enumEntity.value.values.get($index)', '$enumEntity.value.labels.get($index)', #if($enumEntity.value.extraJson.size() > $index)'$enumEntity.value.extraJson.get($index)'#else'{}'#end, $sort, now(), now(), 'admin', 'admin', 0)#if($foreach.hasNext),#else;#end
#end #end
#end #end

View File

@@ -4,7 +4,7 @@
INSERT INTO system_menu INSERT INTO system_menu
(parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted) (parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted)
VALUES VALUES
(0, '${table.comment}管理', 1, 10, 1, 1, 1, '${vue.moduleEntityFirstLower}Module', '1', '1', 0); (0, '${table.comment}管理', 1, 10, 1, 1, 1, '${vue.moduleEntityFirstLower}Module', 'admin', 'admin', 0);
-- 设置临时父菜单id -- 设置临时父菜单id
SELECT @TMP_PARENT_ID:=LAST_INSERT_ID(); SELECT @TMP_PARENT_ID:=LAST_INSERT_ID();
@@ -13,7 +13,7 @@ SELECT @TMP_PARENT_ID:=LAST_INSERT_ID();
INSERT INTO system_menu INSERT INTO system_menu
(parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted) (parent_id, name, type, sort, visible, status, cache, component, creator, updater, deleted)
VALUES VALUES
(@TMP_PARENT_ID, '$table.comment', 2, 10, 1, 1, 1, '$vue.featureEntityFirstLower', '1', '1', 0); (@TMP_PARENT_ID, '$table.comment', 2, 10, 1, 1, 1, '$vue.featureEntityFirstLower', 'admin', 'admin', 0);
-- 设置临时子菜单id -- 设置临时子菜单id
SELECT @TMP_SUB_ID:=LAST_INSERT_ID(); SELECT @TMP_SUB_ID:=LAST_INSERT_ID();
@@ -22,7 +22,7 @@ SELECT @TMP_SUB_ID:=LAST_INSERT_ID();
INSERT INTO system_menu INSERT INTO system_menu
(parent_id, name, permission, type, sort, creator, updater, deleted) (parent_id, name, permission, type, sort, creator, updater, deleted)
VALUES VALUES
(@TMP_SUB_ID, '查询$table.comment', '${package.ModuleName}:${typeHyphen}:query', 3, 10, '1', '1', 0), (@TMP_SUB_ID, '查询$table.comment', '${package.ModuleName}:${typeHyphen}:query', 3, 10, 'admin', 'admin', 0),
(@TMP_SUB_ID, '创建$table.comment', '${package.ModuleName}:${typeHyphen}:create', 3, 20, '1', '1', 0), (@TMP_SUB_ID, '创建$table.comment', '${package.ModuleName}:${typeHyphen}:create', 3, 20, 'admin', 'admin', 0),
(@TMP_SUB_ID, '修改$table.comment', '${package.ModuleName}:${typeHyphen}:update', 3, 30, '1', '1', 0), (@TMP_SUB_ID, '修改$table.comment', '${package.ModuleName}:${typeHyphen}:update', 3, 30, 'admin', 'admin', 0),
(@TMP_SUB_ID, '删除$table.comment', '${package.ModuleName}:${typeHyphen}:delete', 3, 40, '1', '1', 0); (@TMP_SUB_ID, '删除$table.comment', '${package.ModuleName}:${typeHyphen}:delete', 3, 40, 'admin', 'admin', 0);

View File

@@ -8,7 +8,7 @@ const $vue.moduleConst: AppRouteRecordRaw = {
children: [ children: [
{ {
name: '$vue.featureEntityFirstLower', name: '$vue.featureEntityFirstLower',
path: '/$vue.feature', path: '/$vue.module/$vue.feature',
component: () => import('@/views/$vue.module/$vue.feature/index.vue'), component: () => import('@/views/$vue.module/$vue.feature/index.vue'),
}, },
], ],

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = { const fieldConfig = {
rowGap: '10px', rowGap: '10px',
labelSpan: 8, labelSpan: 6,
minHeight: '22px', minHeight: '22px',
fields: [ fields: [
{ {

View File

@@ -38,7 +38,27 @@ public class StaticResourceAuthorizeRequestsCustomizer extends AuthorizeRequests
@Override @Override
public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) { public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
// 静态资源可匿名访问 // 静态资源可匿名访问
registry.antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll(); registry.antMatchers(HttpMethod.GET,
"/*.html",
"/*.css",
"/*.js",
"/*.gz",
"/*.ico",
"/*.jpg",
"/*.png",
"/*.svg",
"/*.json",
"/*.webmanifest",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/**/*.gz",
"/**/*.ico",
"/**/*.jpg",
"/**/*.png",
"/**/*.svg",
"/**/*.json"
).permitAll();
} }
} }

View File

@@ -24,6 +24,7 @@ package org.dromara.visor.framework.test.core.base;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
import org.dromara.visor.common.configuration.SpringConfiguration;
import org.dromara.visor.framework.datasource.configuration.OrionDataSourceAutoConfiguration; import org.dromara.visor.framework.datasource.configuration.OrionDataSourceAutoConfiguration;
import org.dromara.visor.framework.mybatis.configuration.OrionMybatisAutoConfiguration; import org.dromara.visor.framework.mybatis.configuration.OrionMybatisAutoConfiguration;
import org.dromara.visor.framework.redis.configuration.OrionRedisAutoConfiguration; import org.dromara.visor.framework.redis.configuration.OrionRedisAutoConfiguration;
@@ -57,6 +58,8 @@ import org.springframework.transaction.annotation.Transactional;
public class BaseUnitTest { public class BaseUnitTest {
@Import({ @Import({
// spring
SpringConfiguration.class,
// mock // mock
OrionMockBeanTestConfiguration.class, OrionMockBeanTestConfiguration.class,
OrionMockRedisTestConfiguration.class, OrionMockRedisTestConfiguration.class,
@@ -74,7 +77,6 @@ public class BaseUnitTest {
RedisAutoConfiguration.class, RedisAutoConfiguration.class,
RedissonAutoConfiguration.class, RedissonAutoConfiguration.class,
}) })
// TODO
public static class Application { public static class Application {
} }

View File

@@ -42,7 +42,11 @@ import java.util.Optional;
* @version 1.0.0 * @version 1.0.0
* @since 2023/6/19 16:55 * @since 2023/6/19 16:55
*/ */
@SpringBootApplication(scanBasePackages = {"org.dromara.visor.launch", "org.dromara.visor.module"}) @SpringBootApplication(scanBasePackages = {
"org.dromara.visor.launch",
"org.dromara.visor.common",
"org.dromara.visor.module"
})
public class LaunchApplication { public class LaunchApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -39,9 +39,9 @@ import java.util.function.Function;
*/ */
public class ReplaceVersion { public class ReplaceVersion {
private static final String TARGET_VERSION = "2.3.3"; private static final String TARGET_VERSION = "2.3.4";
private static final String REPLACE_VERSION = "2.3.4"; private static final String REPLACE_VERSION = "2.3.5";
private static final String PATH = new File("").getAbsolutePath(); private static final String PATH = new File("").getAbsolutePath();

View File

@@ -82,6 +82,10 @@
<groupId>org.dromara.visor</groupId> <groupId>org.dromara.visor</groupId>
<artifactId>orion-visor-spring-boot-starter-job</artifactId> <artifactId>orion-visor-spring-boot-starter-job</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.dromara.visor</groupId>
<artifactId>orion-visor-spring-boot-starter-test</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -63,7 +63,7 @@ public class AssetAuthorizedDataServiceController {
@IgnoreLog(IgnoreLogMode.RET) @IgnoreLog(IgnoreLogMode.RET)
@GetMapping("/current-host") @GetMapping("/current-host")
@Operation(summary = "查询当前用户已授权的主机") @Operation(summary = "查询当前用户已授权的主机")
public AuthorizedHostWrapperVO getCurrentAuthorizedHost(@RequestParam("type") String type) { public AuthorizedHostWrapperVO getCurrentAuthorizedHost(@RequestParam(value = "type", required = false) String type) {
return assetAuthorizedDataService.getUserAuthorizedHost(SecurityUtils.getLoginUserId(), type); return assetAuthorizedDataService.getUserAuthorizedHost(SecurityUtils.getLoginUserId(), type);
} }

View File

@@ -49,7 +49,17 @@ public interface TerminalConnectLogDAO extends IMapper<TerminalConnectLogDO> {
* @param limit limit * @param limit limit
* @return hostId * @return hostId
*/ */
List<Long> selectLatestConnectHostId(@Param("userId") Long userId, @Param("type") String type, @Param("limit") Integer limit); default List<Long> selectLatestConnectHostId(Long userId, String type, Integer limit) {
return this.of()
.createWrapper(true)
.select(TerminalConnectLogDO::getHostId)
.eq(TerminalConnectLogDO::getUserId, userId)
.eq(TerminalConnectLogDO::getType, type)
.orderByDesc(TerminalConnectLogDO::getId)
.then()
.limit(limit)
.list(TerminalConnectLogDO::getHostId);
}
/** /**
* 查询终端连接日志用户数量 * 查询终端连接日志用户数量

View File

@@ -31,10 +31,10 @@ package org.dromara.visor.module.asset.handler.host.jsch;
*/ */
public interface SessionMessage { public interface SessionMessage {
String AUTHENTICATION_FAILURE = "authentication failed. please check the configuration. - {}"; String AUTHENTICATION_FAILURE = "身份认证失败. {}";
String SERVER_UNREACHABLE = "remote server unreachable. please check the configuration. - {}"; String SERVER_UNREACHABLE = "无法连接至服务器. {}";
String CONNECTION_TIMEOUT = "connection timeout. - {}"; String CONNECTION_TIMEOUT = "连接服务器超时. {}";
} }

View File

@@ -135,13 +135,6 @@ public interface HostService {
*/ */
void deleteHostRelByIdListAsync(List<Long> idList); void deleteHostRelByIdListAsync(List<Long> idList);
/**
* 获取当前更新配置的 hostId
*
* @return hostId
*/
Long getCurrentUpdateConfigHostId();
/** /**
* 清除缓存 * 清除缓存
*/ */

View File

@@ -83,8 +83,6 @@ import java.util.stream.Collectors;
@Service @Service
public class HostServiceImpl implements HostService { public class HostServiceImpl implements HostService {
private static final ThreadLocal<Long> CURRENT_UPDATE_CONFIG_ID = new ThreadLocal<>();
@Resource @Resource
private HostDAO hostDAO; private HostDAO hostDAO;
@@ -185,30 +183,25 @@ public class HostServiceImpl implements HostService {
OperatorLogs.add(ExtraFieldConst.CONFIG, param); OperatorLogs.add(ExtraFieldConst.CONFIG, param);
log.info("HostService-updateHostConfig request: {}", param); log.info("HostService-updateHostConfig request: {}", param);
Long id = request.getId(); Long id = request.getId();
try { // 查询主机信息
CURRENT_UPDATE_CONFIG_ID.set(id); HostDO host = hostDAO.selectById(id);
// 查询主机信息 Valid.notNull(host, ErrorMessage.HOST_ABSENT);
HostDO host = hostDAO.selectById(id); HostTypeEnum type = Valid.valid(HostTypeEnum::of, host.getType());
Valid.notNull(host, ErrorMessage.HOST_ABSENT); GenericsDataModel beforeConfig = type.parse(host.getConfig());
HostTypeEnum type = Valid.valid(HostTypeEnum::of, host.getType()); GenericsDataModel newConfig = type.parse(request.getConfig());
GenericsDataModel beforeConfig = type.parse(host.getConfig()); // 添加日志参数
GenericsDataModel newConfig = type.parse(request.getConfig()); OperatorLogs.add(OperatorLogs.ID, id);
// 添加日志参数 OperatorLogs.add(OperatorLogs.NAME, host.getName());
OperatorLogs.add(OperatorLogs.ID, id); // 更新前校验
OperatorLogs.add(OperatorLogs.NAME, host.getName()); type.doValid(beforeConfig, newConfig);
// 更新前校验 // 修改配置
type.doValid(beforeConfig, newConfig); HostDO updateHost = HostDO.builder()
// 修改配置 .id(id)
HostDO updateHost = HostDO.builder() .config(newConfig.serial())
.id(id) .build();
.config(newConfig.serial()) int effect = hostDAO.updateById(updateHost);
.build(); log.info("HostService-updateHostConfig effect: {}", effect);
int effect = hostDAO.updateById(updateHost); return effect;
log.info("HostService-updateHostConfig effect: {}", effect);
return effect;
} finally {
CURRENT_UPDATE_CONFIG_ID.remove();
}
} }
@Override @Override
@@ -348,11 +341,6 @@ public class HostServiceImpl implements HostService {
dataExtraApi.deleteByRelIdList(DataExtraTypeEnum.HOST, idList); dataExtraApi.deleteByRelIdList(DataExtraTypeEnum.HOST, idList);
} }
@Override
public Long getCurrentUpdateConfigHostId() {
return CURRENT_UPDATE_CONFIG_ID.get();
}
@Override @Override
public void clearCache() { public void clearCache() {
RedisMaps.scanKeysDelete(HostCacheKeyDefine.HOST_INFO.format("*")); RedisMaps.scanKeysDelete(HostCacheKeyDefine.HOST_INFO.format("*"));

View File

@@ -48,6 +48,7 @@ import org.dromara.visor.module.asset.enums.HostIdentityTypeEnum;
import org.dromara.visor.module.asset.enums.HostSshAuthTypeEnum; import org.dromara.visor.module.asset.enums.HostSshAuthTypeEnum;
import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel; import org.dromara.visor.module.asset.handler.host.config.model.HostSshConfigModel;
import org.dromara.visor.module.asset.handler.host.extra.model.HostSshExtraModel; import org.dromara.visor.module.asset.handler.host.extra.model.HostSshExtraModel;
import org.dromara.visor.module.asset.service.AssetAuthorizedDataService;
import org.dromara.visor.module.asset.service.HostConfigService; import org.dromara.visor.module.asset.service.HostConfigService;
import org.dromara.visor.module.asset.service.HostExtraService; import org.dromara.visor.module.asset.service.HostExtraService;
import org.dromara.visor.module.asset.service.TerminalService; import org.dromara.visor.module.asset.service.TerminalService;
@@ -81,7 +82,7 @@ public class TerminalServiceImpl implements TerminalService {
private HostExtraService hostExtraService; private HostExtraService hostExtraService;
@Resource @Resource
private AssetAuthorizedDataServiceImpl assetAuthorizedDataService; private AssetAuthorizedDataService assetAuthorizedDataService;
@Resource @Resource
private HostDAO hostDAO; private HostDAO hostDAO;

View File

@@ -33,16 +33,6 @@
id, user_id, username, host_id, host_name, host_address, type, session_id, status, start_time, end_time, extra_info, create_time, update_time, deleted id, user_id, username, host_id, host_name, host_address, type, session_id, status, start_time, end_time, extra_info, create_time, update_time, deleted
</sql> </sql>
<select id="selectLatestConnectHostId" resultType="java.lang.Long">
SELECT host_id
FROM terminal_connect_log
WHERE deleted = 0
AND type = #{type}
AND user_id = #{userId}
ORDER BY id DESC
LIMIT #{limit}
</select>
<select id="selectConnectLogUserCount" resultMap="CountResultMap"> <select id="selectConnectLogUserCount" resultMap="CountResultMap">
SELECT DATE(create_time) connect_date, COUNT(1) total_count SELECT DATE(create_time) connect_date, COUNT(1) total_count
FROM terminal_connect_log FROM terminal_connect_log

View File

@@ -3,4 +3,4 @@ VITE_API_BASE_URL=http://127.0.0.1:9200/orion-visor/api
# websocket 路径 # websocket 路径
VITE_WS_BASE_URL=ws://127.0.0.1:9200/orion-visor/keep-alive VITE_WS_BASE_URL=ws://127.0.0.1:9200/orion-visor/keep-alive
# 版本号 # 版本号
VITE_APP_VERSION=2.3.4 VITE_APP_VERSION=2.3.5

View File

@@ -3,4 +3,4 @@ VITE_API_BASE_URL=/orion-visor/api
# websocket 路径 # websocket 路径
VITE_WS_BASE_URL=/orion-visor/keep-alive VITE_WS_BASE_URL=/orion-visor/keep-alive
# 版本号 # 版本号
VITE_APP_VERSION=2.3.4 VITE_APP_VERSION=2.3.5

View File

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

View File

@@ -58,7 +58,7 @@
v-model:selected-group="selectedGroup" v-model:selected-group="selectedGroup"
:host-list="hostList" :host-list="hostList"
:groups="hosts?.groupTree as any" :groups="hosts?.groupTree as any"
:nodes="treeNodes" /> :nodes="treeNodes as any" />
<!-- 列表视图 --> <!-- 列表视图 -->
<host-table v-else <host-table v-else
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
@@ -83,13 +83,13 @@
import { onMounted, ref, watch, computed } from 'vue'; import { onMounted, ref, watch, computed } from 'vue';
import { dataColor } from '@/utils'; import { dataColor } from '@/utils';
import { dictKeys, NewConnectionType, newConnectionTypeKey } from './types/const'; import { dictKeys, NewConnectionType, newConnectionTypeKey } from './types/const';
import { useDictStore } from '@/store'; import { useCacheStore, useDictStore } from '@/store';
import { tagLabelFilter } from '@/types/form'; import { tagLabelFilter } from '@/types/form';
import { tagColor } from '@/views/asset/host-list/types/const'; import { tagColor } from '@/views/asset/host-list/types/const';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible'; import useVisible from '@/hooks/visible';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import { getAuthorizedHostOptions } from '@/types/options'; import { getAuthorizedHostOptions } from '@/types/options';
import { getLatestConnectHostId } from '@/api/asset/terminal-connect-log';
import HostTable from './components/host-table.vue'; import HostTable from './components/host-table.vue';
import HostGroup from './components/host-group.vue'; import HostGroup from './components/host-group.vue';
@@ -151,10 +151,13 @@
setLoading(true); setLoading(true);
try { try {
// 加载主机列表 // 加载主机列表
const { data } = await getCurrentAuthorizedHost(props.type); const data = await useCacheStore().loadAuthorizedHosts(props.type);
hosts.value = data;
// 禁用别名 // 禁用别名
data.hostList.forEach(s => s.alias = undefined as unknown as string); data.hostList.forEach(s => s.alias = undefined as unknown as string);
// 查询最近连接的主机
const { data: latestHosts } = await getLatestConnectHostId(props.type as string, 30);
data.latestHosts = latestHosts;
hosts.value = data;
// 设置主机搜索选项 // 设置主机搜索选项
filterOptions.value = getAuthorizedHostOptions(data.hostList); filterOptions.value = getAuthorizedHostOptions(data.hostList);
} catch (e) { } catch (e) {

View File

@@ -27,7 +27,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ExecLogQueryResponse } from '@/api/exec/exec-log'; import type { ExecLogQueryResponse } from '@/api/exec/exec-log';
import type { ExecType, ILogAppender } from '../const'; import type { ExecType, ILogAppender } from '../const';
import { onUnmounted, ref, nextTick, onMounted } from 'vue'; import { onUnmounted, ref, nextTick, onMounted, markRaw } from 'vue';
import { getExecCommandLogStatus } from '@/api/exec/exec-command-log'; import { getExecCommandLogStatus } from '@/api/exec/exec-command-log';
import { getExecJobLogStatus } from '@/api/exec/exec-job-log'; import { getExecJobLogStatus } from '@/api/exec/exec-job-log';
import { dictKeys, ExecHostStatus, ExecStatus } from '../const'; import { dictKeys, ExecHostStatus, ExecStatus } from '../const';
@@ -58,11 +58,11 @@
const { log_webScrollLines } = await useCacheStore().loadSystemSetting(); const { log_webScrollLines } = await useCacheStore().loadSystemSetting();
const scrollLines = toAnonymousNumber(log_webScrollLines) || 1000; const scrollLines = toAnonymousNumber(log_webScrollLines) || 1000;
// 创建 appender // 创建 appender
appender.value = new LogAppender({ appender.value = markRaw(new LogAppender({
id: record.id, id: record.id,
type: props.type, type: props.type,
scrollLines, scrollLines,
}); }));
// 定时查询执行状态 // 定时查询执行状态
if (record.status === ExecStatus.WAITING || if (record.status === ExecStatus.WAITING ||
record.status === ExecStatus.RUNNING) { record.status === ExecStatus.RUNNING) {

View File

@@ -8,17 +8,17 @@ const ASSET_AUDIT: AppRouteRecordRaw = {
children: [ children: [
{ {
name: 'connectLog', name: 'connectLog',
path: '/connect-log', path: '/audit/connect-log',
component: () => import('@/views/asset-audit/connect-log/index.vue'), component: () => import('@/views/asset-audit/connect-log/index.vue'),
}, },
{ {
name: 'connectSession', name: 'connectSession',
path: '/connect-session', path: '/audit/connect-session',
component: () => import('@/views/asset-audit/connect-session/index.vue'), component: () => import('@/views/asset-audit/connect-session/index.vue'),
}, },
{ {
name: 'sftpLog', name: 'sftpLog',
path: '/sftp-log', path: '/audit/sftp-log',
component: () => import('@/views/asset-audit/sftp-log/index.vue'), component: () => import('@/views/asset-audit/sftp-log/index.vue'),
}, },
], ],

View File

@@ -8,19 +8,19 @@ const ASSET: AppRouteRecordRaw = {
children: [ children: [
{ {
name: 'hostList', name: 'hostList',
path: '/host-list', path: '/asset/host',
component: () => import('@/views/asset/host-list/index.vue'), component: () => import('@/views/asset/host-list/index.vue'),
}, { }, {
name: 'hostKey', name: 'hostKey',
path: '/host-key', path: '/asset/host-key',
component: () => import('@/views/asset/host-key/index.vue'), component: () => import('@/views/asset/host-key/index.vue'),
}, { }, {
name: 'hostIdentity', name: 'hostIdentity',
path: '/host-identity', path: '/asset/host-identity',
component: () => import('@/views/asset/host-identity/index.vue'), component: () => import('@/views/asset/host-identity/index.vue'),
}, { }, {
name: 'assetGrant', name: 'assetGrant',
path: '/asset-grant', path: '/asset/grant',
component: () => import('@/views/asset/grant/index.vue'), component: () => import('@/views/asset/grant/index.vue'),
}, },
], ],

View File

@@ -9,37 +9,37 @@ const EXEC: Array<AppRouteRecordRaw> = [
children: [ children: [
{ {
name: 'execCommand', name: 'execCommand',
path: '/exec-command', path: '/exec/command',
component: () => import('@/views/exec/exec-command/index.vue'), component: () => import('@/views/exec/exec-command/index.vue'),
}, },
{ {
name: 'execCommandLog', name: 'execCommandLog',
path: '/exec-log', path: '/exec/command-log',
component: () => import('@/views/exec/exec-command-log/index.vue'), component: () => import('@/views/exec/exec-command-log/index.vue'),
}, },
{ {
name: 'execJob', name: 'execJob',
path: '/exec-job', path: '/exec/job',
component: () => import('@/views/exec/exec-job/index.vue'), component: () => import('@/views/exec/exec-job/index.vue'),
}, },
{ {
name: 'execJobLog', name: 'execJobLog',
path: '/exec-job-log', path: '/exec/job-log',
component: () => import('@/views/exec/exec-job-log/index.vue'), component: () => import('@/views/exec/exec-job-log/index.vue'),
}, },
{ {
name: 'batchUpload', name: 'batchUpload',
path: '/batch-upload', path: '/exec/upload',
component: () => import('@/views/exec/batch-upload/index.vue'), component: () => import('@/views/exec/batch-upload/index.vue'),
}, },
{ {
name: 'uploadTask', name: 'uploadTask',
path: '/upload-task', path: '/exec/upload-task',
component: () => import('@/views/exec/upload-task/index.vue'), component: () => import('@/views/exec/upload-task/index.vue'),
}, },
{ {
name: 'execTemplate', name: 'execTemplate',
path: '/exec-template', path: '/exec/template',
component: () => import('@/views/exec/exec-template/index.vue'), component: () => import('@/views/exec/exec-template/index.vue'),
}, },
], ],
@@ -50,7 +50,7 @@ const EXEC: Array<AppRouteRecordRaw> = [
children: [ children: [
{ {
name: 'execJobLogView', name: 'execJobLogView',
path: '/job-log-view', path: '/exec/job-log/view',
component: () => import('@/views/exec/exec-job-log-view/index.vue'), component: () => import('@/views/exec/exec-job-log-view/index.vue'),
}, },
], ],

View File

@@ -8,17 +8,17 @@ const SYSTEM: AppRouteRecordRaw = {
children: [ children: [
{ {
name: 'systemMenu', name: 'systemMenu',
path: '/menu', path: '/system/menu',
component: () => import('@/views/system/menu/index.vue'), component: () => import('@/views/system/menu/index.vue'),
}, },
{ {
name: 'dictKey', name: 'dictKey',
path: '/dict-key', path: '/system/dict-key',
component: () => import('@/views/system/dict-key/index.vue'), component: () => import('@/views/system/dict-key/index.vue'),
}, },
{ {
name: 'dictValue', name: 'dictValue',
path: '/dict-value', path: '/system/dict-value',
component: () => import('@/views/system/dict-value/index.vue'), component: () => import('@/views/system/dict-value/index.vue'),
}, },
{ {

View File

@@ -8,22 +8,22 @@ const USER: AppRouteRecordRaw = {
children: [ children: [
{ {
name: 'role', name: 'role',
path: '/role', path: '/user/role',
component: () => import('@/views/user/role/index.vue'), component: () => import('@/views/user/role/index.vue'),
}, },
{ {
name: 'user', name: 'user',
path: '/user', path: '/user/list',
component: () => import('@/views/user/user/index.vue'), component: () => import('@/views/user/user/index.vue'),
}, },
{ {
name: 'userInfo', name: 'userInfo',
path: '/user-info', path: '/user/info',
component: () => import('@/views/user/info/index.vue'), component: () => import('@/views/user/info/index.vue'),
}, },
{ {
name: 'operatorLog', name: 'operatorLog',
path: '/operator-log', path: '/user/operator-log',
component: () => import('@/views/user/operator-log/index.vue'), component: () => import('@/views/user/operator-log/index.vue'),
}, },
], ],

View File

@@ -15,7 +15,7 @@ import { getHostKeyList } from '@/api/asset/host-key';
import { getHostIdentityList } from '@/api/asset/host-identity'; import { getHostIdentityList } from '@/api/asset/host-identity';
import { getHostGroupTree } from '@/api/asset/host-group'; import { getHostGroupTree } from '@/api/asset/host-group';
import { getMenuList } from '@/api/system/menu'; import { getMenuList } from '@/api/system/menu';
import { getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@/api/asset/asset-authorized-data'; import { getCurrentAuthorizedHost, getCurrentAuthorizedHostIdentity, getCurrentAuthorizedHostKey } from '@/api/asset/asset-authorized-data';
import { getCommandSnippetGroupList } from '@/api/asset/command-snippet-group'; import { getCommandSnippetGroupList } from '@/api/asset/command-snippet-group';
import { getExecJobList } from '@/api/exec/exec-job'; import { getExecJobList } from '@/api/exec/exec-job';
import { getPathBookmarkGroupList } from '@/api/asset/path-bookmark-group'; import { getPathBookmarkGroupList } from '@/api/asset/path-bookmark-group';
@@ -99,8 +99,8 @@ export default defineStore('cache', {
}, },
// 获取主机列表 // 获取主机列表
async loadHosts(type: HostType, force = false) { async loadHosts(type: HostType = '', force = false) {
return await this.load(`host_${type}`, () => getHostList(type), ['asset:host:query'], force); return await this.load(`host_${type || 'ALL'}`, () => getHostList(type), ['asset:host:query'], force);
}, },
// 获取主机密钥列表 // 获取主机密钥列表
@@ -123,6 +123,11 @@ export default defineStore('cache', {
return await this.load(`${type}_Tags`, () => getTagList(type), undefined, force); return await this.load(`${type}_Tags`, () => getTagList(type), undefined, force);
}, },
// 获取已授权的主机列表
async loadAuthorizedHosts(type: HostType = '', force = false) {
return await this.load(`authorizedHost_${type || 'ALL'}`, () => getCurrentAuthorizedHost(type), undefined, force);
},
// 获取已授权的主机密钥列表 // 获取已授权的主机密钥列表
async loadAuthorizedHostKeys(force = false) { async loadAuthorizedHostKeys(force = false) {
return await this.load('authorizedHostKeys', getCurrentAuthorizedHostKey, undefined, force); return await this.load('authorizedHostKeys', getCurrentAuthorizedHostKey, undefined, force);

View File

@@ -1,6 +1,7 @@
// 缓存类型 // 缓存类型
export type CacheType = 'users' | 'menus' | 'roles' export type CacheType = 'users' | 'menus' | 'roles'
| 'hostGroups' | 'hostKeys' | 'hostIdentities' | 'host_*' | 'hostGroups' | 'host_*' | 'authorizedHost_*'
| 'hostKeys' | 'hostIdentities'
| 'dictKeys' | 'dictKeys'
| 'execJob' | 'execJob'
| 'authorizedHostKeys' | 'authorizedHostIdentities' | 'authorizedHostKeys' | 'authorizedHostIdentities'

View File

@@ -10,16 +10,17 @@ import type {
} from './types'; } from './types';
import type { ISshSession, 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 type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import type { HostQueryResponse } from '@/api/asset/host'; import type { HostQueryResponse } from '@/api/asset/host';
import type { TerminalTheme, TerminalThemeSchema } from '@/api/asset/terminal'; import type { TerminalTheme, TerminalThemeSchema } from '@/api/asset/terminal';
import { getTerminalThemes } from '@/api/asset/terminal'; import { getTerminalThemes } from '@/api/asset/terminal';
import { markRaw } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { getPreference, updatePreference } from '@/api/user/preference'; import { getPreference, updatePreference } from '@/api/user/preference';
import { getLatestConnectHostId } from '@/api/asset/terminal-connect-log'; import { getLatestConnectHostId } from '@/api/asset/terminal-connect-log';
import { nextId } from '@/utils'; import { nextId } from '@/utils';
import { isObject } from '@/utils/is'; import { isObject } from '@/utils/is';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { useCacheStore } from '@/store';
import { PanelSessionType, 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 TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager'; import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager';
@@ -73,7 +74,7 @@ export default defineStore('terminal', {
hosts: {} as AuthorizedHostQueryResponse, hosts: {} as AuthorizedHostQueryResponse,
tabManager: new TerminalTabManager(), tabManager: new TerminalTabManager(),
panelManager: new TerminalPanelManager(), panelManager: new TerminalPanelManager(),
sessionManager: new TerminalSessionManager(), sessionManager: markRaw(new TerminalSessionManager()),
transferManager: new SftpTransferManager(), transferManager: new SftpTransferManager(),
}), }),
@@ -130,18 +131,18 @@ export default defineStore('terminal', {
}, },
// 加载主机列表 // 加载主机列表
async loadHosts() { async loadHostList() {
if (this.hosts.hostList?.length) { if (this.hosts.hostList?.length) {
return; return;
} }
// 查询授权主机 // 查询授权主机
const { data } = await getCurrentAuthorizedHost('SSH'); const data = await useCacheStore().loadAuthorizedHosts();
Object.keys(data).forEach(k => { Object.keys(data).forEach(k => {
this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any; this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any;
}); });
this.hosts.latestHosts = []; this.hosts.latestHosts = [];
// 查询最近连接的主机 // 查询最近连接的主机
const { data: latestHosts } = await getLatestConnectHostId('SSH', 30); const { data: latestHosts } = await getLatestConnectHostId('', 30);
this.hosts.latestHosts = latestHosts; this.hosts.latestHosts = latestHosts;
}, },

View File

@@ -1,5 +1,4 @@
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface'; import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
import { dateFormat } from '@/utils';
const columns = [ const columns = [
{ {
@@ -32,8 +31,8 @@ const columns = [
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
slotName: 'status', slotName: 'status',
align: 'center', align: 'left',
width: 106, width: 118,
}, { }, {
title: '留痕地址', title: '留痕地址',
dataIndex: 'address', dataIndex: 'address',

View File

@@ -93,7 +93,7 @@
try { try {
// 加载组内数据 // 加载组内数据
const { data } = await getHostGroupRelList(groupId as number); const { data } = await getHostGroupRelList(groupId as number);
const hosts = await cacheStore.loadHosts(''); const hosts = await cacheStore.loadHosts();
selectedGroupHosts.value = data.map(s => hosts.find(h => h.id === s) as HostQueryResponse) selectedGroupHosts.value = data.map(s => hosts.find(h => h.id === s) as HostQueryResponse)
.filter(Boolean); .filter(Boolean);
} catch (e) { } catch (e) {
@@ -138,6 +138,8 @@
idList: checkedGroups.value idList: checkedGroups.value
}); });
Message.success('授权成功'); Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHost_ALL', 'authorizedHost_SSH');
} catch (e) { } catch (e) {
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -96,6 +96,8 @@
idList: selectedKeys.value idList: selectedKeys.value
}); });
Message.success('授权成功'); Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHostIdentities');
} catch (e) { } catch (e) {
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -72,6 +72,8 @@
idList: selectedKeys.value idList: selectedKeys.value
}); });
Message.success('授权成功'); Message.success('授权成功');
// 清空缓存
cacheStore.reset('authorizedHostKeys');
} catch (e) { } catch (e) {
} finally { } finally {
setLoading(false); setLoading(false);

View File

@@ -113,7 +113,7 @@
// 加载主机列表 // 加载主机列表
const loadHosts = () => { const loadHosts = () => {
cacheStore.loadHosts('').then(hosts => { cacheStore.loadHosts().then(hosts => {
data.value = hosts.map(s => { data.value = hosts.map(s => {
return { return {
value: String(s.id), value: String(s.id),

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = { const fieldConfig = {
rowGap: '10px', rowGap: '10px',
labelSpan: 8, labelSpan: 6,
minHeight: '22px', minHeight: '22px',
fields: [ fields: [
{ {

View File

@@ -19,6 +19,8 @@ const columns = [
title: '用户名', title: '用户名',
dataIndex: 'username', dataIndex: 'username',
slotName: 'username', slotName: 'username',
ellipsis: true,
tooltip: true
}, { }, {
title: '类型', title: '类型',
dataIndex: 'type', dataIndex: 'type',
@@ -28,6 +30,7 @@ const columns = [
title: '主机密钥', title: '主机密钥',
dataIndex: 'keyId', dataIndex: 'keyId',
slotName: 'keyId', slotName: 'keyId',
width: 180,
}, { }, {
title: '描述', title: '描述',
dataIndex: 'description', dataIndex: 'description',

View File

@@ -3,7 +3,7 @@ import { dateFormat } from '@/utils';
const fieldConfig = { const fieldConfig = {
rowGap: '10px', rowGap: '10px',
labelSpan: 8, labelSpan: 6,
minHeight: '22px', minHeight: '22px',
fields: [ fields: [
{ {

View File

@@ -339,7 +339,8 @@
// 重新加载数据 // 重新加载数据
fetchCardData(); fetchCardData();
// 清空缓存 // 清空缓存
cacheStore.reset('host_', 'host_SSH'); cacheStore.reset('host_ALL', 'host_SSH',
'authorizedHost_ALL', 'authorizedHost_SSH');
}; };
defineExpose({ reload }); defineExpose({ reload });

View File

@@ -373,7 +373,8 @@
// 重新加载数据 // 重新加载数据
fetchTableData(); fetchTableData();
// 清空缓存 // 清空缓存
cacheStore.reset('host_', 'host_SSH'); cacheStore.reset('host_ALL', 'host_SSH',
'authorizedHost_ALL', 'authorizedHost_SSH');
}; };
defineExpose({ reload }); defineExpose({ reload });

View File

@@ -2,7 +2,7 @@ import type { CardField, CardFieldConfig } from '@/types/card';
const fieldConfig = { const fieldConfig = {
rowGap: '10px', rowGap: '10px',
labelSpan: 8, labelSpan: 6,
minHeight: '22px', minHeight: '22px',
fields: [ fields: [
{ {

View File

@@ -135,10 +135,10 @@
const { chartOption: terminalConnectChart } = useChartOption(() => { const { chartOption: terminalConnectChart } = useChartOption(() => {
return { return {
grid: { grid: {
left: 0, left: 8,
right: 0, right: 8,
top: 10, top: 8,
bottom: 0, bottom: 8,
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',
@@ -176,10 +176,10 @@
const { chartOption: execCommandChart } = useChartOption(() => { const { chartOption: execCommandChart } = useChartOption(() => {
return { return {
grid: { grid: {
left: 0, left: 8,
right: 0, right: 8,
top: 10, top: 8,
bottom: 0, bottom: 8,
}, },
xAxis: { xAxis: {
type: 'category', type: 'category',

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="terminal-example" ref="terminal" /> <div class="terminal-example" ref="terminalRef" />
</template> </template>
<script lang="ts"> <script lang="ts">
@@ -11,31 +11,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TerminalThemeSchema } from '@/api/asset/terminal'; import type { TerminalThemeSchema } from '@/api/asset/terminal';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { onMounted, onUnmounted, ref } from 'vue'; import { markRaw, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps<{ const props = defineProps<{
schema: TerminalThemeSchema | Record<string, any>; schema: TerminalThemeSchema | Record<string, any>;
}>(); }>();
const terminal = ref(); const terminalRef = ref();
const term = ref(); const term = ref();
onMounted(() => { onMounted(() => {
term.value = new Terminal({ const terminal = new Terminal({
theme: { ...props.schema, cursor: props.schema.background }, theme: { ...props.schema, cursor: props.schema.background },
cols: 42, cols: 42,
rows: 6, rows: 6,
fontSize: 15, fontSize: 15,
cursorInactiveStyle: 'none', cursorInactiveStyle: 'none',
}); });
term.value.open(terminal.value); terminal.open(terminalRef.value);
term.value.write( terminal.write(
'[root@OrionServer usr]#\r\n' + '[root@OrionServer usr]#\r\n' +
'dr-xr-xr-x. 2 root root bin\r\n' + 'dr-xr-xr-x. 2 root root bin\r\n' +
'dr-xr-xr-x. 2 root root sbin\r\n' + 'dr-xr-xr-x. 2 root root sbin\r\n' +
'drwxr-xr-x. 4 root root src\r\n' + 'drwxr-xr-x. 4 root root src\r\n' +
'lrwxrwxrwx. 1 root root tmp -> ../var/tmp ' 'lrwxrwxrwx. 1 root root tmp -> ../var/tmp '
); );
term.value = markRaw(terminal);
}); });
defineExpose({ term }); defineExpose({ term });

View File

@@ -45,7 +45,7 @@
</div> </div>
</div> </div>
<!-- 已关闭-右侧操作 --> <!-- 已关闭-右侧操作 -->
<div v-if="session?.connected === false && closeMessage !== undefined" <div v-if="session?.status.connected === false && closeMessage !== undefined"
class="sftp-table-header-right"> class="sftp-table-header-right">
<!-- 错误信息 --> <!-- 错误信息 -->
<a-tag class="close-message" <a-tag class="close-message"
@@ -54,7 +54,7 @@
已断开: {{ closeMessage }} 已断开: {{ closeMessage }}
</a-tag> </a-tag>
<!-- 重连 --> <!-- 重连 -->
<a-tooltip v-if="session?.canReconnect" <a-tooltip v-if="session?.status.canReconnect"
position="top" position="top"
:mini="true" :mini="true"
:overlay-inverse="true" :overlay-inverse="true"
@@ -245,7 +245,7 @@
// 设置命令编辑模式 // 设置命令编辑模式
const setPathEditable = (editable: boolean) => { const setPathEditable = (editable: boolean) => {
// 检查是否断开 // 检查是否断开
if (editable && !props.session?.connected) { if (editable && !props.session?.status.connected) {
return; return;
} }
pathEditable.value = editable; pathEditable.value = editable;
@@ -267,7 +267,7 @@
// 加载文件列表 // 加载文件列表
const loadFileList = (path: string = props.currentPath) => { const loadFileList = (path: string = props.currentPath) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
emits('loadFile', path); emits('loadFile', path);

View File

@@ -205,7 +205,7 @@
const clickFilename = (record: TableData) => { const clickFilename = (record: TableData) => {
if (record.isDir) { if (record.isDir) {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
// 进入文件夹 // 进入文件夹
@@ -218,7 +218,7 @@
// 编辑文件 // 编辑文件
const editFile = (record: TableData) => { const editFile = (record: TableData) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
emits('editFile', record.name, record.path); emits('editFile', record.name, record.path);
@@ -228,7 +228,7 @@
// 删除文件 // 删除文件
const deleteFile = (path: string) => { const deleteFile = (path: string) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
emits('deleteFile', [path]); emits('deleteFile', [path]);
@@ -237,7 +237,7 @@
// 下载文件 // 下载文件
const downloadFile = (path: string) => { const downloadFile = (path: string) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
emits('download', [path], false); emits('download', [path], false);
@@ -246,7 +246,7 @@
// 移动文件 // 移动文件
const moveFile = (path: string) => { const moveFile = (path: string) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
openSftpMoveModal(props.session?.sessionId as string, path); openSftpMoveModal(props.session?.sessionId as string, path);
@@ -255,7 +255,7 @@
// 文件提权 // 文件提权
const chmodFile = (path: string, permission: number) => { const chmodFile = (path: string, permission: number) => {
// 检查是否断开 // 检查是否断开
if (!props.session?.connected) { if (!props.session?.status.connected) {
return; return;
} }
openSftpChmodModal(props.session?.sessionId as string, path, permission); openSftpChmodModal(props.session?.sessionId as string, path, permission);

View File

@@ -23,8 +23,8 @@
<!-- 连接状态 --> <!-- 连接状态 -->
<a-badge v-if="preference.actionBarSetting.connectStatus !== false" <a-badge v-if="preference.actionBarSetting.connectStatus !== false"
class="status-bridge" class="status-bridge"
:status="getDictValue(sessionStatusKey, session ? session.status : 0, 'status')" :status="getDictValue(sessionStatusKey, session ? session.status.connectStatus : 0, 'status')"
:text="getDictValue(sessionStatusKey, session ? session.status : 0)" /> :text="getDictValue(sessionStatusKey, session ? session.status.connectStatus : 0)" />
</div> </div>
</div> </div>
</template> </template>

View File

@@ -64,7 +64,7 @@
// 发送命令 // 发送命令
const writeCommand = (value: string) => { const writeCommand = (value: string) => {
if (session.value?.canWrite) { if (session.value?.status.canWrite) {
session.value?.handler.pasteTrimEnd(value); session.value?.handler.pasteTrimEnd(value);
} }
}; };

View File

@@ -1,50 +1,66 @@
import type { ITerminalSession, TerminalPanelTabItem } from '../types/define'; import type { ITerminalSession, TerminalPanelTabItem, TerminalStatus } from '../types/define';
import type { Reactive } from 'vue';
import { reactive } from 'vue';
import { TerminalSessionStatus } from '@/views/host/terminal/types/const';
// 会话基类 // 会话基类
export default abstract class BaseSession implements ITerminalSession { export default abstract class BaseSession<Status extends TerminalStatus> implements ITerminalSession<Status> {
public type: string; public readonly type: string;
public hostId: number; public readonly hostId: number;
public title: string; public readonly title: string;
public address: string; public readonly address: string;
public readonly status: Reactive<Status>;
public sessionId: string; public sessionId: string;
public connected: boolean;
public canReconnect: boolean;
public canWrite: boolean;
protected constructor(type: string, tab: TerminalPanelTabItem) { protected constructor(type: string, tab: TerminalPanelTabItem, status: Partial<Status>) {
this.type = type; this.type = type;
this.hostId = tab.hostId; this.hostId = tab.hostId;
this.title = tab.title; this.title = tab.title;
this.address = tab.address; this.address = tab.address;
this.sessionId = tab.sessionId; this.sessionId = tab.sessionId;
this.connected = false; this.status = reactive({
this.canWrite = false; connectStatus: TerminalSessionStatus.CONNECTING,
this.canReconnect = false; connected: false,
} canWrite: false,
canReconnect: false,
// 设置是否可写 ...status,
setCanWrite(canWrite: boolean): void { } as Status);
this.canWrite = canWrite;
}
// 设置已连接
setConnected(): void {
this.connected = true;
} }
// 连接会话 // 连接会话
connect(): void { connect(): void {
this.status.connectStatus = TerminalSessionStatus.CONNECTING;
} }
// 断开连接 // 断开连接
disconnect(): void { disconnect(): void {
this.connected = false; // 设置已关闭
this.setClosed();
} }
// 关闭 // 关闭
close(): void { close(): void {
this.connected = false; // 设置已关闭
this.setClosed();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
this.status.canWrite = canWrite;
}
// 设置已连接
setConnected(): void {
this.status.connected = true;
this.status.connectStatus = TerminalSessionStatus.CONNECTED;
}
// 设置已关闭
setClosed(): void {
this.status.connected = false;
this.status.canWrite = false;
this.status.connectStatus = TerminalSessionStatus.CLOSED;
} }
} }

View File

@@ -1,4 +1,4 @@
import type { ISftpSession, ISftpSessionResolver, ITerminalChannel, TerminalPanelTabItem } from '../types/define'; import type { ISftpSession, ISftpSessionResolver, ITerminalChannel, TerminalPanelTabItem, TerminalStatus } from '../types/define';
import { h } from 'vue'; import { h } from 'vue';
import { InputProtocol } from '@/types/protocol/terminal.protocol'; import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType } from '../types/const'; import { PanelSessionType } from '../types/const';
@@ -6,7 +6,7 @@ import { Modal } from '@arco-design/web-vue';
import BaseSession from './base-session'; import BaseSession from './base-session';
// sftp 会话实现 // sftp 会话实现
export default class SftpSession extends BaseSession implements ISftpSession { export default class SftpSession extends BaseSession<TerminalStatus> implements ISftpSession {
public resolver: ISftpSessionResolver; public resolver: ISftpSessionResolver;
@@ -16,7 +16,7 @@ export default class SftpSession extends BaseSession implements ISftpSession {
constructor(tab: TerminalPanelTabItem, constructor(tab: TerminalPanelTabItem,
channel: ITerminalChannel) { channel: ITerminalChannel) {
super(PanelSessionType.SFTP.type, tab); super(PanelSessionType.SFTP.type, tab, {});
this.channel = channel; this.channel = channel;
this.showHiddenFile = false; this.showHiddenFile = false;
this.resolver = undefined as unknown as ISftpSessionResolver; this.resolver = undefined as unknown as ISftpSessionResolver;
@@ -27,13 +27,6 @@ export default class SftpSession extends BaseSession implements ISftpSession {
this.resolver = resolver; this.resolver = resolver;
} }
// 设置已连接
setConnected(): void {
super.setConnected();
// 连接回调
this.resolver.connectCallback();
}
// 连接会话 // 连接会话
connect(): void { connect(): void {
super.connect(); super.connect();
@@ -171,4 +164,11 @@ export default class SftpSession extends BaseSession implements ISftpSession {
}); });
} }
// 设置已连接
setConnected(): void {
super.setConnected();
// 连接回调
this.resolver.connectCallback();
}
} }

View File

@@ -91,9 +91,9 @@ export default class SshSessionHandler implements ISshSessionHandler {
case 'openSftp': case 'openSftp':
case 'uploadFile': case 'uploadFile':
case 'checkAppendMissing': case 'checkAppendMissing':
return this.session.canWrite; return this.session.status.canWrite;
case 'disconnect': case 'disconnect':
return this.session.connected; return this.session.status.connected;
default: default:
return true; return true;
} }
@@ -197,7 +197,7 @@ export default class SshSessionHandler implements ISshSessionHandler {
// 字号增加 // 字号增加
private fontSizeAdd(addSize: number) { private fontSizeAdd(addSize: number) {
this.inst.options['fontSize'] = this.inst.options['fontSize'] as number + addSize; this.inst.options['fontSize'] = this.inst.options['fontSize'] as number + addSize;
if (this.session.connected) { if (this.session.status.connected) {
this.session.fit(); this.session.fit();
this.inst.focus(); this.inst.focus();
} }

View File

@@ -2,12 +2,12 @@ import type { UnwrapRef } from 'vue';
import type { ISearchOptions } from '@xterm/addon-search'; import type { ISearchOptions } from '@xterm/addon-search';
import { SearchAddon } from '@xterm/addon-search'; import { SearchAddon } from '@xterm/addon-search';
import type { TerminalPreference } from '@/store/modules/terminal/types'; import type { TerminalPreference } from '@/store/modules/terminal/types';
import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, XtermDomRef } from '../types/define'; import type { ISshSession, ISshSessionHandler, ITerminalChannel, TerminalPanelTabItem, TerminalStatus, XtermDomRef } from '../types/define';
import type { XtermAddons } from '@/types/xterm'; import type { XtermAddons } from '@/types/xterm';
import { defaultFontFamily } from '@/types/xterm'; import { defaultFontFamily } from '@/types/xterm';
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
import { InputProtocol } from '@/types/protocol/terminal.protocol'; import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType, TerminalSessionStatus, TerminalShortcutType } from '../types/const'; import { PanelSessionType, TerminalShortcutType } from '../types/const';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit'; import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links'; import { WebLinksAddon } from '@xterm/addon-web-links';
@@ -21,12 +21,10 @@ import SshSessionHandler from './ssh-session-handler';
import BaseSession from './base-session'; import BaseSession from './base-session';
// ssh 会话实现 // ssh 会话实现
export default class SshSession extends BaseSession implements ISshSession { export default class SshSession extends BaseSession<TerminalStatus> implements ISshSession {
public inst: Terminal; public inst: Terminal;
public status: number;
public handler: ISshSessionHandler; public handler: ISshSessionHandler;
private readonly channel: ITerminalChannel; private readonly channel: ITerminalChannel;
@@ -38,10 +36,9 @@ export default class SshSession extends BaseSession implements ISshSession {
constructor(tab: TerminalPanelTabItem, constructor(tab: TerminalPanelTabItem,
channel: ITerminalChannel, channel: ITerminalChannel,
canUseWebgl: boolean) { canUseWebgl: boolean) {
super(PanelSessionType.SSH.type, tab); super(PanelSessionType.SSH.type, tab, {});
this.channel = channel; this.channel = channel;
this.canUseWebgl = canUseWebgl; this.canUseWebgl = canUseWebgl;
this.status = TerminalSessionStatus.CONNECTING;
this.inst = undefined as unknown as Terminal; this.inst = undefined as unknown as Terminal;
this.handler = undefined as unknown as ISshSessionHandler; this.handler = undefined as unknown as ISshSessionHandler;
this.addons = {} as XtermAddons; this.addons = {} as XtermAddons;
@@ -93,9 +90,9 @@ export default class SshSession extends BaseSession implements ISshSession {
e.preventDefault(); e.preventDefault();
} }
// 检查重新连接 // 检查重新连接
if (!this.connected && this.canReconnect && e.key === 'Enter') { if (!this.status.connected && this.status.canReconnect && e.key === 'Enter') {
// 防止重复回车 // 防止重复回车
this.canReconnect = false; this.status.canReconnect = false;
// 异步作用域重新连接 // 异步作用域重新连接
setTimeout(async () => { setTimeout(async () => {
await useTerminalStore().reOpenSession(this.sessionId); await useTerminalStore().reOpenSession(this.sessionId);
@@ -120,7 +117,7 @@ export default class SshSession extends BaseSession implements ISshSession {
private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) { private registerEvent(dom: HTMLElement, preference: UnwrapRef<TerminalPreference>) {
// 注册输入事件 // 注册输入事件
this.inst.onData(s => { this.inst.onData(s => {
if (!this.canWrite || !this.connected) { if (!this.status.canWrite || !this.status.connected) {
return; return;
} }
// 输入 // 输入
@@ -145,7 +142,7 @@ export default class SshSession extends BaseSession implements ISshSession {
} }
// 注册 resize 事件 // 注册 resize 事件
this.inst.onResize(({ cols, rows }) => { this.inst.onResize(({ cols, rows }) => {
if (!this.connected) { if (!this.status.connected) {
return; return;
} }
this.channel.send(InputProtocol.SSH_RESIZE, { this.channel.send(InputProtocol.SSH_RESIZE, {
@@ -158,7 +155,7 @@ export default class SshSession extends BaseSession implements ISshSession {
addEventListen(dom, 'contextmenu', async () => { addEventListen(dom, 'contextmenu', async () => {
// 右键粘贴逻辑 // 右键粘贴逻辑
if (preference.interactSetting.rightClickPaste) { if (preference.interactSetting.rightClickPaste) {
if (!this.canWrite || !this.connected) { if (!this.status.canWrite || !this.status.connected) {
return; return;
} }
// 未开启右键选中 || 开启并无选中的内容则粘贴 // 未开启右键选中 || 开启并无选中的内容则粘贴
@@ -204,29 +201,9 @@ export default class SshSession extends BaseSession implements ISshSession {
} }
} }
// 设置已连接
setConnected(): void {
super.setConnected();
// 设置状态
this.status = TerminalSessionStatus.CONNECTED;
this.inst.focus();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
super.setCanWrite(canWrite);
if (canWrite) {
this.inst.options.cursorBlink = useTerminalStore().preference.displaySetting.cursorBlink;
} else {
this.inst.options.cursorBlink = false;
}
}
// 连接会话 // 连接会话
connect(): void { connect(): void {
super.connect(); super.connect();
// 设置状态
this.status = TerminalSessionStatus.CONNECTING;
// 发送会话初始化请求 // 发送会话初始化请求
this.channel.send(InputProtocol.CHECK, { this.channel.send(InputProtocol.CHECK, {
sessionId: this.sessionId, sessionId: this.sessionId,
@@ -295,4 +272,20 @@ export default class SshSession extends BaseSession implements ISshSession {
} }
} }
// 设置已连接
setConnected(): void {
super.setConnected();
this.inst.focus();
}
// 设置是否可写
setCanWrite(canWrite: boolean): void {
super.setCanWrite(canWrite);
if (canWrite) {
this.inst.options.cursorBlink = useTerminalStore().preference.displaySetting.cursorBlink;
} else {
this.inst.options.cursorBlink = false;
}
}
} }

View File

@@ -77,7 +77,7 @@ export default class TerminalChannel implements ITerminalChannel {
private closeCallback(): void { private closeCallback(): void {
// 关闭时将手动触发 close 消息, 有可能是其他原因关闭的, 没有接收到 close 消息, 导致已断开是终端还是显示已连接 // 关闭时将手动触发 close 消息, 有可能是其他原因关闭的, 没有接收到 close 消息, 导致已断开是终端还是显示已连接
Object.values(this.sessionManager.sessions).forEach(s => { Object.values(this.sessionManager.sessions).forEach(s => {
if (!s?.connected) { if (!s?.status.connected) {
return; return;
} }
// close 消息 // close 消息

View File

@@ -1,7 +1,7 @@
import type { ISftpSession, ISshSession, ITerminalChannel, ITerminalOutputProcessor, ITerminalSession, ITerminalSessionManager } from '../types/define'; import type { ISftpSession, ISshSession, ITerminalChannel, ITerminalOutputProcessor, ITerminalSession, ITerminalSessionManager } from '../types/define';
import type { OutputPayload } from '@/types/protocol/terminal.protocol'; import type { OutputPayload } from '@/types/protocol/terminal.protocol';
import { InputProtocol } from '@/types/protocol/terminal.protocol'; import { InputProtocol } from '@/types/protocol/terminal.protocol';
import { PanelSessionType, TerminalSessionStatus } from '../types/const'; import { PanelSessionType } from '../types/const';
import { useTerminalStore } from '@/store'; import { useTerminalStore } from '@/store';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
@@ -21,7 +21,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
processCheck({ sessionId, result, msg }: OutputPayload): void { processCheck({ sessionId, result, msg }: OutputPayload): void {
const success = !!Number.parseInt(result); const success = !!Number.parseInt(result);
const session = this.sessionManager.getSession(sessionId); const session = this.sessionManager.getSession(sessionId);
session.canReconnect = !success; session.status.canReconnect = !success;
// 处理 // 处理
this.processWithType(session, ssh => { this.processWithType(session, ssh => {
// ssh 会话 // ssh 会话
@@ -35,9 +35,10 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
rows: ssh.inst.rows rows: ssh.inst.rows
}); });
} else { } else {
// 设置已关闭
session.setClosed();
// 未成功展示错误信息 // 未成功展示错误信息
ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`); ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`);
ssh.status = TerminalSessionStatus.CLOSED;
} }
}, sftp => { }, sftp => {
// sftp 会话 // sftp 会话
@@ -47,6 +48,8 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
sessionId, sessionId,
}); });
} else { } else {
// 设置已关闭
session.setClosed();
// 未成功提示错误信息 // 未成功提示错误信息
sftp.resolver?.onClose(false, msg); sftp.resolver?.onClose(false, msg);
Message.error(msg || '建立 SFTP 失败'); Message.error(msg || '建立 SFTP 失败');
@@ -58,29 +61,25 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
processConnect({ sessionId, result, msg }: OutputPayload): void { processConnect({ sessionId, result, msg }: OutputPayload): void {
const success = !!Number.parseInt(result); const success = !!Number.parseInt(result);
const session = this.sessionManager.getSession(sessionId); const session = this.sessionManager.getSession(sessionId);
session.canReconnect = !success; session.status.canReconnect = !success;
if (success) {
// 设置可写
session.setCanWrite(true);
// 设置已连接
session.setConnected();
} else {
// 设置已关闭
session.setClosed();
}
// 处理 // 处理
this.processWithType(session, ssh => { this.processWithType(session, ssh => {
// ssh 会话 if (!success) {
if (success) { // ssh 会话 未成功展示错误信息
// 设置可写
ssh.setCanWrite(true);
// 设置已连接
ssh.setConnected();
} else {
// 未成功展示错误信息
ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`); ssh.write(`${msg || ''}\r\n\r\n输入回车重新连接...\r\n\r\n`);
ssh.status = TerminalSessionStatus.CLOSED;
} }
}, sftp => { }, sftp => {
// sftp 会话 if (!success) {
if (success) { // sftp 会话 未成功提示错误信息
// 设置可写
sftp.setCanWrite(true);
// 设置已连接
sftp.setConnected();
} else {
// 未成功提示错误信息
sftp.resolver?.onClose(false, msg); sftp.resolver?.onClose(false, msg);
Message.error(msg || '打开 SFTP 失败'); Message.error(msg || '打开 SFTP 失败');
} }
@@ -95,8 +94,9 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
return; return;
} }
const isForceClose = !!Number.parseInt(forceClose); const isForceClose = !!Number.parseInt(forceClose);
session.connected = false; session.status.canReconnect = !isForceClose;
session.canReconnect = !isForceClose; // 设置已关闭
session.setClosed();
// 处理 // 处理
this.processWithType(session, ssh => { this.processWithType(session, ssh => {
// ssh 拼接关闭消息 // ssh 拼接关闭消息
@@ -104,13 +104,7 @@ export default class TerminalOutputProcessor implements ITerminalOutputProcessor
if (!isForceClose) { if (!isForceClose) {
ssh.write('输入回车重新连接...\r\n\r\n'); ssh.write('输入回车重新连接...\r\n\r\n');
} }
// 设置状态
ssh.status = TerminalSessionStatus.CLOSED;
// 设置不可写
ssh.setCanWrite(false);
}, sftp => { }, sftp => {
// 设置不可写
sftp.setCanWrite(false);
// sftp 设置状态 // sftp 设置状态
sftp.resolver?.onClose(isForceClose, msg); sftp.resolver?.onClose(isForceClose, msg);
}); });

View File

@@ -104,7 +104,10 @@ export default class TerminalSessionManager implements ITerminalSessionManager {
// 移除 session // 移除 session
this.sessions[sessionId] = undefined as unknown as ITerminalSession; this.sessions[sessionId] = undefined as unknown as ITerminalSession;
// session 全部关闭后 关闭 channel // session 全部关闭后 关闭 channel
if (Object.values(this.sessions).filter(Boolean).every(s => !s?.connected)) { const allClosed = Object.values(this.sessions)
.filter(Boolean)
.every(s => !s?.status.connected);
if (allClosed) {
this.reset(); this.reset();
} }
} }

View File

@@ -79,7 +79,7 @@
const { const {
fetchPreference, getCurrentSession, openSession, fetchPreference, getCurrentSession, openSession,
layoutState, preference, loadHosts, hosts, tabManager, sessionManager layoutState, preference, loadHostList, hosts, tabManager, sessionManager
} = useTerminalStore(); } = useTerminalStore();
const { loading, setLoading } = useLoading(true); const { loading, setLoading } = useLoading(true);
const { enter: enterFull, exit: exitFull } = useFullscreen(); const { enter: enterFull, exit: exitFull } = useFullscreen();
@@ -165,7 +165,7 @@
onMounted(async () => { onMounted(async () => {
try { try {
// 加载主机 // 加载主机
await loadHosts(); await loadHostList();
// 默认连接主机 // 默认连接主机
const connect = route.query.connect as string; const connect = route.query.connect as string;
if (connect) { if (connect) {

View File

@@ -363,7 +363,7 @@ export const TransferReceiver = {
}; };
// 会话关闭信息 // 会话关闭信息
export const sessionCloseMsg = 'session closed...'; export const sessionCloseMsg = '会话已结束...';
// 打开 settingModal key // 打开 settingModal key
export const openSettingModalKey = Symbol(); export const openSettingModalKey = Symbol();

View File

@@ -1,6 +1,6 @@
import type { Terminal } from '@xterm/xterm'; import type { Terminal } from '@xterm/xterm';
import type { ISearchOptions } from '@xterm/addon-search'; import type { ISearchOptions } from '@xterm/addon-search';
import type { CSSProperties } from 'vue'; import type { CSSProperties, Reactive } from 'vue';
import type { HostQueryResponse } from '@/api/asset/host'; import type { HostQueryResponse } from '@/api/asset/host';
import type { InputPayload, OutputPayload, Protocol } from '@/types/protocol/terminal.protocol'; import type { InputPayload, OutputPayload, Protocol } from '@/types/protocol/terminal.protocol';
@@ -196,38 +196,47 @@ export interface XtermDomRef {
uploadModal: any; uploadModal: any;
} }
// 终端会话定义 // 终端状态
export interface ITerminalSession { export interface TerminalStatus {
type: string; // 连接状态
title: string; connectStatus: number;
address: string;
hostId: number;
sessionId: string;
// 是否已连接 // 是否已连接
connected: boolean; connected: boolean;
// 是否可以重新连接
canReconnect: boolean;
// 是否可写 // 是否可写
canWrite: boolean; canWrite: boolean;
// 是否可以重新连接
canReconnect: boolean;
}
// 终端会话定义
export interface ITerminalSession<Status extends TerminalStatus = TerminalStatus> {
readonly type: string;
readonly title: string;
readonly address: string;
readonly hostId: number;
// 终端状态
readonly status: Reactive<Status>;
sessionId: string;
// 设置是否可写
setCanWrite: (canWrite: boolean) => void;
// 设置已连接
setConnected: () => void;
// 连接会话 // 连接会话
connect: () => void; connect: () => void;
// 断开连接 // 断开连接
disconnect: () => void; disconnect: () => void;
// 关闭 // 关闭
close: () => void; close: () => void;
// 设置是否可写
setCanWrite: (canWrite: boolean) => void;
// 设置已连接
setConnected: () => void;
// 设置已关闭
setClosed: () => void;
} }
// ssh 会话定义 // ssh 会话定义
export interface ISshSession extends ITerminalSession { export interface ISshSession extends ITerminalSession {
// terminal 实例 // terminal 实例
inst: Terminal; inst: Terminal;
// 状态
status: number;
// 处理器 // 处理器
handler: ISshSessionHandler; handler: ISshSessionHandler;

View File

@@ -22,7 +22,7 @@
</modules> </modules>
<properties> <properties>
<revision>2.3.4</revision> <revision>2.3.5</revision>
<maven.compiler.source>8</maven.compiler.source> <maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target> <maven.compiler.target>8</maven.compiler.target>
<maven.surefire.plugin.version>3.0.0-M5</maven.surefire.plugin.version> <maven.surefire.plugin.version>3.0.0-M5</maven.surefire.plugin.version>