Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5aec94207 | ||
|
|
2ff090a974 | ||
|
|
cc823d5bcd | ||
|
|
fade56b12a | ||
|
|
cf188451dd | ||
|
|
1125ef8a5a | ||
|
|
a3d4420754 | ||
|
|
27c1e16e57 |
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
当前版本: **1.0.2**
|
当前版本: **1.0.3**
|
||||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||||
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
version: '3.3'
|
version: '3.3'
|
||||||
services:
|
services:
|
||||||
orion-ops-pro:
|
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:
|
ports:
|
||||||
- 1081:80
|
- 1081:80
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
|
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
|
||||||
mv ../../orion-ops-ui/dist ./dist
|
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/>
|
<br/>
|
||||||
|
|
||||||
当前版本: **1.0.2**
|
当前版本: **1.0.3**
|
||||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||||
文档: https://lijiahangmax.gitee.io/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 规范。
|
> 版本号严格遵循 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`
|
`2024-03-22` `release`
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,7 @@
|
|||||||
* 资产授权 UI 改版
|
* 资产授权 UI 改版
|
||||||
* RDP 远程桌面
|
* RDP 远程桌面
|
||||||
* 接入 config 后端动态配置
|
* 接入 config 后端动态配置
|
||||||
|
|
||||||
|
## 已知问题 🐞
|
||||||
|
|
||||||
|
* 顶部菜单折叠宽度计算有问题 (arco 框架内问题)
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
|
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
|
||||||
|
|
||||||
|
## v1.0.3
|
||||||
|
|
||||||
|
> sql 脚本
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM preference WHERE type = 'TERMINAL';
|
||||||
|
```
|
||||||
|
|
||||||
## v1.0.2
|
## v1.0.2
|
||||||
|
|
||||||
> sql 脚本
|
> sql 脚本
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
|
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>1.0.2</revision>
|
<revision>1.0.3</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>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public interface OrionOpsProConst {
|
|||||||
/**
|
/**
|
||||||
* 同 ${orion.version} 迭代时候需要手动更改
|
* 同 ${orion.version} 迭代时候需要手动更改
|
||||||
*/
|
*/
|
||||||
String VERSION = "1.0.2";
|
String VERSION = "1.0.3";
|
||||||
|
|
||||||
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";
|
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import java.util.List;
|
|||||||
@Component
|
@Component
|
||||||
public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
|
public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
|
||||||
|
|
||||||
|
private static final String HOME_PATH = "~";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handle(WebSocketSession channel, SftpListRequest payload) {
|
public void handle(WebSocketSession channel, SftpListRequest payload) {
|
||||||
// 获取会话
|
// 获取会话
|
||||||
@@ -37,7 +39,7 @@ public class SftpListHandler extends AbstractTerminalHandler<SftpListRequest> {
|
|||||||
List<SftpFileVO> list = Lists.empty();
|
List<SftpFileVO> list = Lists.empty();
|
||||||
try {
|
try {
|
||||||
// 空目录则直接获取 home 目录
|
// 空目录则直接获取 home 目录
|
||||||
if (Strings.isBlank(path)) {
|
if (HOME_PATH.equals(path)) {
|
||||||
path = session.getHome();
|
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("changeToNextSession", true, false, true, "BracketRight", true),
|
||||||
// 终端快捷键
|
// 终端快捷键
|
||||||
new TerminalPreferenceModel.ShortcutKeysModel("copy", true, true, false, "KeyC", 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("toTop", true, true, false, "ArrowUp", true),
|
||||||
new TerminalPreferenceModel.ShortcutKeysModel("toBottom", true, true, false, "ArrowDown", true),
|
new TerminalPreferenceModel.ShortcutKeysModel("toBottom", true, true, false, "ArrowDown", true),
|
||||||
new TerminalPreferenceModel.ShortcutKeysModel("selectAll", true, true, false, "KeyA", true),
|
new TerminalPreferenceModel.ShortcutKeysModel("selectAll", true, true, false, "KeyA", true),
|
||||||
@@ -94,7 +94,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
|
|||||||
.selectAll(false)
|
.selectAll(false)
|
||||||
.search(true)
|
.search(true)
|
||||||
.copy(true)
|
.copy(true)
|
||||||
.paste(true)
|
.paste(false)
|
||||||
.interrupt(false)
|
.interrupt(false)
|
||||||
.enter(false)
|
.enter(false)
|
||||||
.fontSizePlus(false)
|
.fontSizePlus(false)
|
||||||
@@ -112,7 +112,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
|
|||||||
.theme(new JSONObject())
|
.theme(new JSONObject())
|
||||||
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
|
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
|
||||||
.actionBarSetting(JSONObject.parseObject(actionBarSetting))
|
.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))
|
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
|
||||||
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
|
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
|
||||||
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
|
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "orion-ops-pro-ui",
|
"name": "orion-ops-pro-ui",
|
||||||
"description": "Orion Ops Pro for Vue",
|
"description": "Orion Ops Pro for Vue",
|
||||||
"version": "1.0.2",
|
"version": "1.0.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"author": "Jiahang Li",
|
"author": "Jiahang Li",
|
||||||
"license": "Apache 2.0",
|
"license": "Apache 2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --config ./config/vite.config.dev.ts",
|
"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",
|
"build": "vue-tsc --noEmit && vite build --config ./config/vite.config.prod.ts",
|
||||||
"report": "cross-env REPORT=true npm run build",
|
"report": "cross-env REPORT=true npm run build",
|
||||||
"preview": "npm run build && vite preview --host",
|
"preview": "npm run build && vite preview --host",
|
||||||
@@ -29,7 +30,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@arco-design/web-vue": "^2.53.3",
|
"@arco-design/web-vue": "^2.55.0",
|
||||||
"@dangojs/a-query-header": "^0.0.31",
|
"@dangojs/a-query-header": "^0.0.31",
|
||||||
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
|
"@sanqi377/arco-vue-icon-picker": "^1.0.7",
|
||||||
"@vueuse/core": "^9.3.0",
|
"@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>
|
||||||
<!-- 顶部菜单 -->
|
<!-- 顶部菜单 -->
|
||||||
<div class="center-side">
|
<div class="center-side">
|
||||||
<menu-tree v-if="topMenu" />
|
<system-menu-tree v-if="topMenu" />
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧操作 -->
|
<!-- 右侧操作 -->
|
||||||
<ul class="right-side">
|
<ul class="right-side">
|
||||||
@@ -214,7 +214,7 @@
|
|||||||
import { preferenceTipsKey } from './const';
|
import { preferenceTipsKey } from './const';
|
||||||
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
|
import { REDIRECT_ROUTE_NAME, routerToTag } from '@/router/constants';
|
||||||
import { openNewRoute } from '@/router';
|
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 MessageBox from '@/components/system/message-box/index.vue';
|
||||||
import UpdatePasswordModal from '@/components/user/user/update-password-modal/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-box">
|
||||||
<div class="tab-bar-scroll">
|
<div class="tab-bar-scroll">
|
||||||
<div class="tags-wrap">
|
<div class="tags-wrap">
|
||||||
<TabItem v-for="(tag, index) in tagList"
|
<tab-item v-for="(tag, index) in tagList"
|
||||||
:key="tag.fullPath"
|
:key="tag.fullPath"
|
||||||
:index="index"
|
:index="index"
|
||||||
:item-data="tag" />
|
:item-data="tag" />
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ export default class LogAppender implements ILogAppender {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.forEach(s => s.dispose());
|
.forEach(s => s.dispose());
|
||||||
// 卸载终端
|
// 卸载终端
|
||||||
s.terminal?.dispose();
|
setTimeout(() => {
|
||||||
|
s.terminal?.dispose();
|
||||||
|
}, 300);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 卸载可能会报错
|
// 卸载可能会报错
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<a-checkbox-group v-model="checkedKeys" style="display: contents;">
|
<a-checkbox-group v-model="checkedKeys" style="display: contents;">
|
||||||
<template v-for="parentMenu in menuData" :key="parentMenu.id">
|
<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">
|
<td v-if="i === 0" :rowspan="parentMenu.children.length">
|
||||||
<a-checkbox :value="parentMenu.id">
|
<a-checkbox :value="parentMenu.id">
|
||||||
@@ -38,6 +40,23 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 无子菜单 -->
|
||||||
|
<template v-else>
|
||||||
|
<tr>
|
||||||
|
<!-- 父菜单 -->
|
||||||
|
<td>
|
||||||
|
<a-checkbox :value="parentMenu.id">
|
||||||
|
{{ parentMenu.name }}
|
||||||
|
</a-checkbox>
|
||||||
|
</td>
|
||||||
|
<!-- 子菜单 -->
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
<!-- 功能 -->
|
||||||
|
<td>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-checkbox-group>
|
</a-checkbox-group>
|
||||||
</tbody>
|
</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 type { RouteMeta, RouteRecordRaw } from 'vue-router';
|
||||||
import { useRoute, useRouter } 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 { useAppStore } from '@/store';
|
||||||
import { listenerRouteChange } from '@/utils/route-listener';
|
import { listenerRouteChange } from '@/utils/route-listener';
|
||||||
import { openWindow, regexUrl } from '@/utils';
|
import { openWindow, regexUrl } from '@/utils';
|
||||||
import { openNewRoute } from '@/router';
|
import { openNewRoute } from '@/router';
|
||||||
import useMenuTree from './use-menu-tree';
|
import useMenuTree from './use-menu-tree';
|
||||||
|
|
||||||
export default defineComponent({
|
const emits = defineEmits(['collapse']);
|
||||||
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 appStore = useAppStore();
|
||||||
const openKeys = ref<string[]>([]);
|
const router = useRouter();
|
||||||
const selectedKey = ref<string[]>([]);
|
const route = useRoute();
|
||||||
|
const { menuTree } = useMenuTree();
|
||||||
|
|
||||||
// 跳转路由
|
const collapsed = computed({
|
||||||
const goto = (e: any, item: RouteRecordRaw) => {
|
get() {
|
||||||
// 打开外链
|
if (appStore.device === 'desktop') return appStore.menuCollapse;
|
||||||
if (regexUrl.test(item.path)) {
|
return false;
|
||||||
openWindow(item.path);
|
},
|
||||||
return;
|
set(value: boolean) {
|
||||||
}
|
appStore.updateSettings({ menuCollapse: value });
|
||||||
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 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>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<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(() => {
|
const appRoute = computed(() => {
|
||||||
return menuStore.appMenus;
|
return menuStore.appMenus;
|
||||||
});
|
});
|
||||||
const menuTree = computed(() => {
|
const menuTree = computed<RouteRecordNormalized[]>(() => {
|
||||||
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
const copyRouter = cloneDeep(appRoute.value) as RouteRecordNormalized[];
|
||||||
|
|
||||||
|
// 排序
|
||||||
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
copyRouter.sort((a: RouteRecordNormalized, b: RouteRecordNormalized) => {
|
||||||
return (a.meta.order || 0) - (b.meta.order || 0);
|
return (a.meta.order || 0) - (b.meta.order || 0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,11 +10,11 @@
|
|||||||
<div class="pagination-wrapper">
|
<div class="pagination-wrapper">
|
||||||
<a-pagination v-if="pagination"
|
<a-pagination v-if="pagination"
|
||||||
size="mini"
|
size="mini"
|
||||||
v-model:current="(pagination as PaginationProps).current"
|
v-model:current="(pagination as any).current"
|
||||||
v-model:page-size="(pagination as PaginationProps).pageSize"
|
v-model:page-size="(pagination as any).pageSize"
|
||||||
v-bind="pagination as any"
|
v-bind="pagination as any"
|
||||||
:auto-adjust="false"
|
: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)" />
|
@page-size-change="limit => bubblesEmitter(HeaderEmitter.PAGE_CHANGE, 1, limit)" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +104,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { PaginationProps } from '@arco-design/web-vue';
|
|
||||||
import type { CardProps } from '../types/props';
|
import type { CardProps } from '../types/props';
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
|
|||||||
@@ -49,7 +49,10 @@
|
|||||||
'field-value',
|
'field-value',
|
||||||
field.ellipsis ? 'field-value-ellipsis' : ''
|
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]">
|
<a-tooltip v-if="field.tooltip" :content="item[field.dataIndex]">
|
||||||
<span v-if="field.render" v-html="field.render({ record: item, index })" />
|
<span v-if="field.render" v-html="field.render({ record: item, index })" />
|
||||||
<span v-else>{{ item[field.dataIndex] }}</span>
|
<span v-else>{{ item[field.dataIndex] }}</span>
|
||||||
@@ -87,7 +90,7 @@
|
|||||||
|
|
||||||
const props = defineProps<CardProps & {
|
const props = defineProps<CardProps & {
|
||||||
index: number,
|
index: number,
|
||||||
item: CardRecord
|
item: CardRecord,
|
||||||
}>();
|
}>();
|
||||||
const emits = defineEmits(['emitter']);
|
const emits = defineEmits(['emitter']);
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
<!-- 数据卡片 -->
|
<!-- 数据卡片 -->
|
||||||
<a-col v-for="(item, index) in list"
|
<a-col v-for="(item, index) in list"
|
||||||
:key="item[key]"
|
:key="item[rowKey]"
|
||||||
v-bind="cardLayoutCols"
|
v-bind="cardLayoutCols"
|
||||||
:class="{ 'disabled-col': item.disabled === true }">
|
:class="{ 'disabled-col': item.disabled === true }">
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
@@ -41,12 +41,18 @@
|
|||||||
@emitter="dispatchEmitter">
|
@emitter="dispatchEmitter">
|
||||||
<!-- 自定义插槽 -->
|
<!-- 自定义插槽 -->
|
||||||
<template v-for="slot in Object.keys($slots)" :key="slot" #[slot]>
|
<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>
|
</template>
|
||||||
</card-item>
|
</card-item>
|
||||||
<!-- 右键菜单 -->
|
<!-- 右键菜单 -->
|
||||||
<template v-if="contextMenu" #content>
|
<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>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -82,7 +88,7 @@
|
|||||||
import useEmitter from '@/hooks/emitter';
|
import useEmitter from '@/hooks/emitter';
|
||||||
|
|
||||||
const props = withDefaults(defineProps<CardProps>(), {
|
const props = withDefaults(defineProps<CardProps>(), {
|
||||||
key: 'id',
|
rowKey: 'id',
|
||||||
pagination: false,
|
pagination: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
cardHeight: '100%',
|
cardHeight: '100%',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { CardFieldConfig, CardPosition, CardRecord, ColResponsiveValue, Han
|
|||||||
|
|
||||||
// 卡片属性
|
// 卡片属性
|
||||||
export interface CardProps {
|
export interface CardProps {
|
||||||
key?: string;
|
rowKey?: string;
|
||||||
pagination?: PaginationProps | boolean;
|
pagination?: PaginationProps | boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
fieldConfig?: CardFieldConfig;
|
fieldConfig?: CardFieldConfig;
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { useClipboard } from '@vueuse/core';
|
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
const { copy: c } = useClipboard();
|
|
||||||
|
|
||||||
// 复制
|
// 复制
|
||||||
export const copy = async (value: string | undefined, tips: string | boolean = `${value} 已复制`) => {
|
export const copy = async (value: string | undefined, tips: string | boolean = `${value} 已复制`) => {
|
||||||
try {
|
try {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await c(value);
|
await copyToClipboard(value);
|
||||||
if (tips) {
|
if (tips) {
|
||||||
Message.success(tips as string);
|
Message.success(tips as string);
|
||||||
}
|
}
|
||||||
@@ -20,7 +17,52 @@ export const copy = async (value: string | undefined, tips: string | boolean = `
|
|||||||
|
|
||||||
// 获取剪切板内容
|
// 获取剪切板内容
|
||||||
export const readText = () => {
|
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() {
|
export default function useCopy() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
import type { RouteLocationNormalized, RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
|
||||||
import { useMenuStore, useUserStore } from '@/store';
|
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';
|
import { AdminRoleCode } from '@/types/const';
|
||||||
|
|
||||||
export default function usePermission() {
|
export default function usePermission() {
|
||||||
@@ -16,7 +16,7 @@ export default function usePermission() {
|
|||||||
return false;
|
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;
|
let exist = false;
|
||||||
while (menuConfig.length && !exist) {
|
while (menuConfig.length && !exist) {
|
||||||
const element = menuConfig.shift();
|
const element = menuConfig.shift();
|
||||||
@@ -44,7 +44,7 @@ export default function usePermission() {
|
|||||||
hasAnyPermission(permission: string[]) {
|
hasAnyPermission(permission: string[]) {
|
||||||
return userStore.permission?.includes('*') ||
|
return userStore.permission?.includes('*') ||
|
||||||
permission.map(s => userStore.permission?.includes(s))
|
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[]) {
|
hasAnyRole(role: string[]) {
|
||||||
return userStore.roles?.includes(AdminRoleCode) ||
|
return userStore.roles?.includes(AdminRoleCode) ||
|
||||||
role.map(s => userStore.roles?.includes(s))
|
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 }">
|
<a-layout class="layout" :class="{ mobile: appStore.hideMenu }">
|
||||||
<!-- tab bar -->
|
<!-- tab bar -->
|
||||||
<div v-if="navbar" class="layout-navbar">
|
<div v-if="navbar" class="layout-navbar">
|
||||||
<NavBar />
|
<nav-bar />
|
||||||
</div>
|
</div>
|
||||||
<a-layout>
|
<a-layout style="flex-direction: row;">
|
||||||
<!-- 左侧菜单栏 -->
|
<!-- 左侧菜单栏 -->
|
||||||
<a-layout-sider v-if="renderMenu"
|
<a-layout-sider v-if="renderMenu"
|
||||||
v-show="!hideMenu"
|
v-show="!hideMenu"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
:hide-trigger="true"
|
:hide-trigger="true"
|
||||||
@collapse="setCollapsed">
|
@collapse="setCollapsed">
|
||||||
<div class="menu-wrapper">
|
<div class="menu-wrapper">
|
||||||
<menu-tree />
|
<system-menu-tree />
|
||||||
</div>
|
</div>
|
||||||
</a-layout-sider>
|
</a-layout-sider>
|
||||||
<!-- 顶部菜单栏 -->
|
<!-- 顶部菜单栏 -->
|
||||||
@@ -29,12 +29,12 @@
|
|||||||
mask-closable
|
mask-closable
|
||||||
:closable="false"
|
:closable="false"
|
||||||
@cancel="drawerCancel">
|
@cancel="drawerCancel">
|
||||||
<menu-tree style="padding: 12px 16px;" />
|
<system-menu-tree style="padding: 12px 16px;" />
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
<!-- body -->
|
<!-- body -->
|
||||||
<a-layout class="layout-content" :style="paddingStyle">
|
<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%;">
|
<a-layout-content style="height: 100%; width: 100%;">
|
||||||
<!-- 水印 -->
|
<!-- 水印 -->
|
||||||
@@ -43,11 +43,11 @@
|
|||||||
:z-index="9999"
|
:z-index="9999"
|
||||||
style="width: 100%; height: 100%;"
|
style="width: 100%; height: 100%;"
|
||||||
:content="userStore.username || ''">
|
:content="userStore.username || ''">
|
||||||
<PageLayout />
|
<page-layout />
|
||||||
</a-watermark>
|
</a-watermark>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<Footer v-if="visibleFooter" />
|
<app-footer v-if="visibleFooter" />
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
</a-layout>
|
</a-layout>
|
||||||
@@ -60,17 +60,18 @@
|
|||||||
import useResponsive from '@/hooks/responsive';
|
import useResponsive from '@/hooks/responsive';
|
||||||
import { toggleDrawerMenuKey } from '@/types/symbol';
|
import { toggleDrawerMenuKey } from '@/types/symbol';
|
||||||
import PageLayout from './page-layout.vue';
|
import PageLayout from './page-layout.vue';
|
||||||
import MenuTree from '@/components/system/menu/tree/index.vue';
|
|
||||||
import NavBar from '@/components/app/navbar/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 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 appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
useResponsive(true);
|
useResponsive(true);
|
||||||
|
|
||||||
|
const isInit = ref(false);
|
||||||
const navbarHeight = `60px`;
|
const navbarHeight = `60px`;
|
||||||
const navbar = computed(() => appStore.navbar);
|
const navbar = computed(() => appStore.navbar);
|
||||||
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
|
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_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 { PaginationProps, TableRowSelection } from '@arco-design/web-vue';
|
||||||
|
import type { TableExpandable } from '@arco-design/web-vue/es/table/interface';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { isNumber } from '@/utils/is';
|
import { isNumber } from '@/utils/is';
|
||||||
import { TablePageSizeOptions } from '@/types/const';
|
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';
|
const debug = import.meta.env.MODE !== 'production';
|
||||||
|
|
||||||
|
// 当前环境是否为安全环境
|
||||||
|
export const isSecureEnvironment = (() => {
|
||||||
|
return window.location.protocol === 'https:' || window.location.hostname === 'localhost';
|
||||||
|
})();
|
||||||
|
|
||||||
// http base url
|
// http base url
|
||||||
export const httpBaseUrl = (() => {
|
export const httpBaseUrl = (() => {
|
||||||
const configBase = import.meta.env.VITE_API_BASE_URL;
|
const configBase = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|||||||
@@ -8,16 +8,16 @@
|
|||||||
<div class="logo-text">Orion Ops Pro</div>
|
<div class="logo-text">Orion Ops Pro</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 左侧 banner -->
|
<!-- 左侧 banner -->
|
||||||
<LoginBanner />
|
<login-banner />
|
||||||
<!-- 主体部分 -->
|
<!-- 主体部分 -->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="content-inner">
|
<div class="content-inner">
|
||||||
<!-- 登录表单 -->
|
<!-- 登录表单 -->
|
||||||
<LoginForm />
|
<login-form />
|
||||||
</div>
|
</div>
|
||||||
<!-- 页脚 -->
|
<!-- 页脚 -->
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<Footer />
|
<app-footer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,9 +26,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Notification } from '@arco-design/web-vue';
|
import { Notification } from '@arco-design/web-vue';
|
||||||
import { reLoginTipsKey } from '@/types/symbol';
|
import { reLoginTipsKey } from '@/types/symbol';
|
||||||
import Footer from '@/components/app/footer/index.vue';
|
|
||||||
import LoginBanner from './components/banner.vue';
|
import LoginBanner from './components/banner.vue';
|
||||||
import LoginForm from './components/login-form.vue';
|
import LoginForm from './components/login-form.vue';
|
||||||
|
import AppFooter from '@/components/app/app-footer/index.vue';
|
||||||
|
|
||||||
// 登录提示
|
// 登录提示
|
||||||
const tips = window.sessionStorage.getItem(reLoginTipsKey);
|
const tips = window.sessionStorage.getItem(reLoginTipsKey);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
.flat(1)
|
.flat(1)
|
||||||
.filter(s => s.meta)
|
.filter(s => s.meta)
|
||||||
.map(s => s as RouteRecordNormalized)
|
.map(s => s as RouteRecordNormalized)
|
||||||
|
.filter(s => s.meta.hideInMenu !== true)
|
||||||
.slice(0, 15);
|
.slice(0, 15);
|
||||||
|
|
||||||
// 打开路由
|
// 打开路由
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="top-side">
|
<div class="top-side">
|
||||||
<!-- 提示 -->
|
<!-- 提示 -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<Banner />
|
<banner />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-wrapper">
|
<div class="row-wrapper">
|
||||||
|
|||||||
@@ -13,7 +13,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 描述 -->
|
<!-- 描述 -->
|
||||||
<div class="block-form-item-desc">
|
<div class="block-form-item-desc">
|
||||||
{{ desc }}
|
<template v-if="desc">
|
||||||
|
{{ desc }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<slot name="desc" />
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -29,13 +34,14 @@
|
|||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
label: string,
|
label: string,
|
||||||
desc: string,
|
desc?: string,
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.block-form-item-wrapper {
|
.block-form-item-wrapper {
|
||||||
|
width: 458px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 提示 -->
|
<!-- 提示 -->
|
||||||
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
|
<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">
|
<div class="terminal-setting-body block-body setting-body">
|
||||||
<a-form class="terminal-setting-form"
|
<a-form class="terminal-setting-form"
|
||||||
@@ -52,6 +58,7 @@
|
|||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||||
import { ActionBarItems } from '../../../types/terminal.const';
|
import { ActionBarItems } from '../../../types/terminal.const';
|
||||||
|
import { isSecureEnvironment } from '@/utils/env';
|
||||||
import IconActions from '../../layout/icon-actions.vue';
|
import IconActions from '../../layout/icon-actions.vue';
|
||||||
|
|
||||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 提示 -->
|
<!-- 提示 -->
|
||||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
<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">
|
<div class="terminal-setting-body block-body setting-body">
|
||||||
<!-- 功能项 -->
|
<!-- 功能项 -->
|
||||||
@@ -84,6 +90,7 @@
|
|||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||||
import { ActionBarItems } from '../../../types/terminal.const';
|
import { ActionBarItems } from '../../../types/terminal.const';
|
||||||
|
import { isSecureEnvironment } from '@/utils/env';
|
||||||
|
|
||||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 提示 -->
|
<!-- 提示 -->
|
||||||
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
|
<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">
|
<div class="terminal-setting-body setting-body">
|
||||||
<a-row class="mb16" align="stretch" :gutter="16">
|
<a-row class="mb16" align="stretch" :gutter="16">
|
||||||
@@ -40,23 +46,29 @@
|
|||||||
<a-switch type="round"
|
<a-switch type="round"
|
||||||
v-model="formModel.copyAutoTrim" />
|
v-model="formModel.copyAutoTrim" />
|
||||||
</block-setting-item>
|
</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="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
|
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
|
||||||
<a-switch type="round"
|
<a-switch type="round"
|
||||||
v-model="formModel.enableRightClickMenu" />
|
v-model="formModel.enableRightClickMenu" />
|
||||||
</block-setting-item>
|
</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>
|
||||||
<a-row class="mb16" align="stretch" :gutter="16">
|
<a-row class="mb16" align="stretch" :gutter="16">
|
||||||
<!-- 启用响铃 -->
|
<!-- 启用响铃 -->
|
||||||
@@ -87,6 +99,7 @@
|
|||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
import { TerminalPreferenceItem } from '@/store/modules/terminal';
|
||||||
|
import { isSecureEnvironment } from '@/utils/env';
|
||||||
import BlockSettingItem from '../block-setting-item.vue';
|
import BlockSettingItem from '../block-setting-item.vue';
|
||||||
|
|
||||||
const { preference, updateTerminalPreference } = useTerminalStore();
|
const { preference, updateTerminalPreference } = useTerminalStore();
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
<div class="terminal-setting-body setting-body">
|
<div class="terminal-setting-body setting-body">
|
||||||
<!-- 提示 -->
|
<!-- 提示 -->
|
||||||
<a-alert class="mb16">点击保存按钮后需要刷新页面生效 (恢复默认配置后也需要点击保存按钮) (设置时需要避免与浏览器内置快捷键冲突)</a-alert>
|
<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-space class="action-container" size="mini">
|
||||||
<!-- 是否启用 -->
|
<!-- 是否启用 -->
|
||||||
<a-switch v-model="value"
|
<a-switch v-model="value"
|
||||||
@@ -39,6 +45,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { isSecureEnvironment } from '@/utils/env';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<!-- 启用-未修改 -->
|
<!-- 启用-未修改 -->
|
||||||
<span v-else-if="item.enabled">{{ item.shortcutKey }}</span>
|
<span v-else-if="item.enabled">{{ item.shortcutKey }}</span>
|
||||||
<!-- 禁用 -->
|
<!-- 禁用 -->
|
||||||
<span v-else />
|
<span v-else class="disabled-key">已禁用</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 操作 -->
|
<!-- 操作 -->
|
||||||
<a-space class="shortcut-actions-container">
|
<a-space class="shortcut-actions-container">
|
||||||
@@ -52,6 +52,13 @@
|
|||||||
<icon-settings />
|
<icon-settings />
|
||||||
</div>
|
</div>
|
||||||
</a-space>
|
</a-space>
|
||||||
|
<!-- 描述 -->
|
||||||
|
<div class="shortcut-desc">
|
||||||
|
<!-- 粘贴描述 -->
|
||||||
|
<template v-if="item.item === TerminalShortcutKeys.PASTE">
|
||||||
|
Ctrl + Shift + V, Shift + Insert 为浏览器内置快捷键, 不受环境影响
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -68,6 +75,7 @@
|
|||||||
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
|
import type { TerminalShortcutKeyEditable } from '@/store/modules/terminal/types';
|
||||||
import type { VNodeRef } from 'vue';
|
import type { VNodeRef } from 'vue';
|
||||||
import { setAutoFocus } from '@/utils/dom';
|
import { setAutoFocus } from '@/utils/dom';
|
||||||
|
import { TerminalShortcutKeys } from '../../../types/terminal.const';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
title: string;
|
title: string;
|
||||||
@@ -116,6 +124,10 @@
|
|||||||
.shortcut-actions-container {
|
.shortcut-actions-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shortcut-desc {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-name {
|
.shortcut-name {
|
||||||
@@ -132,6 +144,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.shortcut-desc {
|
||||||
|
display: flex;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.shortcut-actions-container {
|
.shortcut-actions-container {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
@@ -144,6 +161,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.disabled-key {
|
||||||
|
color: var(--color-neutral-6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -170,11 +170,11 @@
|
|||||||
|
|
||||||
// 连接成功回调
|
// 连接成功回调
|
||||||
const connectCallback = () => {
|
const connectCallback = () => {
|
||||||
loadFiles(undefined);
|
loadFiles('~');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载文件列表
|
// 加载文件列表
|
||||||
const loadFiles = (path: string | undefined) => {
|
const loadFiles = (path: string) => {
|
||||||
setTableLoading(true);
|
setTableLoading(true);
|
||||||
session.value?.list(path);
|
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, {
|
this.channel.send(InputProtocol.SFTP_LIST, {
|
||||||
sessionId: this.sessionId,
|
sessionId: this.sessionId,
|
||||||
showHiddenFile: ~~this.showHiddenFile,
|
showHiddenFile: ~~this.showHiddenFile,
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ const preventKeys: Array<ShortcutKey> = [
|
|||||||
altKey: false,
|
altKey: false,
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
code: 'KeyC'
|
code: 'KeyC'
|
||||||
}, {
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 内置快捷键
|
||||||
|
const builtinKeys: Array<ShortcutKey> = [
|
||||||
|
{
|
||||||
ctrlKey: true,
|
ctrlKey: true,
|
||||||
altKey: false,
|
altKey: false,
|
||||||
shiftKey: true,
|
shiftKey: true,
|
||||||
@@ -49,16 +54,13 @@ export default class SshSessionHandler implements ISshSessionHandler {
|
|||||||
this.session = session;
|
this.session = session;
|
||||||
this.inst = session.inst;
|
this.inst = session.inst;
|
||||||
this.domRef = domRef;
|
this.domRef = domRef;
|
||||||
const { preference, tabManager } = useTerminalStore();
|
const { preference } = useTerminalStore();
|
||||||
this.interactSetting = preference.interactSetting;
|
this.interactSetting = preference.interactSetting;
|
||||||
this.shortcutKeys = preference.shortcutSetting.keys;
|
this.shortcutKeys = preference.shortcutSetting.keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检测是否忽略默认行为
|
// 检测是否忽略默认行为
|
||||||
checkPreventDefault(e: KeyboardEvent): boolean {
|
checkPreventDefault(e: KeyboardEvent): boolean {
|
||||||
if (e.type !== 'keydown') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !!preventKeys.find(key => {
|
return !!preventKeys.find(key => {
|
||||||
return key.code === e.code
|
return key.code === e.code
|
||||||
&& key.altKey === e.altKey
|
&& 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 {
|
enabledStatus(option: string): boolean {
|
||||||
switch (option) {
|
switch (option) {
|
||||||
|
|||||||
@@ -85,6 +85,10 @@ export default class SshSession implements ISshSession {
|
|||||||
if (e.type !== 'keydown') {
|
if (e.type !== 'keydown') {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// 检测是否为内置快捷键
|
||||||
|
if (this.handler.checkIsBuiltin(e)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// 检测是否阻止默认行为
|
// 检测是否阻止默认行为
|
||||||
if (this.handler.checkPreventDefault(e)) {
|
if (this.handler.checkPreventDefault(e)) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -244,7 +248,9 @@ export default class SshSession implements ISshSession {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.forEach(s => s.dispose());
|
.forEach(s => s.dispose());
|
||||||
// 卸载终端
|
// 卸载终端
|
||||||
this.inst.dispose();
|
setTimeout(() => {
|
||||||
|
this.inst.dispose();
|
||||||
|
}, 300);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 卸载可能会报错
|
// 卸载可能会报错
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,6 +212,8 @@ export const TerminalShortcutKeys = {
|
|||||||
CHANGE_TO_PREV_SESSION: 'changeToPrevSession',
|
CHANGE_TO_PREV_SESSION: 'changeToPrevSession',
|
||||||
// 切换至后一个会话
|
// 切换至后一个会话
|
||||||
CHANGE_TO_NEXT_SESSION: 'changeToNextSession',
|
CHANGE_TO_NEXT_SESSION: 'changeToNextSession',
|
||||||
|
// 粘贴
|
||||||
|
PASTE: 'paste',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 终端操作快捷键
|
// 终端操作快捷键
|
||||||
@@ -257,7 +259,7 @@ export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
|
|||||||
content: '复制',
|
content: '复制',
|
||||||
type: TerminalShortcutType.TERMINAL
|
type: TerminalShortcutType.TERMINAL
|
||||||
}, {
|
}, {
|
||||||
item: 'paste',
|
item: TerminalShortcutKeys.PASTE,
|
||||||
content: '粘贴',
|
content: '粘贴',
|
||||||
type: TerminalShortcutType.TERMINAL
|
type: TerminalShortcutType.TERMINAL
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -267,6 +267,8 @@ export interface ISshSession extends ITerminalSession {
|
|||||||
export interface ISshSessionHandler {
|
export interface ISshSessionHandler {
|
||||||
// 检测是否忽略默认行为
|
// 检测是否忽略默认行为
|
||||||
checkPreventDefault: (e: KeyboardEvent) => boolean;
|
checkPreventDefault: (e: KeyboardEvent) => boolean;
|
||||||
|
// 检测是否为内置快捷键
|
||||||
|
checkIsBuiltin: (e: KeyboardEvent) => boolean;
|
||||||
// 启用状态
|
// 启用状态
|
||||||
enabledStatus: (option: string) => boolean;
|
enabledStatus: (option: string) => boolean;
|
||||||
// 调用处理方法
|
// 调用处理方法
|
||||||
@@ -322,7 +324,7 @@ export interface ISftpSession extends ITerminalSession {
|
|||||||
// 设置显示隐藏文件
|
// 设置显示隐藏文件
|
||||||
setShowHiddenFile: (show: boolean) => void;
|
setShowHiddenFile: (show: boolean) => void;
|
||||||
// 查询文件列表
|
// 查询文件列表
|
||||||
list: (path: string | undefined) => void;
|
list: (path: string) => void;
|
||||||
// 创建文件夹
|
// 创建文件夹
|
||||||
mkdir: (path: string) => void;
|
mkdir: (path: string) => void;
|
||||||
// 创建文件
|
// 创建文件
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -22,7 +22,7 @@
|
|||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>1.0.2</revision>
|
<revision>1.0.3</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user