路径标签.

This commit is contained in:
lijiahang
2024-04-24 13:39:21 +08:00
parent cdf10770d6
commit 8ecb5a687e
32 changed files with 1665 additions and 86 deletions

View File

@@ -11,7 +11,7 @@
<a-spin class="full form-container" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="left"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 名称 -->
@@ -21,7 +21,7 @@
allow-clear />
</a-form-item>
<!-- 分组 -->
<a-form-item field="groupId" label="命令分组">
<a-form-item field="groupId" label="分组">
<command-snippet-group-select v-model="formModel.groupId" />
</a-form-item>
<!-- 代码片段 -->

View File

@@ -6,7 +6,6 @@
<!-- 表头 -->
<template #title>
<span class="snippet-drawer-title usn">
<icon-code />
命令片段
</span>
</template>
@@ -32,21 +31,21 @@
<!-- 搜索框 -->
<a-input-search class="snippet-header-input"
v-model="filterValue"
placeholder="名称"
placeholder="名称/命令"
allow-clear
@search="filterSnippet"
@keyup.enter="filterSnippet" />
</div>
<!-- 加载中 -->
<a-skeleton v-if="loading"
style="padding: 0 12px"
class="loading-skeleton"
:animation="true">
<a-skeleton-line :rows="4"
:line-height="66"
:line-spacing="12" />
</a-skeleton>
<!-- 无数据 -->
<a-empty v-else-if="!snippet || (snippet.groups.length === 0 && snippet.ungroupedItems.length === 0)"
<a-empty v-else-if="!snippetValue || (snippetValue.groups.length === 0 && snippetValue.ungroupedItems.length === 0)"
style="padding: 28px 0">
<span>暂无数据</span><br>
<span>点击上方 '<icon-plus />' 添加一条数据吧~</span>
@@ -54,10 +53,10 @@
<!-- 命令片段 -->
<div v-else class="snippet-list-container">
<!-- 命令片段组 -->
<command-snippet-list-group :snippet="snippet" />
<command-snippet-list-group :value="snippetValue" />
<!-- 未分组命令片段 -->
<div class="ungrouped-snippet-container">
<template v-for="item in snippet.ungroupedItems">
<template v-for="item in snippetValue.ungroupedItems">
<command-snippet-list-item v-if="item.visible"
:key="item.id"
:item="item" />
@@ -97,7 +96,7 @@
const formDrawer = ref();
const filterValue = ref<string>();
const snippet = ref<CommandSnippetWrapperResponse>();
const snippetValue = ref<CommandSnippetWrapperResponse>();
// 打开
const open = async () => {
@@ -110,14 +109,14 @@
// 加载数据
const fetchData = async (force: boolean = false) => {
if (snippet.value && !force) {
if (snippetValue.value && !force) {
return;
}
setLoading(true);
try {
// 查询
const { data } = await getCommandSnippetList();
snippet.value = data;
snippetValue.value = data;
// 设置状态
filterSnippet();
} catch (e) {
@@ -128,14 +127,14 @@
// 过滤
const filterSnippet = () => {
snippet.value?.groups.forEach(g => {
snippetValue.value?.groups.forEach(g => {
g.items?.forEach(s => {
s.visible = !filterValue.value
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|| s.command.toLowerCase().includes(filterValue.value.toLowerCase());
});
});
snippet.value?.ungroupedItems.forEach(s => {
snippetValue.value?.ungroupedItems.forEach(s => {
s.visible = !filterValue.value
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|| s.command.toLowerCase().includes(filterValue.value.toLowerCase());
@@ -166,17 +165,17 @@
// 暴露 删除
provide(removeSnippetKey, async (id: number) => {
if (!snippet.value) {
if (!snippetValue.value) {
return;
}
// 删除
await deleteCommandSnippet(id);
// 查找并且删除未分组的数据
if (findAndSplice(id, snippet.value.ungroupedItems)) {
if (findAndSplice(id, snippetValue.value.ungroupedItems)) {
return;
}
// 查找并且删除分组内数据
for (let group of snippet.value.groups) {
for (let group of snippetValue.value.groups) {
if (findAndSplice(id, group.items)) {
return;
}
@@ -186,14 +185,14 @@
// 添加回调
const onAdded = async (item: CommandSnippetQueryResponse) => {
if (item.groupId) {
let group = snippet.value?.groups.find(g => g.id === item.groupId);
let group = snippetValue.value?.groups.find(g => g.id === item.groupId);
if (group) {
group?.items.push(item);
} else {
const cacheGroups = await cacheStore.loadCommandSnippetGroups();
const findGroup = cacheGroups.find(s => s.id === item.groupId);
if (findGroup) {
snippet.value?.groups.push({
snippetValue.value?.groups.push({
id: item.groupId,
name: findGroup.name,
items: [item]
@@ -201,7 +200,7 @@
}
}
} else {
snippet.value?.ungroupedItems.push(item);
snippetValue.value?.ungroupedItems.push(item);
}
// 重置过滤
filterSnippet();
@@ -209,16 +208,16 @@
// 修改回调
const onUpdated = async (item: CommandSnippetQueryResponse) => {
if (!snippet.value) {
if (!snippetValue.value) {
return;
}
// 查找原始数据
let originItem;
const findInUngrouped = snippet.value.ungroupedItems.find(s => s.id === item.id);
const findInUngrouped = snippetValue.value.ungroupedItems.find(s => s.id === item.id);
if (findInUngrouped) {
originItem = findInUngrouped;
} else {
for (let group of snippet.value.groups) {
for (let group of snippetValue.value.groups) {
const find = group.items.find(s => s.id === item.id);
if (find) {
originItem = find;
@@ -230,12 +229,12 @@
return;
}
// 检查分组是否存在
const findGroup = snippet.value.groups.find(s => s.id === item.groupId);
const findGroup = snippetValue.value.groups.find(s => s.id === item.groupId);
if (!findGroup) {
const cacheGroups = await cacheStore.loadCommandSnippetGroups();
const cacheGroup = cacheGroups.find(s => s.id === item.groupId);
if (cacheGroup) {
snippet.value.groups.push({
snippetValue.value.groups.push({
id: item.groupId,
name: cacheGroup.name,
items: []
@@ -251,22 +250,22 @@
if (item.groupId !== originGroupId) {
// 从原始分组移除
if (originGroupId) {
const findGroup = snippet.value.groups.find(s => s.id === originGroupId);
const findGroup = snippetValue.value.groups.find(s => s.id === originGroupId);
if (findGroup) {
findAndSplice(item.id, findGroup.items);
}
} else {
// 从未分组数据中移除
findAndSplice(item.id, snippet.value.ungroupedItems);
findAndSplice(item.id, snippetValue.value.ungroupedItems);
}
// 添加到新分组
if (item.groupId) {
const findGroup = snippet.value.groups.find(s => s.id === item.groupId);
const findGroup = snippetValue.value.groups.find(s => s.id === item.groupId);
if (findGroup) {
findGroup.items.push(item);
}
} else {
snippet.value.ungroupedItems.push(originItem);
snippetValue.value.ungroupedItems.push(originItem);
}
}
// 重置过滤
@@ -283,7 +282,7 @@
<style lang="less" scoped>
.snippet-drawer-title {
font-size: 14px;
font-size: 16px;
}
.snippet-container {
@@ -320,4 +319,12 @@
padding-bottom: 4px;
}
.loading-skeleton {
padding: 0 12px;
:deep(.arco-skeleton-line-row) {
border-radius: 4px;
}
}
</style>

View File

@@ -1,6 +1,6 @@
<template>
<a-collapse :bordered="false">
<template v-for="group in snippet.groups">
<template v-for="group in value.groups">
<a-collapse-item v-if="calcTotal(group) > 0"
:key="group.id"
:header="group.name">
@@ -21,7 +21,7 @@
<script lang="ts">
export default {
name: 'commandSnippetGroup'
name: 'commandSnippetListGroup'
};
</script>
@@ -31,7 +31,7 @@
import CommandSnippetListItem from './command-snippet-list-item.vue';
defineProps<{
snippet: CommandSnippetWrapperResponse;
value: CommandSnippetWrapperResponse;
}>();
// 计算总量

View File

@@ -108,7 +108,7 @@
<script lang="ts">
export default {
name: 'commandSnippetItem'
name: 'commandSnippetListItem'
};
</script>

View File

@@ -0,0 +1,145 @@
<template>
<a-drawer v-model:visible="visible"
:width="388"
:title="title"
:mask-closable="false"
:unmount-on-close="true"
:ok-button-props="{ disabled: loading }"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="handlerOk"
@cancel="handleClose">
<a-spin class="full form-container" :loading="loading">
<a-form :model="formModel"
ref="formRef"
label-align="right"
:auto-label-width="true"
:rules="formRules">
<!-- 名称 -->
<a-form-item field="name" label="名称">
<a-input v-model="formModel.name"
placeholder="请输入名称"
allow-clear />
</a-form-item>
<!-- 分组 -->
<a-form-item field="groupId" label="分组">
<path-bookmark-group-select v-model="formModel.groupId" />
</a-form-item>
<!-- 文件路径 -->
<a-form-item field="path" label="路径">
<a-textarea v-model="formModel.path"
placeholder="文件路径"
:auto-size="{ minRows: 8, maxRows: 8 }"
allow-clear />
</a-form-item>
</a-form>
</a-spin>
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'pathBookmarkFormDrawer'
};
</script>
<script lang="ts" setup>
import type { PathBookmarkUpdateRequest } from '@/api/asset/path-bookmark';
import { ref } from 'vue';
import useLoading from '@/hooks/loading';
import useVisible from '@/hooks/visible';
import { createPathBookmark, updatePathBookmark } from '@/api/asset/path-bookmark';
import formRules from '../types/form.rules';
import { Message } from '@arco-design/web-vue';
import PathBookmarkGroupSelect from './path-bookmark-group-select.vue';
const { visible, setVisible } = useVisible();
const { loading, setLoading } = useLoading();
const title = ref<string>();
const isAddHandle = ref<boolean>(true);
const defaultForm = (): PathBookmarkUpdateRequest => {
return {
id: undefined,
groupId: undefined,
name: undefined,
path: undefined,
};
};
const formRef = ref<any>();
const formModel = ref<PathBookmarkUpdateRequest>({});
const emits = defineEmits(['added', 'updated']);
// 打开新增
const openAdd = () => {
title.value = '添加路径标签';
isAddHandle.value = true;
renderForm({ ...defaultForm() });
setVisible(true);
};
// 打开修改
const openUpdate = (record: any) => {
title.value = '修改路径标签';
isAddHandle.value = false;
renderForm({ ...defaultForm(), ...record });
setVisible(true);
};
// 渲染表单
const renderForm = (record: any) => {
formModel.value = Object.assign({}, record);
};
defineExpose({ openAdd, openUpdate });
// 确定
const handlerOk = async () => {
setLoading(true);
try {
// 验证参数
const error = await formRef.value.validate();
if (error) {
return false;
}
if (isAddHandle.value) {
// 新增
const { data: id } = await createPathBookmark(formModel.value);
formModel.value.id = id;
Message.success('创建成功');
emits('added', formModel.value);
} else {
// 修改
await updatePathBookmark(formModel.value);
Message.success('修改成功');
emits('updated', formModel.value);
}
// 清空
handlerClear();
} catch (e) {
return false;
} finally {
setLoading(false);
}
};
// 关闭
const handleClose = () => {
handlerClear();
};
// 清空
const handlerClear = () => {
setLoading(false);
};
</script>
<style lang="less" scoped>
.form-container {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,106 @@
<template>
<a-select v-model:model-value="value"
placeholder="请选择路径分组"
:options="optionData"
:loading="loading"
:allow-create="true"
allow-clear />
</template>
<script lang="ts">
export default {
name: 'pathBookmarkGroupSelect'
};
</script>
<script lang="ts" setup>
import type { SelectOptionData } from '@arco-design/web-vue';
import type { PathBookmarkGroupQueryResponse } from '@/api/asset/path-bookmark-group';
import { ref, computed, onBeforeMount } from 'vue';
import { useCacheStore } from '@/store';
import useLoading from '@/hooks/loading';
import { createPathBookmarkGroup } from '@/api/asset/path-bookmark-group';
const props = defineProps<Partial<{
modelValue: number;
}>>();
const emits = defineEmits(['update:modelValue']);
const { loading, setLoading } = useLoading();
const cacheStore = useCacheStore();
const value = computed<number | string>({
get() {
return props.modelValue as number;
},
async set(e) {
const val = await checkCreateGroup(e);
emits('update:modelValue', val);
}
});
const optionData = ref<SelectOptionData[]>([]);
// 检查是否可以创建分组
const checkCreateGroup = async (val: string | number): Promise<number> => {
// 清空
if (!val) {
return undefined as unknown as number;
}
// 为 number 代表为 id 已存在
if (typeof val === 'number') {
return val;
}
// 已存在则跳过
const find = optionData.value.find((o) => o.label === val);
if (find) {
return find.value as number;
}
// 不存在则创建
setLoading(true);
try {
return await doCreateGroup(val);
} catch (e) {
return undefined as unknown as number;
} finally {
setLoading(false);
}
};
// 创建分组
const doCreateGroup = async (name: string) => {
const { data: id } = await createPathBookmarkGroup({
name,
});
// 插入缓存
const groups = await cacheStore.loadPathBookmarkGroups();
groups && groups.push({ id, name } as PathBookmarkGroupQueryResponse);
// 插入 options
optionData.value.push({
label: name,
value: id,
});
return id;
};
// 初始化选项
onBeforeMount(async () => {
setLoading(true);
try {
const tags = await cacheStore.loadPathBookmarkGroups();
optionData.value = tags.map(s => {
return {
label: s.name,
value: s.id,
};
});
} catch (e) {
} finally {
setLoading(false);
}
});
</script>
<style lang="less" scoped>
</style>

View File

@@ -0,0 +1,330 @@
<template>
<a-drawer v-model:visible="visible"
:width="388"
:footer="false"
@close="onClose">
<!-- 表头 -->
<template #title>
<span class="path-drawer-title usn">
路径书签
</span>
</template>
<!-- 路径容器 -->
<div class="path-container">
<!-- 路径头部 -->
<div class="path-header">
<!-- 左侧按钮 -->
<a-space size="small">
<!-- 创建路径 -->
<span class="click-icon-wrapper path-header-icon"
title="创建路径"
@click="openAdd">
<icon-plus />
</span>
<!-- 刷新 -->
<span class="click-icon-wrapper path-header-icon"
title="刷新"
@click="fetchData(true)">
<icon-refresh />
</span>
</a-space>
<!-- 搜索框 -->
<a-input-search class="path-header-input"
v-model="filterValue"
placeholder="名称/路径"
allow-clear
@search="filterPath"
@keyup.enter="filterPath" />
</div>
<!-- 加载中 -->
<a-skeleton v-if="loading"
class="loading-skeleton"
:animation="true">
<a-skeleton-line :rows="4"
:line-height="66"
:line-spacing="12" />
</a-skeleton>
<!-- 无数据 -->
<a-empty v-else-if="!pathBookmarkData || (pathBookmarkData.groups.length === 0 && pathBookmarkData.ungroupedItems.length === 0)"
style="padding: 28px 0">
<span>暂无数据</span><br>
<span>点击上方 '<icon-plus />' 添加一条数据吧~</span>
</a-empty>
<!-- 路径书签 -->
<div v-else class="path-list-container">
<!-- 路径书签组 -->
<path-bookmark-list-group :value="pathBookmarkData" />
<!-- 未分组路径书签 -->
<div class="ungrouped-path-container">
<template v-for="item in pathBookmarkData.ungroupedItems">
<path-bookmark-list-item v-if="item.visible"
:key="item.id"
:item="item" />
</template>
</div>
</div>
</div>
<!-- 路径编辑抽屉 -->
<path-bookmark-form-drawer ref="formDrawer"
@added="onAdded"
@updated="onUpdated" />
</a-drawer>
</template>
<script lang="ts">
export default {
name: 'pathBookmarkListDrawer'
};
</script>
<script lang="ts" setup>
import type { PathBookmarkWrapperResponse, PathBookmarkQueryResponse } from '@/api/asset/path-bookmark';
import { ref, provide } from 'vue';
import useVisible from '@/hooks/visible';
import useLoading from '@/hooks/loading';
import { deletePathBookmark, getPathBookmarkList } from '@/api/asset/path-bookmark';
import { useCacheStore, useTerminalStore } from '@/store';
import { openUpdatePathKey, removePathKey } from '../types/const';
import PathBookmarkListItem from './path-bookmark-list-item.vue';
import PathBookmarkListGroup from './path-bookmark-list-group.vue';
import PathBookmarkFormDrawer from './path-bookmark-form-drawer.vue';
const { loading, setLoading } = useLoading();
const { visible, setVisible } = useVisible();
const { getCurrentSshSession } = useTerminalStore();
const cacheStore = useCacheStore();
const formDrawer = ref();
const filterValue = ref<string>();
const pathBookmarkData = ref<PathBookmarkWrapperResponse>();
// 打开
const open = async () => {
setVisible(true);
// 加载数据
await fetchData();
};
defineExpose({ open });
// 加载数据
const fetchData = async (force: boolean = false) => {
if (pathBookmarkData.value && !force) {
return;
}
setLoading(true);
try {
// 查询
const { data } = await getPathBookmarkList();
pathBookmarkData.value = data;
// 设置状态
filterPath();
} catch (e) {
} finally {
setLoading(false);
}
};
// 过滤
const filterPath = () => {
pathBookmarkData.value?.groups.forEach(g => {
g.items?.forEach(s => {
s.visible = !filterValue.value
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|| s.path.toLowerCase().includes(filterValue.value.toLowerCase());
});
});
pathBookmarkData.value?.ungroupedItems.forEach(s => {
s.visible = !filterValue.value
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|| s.path.toLowerCase().includes(filterValue.value.toLowerCase());
});
};
// 新建
const openAdd = () => {
formDrawer.value.openAdd();
};
// 查找并且删除
const findAndSplice = (id: number, items: Array<PathBookmarkQueryResponse>) => {
if (items) {
const index = items.findIndex(s => s.id === id);
if (index !== -1) {
items.splice(index, 1);
return true;
}
return false;
}
};
// 暴露 修改抽屉
provide(openUpdatePathKey, (e: PathBookmarkQueryResponse) => {
formDrawer.value.openUpdate(e);
});
// 暴露 删除
provide(removePathKey, async (id: number) => {
if (!pathBookmarkData.value) {
return;
}
// 删除
await deletePathBookmark(id);
// 查找并且删除未分组的数据
if (findAndSplice(id, pathBookmarkData.value.ungroupedItems)) {
return;
}
// 查找并且删除分组内数据
for (let group of pathBookmarkData.value.groups) {
if (findAndSplice(id, group.items)) {
return;
}
}
});
// 添加回调
const onAdded = async (item: PathBookmarkQueryResponse) => {
if (item.groupId) {
let group = pathBookmarkData.value?.groups.find(g => g.id === item.groupId);
if (group) {
group?.items.push(item);
} else {
const cacheGroups = await cacheStore.loadPathBookmarkGroups();
const findGroup = cacheGroups.find(s => s.id === item.groupId);
if (findGroup) {
pathBookmarkData.value?.groups.push({
id: item.groupId,
name: findGroup.name,
items: [item]
});
}
}
} else {
pathBookmarkData.value?.ungroupedItems.push(item);
}
// 重置过滤
filterPath();
};
// 修改回调
const onUpdated = async (item: PathBookmarkQueryResponse) => {
if (!pathBookmarkData.value) {
return;
}
// 查找原始数据
let originItem;
const findInUngrouped = pathBookmarkData.value.ungroupedItems.find(s => s.id === item.id);
if (findInUngrouped) {
originItem = findInUngrouped;
} else {
for (let group of pathBookmarkData.value.groups) {
const find = group.items.find(s => s.id === item.id);
if (find) {
originItem = find;
break;
}
}
}
if (!originItem) {
return;
}
// 检查分组是否存在
const findGroup = pathBookmarkData.value.groups.find(s => s.id === item.groupId);
if (!findGroup) {
const cacheGroups = await cacheStore.loadPathBookmarkGroups();
const cacheGroup = cacheGroups.find(s => s.id === item.groupId);
if (cacheGroup) {
pathBookmarkData.value.groups.push({
id: item.groupId,
name: cacheGroup.name,
items: []
});
}
}
// 设置数据
const originGroupId = originItem.groupId;
originItem.groupId = item.groupId;
originItem.name = item.name;
originItem.path = item.path;
// 移动分组
if (item.groupId !== originGroupId) {
// 从原始分组移除
if (originGroupId) {
const findGroup = pathBookmarkData.value.groups.find(s => s.id === originGroupId);
if (findGroup) {
findAndSplice(item.id, findGroup.items);
}
} else {
// 从未分组数据中移除
findAndSplice(item.id, pathBookmarkData.value.ungroupedItems);
}
// 添加到新分组
if (item.groupId) {
const findGroup = pathBookmarkData.value.groups.find(s => s.id === item.groupId);
if (findGroup) {
findGroup.items.push(item);
}
} else {
pathBookmarkData.value.ungroupedItems.push(originItem);
}
}
// 重置过滤
filterPath();
};
// 关闭回调
const onClose = () => {
// 聚焦终端
getCurrentSshSession()?.focus();
};
</script>
<style lang="less" scoped>
.path-drawer-title {
font-size: 16px;
}
.path-container {
position: relative;
background: var(--color-bg-2);
height: 100%;
width: 100%;
display: block;
.path-header {
padding: 12px;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
&-icon {
width: 32px;
height: 32px;
font-size: 16px;
}
&-input {
width: 248px;
user-select: none;
}
}
}
.path-list-container {
position: relative;
height: calc(100% - 56px);
overflow: auto;
padding-bottom: 4px;
}
.loading-skeleton {
padding: 0 12px;
:deep(.arco-skeleton-line-row) {
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<a-collapse :bordered="false">
<template v-for="group in value.groups">
<a-collapse-item v-if="calcTotal(group) > 0"
:key="group.id"
:header="group.name">
<!-- 总量 -->
<template #extra>
{{ calcTotal(group) }}
</template>
<!-- 路径 -->
<template v-for="item in group.items">
<path-bookmark-list-item v-if="item.visible"
:key="item.id"
:item="item" />
</template>
</a-collapse-item>
</template>
</a-collapse>
</template>
<script lang="ts">
export default {
name: 'pathBookmarkListGroup'
};
</script>
<script lang="ts" setup>
import type { PathBookmarkGroupQueryResponse } from '@/api/asset/path-bookmark-group';
import type { PathBookmarkWrapperResponse } from '@/api/asset/path-bookmark';
import PathBookmarkListItem from './path-bookmark-list-item.vue';
defineProps<{
value: PathBookmarkWrapperResponse;
}>();
// 计算总量
const calcTotal = (group: PathBookmarkGroupQueryResponse) => {
return group.items.filter(s => s.visible).length;
};
</script>
<style lang="less" scoped>
:deep(.arco-collapse-item) {
border: none;
&-header {
border: none;
&-title {
user-select: none;
}
&-extra {
user-select: none;
}
}
&-content {
background-color: unset;
padding: 0;
}
&-content-box {
padding: 0;
}
}
</style>

View File

@@ -0,0 +1,288 @@
<template>
<a-dropdown class="terminal-context-menu"
:popup-max-height="false"
trigger="contextMenu"
position="bl"
alignPoint>
<!-- 路径 -->
<div class="path-item-wrapper"
:class="[!!item.expand ? 'path-item-wrapper-expand' : '']"
@click="clickItem">
<div class="path-item">
<div class="path-item-title">
<!-- 名称 -->
<span class="path-item-title-name">
{{ item.name }}
</span>
<!-- 操作 -->
<div class="path-item-title-actions">
<a-space>
<!-- 进入 -->
<a-tag class="pointer usn"
size="small"
:checkable="true"
:checked="true"
@click.stop="changePath">
<template #icon>
<icon-link />
</template>
进入
</a-tag>
<!-- 粘贴 -->
<a-tag class="pointer usn"
size="small"
:checkable="true"
:checked="true"
@click.stop.prevent="paste">
<template #icon>
<icon-paste />
</template>
粘贴
</a-tag>
</a-space>
</div>
</div>
<!-- 路径 -->
<span class="path-item-value" @click="clickPath">
{{ item.path }}
</span>
</div>
</div>
<!-- 右键菜单 -->
<template #content>
<!-- 进入父目录 -->
<a-doption @click="changePath">
<div class="terminal-context-menu-icon">
<icon-link />
</div>
<div>进入父目录</div>
</a-doption>
<!-- 复制 -->
<a-doption @click="copyPath">
<div class="terminal-context-menu-icon">
<icon-copy />
</div>
<div>复制</div>
</a-doption>
<!-- 粘贴 -->
<a-doption @click="paste">
<div class="terminal-context-menu-icon">
<icon-paste />
</div>
<div>粘贴</div>
</a-doption>
<!-- 修改 -->
<a-doption @click="openUpdatePath(item)">
<div class="terminal-context-menu-icon">
<icon-edit />
</div>
<div>修改</div>
</a-doption>
<!-- 删除 -->
<a-doption @click="removePath(item.id)">
<div class="terminal-context-menu-icon">
<icon-delete />
</div>
<div>删除</div>
</a-doption>
<!-- 展开 -->
<a-doption v-if="!item.expand"
@click="() => item.expand = true">
<div class="terminal-context-menu-icon">
<icon-expand />
</div>
<div>展开</div>
</a-doption>
<!-- 收起 -->
<a-doption v-else
@click="() => item.expand = false">
<div class="terminal-context-menu-icon">
<icon-shrink />
</div>
<div>收起</div>
</a-doption>
</template>
</a-dropdown>
</template>
<script lang="ts">
export default {
name: 'pathBookmarkListItem'
};
</script>
<script lang="ts" setup>
import type { PathBookmarkQueryResponse } from '@/api/asset/path-bookmark';
import { useTerminalStore } from '@/store';
import { useDebounceFn } from '@vueuse/core';
import { copy } from '@/hooks/copy';
import { inject } from 'vue';
import { openUpdatePathKey, removePathKey } from '../types/const';
import { getParentPath } from '@/utils/file';
const props = defineProps<{
item: PathBookmarkQueryResponse;
}>();
const { getAndCheckCurrentSshSession } = useTerminalStore();
let clickCount = 0;
// 修改
const openUpdatePath = inject(openUpdatePathKey) as (item: PathBookmarkQueryResponse) => void;
// 删除
const removePath = inject(removePathKey) as (id: number) => void;
// 点击路径
const clickItem = () => {
if (++clickCount == 2) {
clickCount = 0;
changePath();
} else {
expandItem();
}
};
// 展开
const expandItem = useDebounceFn(() => {
setTimeout(() => {
// 为 0 则代表为双击
if (clickCount !== 0) {
props.item.expand = !props.item.expand;
clickCount = 0;
}
}, 50);
});
// 点击路径
const clickPath = (e: Event) => {
if (props.item.expand) {
// 获取选中的文本
const selectedText = window.getSelection()?.toString();
if (selectedText) {
e.stopPropagation();
}
}
};
// 复制路径
const copyPath = () => {
copy(props.item.path, '已复制');
};
// 粘贴
const paste = () => {
write(props.item.path);
};
// 进入父目录
const changePath = () => {
const parentPath = getParentPath(props.item.path);
write( parentPath+ '\r\n');
};
// 写入路径
const write = (command: string) => {
const handler = getAndCheckCurrentSshSession()?.handler;
if (handler && handler.enabledStatus('checkAppendMissing')) {
handler.checkAppendMissing(command);
}
};
</script>
<style lang="less" scoped>
@transform-x: 8px;
@drawer-width: 388px;
@item-wrapper-p-y: 4px;
@item-wrapper-p-x: 12px;
@item-p: 8px;
@item-width: @drawer-width - @item-wrapper-p-x * 2;
@item-width-transform: @item-width + @transform-x;
@item-inline-width: @item-width - @item-p * 2;
@item-actions-width: 124px;
.path-item-wrapper {
padding: @item-wrapper-p-y 0;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
&-expand {
.path-item {
width: @item-width-transform !important;
background: var(--color-fill-3) !important;
.path-item-value {
color: var(--color-text-1);
text-overflow: unset;
word-break: break-all;
white-space: pre-wrap;
user-select: unset;
}
}
}
.path-item {
display: flex;
flex-direction: column;
padding: @item-p;
background: var(--color-fill-2);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
width: @item-width;
&:hover {
width: @item-width-transform;
background: var(--color-fill-3);
.path-item-title {
&-name {
width: calc(@item-inline-width - @item-actions-width);
}
&-actions {
width: @item-actions-width;
display: inline-block;
}
}
}
&-title {
color: var(--color-text-1);
margin-bottom: 8px;
width: @item-inline-width;
height: 24px;
display: flex;
align-items: center;
user-select: none;
&-name {
width: @item-inline-width;
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
&-actions {
display: none;
}
}
&-value {
color: var(--color-text-2);
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: @item-inline-width;
user-select: none;
}
}
}
</style>

View File

@@ -0,0 +1,5 @@
// 打开 updatePath key
export const openUpdatePathKey = Symbol();
// 删除 path key
export const removePathKey = Symbol();

View File

@@ -0,0 +1,24 @@
import type { FieldRule } from '@arco-design/web-vue';
export const groupId = [{
message: '请选择分组'
}] as FieldRule[];
export const name = [{
required: true,
message: '请输入名称'
}, {
maxLength: 64,
message: '名称长度不能大于64位'
}] as FieldRule[];
export const path = [{
required: true,
message: '请输入路径'
}] as FieldRule[];
export default {
groupId,
name,
path,
} as Record<string, FieldRule | FieldRule[]>;

View File

@@ -10,6 +10,8 @@
position="left" />
<!-- 命令片段列表抽屉 -->
<command-snippet-list-drawer ref="snippetRef" />
<!-- 路径书签列表抽屉 -->
<path-bookmark-list-drawer ref="pathRef" />
<!-- 传输列表 -->
<transfer-drawer ref="transferRef" />
</div>
@@ -27,19 +29,25 @@
import { ref } from 'vue';
import IconActions from './icon-actions.vue';
import CommandSnippetListDrawer from '../../../command-snippet/components/command-snippet-list-drawer.vue';
import PathBookmarkListDrawer from '../../../path-bookmark/components/path-bookmark-list-drawer.vue';
import TransferDrawer from '@/views/host/terminal/components/transfer/transfer-drawer.vue';
const { getAndCheckCurrentSshSession } = useTerminalStore();
const snippetRef = ref();
const pathRef = ref();
const transferRef = ref();
// 顶部操作
const topActions = [
{
icon: 'icon-code',
icon: 'icon-code-block',
content: '打开命令片段',
click: () => snippetRef.value.open()
}, {
icon: 'icon-bookmark',
content: '打开路径书签',
click: () => pathRef.value.open()
}, {
icon: 'icon-swap',
content: '文件传输列表',