feat(文件管理): 文件管理页面&树组件调整

This commit is contained in:
baiqi 2023-09-04 17:39:40 +08:00 committed by fit2-zhao
parent 5f6bf18411
commit 8c7ab91265
9 changed files with 975 additions and 5 deletions

View File

@ -42,6 +42,14 @@
</div>
</template>
</a-tree>
<slot name="empty">
<div
v-show="treeData.length === 0 && props.emptyText"
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] text-[var(--color-text-4)]"
>
{{ props.emptyText }}
</div>
</slot>
</template>
<script setup lang="ts">
@ -69,6 +77,7 @@
focusNodeKey?: string | number; // key
nodeMoreActions?: ActionsItem[]; //
expandAll?: boolean; // /true false
emptyText?: string; //
}>(),
{
searchDebounce: 300,
@ -256,7 +265,7 @@
watch(
() => innerFocusNodeKey.value,
(val) => {
if (val) {
if (val.toString() !== '') {
focusEl.value = treeRef.value?.$el.querySelector(`[data-key=${val}]`);
if (focusEl.value) {
focusEl.value.style.backgroundColor = 'rgb(var(--primary-1))';
@ -334,6 +343,7 @@
.ms-tree-node-extra__btn,
.ms-tree-node-extra__more {
padding: 4px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9));
.arco-icon {

View File

@ -0,0 +1,64 @@
<template>
<div class="flex items-center gap-[4px]">
<popConfirm mode="add" :all-names="[]" @close="emit('close')">
<MsButton type="text" size="mini" class="action-btn" @click="emit('add', props.item)">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</popConfirm>
<MsTableMoreAction
:list="folderMoreActions"
trigger="click"
@select="emit('select', $event, props.item)"
@close="emit('actionsClose')"
>
<MsButton type="text" size="mini" class="action-btn" @click="emit('clickMore', props.item)">
<MsIcon type="icon-icon_more_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</MsTableMoreAction>
<popConfirm mode="rename" :title="item.name" :all-names="[]" @close="emit('close')">
<span ref="renameSpanRef" class="relative"></span>
</popConfirm>
</div>
</template>
<script setup lang="ts">
import popConfirm from './popConfirm.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
const props = defineProps<{
item: Record<string, any>;
}>();
const emit = defineEmits(['add', 'close', 'select', 'actionsClose', 'clickMore']);
const folderMoreActions: ActionsItem[] = [
{
label: 'project.fileManagement.rename',
eventTag: 'rename',
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
danger: true,
},
];
</script>
<style lang="less" scoped>
.action-btn {
@apply !mr-0;
padding: 4px;
border-radius: var(--border-radius-mini);
&:hover {
background-color: rgb(var(--primary-9));
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
</style>

View File

@ -0,0 +1,125 @@
<template>
<a-popconfirm
v-model:popup-visible="innerVisible"
class="ms-pop-confirm--hidden-icon"
position="bottom"
:ok-loading="loading"
:cancel-button-props="{ disabled: loading }"
:on-before-ok="beforeConfirm"
:popup-container="props.popupContainer || 'body'"
@popup-visible-change="reset"
>
<template #content>
<div class="mb-[8px] font-medium">
{{ props.mode === 'add' ? t('project.fileManagement.addSubModule') : t('project.fileManagement.rename') }}
</div>
<a-form ref="formRef" :model="form" layout="vertical">
<a-form-item
class="hidden-item"
field="name"
:rules="[{ required: true, message: t('project.fileManagement.nameNotNull') }, { validator: validateName }]"
>
<a-input
v-model:model-value="form.name"
:max-length="50"
:placeholder="props.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]"
@press-enter="beforeConfirm(undefined)"
></a-input>
</a-form-item>
</a-form>
</template>
<slot></slot>
</a-popconfirm>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { Message } from '@arco-design/web-vue';
import type { FormInstance } from '@arco-design/web-vue';
const props = defineProps<{
mode: 'add' | 'rename';
visible?: boolean;
title?: string;
allNames: string[];
popupContainer?: string;
placeholder?: string;
}>();
const emit = defineEmits(['update:visible', 'close']);
const { t } = useI18n();
const innerVisible = ref(props.visible || false);
const form = ref({
name: props.title || '',
});
const formRef = ref<FormInstance>();
const loading = ref(false);
watch(
() => props.title,
(val) => {
form.value.name = val || '';
}
);
watch(
() => props.visible,
(val) => {
innerVisible.value = val;
}
);
watch(
() => innerVisible.value,
(val) => {
if (!val) {
emit('close');
}
emit('update:visible', val);
}
);
function beforeConfirm(done?: (closed: boolean) => void) {
formRef.value?.validate(async (errors) => {
if (!errors) {
try {
loading.value = true;
if (props.mode === 'add') {
Message.success(t('project.fileManagement.addSubModuleSuccess'));
} else {
Message.success(t('project.fileManagement.renameSuccess'));
}
if (done) {
done(true);
} else {
innerVisible.value = false;
}
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
} else if (done) {
done(false);
}
});
}
function validateName(value: any, callback: (error?: string | undefined) => void) {
if (props.allNames.includes(value)) {
callback(t('project.fileManagement.nameExist'));
}
}
function reset() {
form.value.name = '';
formRef.value?.resetFields();
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,64 @@
<template>
<div class="p-[24px]">
<div class="header">
<a-button type="primary">{{ t('project.fileManagement.addFile') }}</a-button>
<div class="header-right">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="w-[240px]"
></a-input-search>
<a-radio-group v-model:model-value="fileType" type="button" class="file-show-type" @change="changeFileType">
<a-radio value="Module" class="show-type-icon">{{ t('project.fileManagement.module') }}</a-radio>
<a-radio value="Storage" class="show-type-icon">{{ t('project.fileManagement.storage') }}</a-radio>
</a-radio-group>
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
<a-radio value="card" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_card_outlined" /></a-radio>
</a-radio-group>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
const { t } = useI18n();
const keyword = ref('');
const fileType = ref('Module');
function changeFileType() {
console.log(fileType.value);
}
const showType = ref('list');
function changeShowType() {
console.log(showType.value);
}
</script>
<style lang="less" scoped>
.header {
@apply flex items-center justify-between;
.header-right {
@apply ml-auto flex items-center justify-end;
width: 70%;
gap: 8px;
.show-type-icon {
:deep(.arco-radio-button-content) {
@apply flex;
padding: 4px;
line-height: 20px;
}
}
}
}
</style>

View File

@ -0,0 +1,664 @@
<template>
<div class="page">
<MsSplitBox>
<template #left>
<div class="p-[24px]">
<div class="folder" @click="setActiveFolder('my')">
<div :class="getFolderClass('my')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.fileManagement.myFile') }}</div>
<div class="folder-count">({{ myFileCount }})</div>
</div>
</div>
<div class="folder">
<div :class="getFolderClass('all')" @click="setActiveFolder('all')">
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
<div class="folder-name">{{ t('project.fileManagement.allFile') }}</div>
<div class="folder-count">({{ allFileCount }})</div>
</div>
<div class="ml-auto flex items-center">
<a-tooltip
:content="isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')"
>
<a-button type="text" size="mini" class="p-[4px]" @click="changeExpand">
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion'" />
</a-button>
</a-tooltip>
<popConfirm mode="add" :all-names="[]">
<a-tooltip :content="t('project.fileManagement.addSubModule')">
<a-button type="text" size="mini" class="p-[2px]">
<MsIcon type="icon-icon_create_planarity" size="18" />
</a-button>
</a-tooltip>
</popConfirm>
</div>
</div>
<a-divider class="my-[8px]" />
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type" @change="changeShowType">
<a-radio value="Module">{{ t('project.fileManagement.module') }}</a-radio>
<a-radio value="Storage">{{ t('project.fileManagement.storage') }}</a-radio>
</a-radio-group>
<div v-show="showType === 'Module'">
<a-input
v-model:model-value="moduleKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[8px]"
></a-input>
<MsTree
v-model:focus-node-key="focusNodeKey"
:data="folderTree"
:keyword="moduleKeyword"
:node-more-actions="folderMoreActions"
:expand-all="isExpandAll"
:empty-text="t('project.fileManagement.noFolder')"
draggable
block-node
@select="folderNodeSelect"
@more-action-select="handleFolderMoreSelect"
@more-actions-close="moreActionsClose"
>
<template #title="nodeData">
<span class="text-[var(--color-text-1)]">{{ nodeData.title }}</span>
<span class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count }})</span>
</template>
<template #extra="nodeData">
<popConfirm mode="add" :all-names="[]" @close="resetFocusNodeKey">
<MsButton
type="text"
size="mini"
class="ms-tree-node-extra__btn !mr-0"
@click="setFocusNodeKe(nodeData)"
>
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
</popConfirm>
<popConfirm mode="rename" :title="renameFolderTitle" :all-names="[]" @close="resetFocusNodeKey">
<span :id="`renameSpan${nodeData.key}`" class="relative"></span>
</popConfirm>
</template>
</MsTree>
</div>
<div v-show="showType === 'Storage'">
<a-input
v-model:model-value="storageKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[8px]"
></a-input>
<a-list
:virtual-list-props="{
height: 'calc(100vh - 310px)',
}"
:data="storageList"
:bordered="false"
:split="false"
>
<template #item="{ item, index }">
<div
:key="index"
:class="['folder', focusNodeKey === item.key ? 'ms-tree-node-extra--focus' : '']"
@click="setActiveFolder(item.key)"
>
<div :class="getFolderClass(item.key)">
<MsIcon type="icon-icon_git" class="folder-icon" />
<div class="folder-name">{{ item.title }}</div>
<div class="folder-count">({{ item.count }})</div>
</div>
<itemActions
:item="item"
@click.stop
@add="setFocusNodeKe"
@close="resetFocusNodeKey"
@actions-close="moreActionsClose"
@click-more="setFocusNodeKe"
/>
</div>
</template>
<template #empty>
<div
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] text-[var(--color-text-4)]"
>
{{ t('project.fileManagement.noStorage') }}
</div>
</template>
</a-list>
</div>
</div>
</template>
<template #right>
<rightBox />
</template>
</MsSplitBox>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import useModal from '@/hooks/useModal';
import popConfirm from './components/popConfirm.vue';
import rightBox from './components/rightBox.vue';
import itemActions from './components/itemActions.vue';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
const { t } = useI18n();
const { openModal } = useModal();
const myFileCount = ref(0);
const allFileCount = ref(0);
const isExpandAll = ref(false);
const activeFolderType = ref<'folder' | 'module' | 'storage'>('folder');
function changeExpand() {
isExpandAll.value = !isExpandAll.value;
}
const activeFolder = ref<string | number>('all');
function setActiveFolder(id: string) {
activeFolder.value = id;
if (['my', 'all'].includes(id)) {
activeFolderType.value = 'folder';
} else {
activeFolderType.value = 'storage';
}
}
function getFolderClass(id: string) {
return activeFolder.value === id ? 'folder-text folder-text--active' : 'folder-text';
}
type FileShowType = 'Module' | 'Storage';
const showType = ref<FileShowType>('Module');
function changeShowType(val: string | number | boolean) {
showType.value = val as FileShowType;
}
const moduleKeyword = ref('');
const folderTree = ref([
{
title: 'Trunk',
key: 'node1',
count: 18,
children: [
{
title: 'Leaf',
key: 'node2',
count: 28,
},
],
},
{
title: 'Trunk',
key: 'node3',
count: 180,
children: [
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
],
},
{
title: 'Trunk',
key: 'node6',
children: [],
count: 0,
},
]);
const focusNodeKey = ref<string | number>('');
function setFocusNodeKe(node: MsTreeNodeData) {
focusNodeKey.value = node.key || '';
}
const folderMoreActions: ActionsItem[] = [
{
label: 'project.fileManagement.rename',
eventTag: 'rename',
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
danger: true,
},
];
const renamePopVisible = ref(false);
/**
* 删除文件夹
* @param node 节点信息
*/
function deleteFolder(node: MsTreeNodeData) {
openModal({
type: 'error',
title: t('project.fileManagement.deleteTipTitle', { name: node.title }),
content: t('project.fileManagement.deleteTipContent'),
okText: t('project.fileManagement.deleteConfirm'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
Message.success(t('project.fileManagement.deleteSuccess'));
} catch (error) {
console.log(error);
}
},
hideCancel: false,
});
}
const renameFolderTitle = ref(''); //
function resetFocusNodeKey() {
focusNodeKey.value = '';
renamePopVisible.value = false;
renameFolderTitle.value = '';
}
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(selectedKeys: (string | number)[]) {
[activeFolder.value] = selectedKeys;
activeFolderType.value = 'module';
}
/**
* 处理树节点更多按钮事件
* @param item
*/
function handleFolderMoreSelect(item: ActionsItem, node: MsTreeNodeData) {
switch (item.eventTag) {
case 'delete':
deleteFolder(node);
resetFocusNodeKey();
break;
case 'rename':
renameFolderTitle.value = node.title || '';
renamePopVisible.value = true;
document.querySelector(`#renameSpan${node.key}`)?.dispatchEvent(new Event('click'));
break;
default:
break;
}
}
function moreActionsClose() {
if (!renamePopVisible.value) {
// key
resetFocusNodeKey();
}
}
const storageKeyword = ref('');
const originStorageList = ref([
{
title: 'storage1',
key: '1',
count: 129,
},
{
title: 'storage2',
key: '2',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage1',
key: '1',
count: 129,
},
{
title: 'storage2',
key: '2',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage1',
key: '1',
count: 129,
},
{
title: 'storage2',
key: '2',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
{
title: 'storage3',
key: '3',
count: 129,
},
]);
const storageList = ref(originStorageList.value);
const searchStorage = debounce(() => {
storageList.value = originStorageList.value.filter((item) => item.title.includes(storageKeyword.value));
}, 300);
watch(
() => storageKeyword.value,
() => {
if (storageKeyword.value === '') {
storageList.value = originStorageList.value;
}
searchStorage();
}
);
</script>
<style lang="less" scoped>
.page {
@apply bg-white;
height: calc(100vh - 88px);
border-radius: var(--border-radius-large);
.folder {
@apply flex cursor-pointer items-center justify-between;
padding: 8px 4px;
border-radius: var(--border-radius-small);
&:hover {
background-color: rgb(var(--primary-1));
}
.folder-text {
@apply flex cursor-pointer items-center;
.folder-icon {
margin-right: 4px;
color: var(--color-text-4);
}
.folder-name {
color: var(--color-text-1);
}
.folder-count {
margin-left: 4px;
color: var(--color-text-4);
}
}
.folder-text--active {
.folder-icon,
.folder-name,
.folder-count {
color: rgb(var(--primary-5));
}
}
}
.file-show-type {
@apply grid grid-cols-2;
margin-bottom: 8px;
:deep(.arco-radio-button-content) {
@apply text-center;
}
}
}
</style>

View File

@ -0,0 +1 @@
export default {};

View File

@ -0,0 +1,25 @@
export default {
'project.fileManagement.myFile': '我的文件',
'project.fileManagement.allFile': '全部文件',
'project.fileManagement.defaultFile': '默认文件',
'project.fileManagement.expandAll': '展开全部子模块',
'project.fileManagement.collapseAll': '收起全部子模块',
'project.fileManagement.addSubModule': '添加子模块',
'project.fileManagement.rename': '重命名',
'project.fileManagement.nameNotNull': '名字不能为空',
'project.fileManagement.namePlaceholder': '请输入分组名称,按回车键保存',
'project.fileManagement.renameSuccess': '重命名成功',
'project.fileManagement.addSubModuleSuccess': '添加成功',
'project.fileManagement.nameExist': '该层级已有此模块名称',
'project.fileManagement.module': '模块',
'project.fileManagement.storage': '存储库',
'project.fileManagement.folderSearchPlaceholder': '输入名称搜索',
'project.fileManagement.delete': '删除',
'project.fileManagement.deleteSuccess': '删除成功',
'project.fileManagement.deleteTipTitle': '是否删除 `{name}` 模块?',
'project.fileManagement.deleteTipContent': '该操作会删除模块及其下所有资源,请谨慎操作!',
'project.fileManagement.deleteConfirm': '确认删除',
'project.fileManagement.noFolder': '暂无匹配的相关模块',
'project.fileManagement.noStorage': '暂无匹配的相关存储库',
'project.fileManagement.addFile': '添加文件',
};

View File

@ -437,11 +437,27 @@
);
function searchLog() {
const ranges = operateRange.value.map((e) => e);
const ranges = operateRange.value.map((e) => {
if (typeof e === 'object') {
return e.value;
}
return e;
});
let projectIds = [];
let organizationIds = [];
if (!MENU_LEVEL.includes(level.value) && typeof operateRange.value[0] === 'object') {
if (operateRange.value[0].level === MENU_LEVEL[1]) {
organizationIds = ranges;
} else if (operateRange.value[0].level === MENU_LEVEL[2]) {
projectIds = ranges;
}
}
setLoadListParams({
operUser: operUser.value,
projectIds: level.value === 'PROJECT' && ranges[0] !== 'PROJECT' ? ranges : [],
organizationIds: level.value === 'ORGANIZATION' && ranges[0] !== 'ORGANIZATION' ? ranges : [],
projectIds,
organizationIds,
type: type.value,
module: _module.value,
content: content.value,

View File

@ -268,12 +268,14 @@
title: 'system.user.tableColumnOrg',
slotName: 'organization',
dataIndex: 'organizationList',
showTooltip: true,
showInTable: true,
},
{
title: 'system.user.tableColumnUserGroup',
slotName: 'userRole',
dataIndex: 'userRoleList',
showTooltip: true,
showInTable: true,
},
{
@ -297,7 +299,6 @@
{
tableKey: TableKeyEnum.SYSTEM_USER,
columns,
scroll: { y: 'auto' },
selectable: true,
},
(record) => ({