Compare commits

...

9 Commits

Author SHA1 Message Date
李佳航
6d4952c483 Merge pull request #109 from dromara/dev
Dev
2025-06-06 16:39:28 +08:00
lijiahangmax
7fab68f8c0 升级版本. 2025-06-06 16:27:29 +08:00
lijiahangmax
3a586c47a3 添加更新日期. 2025-06-06 16:18:48 +08:00
lijiahangmax
1767079249 复制主机. 2025-06-06 15:58:49 +08:00
lijiahangmax
fdd3be5a91 🐛 复制主机. 2025-05-30 01:29:28 +08:00
lijiahangmax
2457a015e4 🐛 修复分组无法删除的问题. 2025-05-29 22:15:08 +08:00
李佳航
40a99eb67a Merge pull request #106 from dromara/dev
Dev
2025-05-29 19:14:53 +08:00
lijiahangmax
1fcf239561 ✏️ 修改文档. 2025-05-29 19:07:16 +08:00
lijiahangmax
bbb1bb0db6 🔨 自定义标签名称. 2025-05-28 20:39:28 +08:00
36 changed files with 281 additions and 108 deletions

View File

@@ -146,6 +146,10 @@ QQ群: 755242157
本项目遵循 [Apache-2.0](https://github.com/dromara/orion-visor/blob/main/LICENSE) 开源许可证。 本项目遵循 [Apache-2.0](https://github.com/dromara/orion-visor/blob/main/LICENSE) 开源许可证。
## Gitee 最有价值开源项目 GVP ## Gitee 最有价值开源项目 GVP
![GVP](https://bjuimg.obs.cn-north-4.myhuaweicloud.com/images/2024/10/24/8dd98b8d-9de5-44e6-86d3-04e27ec66123.jpg "GVP") ![GVP](https://bjuimg.obs.cn-north-4.myhuaweicloud.com/images/2024/10/24/8dd98b8d-9de5-44e6-86d3-04e27ec66123.jpg "GVP")
## GitCode 最有影响力的开源项目 G-Star
![GSTAR](https://bjuimg.obs.cn-north-4.myhuaweicloud.com/images/2025/5/29/d6c17f1c-eddd-408a-ba67-1052a3dad9e9.jpg "GSTAR")

View File

@@ -1,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
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,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
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,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
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,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
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,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
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
docker build -t orion-visor-service:${version} . docker build -t orion-visor-service:${version} .
rm -rf ./orion-visor-launch.jar rm -rf ./orion-visor-launch.jar

View File

@@ -1,6 +1,6 @@
#/bin/bash #/bin/bash
set -e set -e
version=2.3.8 version=2.3.9
mv ../../orion-visor-ui/dist ./dist mv ../../orion-visor-ui/dist ./dist
docker build -t orion-visor-ui:${version} . docker build -t orion-visor-ui:${version} .
rm -rf ./orion-visor-launch.jar rm -rf ./orion-visor-launch.jar

View File

@@ -36,7 +36,7 @@ public interface AppConst extends OrionConst {
/** /**
* 同 ${orion.version} 迭代时候需要手动更改 * 同 ${orion.version} 迭代时候需要手动更改
*/ */
String VERSION = "2.3.8"; String VERSION = "2.3.9";
/** /**
* 同 ${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.8</revision> <revision>2.3.9</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

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

View File

@@ -84,6 +84,17 @@ public class HostController {
return hostService.updateHostById(request); return hostService.updateHostById(request);
} }
@DemoDisableApi
@OperatorLog(HostOperatorType.CREATE)
@PostMapping("/copy")
@Operation(summary = "复制主机")
@PreAuthorize("@ss.hasPermission('asset:host:create')")
public Long copyHost(@Validated @RequestBody HostUpdateRequest request) {
Long id = request.getId();
request.setId(null);
return hostService.copyHost(id, request);
}
@DemoDisableApi @DemoDisableApi
@OperatorLog(HostOperatorType.UPDATE_STATUS) @OperatorLog(HostOperatorType.UPDATE_STATUS)
@PutMapping("/update-status") @PutMapping("/update-status")

View File

@@ -61,6 +61,8 @@ public interface HostConvert {
HostBaseVO toBase(HostDO domain); HostBaseVO toBase(HostDO domain);
HostCreateRequest toCreate(HostUpdateRequest request);
List<HostVO> toList(List<HostDO> domain); List<HostVO> toList(List<HostDO> domain);
List<HostBaseVO> toBaseList(List<HostDO> domain); List<HostBaseVO> toBaseList(List<HostDO> domain);

View File

@@ -57,6 +57,20 @@ public interface HostConfigDAO extends IMapper<HostConfigDO> {
.getOne(); .getOne();
} }
/**
* 通过 hostId 查询
*
* @param hostId hostId
* @return config
*/
default List<HostConfigDO> selectByHostId(Long hostId) {
return this.of()
.createWrapper()
.eq(HostConfigDO::getHostId, hostId)
.then()
.list();
}
/** /**
* 更新配置状态 * 更新配置状态
* *

View File

@@ -26,6 +26,8 @@ import org.dromara.visor.common.handler.data.model.GenericsDataModel;
import org.dromara.visor.module.asset.entity.request.host.HostConfigQueryRequest; import org.dromara.visor.module.asset.entity.request.host.HostConfigQueryRequest;
import org.dromara.visor.module.asset.entity.request.host.HostConfigUpdateRequest; import org.dromara.visor.module.asset.entity.request.host.HostConfigUpdateRequest;
import java.util.List;
/** /**
* 主机配置 服务类 * 主机配置 服务类
* *
@@ -43,6 +45,15 @@ public interface HostConfigService {
*/ */
Integer updateHostConfig(HostConfigUpdateRequest request); Integer updateHostConfig(HostConfigUpdateRequest request);
/**
* 复制主机配置
*
* @param originId originId
* @param newId newId
* @param types types
*/
void copyHostConfig(Long originId, Long newId, List<String> types);
/** /**
* 获取主机配置 * 获取主机配置
* *

View File

@@ -75,4 +75,12 @@ public interface HostExtraService {
*/ */
Integer updateHostExtra(HostExtraUpdateRequest request); Integer updateHostExtra(HostExtraUpdateRequest request);
/**
* 复制主机拓展信息
*
* @param originId originId
* @param newId newId
*/
void copyHostExtra(Long originId, Long newId);
} }

View File

@@ -47,6 +47,15 @@ public interface HostService {
*/ */
Long createHost(HostCreateRequest request); Long createHost(HostCreateRequest request);
/**
* 复制主机
*
* @param originId originId
* @param request request
* @return id
*/
Long copyHost(Long originId, HostUpdateRequest request);
/** /**
* 通过 id 更新主机 * 通过 id 更新主机
* *

View File

@@ -22,6 +22,8 @@
*/ */
package org.dromara.visor.module.asset.service.impl; package org.dromara.visor.module.asset.service.impl;
import cn.orionsec.kit.lang.function.Functions;
import cn.orionsec.kit.lang.utils.Strings;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.visor.common.constant.ErrorMessage; import org.dromara.visor.common.constant.ErrorMessage;
@@ -42,6 +44,10 @@ import org.dromara.visor.module.asset.service.HostConfigService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** /**
* 主机配置 服务实现类 * 主机配置 服务实现类
@@ -102,6 +108,34 @@ public class HostConfigServiceImpl implements HostConfigService {
} }
} }
@Override
public void copyHostConfig(Long originId, Long newId, List<String> types) {
// 查询原始主机配置
Map<String, String> originHostConfigMap = hostConfigDAO.selectByHostId(originId)
.stream()
.collect(Collectors.toMap(HostConfigDO::getType,
HostConfigDO::getConfig,
Functions.right()));
// 新增
List<HostConfigDO> records = new ArrayList<>();
for (String type : types) {
// 获取原始配置
String configValue = originHostConfigMap.get(type);
if (Strings.isBlank(configValue)) {
// 获取默认值
configValue = HostTypeEnum.of(type).getDefault().serial();
}
HostConfigDO newConfig = HostConfigDO.builder()
.hostId(newId)
.type(type)
.status(EnableStatus.ENABLED.name())
.config(configValue)
.build();
records.add(newConfig);
}
hostConfigDAO.insertBatch(records);
}
@Override @Override
public <T extends GenericsDataModel> T getHostConfig(Long hostId, String type) { public <T extends GenericsDataModel> T getHostConfig(Long hostId, String type) {
// 查询配置信息 // 查询配置信息

View File

@@ -123,6 +123,29 @@ public class HostExtraServiceImpl implements HostExtraService {
return dataExtraApi.updateExtraValue(beforeExtraItem.getId(), newExtra.serial()); return dataExtraApi.updateExtraValue(beforeExtraItem.getId(), newExtra.serial());
} }
@Override
public void copyHostExtra(Long originId, Long newId) {
// 查询原始配置
DataExtraQueryDTO query = DataExtraQueryDTO.builder()
.userId(Const.SYSTEM_USER_ID)
.relId(originId)
.build();
List<DataExtraDTO> items = dataExtraApi.getExtraItems(query, DataExtraTypeEnum.HOST);
if (items.isEmpty()) {
return;
}
// 插入新配置
List<DataExtraSetDTO> newItems = items.stream()
.map(s -> DataExtraSetDTO.builder()
.userId(Const.SYSTEM_USER_ID)
.relId(newId)
.item(s.getItem())
.value(s.getValue())
.build())
.collect(Collectors.toList());
dataExtraApi.addExtraItems(newItems, DataExtraTypeEnum.HOST);
}
/** /**
* 检查配置项并且转为视图 (不存在则初始化默认值) * 检查配置项并且转为视图 (不存在则初始化默认值)
* *

View File

@@ -49,10 +49,7 @@ import org.dromara.visor.module.asset.entity.vo.HostVO;
import org.dromara.visor.module.asset.enums.HostExtraItemEnum; import org.dromara.visor.module.asset.enums.HostExtraItemEnum;
import org.dromara.visor.module.asset.enums.HostStatusEnum; import org.dromara.visor.module.asset.enums.HostStatusEnum;
import org.dromara.visor.module.asset.handler.host.extra.model.HostSpecExtraModel; import org.dromara.visor.module.asset.handler.host.extra.model.HostSpecExtraModel;
import org.dromara.visor.module.asset.service.ExecJobHostService; import org.dromara.visor.module.asset.service.*;
import org.dromara.visor.module.asset.service.ExecTemplateHostService;
import org.dromara.visor.module.asset.service.HostExtraService;
import org.dromara.visor.module.asset.service.HostService;
import org.dromara.visor.module.infra.api.DataExtraApi; import org.dromara.visor.module.infra.api.DataExtraApi;
import org.dromara.visor.module.infra.api.DataGroupRelApi; import org.dromara.visor.module.infra.api.DataGroupRelApi;
import org.dromara.visor.module.infra.api.FavoriteApi; import org.dromara.visor.module.infra.api.FavoriteApi;
@@ -91,6 +88,9 @@ public class HostServiceImpl implements HostService {
@Resource @Resource
private HostConfigDAO hostConfigDAO; private HostConfigDAO hostConfigDAO;
@Resource
private HostConfigService hostConfigService;
@Resource @Resource
private HostExtraService hostExtraService; private HostExtraService hostExtraService;
@@ -138,6 +138,23 @@ public class HostServiceImpl implements HostService {
return id; return id;
} }
@Override
@Transactional(rollbackFor = Exception.class)
public Long copyHost(Long originId, HostUpdateRequest request) {
log.info("HostService-copyHost originId: {}, request: {}", originId, JSON.toJSONString(request));
// 查询原始主机
HostDO originHost = hostDAO.selectById(originId);
Valid.notNull(originHost, ErrorMessage.HOST_ABSENT);
// 创建主机
Long newId = SpringHolder.getBean(HostService.class)
.createHost(HostConvert.MAPPER.toCreate(request));
// 复制主机额外信息
hostExtraService.copyHostExtra(originId, newId);
// 复制主机配置信息
hostConfigService.copyHostConfig(originId, newId, request.getTypes());
return newId;
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Integer updateHostById(HostUpdateRequest request) { public Integer updateHostById(HostUpdateRequest request) {

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.8 VITE_APP_VERSION=2.3.9

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.8 VITE_APP_VERSION=2.3.9

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.8", "version": "2.3.9",
"private": true, "private": true,
"author": "Jiahang Li", "author": "Jiahang Li",
"license": "Apache 2.0", "license": "Apache 2.0",

View File

@@ -1,4 +1,4 @@
import type { HostSpecExtraModel, HostExtraUpdateRequest } from './host-extra'; import type { HostExtraUpdateRequest, HostSpecExtraModel } from './host-extra';
import type { TableData } from '@arco-design/web-vue'; import type { TableData } from '@arco-design/web-vue';
import type { DataGrid, OrderDirection, Pagination } from '@/types/global'; import type { DataGrid, OrderDirection, Pagination } from '@/types/global';
import axios from 'axios'; import axios from 'axios';
@@ -114,6 +114,13 @@ export function updateHost(request: HostUpdateRequest) {
return axios.put('/asset/host/update', request); return axios.put('/asset/host/update', request);
} }
/**
* 复制主机
*/
export function copyHost(request: HostUpdateRequest) {
return axios.post('/asset/host/copy', request);
}
/** /**
* 通过 id 更新主机状态 * 通过 id 更新主机状态
*/ */

View File

@@ -23,6 +23,7 @@ export interface AppInfoResponse {
*/ */
export interface AppReleaseResponse { export interface AppReleaseResponse {
tagName: string; tagName: string;
releaseTime: string;
body: string; body: string;
} }

View File

@@ -215,8 +215,8 @@
import { triggerMouseEvent } from '@/utils/event'; import { triggerMouseEvent } from '@/utils/event';
import { openAppSettingKey, toggleDrawerMenuKey } from '@/types/symbol'; import { openAppSettingKey, toggleDrawerMenuKey } from '@/types/symbol';
import { preferenceTipsKey } from './const'; import { preferenceTipsKey } from './const';
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants'; import { getRouteTag, openNewRoute } from '@/router';
import { openNewRoute } from '@/router'; import { REDIRECT_ROUTE_NAME } from '@/router/constants';
import { checkHasUnreadMessage } from '@/api/system/message'; import { checkHasUnreadMessage } from '@/api/system/message';
import SystemMenuTree from '@/components/system/menu/tree/index.vue'; import SystemMenuTree from '@/components/system/menu/tree/index.vue';
import MessageBox from '@/components/system/message-box/index.vue'; import MessageBox from '@/components/system/message-box/index.vue';
@@ -291,13 +291,13 @@
const reloadCurrent = async () => { const reloadCurrent = async () => {
if (appStore.tabBar) { if (appStore.tabBar) {
// 重新加载 tab // 重新加载 tab
const itemData = routerToTag(route); const tag = getRouteTag(route);
tabBarStore.deleteCache(itemData); tabBarStore.deleteCache(tag);
await router.push({ await router.push({
name: REDIRECT_ROUTE_NAME, name: REDIRECT_ROUTE_NAME,
params: { path: route.fullPath }, params: { path: route.fullPath },
}); });
tabBarStore.addCache(itemData.name); tabBarStore.addCache(tag.name);
} else { } else {
// 刷新页面 // 刷新页面
router.go(0); router.go(0);

View File

@@ -18,12 +18,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RouteLocationNormalized } from 'vue-router'; import type { RouteLocationNormalized } from 'vue-router';
import { useRouter } from 'vue-router';
import { computed, onUnmounted, ref, watch } from 'vue'; import { computed, onUnmounted, ref, watch } from 'vue';
import { routerToTag } from '@/router/constants'; import { getRouteTag, getRouteTitle } from '@/router';
import { listenerRouteChange, removeRouteListener, } from '@/utils/route-listener'; import { listenerRouteChange, removeRouteListener } from '@/utils/route-listener';
import { useAppStore, useTabBarStore } from '@/store'; import { useAppStore, useTabBarStore } from '@/store';
import qs from 'query-string';
import TabItem from './tab-item.vue'; import TabItem from './tab-item.vue';
const router = useRouter();
const appStore = useAppStore(); const appStore = useAppStore();
const tabBarStore = useTabBarStore(); const tabBarStore = useTabBarStore();
@@ -35,27 +39,33 @@
return appStore.navbar ? 60 : 0; return appStore.navbar ? 60 : 0;
}); });
watch( // 监听修改位置
() => appStore.navbar, watch(() => appStore.navbar, () => {
() => {
affixRef.value.updatePosition(); affixRef.value.updatePosition();
} });
);
// 监听路由变化 // 监听路由变化
listenerRouteChange((route: RouteLocationNormalized) => { listenerRouteChange((route: RouteLocationNormalized) => {
if ( // 不固定
!route.meta.noAffix && if (route.meta.noAffix) {
!tagList.value.some((tag) => tag.path === route.path) return;
) { }
// 固定并且没有此 tab 则添加 const tag = tagList.value.find((tag) => tag.path === route.path);
tabBarStore.addTab(routerToTag(route), route.meta?.ignoreCache as unknown as boolean); if (tag) {
// 找到 更新信息
tag.fullPath = route.fullPath;
tag.query = qs.parseUrl(route.fullPath).query;
tag.title = getRouteTitle(route);
} else {
// 未找到 添加标签
tabBarStore.addTab(getRouteTag(route), route.meta?.ignoreCache as unknown as boolean);
} }
}, true); }, true);
onUnmounted(() => { onUnmounted(() => {
removeRouteListener(); removeRouteListener();
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -60,7 +60,7 @@
position="left" position="left"
type="warning" type="warning"
@ok="deleteNode(node.key)"> @ok="deleteNode(node.key)">
<span v-permission="['asset:host-group:delete']" <span v-permission="['asset:host-group:update']"
class="tree-icon" class="tree-icon"
title="删除"> title="删除">
<icon-delete /> <icon-delete />

View File

@@ -1,6 +1,3 @@
import type { RouteLocationNormalized } from 'vue-router';
import type { TagProps } from '@/store/modules/tab-bar/types';
export const LOGIN_ROUTE_NAME = 'login'; export const LOGIN_ROUTE_NAME = 'login';
export const REDIRECT_ROUTE_NAME = 'redirect'; export const REDIRECT_ROUTE_NAME = 'redirect';
@@ -36,18 +33,3 @@ export const STATUS_ROUTER_LIST = [
{ name: NOT_FOUND_ROUTER_NAME, children: [] }, { name: NOT_FOUND_ROUTER_NAME, children: [] },
{ name: FORBIDDEN_ROUTER_NAME, children: [] }, { name: FORBIDDEN_ROUTER_NAME, children: [] },
]; ];
/**
* router 转 tag
*/
export const routerToTag = (route: RouteLocationNormalized): TagProps => {
const { name, meta, path, fullPath, query } = route;
return {
title: meta.locale || String(name),
name: String(name),
path,
fullPath,
query,
ignoreCache: meta.ignoreCache,
};
};

View File

@@ -1,5 +1,6 @@
import type { Router } from 'vue-router'; import type { Router } from 'vue-router';
import { useMenuStore } from '@/store'; import { useMenuStore } from '@/store';
import { getRouteTitle } from '@/router';
import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants'; import { NOT_FOUND_ROUTER_NAME, WHITE_ROUTER_LIST } from '../constants';
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import usePermission from '@/hooks/permission'; import usePermission from '@/hooks/permission';
@@ -30,10 +31,7 @@ export default function setupPermissionGuard(router: Router) {
next({ name: NOT_FOUND_ROUTER_NAME }); next({ name: NOT_FOUND_ROUTER_NAME });
} }
// 修改页面标题 // 修改页面标题
const locale = to.meta?.locale; document.title = getRouteTitle(to);
if (locale) {
document.title = locale;
}
NProgress.done(); NProgress.done();
}); });
} }

View File

@@ -1,4 +1,5 @@
import type { RouteLocationRaw } from 'vue-router'; import type { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
import type { TagProps } from '@/store/modules/tab-bar/types';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import { appRoutes } from './routes'; import { appRoutes } from './routes';
import { openWindow } from '@/utils'; import { openWindow } from '@/utils';
@@ -37,4 +38,27 @@ export const openNewRoute = (route: RouteLocationRaw) => {
} }
}; };
// route 转 tag
export const getRouteTag = (route: RouteLocationNormalized): TagProps => {
const { name, meta, path, fullPath, query } = route;
return {
title: getRouteTitle(route),
name: String(name),
path,
fullPath,
query,
ignoreCache: meta.ignoreCache,
};
};
// 获取 router title
export const getRouteTitle = (route: RouteLocationNormalized) => {
const { meta } = route;
// 如果 meta.localeTemplate 则根据路由生成
if (meta.localeTemplate) {
return meta?.localeTemplate(route) || meta.locale || '';
}
return meta.locale || '';
};
export default router; export default router;

View File

@@ -1,3 +1,4 @@
import type { RouteLocationNormalized } from 'vue-router';
import 'vue-router'; import 'vue-router';
/** /**
@@ -21,5 +22,7 @@ declare module 'vue-router' {
newWindow?: boolean; newWindow?: boolean;
// 是否活跃 // 是否活跃
activeMenu?: string; activeMenu?: string;
// 名称模板
localeTemplate?: (key: RouteLocationNormalized) => string;
} }
} }

View File

@@ -21,7 +21,7 @@
<host-form-info ref="infoRef" <host-form-info ref="infoRef"
class="form-panel" class="form-panel"
@change-type="(ts: string[]) => types = ts" @change-type="(ts: string[]) => types = ts"
@updated="updateHostInfo" /> @updated="onUpdateHostInfo" />
</a-tab-pane> </a-tab-pane>
<!-- 规格配置 --> <!-- 规格配置 -->
<a-tab-pane v-permission="['asset:host:update']" <a-tab-pane v-permission="['asset:host:update']"
@@ -31,7 +31,7 @@
<host-form-spec v-if="hostId" <host-form-spec v-if="hostId"
class="form-panel" class="form-panel"
:hostId="hostId" :hostId="hostId"
@updated="incrUpdatedCount" /> @updated="onUpdateHostSpec" />
</a-tab-pane> </a-tab-pane>
<!-- SSH 配置 --> <!-- SSH 配置 -->
<a-tab-pane v-permission="['asset:host:update-config']" <a-tab-pane v-permission="['asset:host:update-config']"
@@ -59,12 +59,10 @@
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { useCacheStore } from '@/store'; import { useCacheStore } from '@/store';
import { HostType } from '../types/const'; import { HostType } from '../types/const';
import { useCounter } from '@vueuse/core';
import HostFormInfo from './host-form-info.vue'; import HostFormInfo from './host-form-info.vue';
import HostFormSsh from './host-form-ssh.vue';
import HostFormSpec from './host-form-spec.vue'; import HostFormSpec from './host-form-spec.vue';
import HostFormSsh from './host-form-ssh.vue';
const { count: updatedCount, inc: incrUpdatedCount, reset: resetCounter } = useCounter();
const { visible, setVisible } = useVisible(); const { visible, setVisible } = useVisible();
const activeTab = ref<string>('info'); const activeTab = ref<string>('info');
@@ -72,6 +70,7 @@
const hostId = ref<number>(); const hostId = ref<number>();
const types = ref<string[]>([]); const types = ref<string[]>([]);
const infoRef = ref(); const infoRef = ref();
const hostViewUpdated = ref(false);
const emits = defineEmits(['reload']); const emits = defineEmits(['reload']);
@@ -93,7 +92,7 @@
// 打开复制 // 打开复制
const openCopy = (id: number) => { const openCopy = (id: number) => {
init('复制主机', id); init('复制主机', undefined);
nextTick(() => { nextTick(() => {
infoRef.value.openCopy(id); infoRef.value.openCopy(id);
}); });
@@ -104,9 +103,9 @@
title.value = _title; title.value = _title;
activeTab.value = 'info'; activeTab.value = 'info';
hostId.value = id; hostId.value = id;
hostViewUpdated.value = false;
types.value = []; types.value = [];
checkHostGroup(); checkHostGroup();
resetCounter();
setVisible(true); setVisible(true);
}; };
@@ -123,14 +122,20 @@
defineExpose({ openAdd, openUpdate, openCopy }); defineExpose({ openAdd, openUpdate, openCopy });
// 更新主机信息 // 更新主机信息
const updateHostInfo = (id: number) => { const onUpdateHostInfo = (id: number) => {
hostId.value = id; hostId.value = id;
incrUpdatedCount(); hostViewUpdated.value = true;
};
// 更新主机信息
const onUpdateHostSpec = () => {
hostViewUpdated.value = true;
}; };
// 处理关闭 // 处理关闭
const handleClose = () => { const handleClose = () => {
if (updatedCount.value) { // 修改主机视图信息后刷新列表
if (hostViewUpdated.value) {
emits('reload'); emits('reload');
} }
}; };

View File

@@ -5,28 +5,6 @@
label-align="right" label-align="right"
:auto-label-width="true" :auto-label-width="true"
:rules="hostFormRules"> :rules="hostFormRules">
<!-- 主机协议 -->
<a-form-item field="types" label="主机协议">
<a-select v-model="formModel.types"
placeholder="请选择支持的主机协议"
:options="toOptions(hostTypeKey)"
multiple
allow-clear />
</a-form-item>
<!-- 系统类型 -->
<a-form-item field="osType" label="系统类型">
<a-select v-model="formModel.osType"
placeholder="请选择系统类型"
:options="toOptions(hostOsTypeKey)"
allow-clear />
</a-form-item>
<!-- 系统架构 -->
<a-form-item field="archType" label="系统架构">
<a-select v-model="formModel.archType"
placeholder="请选择系统架构"
:options="toOptions(hostArchTypeKey)"
allow-clear />
</a-form-item>
<!-- 主机名称 --> <!-- 主机名称 -->
<a-form-item field="name" label="主机名称"> <a-form-item field="name" label="主机名称">
<a-input v-model="formModel.name" <a-input v-model="formModel.name"
@@ -45,6 +23,28 @@
placeholder="请输入主机地址" placeholder="请输入主机地址"
allow-clear /> allow-clear />
</a-form-item> </a-form-item>
<!-- 系统类型 -->
<a-form-item field="osType" label="系统类型">
<a-select v-model="formModel.osType"
placeholder="请选择系统类型"
:options="toOptions(hostOsTypeKey)"
allow-clear />
</a-form-item>
<!-- 系统架构 -->
<a-form-item field="archType" label="系统架构">
<a-select v-model="formModel.archType"
placeholder="请选择系统架构"
:options="toOptions(hostArchTypeKey)"
allow-clear />
</a-form-item>
<!-- 主机协议 -->
<a-form-item field="types" label="主机协议">
<a-select v-model="formModel.types"
placeholder="请选择支持的主机协议"
:options="toOptions(hostTypeKey)"
multiple
allow-clear />
</a-form-item>
<!-- 主机分组 --> <!-- 主机分组 -->
<a-form-item field="groupIdList" label="主机分组"> <a-form-item field="groupIdList" label="主机分组">
<host-group-tree-selector v-model="formModel.groupIdList" <host-group-tree-selector v-model="formModel.groupIdList"
@@ -90,7 +90,7 @@
import { ref } from 'vue'; import { ref } from 'vue';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { hostFormRules } from '../types/form.rules'; import { hostFormRules } from '../types/form.rules';
import { createHost, getHost, updateHost } from '@/api/asset/host'; import { createHost, getHost, updateHost, copyHost } from '@/api/asset/host';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { pick } from 'lodash'; import { pick } from 'lodash';
import { tagColor, hostTypeKey, hostOsTypeKey, HostOsType, hostArchTypeKey } from '../types/const'; import { tagColor, hostTypeKey, hostOsTypeKey, HostOsType, hostArchTypeKey } from '../types/const';
@@ -118,6 +118,7 @@
}; };
}; };
const isCopy = ref(false);
const formRef = ref(); const formRef = ref();
const formModel = ref<HostUpdateRequest>({}); const formModel = ref<HostUpdateRequest>({});
@@ -135,6 +136,7 @@
// 打开复制 // 打开复制
const openCopy = async (id: number) => { const openCopy = async (id: number) => {
renderForm({ ...defaultForm() }); renderForm({ ...defaultForm() });
isCopy.value = true;
await fetchHostRender(id); await fetchHostRender(id);
}; };
@@ -188,7 +190,12 @@
if (error) { if (error) {
return; return;
} }
if (!formModel.value.id) { if (isCopy.value) {
// 复制
const { data } = await copyHost(formModel.value);
Message.success('复制成功');
emits('updated', data);
} else if (!formModel.value.id) {
// 新增 // 新增
const { data } = await createHost(formModel.value); const { data } = await createHost(formModel.value);
Message.success('创建成功'); Message.success('创建成功');

View File

@@ -14,7 +14,7 @@ const columns = [
title: '主机信息', title: '主机信息',
dataIndex: 'hostInfo', dataIndex: 'hostInfo',
slotName: 'hostInfo', slotName: 'hostInfo',
minWidth: 344, width: 288,
align: 'left', align: 'left',
fixed: 'left', fixed: 'left',
default: true, default: true,
@@ -22,7 +22,7 @@ const columns = [
title: '主机规格', title: '主机规格',
dataIndex: 'hostSpec', dataIndex: 'hostSpec',
slotName: 'hostSpec', slotName: 'hostSpec',
width: 188, width: 198,
align: 'left', align: 'left',
default: true, default: true,
}, { }, {

View File

@@ -32,7 +32,11 @@
<b v-if="app.version && repo.tagName && ('v' + app.version) !== repo.tagName" <b v-if="app.version && repo.tagName && ('v' + app.version) !== repo.tagName"
class="span-green ml8">新版本已发布, 请及时升级版本</b> class="span-green ml8">新版本已发布, 请及时升级版本</b>
</a-descriptions-item> </a-descriptions-item>
<!-- 当前后端版本 --> <!-- 最近更新时间 -->
<a-descriptions-item label="最近更新时间">
<span>{{ repo.releaseTime }}</span>
</a-descriptions-item>
<!-- 最新更新日志 -->
<a-descriptions-item label="最新更新日志"> <a-descriptions-item label="最新更新日志">
<a-textarea v-model="repo.body" <a-textarea v-model="repo.body"
:auto-size="{ minRows: 3, maxRows: 16 }" :auto-size="{ minRows: 3, maxRows: 16 }"
@@ -51,22 +55,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { AppInfoResponse, AppReleaseResponse } from '@/api/system/setting'; import type { AppInfoResponse, AppReleaseResponse } from '@/api/system/setting';
import { onMounted, reactive } from 'vue'; import { onMounted, ref } from 'vue';
import { getAppLatestRelease, getSystemAppInfo } from '@/api/system/setting';
import { copy } from '@/hooks/copy'; import { copy } from '@/hooks/copy';
import useLoading from '@/hooks/loading'; import useLoading from '@/hooks/loading';
import { getAppLatestRelease, getSystemAppInfo } from '@/api/system/setting';
const { loading, setLoading } = useLoading(); const { loading, setLoading } = useLoading();
const webVersion = import.meta.env.VITE_APP_VERSION; const webVersion = import.meta.env.VITE_APP_VERSION;
const app = reactive<AppInfoResponse>({ const app = ref<AppInfoResponse>({
version: '', version: '',
uuid: '', uuid: '',
}); });
const repo = reactive<AppReleaseResponse>({ const repo = ref<AppReleaseResponse>({
tagName: '', tagName: '',
releaseTime: '',
body: '', body: '',
}); });
@@ -75,8 +80,7 @@
setLoading(true); setLoading(true);
try { try {
const { data } = await getSystemAppInfo(); const { data } = await getSystemAppInfo();
app.version = data.version; app.value = data;
app.uuid = data.uuid;
} catch (e) { } catch (e) {
} finally { } finally {
setLoading(false); setLoading(false);
@@ -87,8 +91,7 @@
onMounted(async () => { onMounted(async () => {
try { try {
const { data } = await getAppLatestRelease(); const { data } = await getAppLatestRelease();
repo.tagName = data.tagName; repo.value = data;
repo.body = data.body;
} catch (e) { } catch (e) {
} }
}); });

View File

@@ -22,7 +22,7 @@
</modules> </modules>
<properties> <properties>
<revision>2.3.8</revision> <revision>2.3.9</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>