Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5aec94207 | ||
|
|
2ff090a974 | ||
|
|
cc823d5bcd | ||
|
|
fade56b12a | ||
|
|
cf188451dd | ||
|
|
1125ef8a5a | ||
|
|
a3d4420754 | ||
|
|
27c1e16e57 |
@@ -28,7 +28,7 @@
|
||||
|
||||
<br/>
|
||||
|
||||
当前版本: **1.0.2**
|
||||
当前版本: **1.0.3**
|
||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
orion-ops-pro:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.2
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.3
|
||||
ports:
|
||||
- 1081:80
|
||||
environment:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
|
||||
mv ../../orion-ops-ui/dist ./dist
|
||||
docker build -t orion-ops-pro:1.0.2 .
|
||||
docker build -t orion-ops-pro:1.0.3 .
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<br/>
|
||||
|
||||
当前版本: **1.0.2**
|
||||
当前版本: **1.0.3**
|
||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# orion-ops-pro <small>1.0.2</small>
|
||||
# orion-ops-pro <small>1.0.3</small>
|
||||
|
||||
> 一款开箱即用的运维平台。
|
||||
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
> 版本号严格遵循 Semver 规范。
|
||||
|
||||
## v1.0.2
|
||||
## v1.0.3
|
||||
|
||||
`2024-03-25` `release`
|
||||
|
||||
* 🚀 升级 `arco design` 到 `2.55.0`
|
||||
* 🐞 修复 新创建的用户登录会跳转到 **404**
|
||||
* 🐞 修复 分配菜单模态框没有子菜单不显示的问题
|
||||
* 🐞 修复 工作台页面快捷操作面板会展示隐藏的菜单
|
||||
* 🐞 修复 主机终端无法粘贴 (浏览器安全策略)
|
||||
* 🐞 修复 卡片列表组件控制台 warn 提示
|
||||
* 🐞 修复 关闭终端时控制台提示 handleResize 错误信息
|
||||
* 🔨 修改 系统菜单渲染逻辑 (移除 JSX 构建时不会提示 JSX.IntrinsicElements)
|
||||
|
||||
[如何升级](/about/update.md?id=_v103)
|
||||
|
||||
## v1.0.3
|
||||
|
||||
`2024-03-22` `release`
|
||||
|
||||
|
||||
@@ -12,3 +12,7 @@
|
||||
* 资产授权 UI 改版
|
||||
* RDP 远程桌面
|
||||
* 接入 config 后端动态配置
|
||||
|
||||
## 已知问题 🐞
|
||||
|
||||
* 顶部菜单折叠宽度计算有问题 (arco 框架内问题)
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
|
||||
|
||||
## v1.0.3
|
||||
|
||||
> sql 脚本
|
||||
|
||||
```sql
|
||||
DELETE FROM preference WHERE type = 'TERMINAL';
|
||||
```
|
||||
|
||||
## v1.0.2
|
||||
|
||||
> sql 脚本
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.2</revision>
|
||||
<revision>1.0.3</revision>
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
|
||||
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface OrionOpsProConst {
|
||||
/**
|
||||
* 同 ${orion.version} 迭代时候需要手动更改
|
||||
*/
|
||||
String VERSION = "1.0.2";
|
||||
String VERSION = "1.0.3";
|
||||
|
||||
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.util.List;
|
||||
@Component
|
||||
public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
|
||||
|
||||
private static final String HOME_PATH = "~";
|
||||
|
||||
@Override
|
||||
public void handle(WebSocketSession channel, SftpListRequest payload) {
|
||||
// 获取会话
|
||||
@@ -37,7 +39,7 @@ public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
|
||||
List<SftpFileVO> list = Lists.empty();
|
||||
try {
|
||||
// 空目录则直接获取 home 目录
|
||||
if (Strings.isBlank(path)) {
|
||||
if (HOME_PATH.equals(path)) {
|
||||
path = session.getHome();
|
||||
}
|
||||
// 文件列表
|
||||
|
||||
@@ -74,7 +74,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("changeToNextSession", true, false, true, "BracketRight", true),
|
||||
// 终端快捷键
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("copy", true, true, false, "KeyC", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("paste", true, true, false, "KeyV", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("paste", true, true, false, "Insert", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("toTop", true, true, false, "ArrowUp", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("toBottom", true, true, false, "ArrowDown", true),
|
||||
new TerminalPreferenceModel.ShortcutKeysModel("selectAll", true, true, false, "KeyA", true),
|
||||
@@ -94,7 +94,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
|
||||
.selectAll(false)
|
||||
.search(true)
|
||||
.copy(true)
|
||||
.paste(true)
|
||||
.paste(false)
|
||||
.interrupt(false)
|
||||
.enter(false)
|
||||
.fontSizePlus(false)
|
||||
@@ -112,7 +112,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
|
||||
.theme(new JSONObject())
|
||||
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
|
||||
.actionBarSetting(JSONObject.parseObject(actionBarSetting))
|
||||
.rightMenuSetting(Lists.of("copy", "paste", "checkAll", "search", "clear"))
|
||||
.rightMenuSetting(Lists.of("selectAll", "copy", "fontSizePlus", "fontSizeSubtract", "search", "clear"))
|
||||
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
|
||||
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
|
||||
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "orion-ops-pro-ui",
|
||||
"description": "Orion Ops Pro for Vue",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"private": true,
|
||||
"author": "Jiahang Li",
|
||||
"license": "Apache 2.0",
|
||||
"scripts": {
|
||||
"dev": "vite --config ./config/vite.config.dev.ts",
|
||||
"dev:host": "vite --config ./config/vite.config.dev.ts --host",
|
||||
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
|
||||
"report": "cross-env REPORT=true npm run build",
|
||||
"preview": "npm run build && vite preview --host",
|
||||
@@ -29,7 +30,7 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@arco-design/web-vue": "^2.53.3",
|
||||
"@arco-design/web-vue": "^2.55.0",
|
||||
"@dangojs/a-query-header": "^0.0.31",
|
||||
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
|
||||
"@vueuse/core": "^9.3.0",
|
||||
|
||||
11570
orion-ops-ui/pnpm-lock.yaml
generated
11570
orion-ops-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
<!-- 顶部菜单 -->
|
||||
<div class="center-side">
|
||||
<menu-tree v-if="topMenu" />
|
||||
<system-menu-tree v-if="topMenu" />
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<ul class="right-side">
|
||||
@@ -214,7 +214,7 @@
|
||||
import { preferenceTipsKey } from './const';
|
||||
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
|
||||
import { openNewRoute } from '@/router';
|
||||
import MenuTree 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 UpdatePasswordModal from '@/components/user/user/update-password-modal/index.vue';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="tab-bar-box">
|
||||
<div class="tab-bar-scroll">
|
||||
<div class="tags-wrap">
|
||||
<TabItem v-for="(tag, index) in tagList"
|
||||
<tab-item v-for="(tag, index) in tagList"
|
||||
:key="tag.fullPath"
|
||||
:index="index"
|
||||
:item-data="tag" />
|
||||
|
||||
@@ -272,7 +272,9 @@ export default class LogAppender implements ILogAppender {
|
||||
.filter(Boolean)
|
||||
.forEach(s => s.dispose());
|
||||
// 卸载终端
|
||||
s.terminal?.dispose();
|
||||
setTimeout(() => {
|
||||
s.terminal?.dispose();
|
||||
}, 300);
|
||||
} catch (e) {
|
||||
// 卸载可能会报错
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
<tbody>
|
||||
<a-checkbox-group v-model="checkedKeys" style="display: contents;">
|
||||
<template v-for="parentMenu in menuData" :key="parentMenu.id">
|
||||
<template v-for="(childrenMenu, i) in parentMenu.children" :key="childrenMenu.id">
|
||||
<tr>
|
||||
<!-- 有子菜单 -->
|
||||
<template v-if="parentMenu.children?.length">
|
||||
<tr v-for="(childrenMenu, i) in parentMenu.children"
|
||||
:key="childrenMenu.id">
|
||||
<!-- 父菜单 -->
|
||||
<td v-if="i === 0" :rowspan="parentMenu.children.length">
|
||||
<a-checkbox :value="parentMenu.id">
|
||||
@@ -38,6 +40,23 @@
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<!-- 无子菜单 -->
|
||||
<template v-else>
|
||||
<tr>
|
||||
<!-- 父菜单 -->
|
||||
<td>
|
||||
<a-checkbox :value="parentMenu.id">
|
||||
{{ parentMenu.name }}
|
||||
</a-checkbox>
|
||||
</td>
|
||||
<!-- 子菜单 -->
|
||||
<td>
|
||||
</td>
|
||||
<!-- 功能 -->
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</template>
|
||||
</a-checkbox-group>
|
||||
</tbody>
|
||||
|
||||
@@ -1,163 +1,160 @@
|
||||
<script lang="tsx">
|
||||
<template>
|
||||
<a-menu class="full"
|
||||
:mode="topMenu ? 'horizontal' : 'vertical'"
|
||||
v-model:collapsed="collapsed"
|
||||
v-model:open-keys="openKeys"
|
||||
v-model:selected-keys="selectedKey"
|
||||
:show-collapse-button="appStore.device !== 'mobile'"
|
||||
:auto-open="false"
|
||||
:auto-open-selected="true"
|
||||
:level-indent="34"
|
||||
@collapse="setCollapse">
|
||||
<template v-for="menu in menuTree">
|
||||
<!-- 一级菜单 -->
|
||||
<a-menu-item v-if="!menu.children?.length"
|
||||
:key="menu.name"
|
||||
@click="(e) => goto(e, menu)">
|
||||
<!-- 图标 -->
|
||||
<template #icon>
|
||||
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
|
||||
</template>
|
||||
<!-- 名称 -->
|
||||
{{ menu.meta?.locale || '' }}
|
||||
</a-menu-item>
|
||||
<!-- 父菜单 -->
|
||||
<a-sub-menu v-else :key="menu.name">
|
||||
<!-- 图标 -->
|
||||
<template #icon>
|
||||
<component v-if="menu.meta?.icon" :is="menu.meta?.icon" />
|
||||
</template>
|
||||
<!-- 名称 -->
|
||||
<template #title>
|
||||
{{ menu.meta?.locale || '' }}
|
||||
</template>
|
||||
<!-- 子菜单 -->
|
||||
<a-menu-item v-for="child in menu.children"
|
||||
:key="child.name"
|
||||
@click="(e) => goto(e, child)">
|
||||
<!-- 图标 -->
|
||||
<template #icon v-if="child.meta?.icon">
|
||||
<component :is="child.meta?.icon" />
|
||||
</template>
|
||||
<!-- 名称 -->
|
||||
{{ child.meta?.locale || '' }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'systemMenuTree'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { RouteMeta, RouteRecordRaw } from 'vue-router';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { compile, computed, defineComponent, h, ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import { listenerRouteChange } from '@/utils/route-listener';
|
||||
import { openWindow, regexUrl } from '@/utils';
|
||||
import { openNewRoute } from '@/router';
|
||||
import useMenuTree from './use-menu-tree';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'menuTree',
|
||||
emit: ['collapse'],
|
||||
setup() {
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { menuTree } = useMenuTree();
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||
return false;
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value });
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['collapse']);
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
const appStore = useAppStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const { menuTree } = useMenuTree();
|
||||
|
||||
// 跳转路由
|
||||
const goto = (e: any, item: RouteRecordRaw) => {
|
||||
// 打开外链
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path);
|
||||
return;
|
||||
}
|
||||
const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;
|
||||
// 新页面打开
|
||||
if (newWindow || e.ctrlKey) {
|
||||
openNewRoute({
|
||||
name: item.name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 设置 selectedKey
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// 触发跳转
|
||||
router.push({
|
||||
name: item.name,
|
||||
});
|
||||
};
|
||||
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = [];
|
||||
let isFind = false;
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true;
|
||||
result.push(...keys);
|
||||
return;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
backtrack(el, [...keys, el.name as string]);
|
||||
});
|
||||
}
|
||||
};
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return;
|
||||
backtrack(el, [el.name as string]);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// 监听路由 设置打开的 key
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { activeMenu, hideInMenu } = newRoute.meta;
|
||||
if (!hideInMenu || activeMenu) {
|
||||
const menuOpenKeys = findMenuOpenKeys(
|
||||
(activeMenu || newRoute.name) as string
|
||||
);
|
||||
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||
openKeys.value = [...keySet];
|
||||
|
||||
selectedKey.value = [
|
||||
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
|
||||
];
|
||||
}
|
||||
}, true);
|
||||
|
||||
// 展开菜单
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop')
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
|
||||
// 渲染菜单
|
||||
const renderSubMenu = () => {
|
||||
function travel(_route: RouteRecordRaw[], nodes = []) {
|
||||
if (_route) {
|
||||
_route.forEach((element) => {
|
||||
// This is demo, modify nodes as needed
|
||||
const icon = element?.meta?.icon
|
||||
? () => h(compile(`<${element?.meta?.icon}/>`))
|
||||
: null;
|
||||
const node =
|
||||
element?.children && element?.children.length !== 0 ? (
|
||||
<a-sub-menu
|
||||
key={element?.name}
|
||||
v-slots={{
|
||||
icon,
|
||||
// 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),
|
||||
title: () => h(compile(element?.meta?.locale || '')),
|
||||
}}
|
||||
>
|
||||
{travel(element?.children)}
|
||||
</a-sub-menu>
|
||||
) : (
|
||||
<a-menu-item
|
||||
key={element?.name}
|
||||
v-slots={{ icon }}
|
||||
onClick={($event: any) => goto($event, element)}
|
||||
>
|
||||
{element?.meta?.locale || ''}
|
||||
</a-menu-item>
|
||||
);
|
||||
nodes.push(node as never);
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
return travel(menuTree.value);
|
||||
};
|
||||
|
||||
return () => (
|
||||
<a-menu
|
||||
mode={topMenu.value ? 'horizontal' : 'vertical'}
|
||||
v-model:collapsed={collapsed.value}
|
||||
v-model:open-keys={openKeys.value}
|
||||
show-collapse-button={appStore.device !== 'mobile'}
|
||||
auto-open={false}
|
||||
selected-keys={selectedKey.value}
|
||||
auto-open-selected={true}
|
||||
level-indent={34}
|
||||
style="height: 100%; width:100%;"
|
||||
onCollapse={setCollapse}
|
||||
>
|
||||
{renderSubMenu()}
|
||||
</a-menu>
|
||||
);
|
||||
const collapsed = computed({
|
||||
get() {
|
||||
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||
return false;
|
||||
},
|
||||
set(value: boolean) {
|
||||
appStore.updateSettings({ menuCollapse: value });
|
||||
},
|
||||
});
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
|
||||
// 跳转路由
|
||||
const goto = (e: any, item: RouteRecordRaw) => {
|
||||
// 打开外链
|
||||
if (regexUrl.test(item.path)) {
|
||||
openWindow(item.path);
|
||||
return;
|
||||
}
|
||||
const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;
|
||||
// 新页面打开
|
||||
if (newWindow || e.ctrlKey) {
|
||||
openNewRoute({
|
||||
name: item.name,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 设置 selectedKey
|
||||
if (route.name === item.name && !hideInMenu && !activeMenu) {
|
||||
selectedKey.value = [item.name as string];
|
||||
return;
|
||||
}
|
||||
// 触发跳转
|
||||
router.push({
|
||||
name: item.name,
|
||||
});
|
||||
};
|
||||
|
||||
const findMenuOpenKeys = (target: string) => {
|
||||
const result: string[] = [];
|
||||
let isFind = false;
|
||||
const backtrack = (item: RouteRecordRaw, keys: string[]) => {
|
||||
if (item.name === target) {
|
||||
isFind = true;
|
||||
result.push(...keys);
|
||||
return;
|
||||
}
|
||||
if (item.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
backtrack(el, [...keys, el.name as string]);
|
||||
});
|
||||
}
|
||||
};
|
||||
menuTree.value.forEach((el: RouteRecordRaw) => {
|
||||
if (isFind) return;
|
||||
backtrack(el, [el.name as string]);
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
// 监听路由 设置打开的 key
|
||||
listenerRouteChange((newRoute) => {
|
||||
const { activeMenu, hideInMenu } = newRoute.meta;
|
||||
if (!hideInMenu || activeMenu) {
|
||||
const menuOpenKeys = findMenuOpenKeys(
|
||||
(activeMenu || newRoute.name) as string
|
||||
);
|
||||
|
||||
const keySet = new Set([...menuOpenKeys, ...openKeys.value]);
|
||||
openKeys.value = [...keySet];
|
||||
|
||||
selectedKey.value = [
|
||||
activeMenu || menuOpenKeys[menuOpenKeys.length - 1],
|
||||
];
|
||||
}
|
||||
}, true);
|
||||
|
||||
// 展开菜单
|
||||
const setCollapse = (val: boolean) => {
|
||||
if (appStore.device === 'desktop')
|
||||
appStore.updateSettings({ menuCollapse: val });
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
189
orion-ops-ui/src/components/system/menu/tree/index1.vue
Normal file
189
orion-ops-ui/src/components/system/menu/tree/index1.vue
Normal file
@@ -0,0 +1,189 @@
|
||||
<!--<script lang="tsx">-->
|
||||
<!-- import type { RouteMeta, RouteRecordRaw } from 'vue-router';-->
|
||||
<!-- import { useRoute, useRouter } from 'vue-router';-->
|
||||
<!-- import { compile, computed, defineComponent, h, ref } from 'vue';-->
|
||||
<!-- import { useAppStore } from '@/store';-->
|
||||
<!-- import { listenerRouteChange } from '@/utils/route-listener';-->
|
||||
<!-- import { openWindow, regexUrl } from '@/utils';-->
|
||||
<!-- import { openNewRoute } from '@/router';-->
|
||||
<!-- import useMenuTree from './use-menu-tree';-->
|
||||
|
||||
<!-- export default defineComponent({-->
|
||||
<!-- name: 'menuTree',-->
|
||||
<!-- emit: ['collapse'],-->
|
||||
<!-- setup() {-->
|
||||
<!-- const appStore = useAppStore();-->
|
||||
<!-- const router = useRouter();-->
|
||||
<!-- const route = useRoute();-->
|
||||
<!-- const { menuTree } = useMenuTree();-->
|
||||
<!-- const collapsed = computed({-->
|
||||
<!-- get() {-->
|
||||
<!-- if (appStore.device === 'desktop') return appStore.menuCollapse;-->
|
||||
<!-- return false;-->
|
||||
<!-- },-->
|
||||
<!-- set(value: boolean) {-->
|
||||
<!-- appStore.updateSettings({ menuCollapse: value });-->
|
||||
<!-- },-->
|
||||
<!-- });-->
|
||||
|
||||
<!-- const topMenu = computed(() => appStore.topMenu);-->
|
||||
<!-- const openKeys = ref<string[]>([]);-->
|
||||
<!-- const selectedKey = ref<string[]>([]);-->
|
||||
|
||||
<!-- // 跳转路由-->
|
||||
<!-- const goto = (e: any, item: RouteRecordRaw) => {-->
|
||||
<!-- // 打开外链-->
|
||||
<!-- if (regexUrl.test(item.path)) {-->
|
||||
<!-- openWindow(item.path);-->
|
||||
<!-- return;-->
|
||||
<!-- }-->
|
||||
<!-- const { hideInMenu, activeMenu, newWindow } = item.meta as RouteMeta;-->
|
||||
<!-- // 新页面打开-->
|
||||
<!-- if (newWindow || e.ctrlKey) {-->
|
||||
<!-- openNewRoute({-->
|
||||
<!-- name: item.name,-->
|
||||
<!-- });-->
|
||||
<!-- return;-->
|
||||
<!-- }-->
|
||||
<!-- // 设置 selectedKey-->
|
||||
<!-- if (route.name === item.name && !hideInMenu && !activeMenu) {-->
|
||||
<!-- selectedKey.value = [item.name as string];-->
|
||||
<!-- return;-->
|
||||
<!-- }-->
|
||||
<!-- // 触发跳转-->
|
||||
<!-- router.push({-->
|
||||
<!-- name: item.name,-->
|
||||
<!-- });-->
|
||||
<!-- };-->
|
||||
|
||||
<!-- const findMenuOpenKeys = (target: string) => {-->
|
||||
<!-- const result: string[] = [];-->
|
||||
<!-- let isFind = false;-->
|
||||
<!-- const backtrack = (item: RouteRecordRaw, keys: string[]) => {-->
|
||||
<!-- if (item.name === target) {-->
|
||||
<!-- isFind = true;-->
|
||||
<!-- result.push(...keys);-->
|
||||
<!-- return;-->
|
||||
<!-- }-->
|
||||
<!-- if (item.children?.length) {-->
|
||||
<!-- item.children.forEach((el) => {-->
|
||||
<!-- backtrack(el, [...keys, el.name as string]);-->
|
||||
<!-- });-->
|
||||
<!-- }-->
|
||||
<!-- };-->
|
||||
<!-- menuTree.value.forEach((el: RouteRecordRaw) => {-->
|
||||
<!-- if (isFind) return;-->
|
||||
<!-- backtrack(el, [el.name as string]);-->
|
||||
<!-- });-->
|
||||
<!-- return result;-->
|
||||
<!-- };-->
|
||||
|
||||
<!-- // 监听路由 设置打开的 key-->
|
||||
<!-- listenerRouteChange((newRoute) => {-->
|
||||
<!-- const { activeMenu, hideInMenu } = newRoute.meta;-->
|
||||
<!-- if (!hideInMenu || activeMenu) {-->
|
||||
<!-- const menuOpenKeys = findMenuOpenKeys(-->
|
||||
<!-- (activeMenu || newRoute.name) as string-->
|
||||
<!-- );-->
|
||||
|
||||
<!-- const keySet = new Set([...menuOpenKeys, ...openKeys.value]);-->
|
||||
<!-- openKeys.value = [...keySet];-->
|
||||
|
||||
<!-- selectedKey.value = [-->
|
||||
<!-- activeMenu || menuOpenKeys[menuOpenKeys.length - 1],-->
|
||||
<!-- ];-->
|
||||
<!-- }-->
|
||||
<!-- }, true);-->
|
||||
|
||||
<!-- // 展开菜单-->
|
||||
<!-- const setCollapse = (val: boolean) => {-->
|
||||
<!-- if (appStore.device === 'desktop')-->
|
||||
<!-- appStore.updateSettings({ menuCollapse: val });-->
|
||||
<!-- };-->
|
||||
|
||||
<!-- // 渲染菜单-->
|
||||
<!-- const renderSubMenu = () => {-->
|
||||
<!-- function travel(_route: RouteRecordRaw[], nodes = []) {-->
|
||||
<!-- if (_route) {-->
|
||||
<!-- _route.forEach((element) => {-->
|
||||
<!-- // This is demo, modify nodes as needed-->
|
||||
<!-- const icon = element?.meta?.icon-->
|
||||
<!-- ? () => h(compile(`<${element?.meta?.icon}/>`))-->
|
||||
<!-- : null;-->
|
||||
<!-- const node =-->
|
||||
<!-- element?.children && element?.children.length !== 0 ? (-->
|
||||
<!-- <a-sub-menu-->
|
||||
<!-- key={element?.name}-->
|
||||
<!-- v-slots={{-->
|
||||
<!-- icon,-->
|
||||
<!-- // 去除国际化 title: () => h(compile(t(element?.meta?.locale || ''))),-->
|
||||
<!-- title: () => h(compile(element?.meta?.locale || '')),-->
|
||||
<!-- }}-->
|
||||
<!-- >-->
|
||||
<!-- {travel(element?.children)}-->
|
||||
<!-- </a-sub-menu>-->
|
||||
<!-- ) : (-->
|
||||
<!-- <a-menu-item-->
|
||||
<!-- key={element?.name}-->
|
||||
<!-- v-slots={{ icon }}-->
|
||||
<!-- onClick={($event: any) => goto($event, element)}-->
|
||||
<!-- >-->
|
||||
<!-- {element?.meta?.locale || ''}-->
|
||||
<!-- </a-menu-item>-->
|
||||
<!-- );-->
|
||||
<!-- nodes.push(node as never);-->
|
||||
<!-- });-->
|
||||
<!-- }-->
|
||||
<!-- return nodes;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- return travel(menuTree.value);-->
|
||||
<!-- };-->
|
||||
|
||||
<!-- return () => (-->
|
||||
<!-- <a-menu-->
|
||||
<!-- mode={topMenu.value ? 'horizontal' : 'vertical'}-->
|
||||
<!-- v-model:collapsed={collapsed.value}-->
|
||||
<!-- v-model:open-keys={openKeys.value}-->
|
||||
<!-- show-collapse-button={appStore.device !== 'mobile'}-->
|
||||
<!-- auto-open={false}-->
|
||||
<!-- selected-keys={selectedKey.value}-->
|
||||
<!-- auto-open-selected={true}-->
|
||||
<!-- level-indent={34}-->
|
||||
<!-- style="height: 100%; width:100%;"-->
|
||||
<!-- onCollapse={setCollapse}-->
|
||||
<!-- >-->
|
||||
<!-- {renderSubMenu()}-->
|
||||
<!-- </a-menu>-->
|
||||
<!-- );-->
|
||||
<!-- },-->
|
||||
<!-- });-->
|
||||
<!--</script>-->
|
||||
|
||||
<!--<style lang="less" scoped>-->
|
||||
<!-- :deep(.arco-menu-inner) {-->
|
||||
<!-- .arco-menu-inline-header {-->
|
||||
<!-- display: flex;-->
|
||||
<!-- align-items: center;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .arco-icon {-->
|
||||
<!-- &:not(.arco-icon-down) {-->
|
||||
<!-- font-size: 18px;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .arco-menu-icon {-->
|
||||
<!-- margin-right: 10px !important;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .arco-menu-indent-list {-->
|
||||
<!-- width: 28px;-->
|
||||
<!-- display: inline-block;-->
|
||||
<!-- }-->
|
||||
|
||||
<!-- .arco-menu-title {-->
|
||||
<!-- user-select: none;-->
|
||||
<!-- }-->
|
||||
<!-- }-->
|
||||
<!--</style>-->
|
||||
@@ -8,8 +8,10 @@ export default function useMenuTree() {
|
||||
const appRoute = computed(() => {
|
||||
return menuStore.appMenus;
|
||||
});
|
||||
const menuTree = computed(() => {
|
||||
const menuTree = computed<RouteRecordNormalized[]>(() => {
|
||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
||||
|
||||
// 排序
|
||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||
return (a.meta.order || 0) - (b.meta.order || 0);
|
||||
});
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<div class="pagination-wrapper">
|
||||
<a-pagination v-if="pagination"
|
||||
size="mini"
|
||||
v-model:current="(pagination as PaginationProps).current"
|
||||
v-model:page-size="(pagination as PaginationProps).pageSize"
|
||||
v-model:current="(pagination as any).current"
|
||||
v-model:page-size="(pagination as any).pageSize"
|
||||
v-bind="pagination as any"
|
||||
:auto-adjust="false"
|
||||
@change="page => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, page, (pagination as PaginationProps).pageSize)"
|
||||
@change="page => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, page, (pagination as any).pageSize)"
|
||||
@page-size-change="limit => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, 1, limit)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -104,7 +104,6 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { PaginationProps } from '@arco-design/web-vue';
|
||||
import type { CardProps } from '../types/props';
|
||||
import { ref, computed } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
@@ -49,7 +49,10 @@
|
||||
'field-value',
|
||||
field.ellipsis ? 'field-value-ellipsis' : ''
|
||||
]">
|
||||
<slot :name="field.slotName" :record="item" :index="index" :key="item[key as string]">
|
||||
<slot :name="field.slotName"
|
||||
:record="item"
|
||||
:index="index"
|
||||
:rowKey="item[rowKey as string]">
|
||||
<a-tooltip v-if="field.tooltip" :content="item[field.dataIndex]">
|
||||
<span v-if="field.render" v-html="field.render({ record: item, index })" />
|
||||
<span v-else>{{ item[field.dataIndex] }}</span>
|
||||
@@ -87,7 +90,7 @@
|
||||
|
||||
const props = defineProps<CardProps & {
|
||||
index: number,
|
||||
item: CardRecord
|
||||
item: CardRecord,
|
||||
}>();
|
||||
const emits = defineEmits(['emitter']);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</a-col>
|
||||
<!-- 数据卡片 -->
|
||||
<a-col v-for="(item, index) in list"
|
||||
:key="item[key]"
|
||||
:key="item[rowKey]"
|
||||
v-bind="cardLayoutCols"
|
||||
:class="{ 'disabled-col': item.disabled === true }">
|
||||
<!-- 右键菜单 -->
|
||||
@@ -41,12 +41,18 @@
|
||||
@emitter="dispatchEmitter">
|
||||
<!-- 自定义插槽 -->
|
||||
<template v-for="slot in Object.keys($slots)" :key="slot" #[slot]>
|
||||
<slot :name="slot" :record="item" :index="index" :key="item[key]" />
|
||||
<slot :name="slot"
|
||||
:record="item"
|
||||
:index="index"
|
||||
:rowKey="item[rowKey]" />
|
||||
</template>
|
||||
</card-item>
|
||||
<!-- 右键菜单 -->
|
||||
<template v-if="contextMenu" #content>
|
||||
<slot name="contextMenu" :record="item" :index="index" :key="item[key]" />
|
||||
<slot name="contextMenu"
|
||||
:record="item"
|
||||
:index="index"
|
||||
:rowKey="item[rowKey]" />
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
@@ -82,7 +88,7 @@
|
||||
import useEmitter from '@/hooks/emitter';
|
||||
|
||||
const props = withDefaults(defineProps<CardProps>(), {
|
||||
key: 'id',
|
||||
rowKey: 'id',
|
||||
pagination: false,
|
||||
loading: false,
|
||||
cardHeight: '100%',
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { CardFieldConfig, CardPosition, CardRecord, ColResponsiveValue, Han
|
||||
|
||||
// 卡片属性
|
||||
export interface CardProps {
|
||||
key?: string;
|
||||
rowKey?: string;
|
||||
pagination?: PaginationProps | boolean;
|
||||
loading?: boolean;
|
||||
fieldConfig?: CardFieldConfig;
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
|
||||
const { copy: c } = useClipboard();
|
||||
|
||||
// 复制
|
||||
export const copy = async (value: string | undefined, tips: string | boolean = `${value} 已复制`) => {
|
||||
try {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
await c(value);
|
||||
await copyToClipboard(value);
|
||||
if (tips) {
|
||||
Message.success(tips as string);
|
||||
}
|
||||
@@ -20,7 +17,52 @@ export const copy = async (value: string | undefined, tips: string | boolean = `
|
||||
|
||||
// 获取剪切板内容
|
||||
export const readText = () => {
|
||||
return navigator.clipboard.readText();
|
||||
if (navigator.clipboard) {
|
||||
return navigator.clipboard.readText();
|
||||
} else {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const success = document.execCommand('paste');
|
||||
if (!success) {
|
||||
Message.error('当前环境无法读取剪切板内容');
|
||||
}
|
||||
resolve(textarea.value);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 复制到剪切板
|
||||
export const copyToClipboard = async (value: string) => {
|
||||
if (navigator.clipboard) {
|
||||
await navigator.clipboard.writeText(value);
|
||||
} else {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.textContent = value;
|
||||
textarea.style.position = 'absolute';
|
||||
textarea.style.left = '-9999px';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
const success = document.execCommand('copy');
|
||||
if (!success) {
|
||||
Message.error('当前环境无法复制到剪切板');
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function useCopy() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||
import { useMenuStore, useUserStore } from '@/store';
|
||||
import { STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants';
|
||||
import { DEFAULT_ROUTER, STATUS_ROUTER_LIST, WHITE_ROUTER_LIST } from '@/router/constants';
|
||||
import { AdminRoleCode } from '@/types/const';
|
||||
|
||||
export default function usePermission() {
|
||||
@@ -16,7 +16,7 @@ export default function usePermission() {
|
||||
return false;
|
||||
}
|
||||
// 检查路由是否存在于授权路由中
|
||||
const menuConfig = [...menuStore.appMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST];
|
||||
const menuConfig = [...menuStore.appMenus, ...WHITE_ROUTER_LIST, ...STATUS_ROUTER_LIST, DEFAULT_ROUTER];
|
||||
let exist = false;
|
||||
while (menuConfig.length && !exist) {
|
||||
const element = menuConfig.shift();
|
||||
@@ -44,7 +44,7 @@ export default function usePermission() {
|
||||
hasAnyPermission(permission: string[]) {
|
||||
return userStore.permission?.includes('*') ||
|
||||
permission.map(s => userStore.permission?.includes(s))
|
||||
.filter(Boolean).length > 0;
|
||||
.filter(Boolean).length > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ export default function usePermission() {
|
||||
hasAnyRole(role: string[]) {
|
||||
return userStore.roles?.includes(AdminRoleCode) ||
|
||||
role.map(s => userStore.roles?.includes(s))
|
||||
.filter(Boolean).length > 0;
|
||||
.filter(Boolean).length > 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||
<!-- tab bar -->
|
||||
<div v-if="navbar" class="layout-navbar">
|
||||
<NavBar />
|
||||
<nav-bar />
|
||||
</div>
|
||||
<a-layout>
|
||||
<a-layout style="flex-direction: row;">
|
||||
<!-- 左侧菜单栏 -->
|
||||
<a-layout-sider v-if="renderMenu"
|
||||
v-show="!hideMenu"
|
||||
@@ -17,7 +17,7 @@
|
||||
:hide-trigger="true"
|
||||
@collapse="setCollapsed">
|
||||
<div class="menu-wrapper">
|
||||
<menu-tree />
|
||||
<system-menu-tree />
|
||||
</div>
|
||||
</a-layout-sider>
|
||||
<!-- 顶部菜单栏 -->
|
||||
@@ -29,12 +29,12 @@
|
||||
mask-closable
|
||||
:closable="false"
|
||||
@cancel="drawerCancel">
|
||||
<menu-tree style="padding: 12px 16px;" />
|
||||
<system-menu-tree style="padding: 12px 16px;" />
|
||||
</a-drawer>
|
||||
<!-- body -->
|
||||
<a-layout class="layout-content" :style="paddingStyle">
|
||||
<!-- 页签 -->
|
||||
<TabBar v-if="appStore.tabBar" />
|
||||
<tab-bar v-if="appStore.tabBar" />
|
||||
<!-- 页面 -->
|
||||
<a-layout-content style="height: 100%; width: 100%;">
|
||||
<!-- 水印 -->
|
||||
@@ -43,11 +43,11 @@
|
||||
:z-index="9999"
|
||||
style="width: 100%; height: 100%;"
|
||||
:content="userStore.username || ''">
|
||||
<PageLayout />
|
||||
<page-layout />
|
||||
</a-watermark>
|
||||
</a-layout-content>
|
||||
<!-- 页脚 -->
|
||||
<Footer v-if="visibleFooter" />
|
||||
<app-footer v-if="visibleFooter" />
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
@@ -60,17 +60,18 @@
|
||||
import useResponsive from '@/hooks/responsive';
|
||||
import { toggleDrawerMenuKey } from '@/types/symbol';
|
||||
import PageLayout from './page-layout.vue';
|
||||
import MenuTree from '@/components/system/menu/tree/index.vue';
|
||||
import NavBar from '@/components/app/navbar/index.vue';
|
||||
import Footer from '@/components/app/footer/index.vue';
|
||||
import TabBar from '@/components/app/tab-bar/index.vue';
|
||||
import AppFooter from '@/components/app/app-footer/index.vue';
|
||||
import SystemMenuTree from '@/components/system/menu/tree/index.vue';
|
||||
|
||||
const isInit = ref(false);
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
useResponsive(true);
|
||||
|
||||
const isInit = ref(false);
|
||||
const navbarHeight = `60px`;
|
||||
const navbar = computed(() => appStore.navbar);
|
||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
|
||||
|
||||
@@ -13,6 +13,11 @@ export const DEFAULT_ROUTE_NAME = 'workplace';
|
||||
|
||||
export const DEFAULT_ROUTE_FULL_PATH = '/workplace';
|
||||
|
||||
/**
|
||||
* 默认路由
|
||||
*/
|
||||
export const DEFAULT_ROUTER = { name: DEFAULT_ROUTE_NAME, children: [] };
|
||||
|
||||
/**
|
||||
* 路由白名单
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { PaginationProps, TableRowSelection } from '@arco-design/web-vue';
|
||||
import type { TableExpandable } from '@arco-design/web-vue/es/table/interface';
|
||||
import { reactive } from 'vue';
|
||||
import { useAppStore } from '@/store';
|
||||
import { isNumber } from '@/utils/is';
|
||||
import { TablePageSizeOptions } from '@/types/const';
|
||||
import { TableExpandable } from '@arco-design/web-vue/es/table/interface';
|
||||
|
||||
/**
|
||||
* 创建列表分页
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
const debug = import.meta.env.MODE !== 'production';
|
||||
|
||||
// 当前环境是否为安全环境
|
||||
export const isSecureEnvironment = (() => {
|
||||
return window.location.protocol === 'https:' || window.location.hostname === 'localhost';
|
||||
})();
|
||||
|
||||
// http base url
|
||||
export const httpBaseUrl = (() => {
|
||||
const configBase = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
<div class="logo-text">Orion Ops Pro</div>
|
||||
</div>
|
||||
<!-- 左侧 banner -->
|
||||
<LoginBanner />
|
||||
<login-banner />
|
||||
<!-- 主体部分 -->
|
||||
<div class="content">
|
||||
<div class="content-inner">
|
||||
<!-- 登录表单 -->
|
||||
<LoginForm />
|
||||
<login-form />
|
||||
</div>
|
||||
<!-- 页脚 -->
|
||||
<div class="footer">
|
||||
<Footer />
|
||||
<app-footer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -26,9 +26,9 @@
|
||||
<script lang="ts" setup>
|
||||
import { Notification } from '@arco-design/web-vue';
|
||||
import { reLoginTipsKey } from '@/types/symbol';
|
||||
import Footer from '@/components/app/footer/index.vue';
|
||||
import LoginBanner from './components/banner.vue';
|
||||
import LoginForm from './components/login-form.vue';
|
||||
import AppFooter from '@/components/app/app-footer/index.vue';
|
||||
|
||||
// 登录提示
|
||||
const tips = window.sessionStorage.getItem(reLoginTipsKey);
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
.flat(1)
|
||||
.filter(s => s.meta)
|
||||
.map(s => s as RouteRecordNormalized)
|
||||
.filter(s => s.meta.hideInMenu !== true)
|
||||
.slice(0, 15);
|
||||
|
||||
// 打开路由
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="top-side">
|
||||
<!-- 提示 -->
|
||||
<div class="panel">
|
||||
<Banner />
|
||||
<banner />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-wrapper">
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div class="block-form-item-desc">
|
||||
{{ desc }}
|
||||
<template v-if="desc">
|
||||
{{ desc }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<slot name="desc" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
@@ -29,13 +34,14 @@
|
||||
|
||||
defineProps<{
|
||||
label: string,
|
||||
desc: string,
|
||||
desc?: string,
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.block-form-item-wrapper {
|
||||
width: 458px;
|
||||
height: 100%;
|
||||
min-height: 64px;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
class="mb16">
|
||||
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
|
||||
</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body block-body setting-body">
|
||||
<a-form class="terminal-setting-form"
|
||||
@@ -52,6 +58,7 @@
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||
import { ActionBarItems } from '../../../types/terminal.const';
|
||||
import { isSecureEnvironment } from '@/utils/env';
|
||||
import IconActions from '../../layout/icon-actions.vue';
|
||||
|
||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
class="mb16">
|
||||
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
|
||||
</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body block-body setting-body">
|
||||
<!-- 功能项 -->
|
||||
@@ -84,6 +90,7 @@
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||
import { ActionBarItems } from '../../../types/terminal.const';
|
||||
import { isSecureEnvironment } from '@/utils/env';
|
||||
|
||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
class="mb16">
|
||||
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
|
||||
</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="terminal-setting-body setting-body">
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
@@ -40,23 +46,29 @@
|
||||
<a-switch type="round"
|
||||
v-model="formModel.copyAutoTrim" />
|
||||
</block-setting-item>
|
||||
<!-- 粘贴去除空格 -->
|
||||
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.pasteAutoTrim" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键粘贴 -->
|
||||
<block-setting-item label="右键粘贴" desc="右键自动粘贴, 启用后需要关闭右键菜单 (若开启了右键选中词条, 有选中的文本时, 右键粘贴无效)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickPaste" />
|
||||
</block-setting-item>
|
||||
<!-- 启用右键菜单 -->
|
||||
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableRightClickMenu" />
|
||||
</block-setting-item>
|
||||
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键粘贴 -->
|
||||
<block-setting-item label="右键粘贴"
|
||||
desc="启用右键自动粘贴需要关闭右键菜单. 如果启用右键选中词条且选中有文本时, 右键粘贴无效. 因浏览器安全策略限制, 此功能需要在 HTTPS 环境下使用">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickPaste" />
|
||||
</block-setting-item>
|
||||
<!-- 粘贴去除空格 -->
|
||||
<block-setting-item label="粘贴去除空格"
|
||||
desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键. 默认粘贴快捷键无法去除空格">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.pasteAutoTrim" />
|
||||
<template #desc>
|
||||
|
||||
</template>
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 启用响铃 -->
|
||||
@@ -87,6 +99,7 @@
|
||||
import { ref, watch } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||
import { isSecureEnvironment } from '@/utils/env';
|
||||
import BlockSettingItem from '../block-setting-item.vue';
|
||||
|
||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||
|
||||
@@ -10,6 +10,12 @@
|
||||
<div class="terminal-setting-body setting-body">
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">点击保存按钮后需要刷新页面生效 (恢复默认配置后也需要点击保存按钮) (设置时需要避免与浏览器内置快捷键冲突)</a-alert>
|
||||
<!-- 非安全环境提示 -->
|
||||
<a-alert v-if="!isSecureEnvironment"
|
||||
type="warning"
|
||||
class="mb16">
|
||||
当前环境非 HTTPS 环境, 因浏览器安全策略限制, 自定义 '粘贴' 功能无法使用
|
||||
</a-alert>
|
||||
<a-space class="action-container" size="mini">
|
||||
<!-- 是否启用 -->
|
||||
<a-switch v-model="value"
|
||||
@@ -39,6 +45,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { isSecureEnvironment } from '@/utils/env';
|
||||
|
||||
const props = defineProps<{
|
||||
enabled: boolean
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<!-- 启用-未修改 -->
|
||||
<span v-else-if="item.enabled">{{ item.shortcutKey }}</span>
|
||||
<!-- 禁用 -->
|
||||
<span v-else />
|
||||
<span v-else class="disabled-key">已禁用</span>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<a-space class="shortcut-actions-container">
|
||||
@@ -52,6 +52,13 @@
|
||||
<icon-settings />
|
||||
</div>
|
||||
</a-space>
|
||||
<!-- 描述 -->
|
||||
<div class="shortcut-desc">
|
||||
<!-- 粘贴描述 -->
|
||||
<template v-if="item.item === TerminalShortcutKeys.PASTE">
|
||||
Ctrl + Shift + V, Shift + Insert 为浏览器内置快捷键, 不受环境影响
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -68,6 +75,7 @@
|
||||
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
|
||||
import type { VNodeRef } from 'vue';
|
||||
import { setAutoFocus } from '@/utils/dom';
|
||||
import { TerminalShortcutKeys } from '../../../types/terminal.const';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
@@ -116,6 +124,10 @@
|
||||
.shortcut-actions-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.shortcut-desc {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-name {
|
||||
@@ -132,6 +144,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-desc {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.shortcut-actions-container {
|
||||
display: none;
|
||||
|
||||
@@ -144,6 +161,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.disabled-key {
|
||||
color: var(--color-neutral-6);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -170,11 +170,11 @@
|
||||
|
||||
// 连接成功回调
|
||||
const connectCallback = () => {
|
||||
loadFiles(undefined);
|
||||
loadFiles('~');
|
||||
};
|
||||
|
||||
// 加载文件列表
|
||||
const loadFiles = (path: string | undefined) => {
|
||||
const loadFiles = (path: string) => {
|
||||
setTableLoading(true);
|
||||
session.value?.list(path);
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export default class SftpSession implements ISftpSession {
|
||||
}
|
||||
|
||||
// 查询文件列表
|
||||
list(path: string | undefined) {
|
||||
list(path: string) {
|
||||
this.channel.send(InputProtocol.SFTP_LIST, {
|
||||
sessionId: this.sessionId,
|
||||
showHiddenFile: ~~this.showHiddenFile,
|
||||
|
||||
@@ -16,7 +16,12 @@ const preventKeys: Array<ShortcutKey> = [
|
||||
altKey: false,
|
||||
shiftKey: true,
|
||||
code: 'KeyC'
|
||||
}, {
|
||||
},
|
||||
];
|
||||
|
||||
// 内置快捷键
|
||||
const builtinKeys: Array<ShortcutKey> = [
|
||||
{
|
||||
ctrlKey: true,
|
||||
altKey: false,
|
||||
shiftKey: true,
|
||||
@@ -49,16 +54,13 @@ export default class SshSessionHandler implements ISshSessionHandler {
|
||||
this.session = session;
|
||||
this.inst = session.inst;
|
||||
this.domRef = domRef;
|
||||
const { preference, tabManager } = useTerminalStore();
|
||||
const { preference } = useTerminalStore();
|
||||
this.interactSetting = preference.interactSetting;
|
||||
this.shortcutKeys = preference.shortcutSetting.keys;
|
||||
}
|
||||
|
||||
// 检测是否忽略默认行为
|
||||
checkPreventDefault(e: KeyboardEvent): boolean {
|
||||
if (e.type !== 'keydown') {
|
||||
return false;
|
||||
}
|
||||
return !!preventKeys.find(key => {
|
||||
return key.code === e.code
|
||||
&& key.altKey === e.altKey
|
||||
@@ -67,6 +69,16 @@ export default class SshSessionHandler implements ISshSessionHandler {
|
||||
});
|
||||
}
|
||||
|
||||
// 检测是否为内置快捷键
|
||||
checkIsBuiltin(e: KeyboardEvent): boolean {
|
||||
return !!builtinKeys.find(key => {
|
||||
return key.code === e.code
|
||||
&& key.altKey === e.altKey
|
||||
&& key.shiftKey === e.shiftKey
|
||||
&& key.ctrlKey === e.ctrlKey;
|
||||
});
|
||||
}
|
||||
|
||||
// 启用状态
|
||||
enabledStatus(option: string): boolean {
|
||||
switch (option) {
|
||||
|
||||
@@ -85,6 +85,10 @@ export default class SshSession implements ISshSession {
|
||||
if (e.type !== 'keydown') {
|
||||
return true;
|
||||
}
|
||||
// 检测是否为内置快捷键
|
||||
if (this.handler.checkIsBuiltin(e)) {
|
||||
return true;
|
||||
}
|
||||
// 检测是否阻止默认行为
|
||||
if (this.handler.checkPreventDefault(e)) {
|
||||
e.preventDefault();
|
||||
@@ -244,7 +248,9 @@ export default class SshSession implements ISshSession {
|
||||
.filter(Boolean)
|
||||
.forEach(s => s.dispose());
|
||||
// 卸载终端
|
||||
this.inst.dispose();
|
||||
setTimeout(() => {
|
||||
this.inst.dispose();
|
||||
}, 300);
|
||||
} catch (e) {
|
||||
// 卸载可能会报错
|
||||
}
|
||||
|
||||
@@ -212,6 +212,8 @@ export const TerminalShortcutKeys = {
|
||||
CHANGE_TO_PREV_SESSION: 'changeToPrevSession',
|
||||
// 切换至后一个会话
|
||||
CHANGE_TO_NEXT_SESSION: 'changeToNextSession',
|
||||
// 粘贴
|
||||
PASTE: 'paste',
|
||||
};
|
||||
|
||||
// 终端操作快捷键
|
||||
@@ -257,7 +259,7 @@ export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
|
||||
content: '复制',
|
||||
type: TerminalShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'paste',
|
||||
item: TerminalShortcutKeys.PASTE,
|
||||
content: '粘贴',
|
||||
type: TerminalShortcutType.TERMINAL
|
||||
}, {
|
||||
|
||||
@@ -267,6 +267,8 @@ export interface ISshSession extends ITerminalSession {
|
||||
export interface ISshSessionHandler {
|
||||
// 检测是否忽略默认行为
|
||||
checkPreventDefault: (e: KeyboardEvent) => boolean;
|
||||
// 检测是否为内置快捷键
|
||||
checkIsBuiltin: (e: KeyboardEvent) => boolean;
|
||||
// 启用状态
|
||||
enabledStatus: (option: string) => boolean;
|
||||
// 调用处理方法
|
||||
@@ -322,7 +324,7 @@ export interface ISftpSession extends ITerminalSession {
|
||||
// 设置显示隐藏文件
|
||||
setShowHiddenFile: (show: boolean) => void;
|
||||
// 查询文件列表
|
||||
list: (path: string | undefined) => void;
|
||||
list: (path: string) => void;
|
||||
// 创建文件夹
|
||||
mkdir: (path: string) => void;
|
||||
// 创建文件
|
||||
|
||||
Reference in New Issue
Block a user