🔖 项目重命名.
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
:width="388"
|
||||
:footer="false"
|
||||
@close="onClose">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="snippet-drawer-title usn">
|
||||
命令片段
|
||||
</span>
|
||||
</template>
|
||||
<!-- 命令容器 -->
|
||||
<div class="snippet-container">
|
||||
<!-- 命令头部 -->
|
||||
<div class="snippet-header">
|
||||
<!-- 左侧按钮 -->
|
||||
<a-space size="small">
|
||||
<!-- 创建命令 -->
|
||||
<span class="click-icon-wrapper snippet-header-icon"
|
||||
title="创建命令"
|
||||
@click="openAdd">
|
||||
<icon-plus />
|
||||
</span>
|
||||
<!-- 刷新 -->
|
||||
<span class="click-icon-wrapper snippet-header-icon"
|
||||
title="刷新"
|
||||
@click="fetchData(true)">
|
||||
<icon-refresh />
|
||||
</span>
|
||||
</a-space>
|
||||
<!-- 搜索框 -->
|
||||
<a-input-search class="snippet-header-input"
|
||||
v-model="filterValue"
|
||||
placeholder="名称/命令"
|
||||
allow-clear />
|
||||
</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="!snippetValue || (snippetValue.groups.length === 0 && snippetValue.ungroupedItems.length === 0)"
|
||||
style="padding: 28px 0">
|
||||
<span>暂无数据</span><br>
|
||||
<span>点击上方 '<icon-plus />' 添加一条数据吧~</span>
|
||||
</a-empty>
|
||||
<!-- 命令片段 -->
|
||||
<div v-else class="snippet-list-container">
|
||||
<!-- 命令片段组 -->
|
||||
<command-snippet-list-group :value="snippetValue" />
|
||||
<!-- 未分组命令片段 -->
|
||||
<div class="ungrouped-snippet-container">
|
||||
<template v-for="item in snippetValue.ungroupedItems">
|
||||
<command-snippet-list-item v-if="item.visible"
|
||||
:key="item.id"
|
||||
:item="item" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 命令编辑抽屉 -->
|
||||
<command-snippet-form-drawer ref="formDrawer"
|
||||
@added="onAdded"
|
||||
@updated="onUpdated" />
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'commandSnippetDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession } from '@/views/host/terminal/types/terminal.type';
|
||||
import type { CommandSnippetWrapperResponse, CommandSnippetQueryResponse } from '@/api/asset/command-snippet';
|
||||
import { ref, watch, provide } from 'vue';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { deleteCommandSnippet, getCommandSnippetList } from '@/api/asset/command-snippet';
|
||||
import { useCacheStore, useTerminalStore } from '@/store';
|
||||
import { openUpdateSnippetKey, removeSnippetKey } from '../types/const';
|
||||
import { PanelSessionType } from '@/views/host/terminal/types/terminal.const';
|
||||
import CommandSnippetListItem from './command-snippet-list-item.vue';
|
||||
import CommandSnippetListGroup from './command-snippet-list-group.vue';
|
||||
import CommandSnippetFormDrawer from './command-snippet-form-drawer.vue';
|
||||
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { getCurrentSession } = useTerminalStore();
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
const formDrawer = ref();
|
||||
const filterValue = ref<string>();
|
||||
const snippetValue = ref<CommandSnippetWrapperResponse>();
|
||||
|
||||
// 打开
|
||||
const open = async () => {
|
||||
setVisible(true);
|
||||
// 加载数据
|
||||
await fetchData();
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 加载数据
|
||||
const fetchData = async (force: boolean = false) => {
|
||||
if (snippetValue.value && !force) {
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
// 查询
|
||||
const { data } = await getCommandSnippetList();
|
||||
snippetValue.value = data;
|
||||
// 设置状态
|
||||
filterSnippet();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 过滤
|
||||
const filterSnippet = () => {
|
||||
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());
|
||||
});
|
||||
});
|
||||
snippetValue.value?.ungroupedItems.forEach(s => {
|
||||
s.visible = !filterValue.value
|
||||
|| s.name.toLowerCase().includes(filterValue.value.toLowerCase())
|
||||
|| s.command.toLowerCase().includes(filterValue.value.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
// 过滤列表
|
||||
watch(filterValue, filterSnippet);
|
||||
|
||||
// 新建
|
||||
const openAdd = () => {
|
||||
formDrawer.value.openAdd();
|
||||
};
|
||||
|
||||
// 查找并且删除
|
||||
const findAndSplice = (id: number, items: Array<CommandSnippetQueryResponse>) => {
|
||||
if (items) {
|
||||
const index = items.findIndex(s => s.id === id);
|
||||
if (index !== -1) {
|
||||
items.splice(index, 1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露 修改抽屉
|
||||
provide(openUpdateSnippetKey, (e: CommandSnippetQueryResponse) => {
|
||||
formDrawer.value.openUpdate(e);
|
||||
});
|
||||
|
||||
// 暴露 删除
|
||||
provide(removeSnippetKey, async (id: number) => {
|
||||
if (!snippetValue.value) {
|
||||
return;
|
||||
}
|
||||
// 删除
|
||||
await deleteCommandSnippet(id);
|
||||
// 查找并且删除未分组的数据
|
||||
if (findAndSplice(id, snippetValue.value.ungroupedItems)) {
|
||||
return;
|
||||
}
|
||||
// 查找并且删除分组内数据
|
||||
for (let group of snippetValue.value.groups) {
|
||||
if (findAndSplice(id, group.items)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加回调
|
||||
const onAdded = async (item: CommandSnippetQueryResponse) => {
|
||||
if (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) {
|
||||
snippetValue.value?.groups.push({
|
||||
id: item.groupId,
|
||||
name: findGroup.name,
|
||||
items: [item]
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
snippetValue.value?.ungroupedItems.push(item);
|
||||
}
|
||||
// 重置过滤
|
||||
filterSnippet();
|
||||
};
|
||||
|
||||
// 修改回调
|
||||
const onUpdated = async (item: CommandSnippetQueryResponse) => {
|
||||
if (!snippetValue.value) {
|
||||
return;
|
||||
}
|
||||
// 查找原始数据
|
||||
let originItem;
|
||||
const findInUngrouped = snippetValue.value.ungroupedItems.find(s => s.id === item.id);
|
||||
if (findInUngrouped) {
|
||||
originItem = findInUngrouped;
|
||||
} else {
|
||||
for (let group of snippetValue.value.groups) {
|
||||
const find = group.items.find(s => s.id === item.id);
|
||||
if (find) {
|
||||
originItem = find;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!originItem) {
|
||||
return;
|
||||
}
|
||||
// 检查分组是否存在
|
||||
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) {
|
||||
snippetValue.value.groups.push({
|
||||
id: item.groupId,
|
||||
name: cacheGroup.name,
|
||||
items: []
|
||||
});
|
||||
}
|
||||
}
|
||||
// 设置数据
|
||||
const originGroupId = originItem.groupId;
|
||||
originItem.groupId = item.groupId;
|
||||
originItem.name = item.name;
|
||||
originItem.command = item.command;
|
||||
// 移动分组
|
||||
if (item.groupId !== originGroupId) {
|
||||
// 从原始分组移除
|
||||
if (originGroupId) {
|
||||
const findGroup = snippetValue.value.groups.find(s => s.id === originGroupId);
|
||||
if (findGroup) {
|
||||
findAndSplice(item.id, findGroup.items);
|
||||
}
|
||||
} else {
|
||||
// 从未分组数据中移除
|
||||
findAndSplice(item.id, snippetValue.value.ungroupedItems);
|
||||
}
|
||||
// 添加到新分组
|
||||
if (item.groupId) {
|
||||
const findGroup = snippetValue.value.groups.find(s => s.id === item.groupId);
|
||||
if (findGroup) {
|
||||
findGroup.items.push(item);
|
||||
}
|
||||
} else {
|
||||
snippetValue.value.ungroupedItems.push(originItem);
|
||||
}
|
||||
}
|
||||
// 重置过滤
|
||||
filterSnippet();
|
||||
};
|
||||
|
||||
// 关闭回调
|
||||
const onClose = () => {
|
||||
// 关闭时候如果打开的是终端 则聚焦终端
|
||||
getCurrentSession<ISshSession>(PanelSessionType.SSH.type)?.focus();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.snippet-drawer-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.snippet-container {
|
||||
position: relative;
|
||||
background: var(--color-bg-2);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: block;
|
||||
|
||||
.snippet-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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.snippet-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>
|
||||
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
width="40%"
|
||||
: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="分组">
|
||||
<command-snippet-group-select v-model="formModel.groupId" />
|
||||
</a-form-item>
|
||||
<!-- 代码片段 -->
|
||||
<a-form-item field="command"
|
||||
label="代码片段"
|
||||
:hide-label="true">
|
||||
<editor v-model="formModel.command"
|
||||
container-class="command-editor"
|
||||
language="shell"
|
||||
theme="vs-dark"
|
||||
:suggestions="true"
|
||||
:auto-focus="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'commandSnippetFormDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CommandSnippetUpdateRequest } from '@/api/asset/command-snippet';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { createCommandSnippet, updateCommandSnippet } from '@/api/asset/command-snippet';
|
||||
import formRules from '../types/form.rules';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import CommandSnippetGroupSelect from './command-snippet-group-select.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const isAddHandle = ref<boolean>(true);
|
||||
|
||||
const defaultForm = (): CommandSnippetUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
groupId: undefined,
|
||||
name: undefined,
|
||||
command: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const formRef = ref<any>();
|
||||
const formModel = ref<CommandSnippetUpdateRequest>({});
|
||||
|
||||
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 createCommandSnippet(formModel.value);
|
||||
formModel.value.id = id;
|
||||
Message.success('创建成功');
|
||||
emits('added', formModel.value);
|
||||
} else {
|
||||
// 修改
|
||||
await updateCommandSnippet(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;
|
||||
}
|
||||
|
||||
.command-editor {
|
||||
height: calc(100vh - 262px);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -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: 'commandSnippetGroupSelect'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import type { CommandSnippetGroupQueryResponse } from '@/api/asset/command-snippet-group';
|
||||
import { ref, computed, onBeforeMount } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { createCommandSnippetGroup } from '@/api/asset/command-snippet-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 createCommandSnippetGroup({
|
||||
name,
|
||||
});
|
||||
// 插入缓存
|
||||
const groups = await cacheStore.loadCommandSnippetGroups();
|
||||
groups && groups.push({ id, name } as CommandSnippetGroupQueryResponse);
|
||||
// 插入 options
|
||||
optionData.value.push({
|
||||
label: name,
|
||||
value: id,
|
||||
});
|
||||
return id;
|
||||
};
|
||||
|
||||
// 初始化选项
|
||||
onBeforeMount(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const tags = await cacheStore.loadCommandSnippetGroups();
|
||||
optionData.value = tags.map(s => {
|
||||
return {
|
||||
label: s.name,
|
||||
value: s.id,
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -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>
|
||||
<!-- snippet -->
|
||||
<template v-for="item in group.items">
|
||||
<command-snippet-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: 'commandSnippetListGroup'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { CommandSnippetGroupQueryResponse } from '@/api/asset/command-snippet-group';
|
||||
import type { CommandSnippetWrapperResponse } from '@/api/asset/command-snippet';
|
||||
import CommandSnippetListItem from './command-snippet-list-item.vue';
|
||||
|
||||
defineProps<{
|
||||
value: CommandSnippetWrapperResponse;
|
||||
}>();
|
||||
|
||||
// 计算总量
|
||||
const calcTotal = (group: CommandSnippetGroupQueryResponse) => {
|
||||
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>
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<a-dropdown class="terminal-context-menu"
|
||||
:popup-max-height="false"
|
||||
trigger="contextMenu"
|
||||
position="bl"
|
||||
alignPoint>
|
||||
<!-- 命令 -->
|
||||
<div class="snippet-item-wrapper"
|
||||
:class="[!!item.expand ? 'snippet-item-wrapper-expand' : '']"
|
||||
@click="clickItem">
|
||||
<div class="snippet-item">
|
||||
<div class="snippet-item-title">
|
||||
<!-- 名称 -->
|
||||
<span class="snippet-item-title-name">
|
||||
{{ item.name }}
|
||||
</span>
|
||||
<!-- 操作 -->
|
||||
<div class="snippet-item-title-actions">
|
||||
<a-space>
|
||||
<!-- 粘贴 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop.prevent="paste">
|
||||
<template #icon>
|
||||
<icon-paste />
|
||||
</template>
|
||||
粘贴
|
||||
</a-tag>
|
||||
<!-- 执行 -->
|
||||
<a-tag class="pointer usn"
|
||||
size="small"
|
||||
:checkable="true"
|
||||
:checked="true"
|
||||
@click.stop="exec">
|
||||
<template #icon>
|
||||
<icon-thunderbolt />
|
||||
</template>
|
||||
执行
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 命令 -->
|
||||
<span class="snippet-item-command"
|
||||
@click="clickCommand">
|
||||
{{ item.command }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右键菜单 -->
|
||||
<template #content>
|
||||
<!-- 复制 -->
|
||||
<a-doption @click="copyCommand">
|
||||
<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="exec">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
<div>执行</div>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption @click="openUpdateSnippet(item)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<icon-edit />
|
||||
</div>
|
||||
<div>修改</div>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption @click="removeSnippet(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: 'commandSnippetListItem'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ISshSession } from '@/views/host/terminal/types/terminal.type';
|
||||
import type { CommandSnippetQueryResponse } from '@/api/asset/command-snippet';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { inject } from 'vue';
|
||||
import { openUpdateSnippetKey, removeSnippetKey } from '../types/const';
|
||||
import { PanelSessionType } from '@/views/host/terminal/types/terminal.const';
|
||||
|
||||
const props = defineProps<{
|
||||
item: CommandSnippetQueryResponse;
|
||||
}>();
|
||||
|
||||
const { getCurrentSession } = useTerminalStore();
|
||||
|
||||
let clickCount = 0;
|
||||
|
||||
// 修改
|
||||
const openUpdateSnippet = inject(openUpdateSnippetKey) as (item: CommandSnippetQueryResponse) => void;
|
||||
|
||||
// 删除
|
||||
const removeSnippet = inject(removeSnippetKey) as (id: number) => void;
|
||||
|
||||
// 点击命令
|
||||
const clickItem = () => {
|
||||
if (++clickCount == 2) {
|
||||
// 双击执行
|
||||
clickCount = 0;
|
||||
exec();
|
||||
} else {
|
||||
// 单击展开
|
||||
expandItem();
|
||||
}
|
||||
};
|
||||
|
||||
// 展开
|
||||
const expandItem = useDebounceFn(() => {
|
||||
setTimeout(() => {
|
||||
// 为 0 则代表为双击
|
||||
if (clickCount !== 0) {
|
||||
props.item.expand = !props.item.expand;
|
||||
clickCount = 0;
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 点击命令
|
||||
const clickCommand = (e: Event) => {
|
||||
if (props.item.expand) {
|
||||
// 获取选中的文本
|
||||
const selectedText = window.getSelection()?.toString();
|
||||
if (selectedText) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 复制命令
|
||||
const copyCommand = () => {
|
||||
copy(props.item.command, '已复制');
|
||||
};
|
||||
|
||||
// 粘贴
|
||||
const paste = () => {
|
||||
write(props.item.command);
|
||||
};
|
||||
|
||||
// 执行
|
||||
const exec = () => {
|
||||
write(props.item.command + '\r\n');
|
||||
};
|
||||
|
||||
// 写入命令
|
||||
const write = (command: string) => {
|
||||
const handler = getCurrentSession<ISshSession>(PanelSessionType.SSH.type, true)?.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;
|
||||
|
||||
.snippet-item-wrapper {
|
||||
padding: @item-wrapper-p-y 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&-expand {
|
||||
|
||||
.snippet-item {
|
||||
width: @item-width-transform !important;
|
||||
background: var(--color-fill-3) !important;
|
||||
|
||||
.snippet-item-command {
|
||||
color: var(--color-text-1);
|
||||
text-overflow: unset;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
user-select: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.snippet-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);
|
||||
|
||||
.snippet-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;
|
||||
}
|
||||
}
|
||||
|
||||
&-command {
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: @item-inline-width;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
// 打开 updateSnippet key
|
||||
export const openUpdateSnippetKey = Symbol();
|
||||
|
||||
// 删除 snippet key
|
||||
export const removeSnippetKey = Symbol();
|
||||
@@ -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 command = [{
|
||||
required: true,
|
||||
message: '请输入代码片段'
|
||||
}] as FieldRule[];
|
||||
|
||||
export default {
|
||||
groupId,
|
||||
name,
|
||||
command,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
Reference in New Issue
Block a user