feat(文件管理): 文件管理页面

This commit is contained in:
baiqi 2023-09-25 13:34:50 +08:00 committed by 刘瑞斌
parent eed7fdb0b2
commit 3e4b1ffcf2
18 changed files with 1800 additions and 141 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,7 @@
export default {
'common.pleaseSelectMember': 'Please select member',
'common.add': 'Add',
'common.saveAndContinue': 'Save & Continue',
'common.edit': 'Edit',
'common.delete': 'Delete',
'common.save': 'Save',

View File

@ -1,6 +1,7 @@
export default {
'common.pleaseSelectMember': '请选择成员',
'common.add': '添加',
'common.saveAndContinue': '保存并继续添加',
'common.edit': '编辑',
'common.delete': '删除',
'common.save': '保存',

View File

@ -1,7 +1,7 @@
// 路由白名单,无需校验权限与登录状态
export const WHITE_LIST = [
{ name: 'notFound', children: [] },
{ name: 'invite', children: [] },
{ name: 'notFound', path: '/notFound', children: [] },
{ name: 'invite', path: '/invite', children: [] },
];
// 左侧菜单底部对齐的菜单数组数组项为一级路由的name

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia';
import { useRoute } from 'vue-router';
import { login as userLogin, logout as userLogout, isLogin as userIsLogin } from '@/api/modules/user';
import { getHashParameters } from '@/utils';
import { setToken, clearToken } from '@/utils/auth';
import { removeRouteListener } from '@/utils/route-listener';
import useAppStore from '../app';
@ -98,14 +98,12 @@ const useUserStore = defineStore('user', {
const appStore = useAppStore();
setToken(res.sessionId, res.csrfToken);
this.setInfo(res);
const route = useRoute();
const urlOrgId = route.query.organizationId;
const urlProjectId = route.query.projectId;
const { organizationId, projectId } = getHashParameters();
// 如果访问页面的时候携带了组织 ID和项目 ID则不设置
if (!urlOrgId) {
if (!organizationId) {
appStore.setCurrentOrgId(res.lastOrganizationId || '');
}
if (!urlProjectId) {
if (!projectId) {
appStore.setCurrentProjectId(res.lastProjectId || '');
}
return true;

View File

@ -282,3 +282,23 @@ export const downloadUrlFile = (url: string, fileName: string) => {
export const getTime = (time: string): string => {
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
};
/**
* URL
* @returns
*/
export const getHashParameters = (): Record<string, string> => {
const query = window.location.hash.split('?')[1]; // 获取 URL 哈希参数部分
const paramsArray = query.split('&'); // 将哈希参数字符串分割成数组
const params: Record<string, string> = {};
// 遍历数组并解析参数
paramsArray.forEach((param) => {
const [key, value] = param.split('=');
if (key && value) {
params[key] = decodeURIComponent(value); // 解码参数值
}
});
return params;
};

View File

@ -77,20 +77,31 @@
</div>
</template>
<div class="flex h-full">
<a-spin :loading="fileLoading">
<div class="file-detail">
<div class="file-detail-icon" @click="handleFileIconClick">
<img
v-if="fileType === 'image'"
src="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/6480dbc69be1b5de95010289787d64f1.png~tplv-uwbnlip3yd-webp.webp"
class="h-full w-full"
<a-skeleton v-if="fileLoading" :loading="fileLoading" :animation="true">
<a-skeleton-shape size="large" class="mb-[16px] h-[102px] w-[102px]" />
<a-space direction="vertical" class="w-[28%]" size="large">
<a-skeleton-line :rows="11" :line-height="24" />
</a-space>
<a-space direction="vertical" class="ml-[4%] w-[68%]" size="large">
<a-skeleton-line :rows="11" :line-height="24" />
</a-space>
</a-skeleton>
<template v-else>
<div class="mb-[16px] w-[102px]">
<MsPreviewCard
mode="hover"
:type="fileDetail?.type"
:url="fileDetail?.url"
:footer-text="t('project.fileManagement.replaceFile')"
@click="handleFileIconClick"
/>
<MsIcon v-else :type="FileIconMap[fileType][UploadStatus.done]" class="h-full w-full" />
<div class="file-detail-icon-footer">
{{ t('project.fileManagement.replaceFile') }}
</div>
</div>
<MsDescription :descriptions="fileDescriptions" label-width="80px" :add-tag-func="addFileTag">
<MsDescription
:descriptions="fileDescriptions"
:label-width="currentLocale === 'zh-CN' ? '80px' : '100px'"
:add-tag-func="addFileTag"
>
<template #value="{ item }">
<div class="flex flex-wrap items-center">
<a-tooltip
@ -99,7 +110,7 @@
:disabled="item.value === undefined || item.value === null || item.value?.toString() === ''"
mini
>
<div :class="['one-line-text', item.key === 'name' ? 'max-w-[100px]' : '']">
<div :class="['one-line-text', 'flex-1']">
{{
item.value === undefined || item.value === null || item.value?.toString() === ''
? '-'
@ -108,18 +119,43 @@
</div>
</a-tooltip>
<template v-if="item.key === 'name'">
<popConfirm mode="rename" :title="t('common.rename')" :all-names="[]">
<MsButton class="!mr-[4px] ml-[8px]">{{ t('common.rename') }}</MsButton>
<popConfirm
mode="rename"
:field-config="{ placeholder: t('project.fileManagement.fileNamePlaceholder') }"
:all-names="[]"
>
<MsButton class="!mr-0 ml-[8px]">{{ t('common.rename') }}</MsButton>
</popConfirm>
<MsButton v-if="fileType === 'image'" class="ml-0" @click="previewVisible = true">
<template v-if="fileType === 'image'">
<a-divider
direction="vertical"
class="mx-[8px] min-h-[12px] rounded-[var(--border-radius-small)]"
/>
<MsButton class="ml-0" @click="previewVisible = true">
{{ t('common.preview') }}
</MsButton>
</template>
</template>
<template v-if="item.key === 'desc'">
<popConfirm
mode="rename"
:title="t('project.fileManagement.desc')"
:field-config="{
field: item.value as string,
placeholder: t('project.fileManagement.descPlaceholder'),
maxLength: 250,
isTextArea: true,
}"
:all-names="[]"
>
<MsButton class="ml-[8px]"><MsIcon type="icon-icon_edit_outlined"></MsIcon></MsButton>
</popConfirm>
</template>
</div>
</template>
</MsDescription>
</template>
</div>
</a-spin>
<div class="file-relation">
<a-tabs v-model:active-key="activeTab" :disabled="fileLoading" class="no-content">
<a-tab-pane key="case" :title="t('project.fileManagement.cases')" />
@ -170,15 +206,16 @@
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { getFileEnum, FileIconMap } from '@/components/pure/ms-upload/iconMap';
import { getFileEnum } from '@/components/pure/ms-upload/iconMap';
import MsDescription, { type Description } from '@/components/pure/ms-description/index.vue';
import popConfirm from './popConfirm.vue';
import { UploadStatus } from '@/enums/uploadEnum';
import useTable from '@/components/pure/ms-table/useTable';
import { getFileDetail, getFileCases, getFileVersions } from '@/api/modules/project-management/fileManagement';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import MsPreviewCard from '@/components/business/ms-thumbnail-card/index.vue';
import { TableKeyEnum } from '@/enums/tableEnum';
import { downloadUrlFile } from '@/utils';
import useLocale from '@/locale/useLocale';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
@ -193,6 +230,7 @@
const { file: newFile, open } = useFileSystemAccess();
const { t } = useI18n();
const { currentLocale } = useLocale();
const innerVisible = ref(false);
const fileDetail = ref();
@ -228,6 +266,11 @@
value: fileDetail.value.name,
key: 'name',
},
{
label: t('project.fileManagement.desc'),
value: fileDetail.value.desc,
key: 'desc',
},
{
label: t('project.fileManagement.type'),
value: fileDetail.value.type,
@ -289,9 +332,6 @@
() => props.fileId,
() => {
initFileDetail();
},
{
immediate: true,
}
);
@ -414,14 +454,17 @@
{
title: 'project.fileManagement.record',
dataIndex: 'record',
showTooltip: true,
},
{
title: 'project.fileManagement.creator',
dataIndex: 'creator',
showTooltip: true,
},
{
title: 'project.fileManagement.createTime',
dataIndex: 'createTime',
width: 180,
},
];
const {
@ -436,11 +479,13 @@
});
watchEffect(() => {
if (innerVisible.value) {
if (activeTab.value === 'case') {
loadCaseList();
} else {
loadVersionList();
}
}
});
</script>
@ -451,30 +496,6 @@
padding: 16px;
width: 300px;
border-right: 1px solid var(--color-text-n8);
.file-detail-icon {
@apply relative inline-block cursor-pointer overflow-hidden;
margin-bottom: 16px;
width: 102px;
height: 102px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
&:hover {
.file-detail-icon-footer {
@apply visible;
}
}
.file-detail-icon-footer {
@apply invisible absolute w-full text-center;
bottom: 0;
padding: 2px 0;
font-size: 12px;
font-weight: 500;
color: #ffffff;
background-color: #00000050;
}
}
}
.file-relation {
width: 660px;

View File

@ -3,7 +3,7 @@
v-model:model-value="moduleKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear
class="mb-[8px]"
class="mb-[16px]"
></a-input>
<MsTree
v-model:focus-node-key="focusNodeKey"
@ -13,7 +13,8 @@
:node-more-actions="folderMoreActions"
:expand-all="props.isExpandAll"
:empty-text="t('project.fileManagement.noFolder')"
draggable
:draggable="!props.isModal"
:virtual-list-props="virtualListProps"
block-node
@select="folderNodeSelect"
@more-action-select="handleFolderMoreSelect"
@ -21,15 +22,15 @@
>
<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>
<span v-if="!props.isModal" class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count }})</span>
</template>
<template #extra="nodeData">
<template v-if="!props.isModal" #extra="nodeData">
<popConfirm mode="add" :all-names="[]" @close="resetFocusNodeKey">
<MsButton type="icon" 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">
<popConfirm mode="rename" :field-config="{ field: renameFolderTitle }" :all-names="[]" @close="resetFocusNodeKey">
<span :id="`renameSpan${nodeData.key}`" class="relative"></span>
</popConfirm>
</template>
@ -37,7 +38,7 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import useModal from '@/hooks/useModal';
import { useI18n } from '@/hooks/useI18n';
@ -52,12 +53,23 @@
const props = defineProps<{
isExpandAll: boolean;
selectedKeys?: Array<string | number>; // key
isModal?: boolean; //
}>();
const emit = defineEmits(['update:selectedKeys', 'folderNodeSelect']);
const { t } = useI18n();
const { openModal } = useModal();
const virtualListProps = computed(() => {
if (props.isModal) {
return {
height: 'calc(60vh - 190px)',
};
}
return {
height: 'calc(100vh - 320px)',
};
});
const moduleKeyword = ref('');
const folderTree = ref([
{
@ -70,6 +82,36 @@
key: 'node2',
count: 28,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
],
},
{
@ -87,6 +129,36 @@
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
{
title: 'Leaf',
key: 'node4',
count: 138,
},
{
title: 'Leaf',
key: 'node5',
count: 108,
},
],
},
{

View File

@ -11,21 +11,35 @@
>
<template #content>
<div class="mb-[8px] font-medium">
{{ props.mode === 'add' ? t('project.fileManagement.addSubModule') : t('project.fileManagement.rename') }}
{{
props.title ||
(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"
field="field"
: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')"
<a-textarea
v-if="props.fieldConfig?.isTextArea"
v-model:model-value="form.field"
:max-length="props.fieldConfig?.maxLength"
:auto-size="{ maxRows: 4 }"
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]"
@press-enter="beforeConfirm(undefined)"
></a-input>
>
</a-textarea>
<a-input
v-else
v-model:model-value="form.field"
:max-length="props.fieldConfig?.maxLength"
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]"
@press-enter="beforeConfirm(undefined)"
/>
</a-form-item>
</a-form>
</template>
@ -34,11 +48,19 @@
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { Message } from '@arco-design/web-vue';
import type { FormInstance } from '@arco-design/web-vue';
import type { FormInstance, FieldRule } from '@arco-design/web-vue';
interface FieldConfig {
field?: string;
rules?: FieldRule[];
placeholder?: string;
maxLength?: number;
isTextArea?: boolean;
}
const props = defineProps<{
mode: 'add' | 'rename';
@ -46,7 +68,7 @@
title?: string;
allNames: string[];
popupContainer?: string;
placeholder?: string;
fieldConfig?: FieldConfig;
}>();
const emit = defineEmits(['update:visible', 'close']);
@ -55,15 +77,15 @@
const innerVisible = ref(props.visible || false);
const form = ref({
name: props.title || '',
field: props.fieldConfig?.field || '',
});
const formRef = ref<FormInstance>();
const loading = ref(false);
watch(
() => props.title,
() => props.fieldConfig?.field,
(val) => {
form.value.name = val || '';
form.value.field = val || '';
}
);
@ -117,7 +139,7 @@
}
function reset() {
form.value.name = '';
form.value.field = '';
formRef.value?.resetFields();
}
</script>

View File

@ -1,14 +1,20 @@
<template>
<div class="p-[24px]">
<div class="flex h-[calc(100vh-88px)] flex-col overflow-hidden p-[24px]">
<div class="header">
<a-button type="primary" @click="uploadDrawerVisible = true">{{ t('project.fileManagement.addFile') }}</a-button>
<a-button type="primary" @click="handleAddClick">{{ t('project.fileManagement.addFile') }}</a-button>
<div class="header-right">
<a-select v-model="tableFileType" class="w-[240px]" @change="searchList">
<a-option key="" value="">{{ t('common.all') }}</a-option>
<a-option v-for="item of tableFileTypeOptions" :key="item" :value="item">
{{ item }}
</a-option>
</a-select>
<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-if="props.activeFolderType === 'folder'"
v-model:model-value="fileType"
@ -25,7 +31,15 @@
</a-radio-group>
</div>
</div>
<ms-base-table v-bind="propsRes" no-disable v-on="propsEvent">
<ms-base-table
v-if="showType === 'list'"
v-bind="propsRes"
:action-config="fileType === 'module' ? moduleFileBatchActions : storageFileBatchActions"
no-disable
v-on="propsEvent"
@selected-change="handleTableSelect"
@batch-action="handleTableBatch"
>
<template #name="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="openFileDetail(record.id, rowIndex)">{{ record.name }}</a-button>
</template>
@ -38,18 +52,37 @@
</MsButton>
<MsTableMoreAction
:list="record.type === 'JAR' ? jarFileActions : normalFileActions"
@select="handleSelect($event, record)"
></MsTableMoreAction>
@select="handleMoreActionSelect($event, record)"
/>
</template>
<template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('project.fileManagement.tableNoFile') }}
<MsButton class="ml-[8px]" @click="uploadDrawerVisible = true">
<MsButton class="ml-[8px]" @click="handleAddClick">
{{ t('project.fileManagement.addFile') }}
</MsButton>
</div>
</template>
</ms-base-table>
<MsCardList
v-else-if="showType === 'card'"
mode="remote"
:remote-func="getFileList"
:shadow-limit="50"
:card-min-width="102"
class="flex-1"
>
<template #item="{ item, index }">
<MsThumbnailCard
:type="item.type"
:url="item.url"
:footer-text="item.name"
:more-actions="item.type === 'JAR' ? jarFileActions : normalFileActions"
@click="openFileDetail(item.id, index)"
@action-select="handleMoreActionSelect($event, item)"
/>
</template>
</MsCardList>
</div>
<MsDrawer v-model:visible="uploadDrawerVisible" :title="t('project.fileManagement.addFile')" :width="680">
<div class="mb-[8px] flex items-center justify-between text-[var(--color-text-1)]">
@ -123,6 +156,86 @@
</a-button>
</template>
</MsDrawer>
<a-modal
v-model:visible="storageDialogVisible"
:title="t('project.fileManagement.addFile')"
title-align="start"
class="ms-modal-form ms-modal-medium"
:mask-closable="false"
@close="handleStorageModalCancel"
>
<a-form ref="storageFormRef" class="rounded-[4px]" :model="storageForm" layout="vertical">
<a-form-item
field="branch"
:label="t('project.fileManagement.gitBranch')"
:rules="[{ required: true, message: t('project.fileManagement.gitBranchNotNull') }]"
required
asterisk-position="end"
>
<a-input
v-model:model-value="storageForm.branch"
:placeholder="t('project.fileManagement.gitBranchPlaceholder')"
:max-length="250"
></a-input>
</a-form-item>
<a-form-item
field="path"
:label="t('project.fileManagement.gitFilePath')"
:rules="[{ required: true, message: t('project.fileManagement.gitFilePathNotNull') }]"
required
asterisk-position="end"
>
<a-input
v-model:model-value="storageForm.path"
:placeholder="t('project.fileManagement.gitFilePathPlaceholder')"
:max-length="250"
></a-input>
<MsFormItemSub :text="t('project.fileManagement.gitFilePathSub')" :show-fill-icon="false" />
</a-form-item>
</a-form>
<template #footer>
<a-button type="secondary" :disabled="storageModalLoading" @click="handleStorageModalCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="secondary" :loading="storageModalLoading" @click="saveAndContinue">
{{ t('common.saveAndContinue') }}
</a-button>
<a-button type="primary" :loading="storageModalLoading" @click="beforeAddStorageFile">
{{ t('common.add') }}
</a-button>
</template>
</a-modal>
<a-modal
v-model:visible="moveModalVisible"
title-align="start"
class="ms-modal-no-padding ms-modal-small"
:mask-closable="false"
:ok-text="t('project.fileManagement.batchMoveConfirm')"
:ok-button-props="{ disabled: selectedModuleKeys.length === 0 }"
:cancel-button-props="{ disabled: batchMoveFileLoading }"
:on-before-ok="batchMoveFile"
@close="handleMoveFileModalCancel"
>
<template #title>
<div class="flex items-center">
{{ isBatchMove ? t('project.fileManagement.batchMoveTitle') : t('project.fileManagement.singleMoveTitle') }}
<div class="ml-[4px] text-[var(--color-text-4)]">
{{
isBatchMove
? t('project.fileManagement.batchMoveTitleSub', { count: tableSelected.length })
: `(${activeFile?.name})`
}}
</div>
</div>
</template>
<folderTree
v-if="moveModalVisible"
v-model:selected-keys="selectedModuleKeys"
:is-expand-all="true"
is-modal
@folder-node-select="folderNodeSelect"
/>
</a-modal>
<fileDetailDrawerVue
v-model:visible="showDetailDrawer"
:file-id="activeFileId"
@ -136,7 +249,7 @@
<script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { Message, ValidatedError } from '@arco-design/web-vue';
import { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/store/modules/ms-table';
@ -156,9 +269,14 @@
import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.vue';
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import MsThumbnailCard from '@/components/business/ms-thumbnail-card/index.vue';
import MsCardList from '@/components/business/ms-card-list/index.vue';
import fileDetailDrawerVue from './fileDetailDrawer.vue';
import folderTree from './folderTree.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type';
import type { FormInstance } from '@arco-design/web-vue';
import type { BatchActionParams, MsTableColumn } from '@/components/pure/ms-table/type';
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
@ -172,9 +290,8 @@
const asyncTaskStore = useAsyncTaskStore();
const { openModal } = useModal();
const keyword = ref('');
const fileType = ref('module');
const acceptType = ref<UploadType>('none');
const fileType = ref('module'); // /
const acceptType = ref<UploadType>('none'); // -
const isUploading = ref(false);
watch(
@ -188,15 +305,11 @@
}
);
function changeFileType() {
console.log(fileType.value);
}
function changeFileType() {}
const showType = ref('list');
const showType = ref<'list' | 'card'>('list'); //
function changeShowType() {
console.log(showType.value);
}
function changeShowType() {}
function getCardClass(type: 'none' | 'jar') {
if (acceptType.value !== type && isUploading.value) {
@ -209,6 +322,13 @@
}
const normalFileActions: ActionsItem[] = [
{
label: 'project.fileManagement.move',
eventTag: 'move',
},
{
isDivider: true,
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
@ -217,6 +337,10 @@
];
const jarFileActions: ActionsItem[] = [
{
label: 'project.fileManagement.move',
eventTag: 'move',
},
{
label: 'common.disable',
eventTag: 'disabled',
@ -241,6 +365,7 @@
{
title: 'project.fileManagement.type',
dataIndex: 'type',
width: 90,
},
{
title: 'project.fileManagement.tag',
@ -251,20 +376,22 @@
{
title: 'project.fileManagement.creator',
dataIndex: 'creator',
showTooltip: true,
},
{
title: 'project.fileManagement.updater',
dataIndex: 'updater',
showTooltip: true,
},
{
title: 'project.fileManagement.updateTime',
dataIndex: 'updateTime',
width: 170,
width: 180,
},
{
title: 'project.fileManagement.createTime',
dataIndex: 'createTime',
width: 170,
width: 180,
},
{
title: 'common.operation',
@ -276,32 +403,67 @@
];
const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.FILE_MANAGEMENT_FILE, columns, 'drawer');
const { propsRes, propsEvent, loadList } = useTable(getFileList, {
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getFileList, {
tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
columns,
showSetting: true,
selectable: true,
showSelectAll: true,
});
watch(
() => props.activeFolder,
() => {
keyword.value = '';
debounce(loadList, 200)();
const moduleFileBatchActions = {
baseAction: [
{
label: 'project.fileManagement.download',
eventTag: 'download',
},
{ immediate: true }
);
{
label: 'project.fileManagement.move',
eventTag: 'move',
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
danger: true,
},
],
};
const storageFileBatchActions = {
baseAction: [
{
label: 'project.fileManagement.download',
eventTag: 'download',
},
{
label: 'project.fileManagement.delete',
eventTag: 'delete',
danger: true,
},
],
};
const tableSelected = ref<(string | number)[]>([]);
function downloadFile(url: string, name: string) {
downloadUrlFile(url, name);
/**
* 处理表格选中
*/
function handleTableSelect(arr: (string | number)[]) {
tableSelected.value = arr;
}
function batchDownload() {}
/**
* 删除文件
*/
function delFile(record: any) {
function delFile(record: any, isBatch?: boolean) {
let title = t('project.fileManagement.deleteFileTipTitle', { name: characterLimit(record?.name) });
let selectIds = [record?.id];
if (isBatch) {
title = t('project.fileManagement.batchDeleteFileTipTitle', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[];
}
openModal({
type: 'error',
title: t('project.fileManagement.deleteFileTipTitle', { name: characterLimit(record.name) }),
title,
content: t('project.fileManagement.deleteFileTipContent'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
@ -311,6 +473,8 @@
maskClosable: false,
onBeforeOk: async () => {
try {
console.log(selectIds);
Message.success(t('common.deleteSuccess'));
loadList();
} catch (error) {
@ -321,6 +485,102 @@
});
}
const moveModalVisible = ref(false); //
const selectedModuleKeys = ref<(string | number)[]>([]); //
const isBatchMove = ref(false); //
const activeFile = ref<any>(null); //
/**
* 处理文件夹树节点选中事件
*/
function folderNodeSelect(keys: (string | number)[]) {
selectedModuleKeys.value = keys;
}
/**
* 处理表格选中后批量操作
* @param event 批量操作事件对象
*/
function handleTableBatch(event: BatchActionParams) {
switch (event.eventTag) {
case 'download':
batchDownload();
break;
case 'move':
moveModalVisible.value = true;
isBatchMove.value = true;
break;
case 'delete':
delFile(null, true);
break;
default:
break;
}
}
const batchMoveFileLoading = ref(false);
/**
* 单个/批量移动文件
*/
async function batchMoveFile() {
try {
batchMoveFileLoading.value = true;
await new Promise((resolve) => {
setTimeout(() => resolve(true), 2000);
});
Message.success(t('project.fileManagement.batchMoveSuccess'));
if (isBatchMove.value) {
tableSelected.value = [];
isBatchMove.value = false;
} else {
activeFile.value = null;
}
loadList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
batchMoveFileLoading.value = false;
}
}
function handleMoveFileModalCancel() {
moveModalVisible.value = false;
selectedModuleKeys.value = [];
}
const keyword = ref('');
const tableFileType = ref('');
const tableFileTypeOptions = ref(['JPG', 'PNG']);
const searchList = debounce(() => {
setLoadListParams({
fileType: tableFileType.value,
keyword: keyword.value,
});
loadList();
}, 300);
watch(
() => props.activeFolder,
() => {
keyword.value = '';
searchList();
},
{ immediate: true }
);
watch(
() => keyword.value,
() => {
searchList();
}
);
function downloadFile(url: string, name: string) {
downloadUrlFile(url, name);
}
/**
* 禁用 jar 文件
*/
@ -348,8 +608,13 @@
* 处理表格更多按钮事件
* @param item
*/
function handleSelect(item: ActionsItem, record: any) {
function handleMoreActionSelect(item: ActionsItem, record: any) {
switch (item.eventTag) {
case 'move':
isBatchMove.value = false;
activeFile.value = record;
moveModalVisible.value = true;
break;
case 'delete':
delFile(record);
break;
@ -376,7 +641,7 @@
propsRes.value.msPagination!.total
);
async function openFileDetail(id: string, index: number) {
async function openFileDetail(id: string | number, index: number) {
showDetailDrawer.value = true;
activeFileId.value = id;
activeFileIndex.value = index;
@ -414,15 +679,19 @@
}
}
const uploadDrawerVisible = ref(false);
const uploadDrawerVisible = ref(false); // -
const fileList = ref<MsFileItem[]>(asyncTaskStore.uploadFileTask.fileList);
//
const noWaitingUpload = computed(
() =>
fileList.value.filter((e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading))
.length === 0
);
/**
* 设置上传文件类型
* @param type 文件类型
*/
function setAcceptType(type: UploadType) {
if (isUploading.value) return;
acceptType.value = type;
@ -461,6 +730,9 @@
isUploading.value = true;
}
/**
* 后台上传
*/
function backstageUpload() {
fileListRef.value?.backstageUpload();
uploadDrawerVisible.value = false;
@ -525,6 +797,81 @@
}
}
);
const storageDialogVisible = ref(false); // -
const storageForm = ref({
branch: '',
path: '',
}); // -
const storageFormRef = ref<FormInstance>(); // -ref
const storageModalLoading = ref(false); // -loading
/**
* 处理添加文件按钮点击事件根据当前查看的文件类型打开不同的弹窗
*/
function handleAddClick() {
if (fileType.value === 'module') {
uploadDrawerVisible.value = true;
} else if (fileType.value === 'storage') {
storageDialogVisible.value = true;
}
}
function handleStorageModalCancel() {
storageFormRef.value?.resetFields();
storageDialogVisible.value = false;
}
/**
* 存储库-添加文件
* @param isContinue 是否继续添加
*/
async function addStorageFile(isContinue?: boolean) {
const params = {
branch: storageForm.value.branch,
path: storageForm.value.path,
};
// await batchCreateUser(params);
Message.success(t('common.addSuccess'));
if (!isContinue) {
storageDialogVisible.value = false;
}
loadList();
}
/**
* 存储库-触发添加文件表单校验
* @param cb 校验通过后执行回调
*/
function storageFormValidate(cb: () => Promise<any>) {
storageFormRef.value?.validate(async (errors: undefined | Record<string, ValidatedError>) => {
if (!errors) {
try {
storageModalLoading.value = true;
await cb();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
storageModalLoading.value = false;
}
}
});
}
function saveAndContinue() {
storageFormValidate(async () => {
await addStorageFile(true);
storageFormRef.value?.resetFields();
});
}
function beforeAddStorageFile() {
storageFormValidate(async () => {
await addStorageFile();
handleStorageModalCancel();
});
}
</script>
<style lang="less" scoped>
@ -583,4 +930,13 @@
color: var(--color-text-4);
}
}
.card-list {
@apply grid flex-1 overflow-auto;
.ms-scroll-bar();
.ms-container--shadow();
gap: 24px;
grid-template-columns: repeat(auto-fill, minmax(102px, 1fr));
aspect-ratio: 1/1;
}
</style>

View File

@ -29,7 +29,12 @@
</div>
</template>
<template #itemAction="{ item }">
<popConfirm mode="rename" :title="renameStorageTitle" :all-names="[]" @close="resetFocusItemKey">
<popConfirm
mode="rename"
:field-config="{ field: renameStorageTitle }"
:all-names="[]"
@close="resetFocusItemKey"
>
<span :id="`renameSpan${item.key}`" class="relative"></span>
</popConfirm>
</template>

View File

@ -68,7 +68,11 @@ export default {
'After cancellation, files that have not been successfully uploaded will not be saved, so please operate with caution!',
'project.fileManagement.cancelConfirm': 'Cancel upload',
'project.fileManagement.name': 'File name',
'project.fileManagement.type': 'Type',
'project.fileManagement.type': 'Format',
'project.fileManagement.fileNamePlaceholder': 'Please enter the file name and press Enter to save',
'project.fileManagement.desc': 'Description',
'project.fileManagement.updateDesc': 'Update description',
'project.fileManagement.descPlaceholder': 'Please enter content',
'project.fileManagement.tag': 'Tag',
'project.fileManagement.creator': 'Creator',
'project.fileManagement.updater': 'Updater',
@ -76,10 +80,47 @@ export default {
'project.fileManagement.createTime': 'Created time',
'project.fileManagement.download': 'Download',
'project.fileManagement.disabled': 'Disable',
'project.fileManagement.move': 'Move to',
'project.fileManagement.deleteFileTipTitle': 'Are you sure you want to delete the file {name}?',
'project.fileManagement.deleteFileTipContent':
'After deletion, the use cases associated with the file will fail to execute. Please operate with caution!',
'project.fileManagement.disabledFileTipTitle': 'Are you sure you want to disable the file {name}?',
'project.fileManagement.disabledFileTipContent':
'After being disabled, the custom script associated with the file will fail to execute, so please operate with caution!',
'project.fileManagement.batchDeleteFileTipTitle': 'Are you sure you want to delete these {count} files?',
'project.fileManagement.batchDeleteFileTipContent':
'After deletion, the use cases associated with these files will fail to execute, so please operate with caution!',
'project.fileManagement.detail': 'File details',
'project.fileManagement.prev': 'Prev',
'project.fileManagement.noPrev': 'Currently the first',
'project.fileManagement.next': 'Next',
'project.fileManagement.noNext': 'Currently the last one',
'project.fileManagement.updateFile': 'Update file',
'project.fileManagement.replaceFile': 'Replace file',
'project.fileManagement.replaceFileSuccess': 'File replacement successful',
'project.fileManagement.size': 'File size',
'project.fileManagement.fileModule': 'Module',
'project.fileManagement.gitBranch': 'Git branch',
'project.fileManagement.gitPath': 'Git path',
'project.fileManagement.gitVersion': 'Git version',
'project.fileManagement.cases': 'Related Use Cases',
'project.fileManagement.versionHistory': 'Version history',
'project.fileManagement.updateCaseFile': 'Update use case file',
'project.fileManagement.id': 'ID',
'project.fileManagement.fileVersion': 'File version',
'project.fileManagement.record': 'Update history',
'project.fileManagement.caseList': 'Use case list',
'project.fileManagement.search': 'Enter name to search',
'project.fileManagement.gitBranchNotNull': 'Git branch cannot be empty',
'project.fileManagement.gitBranchPlaceholder': 'Please enter the git branch, such as: master',
'project.fileManagement.gitFilePath': 'File path',
'project.fileManagement.gitFilePathNotNull': 'File path cannot be empty',
'project.fileManagement.gitFilePathPlaceholder': 'Please enter the file path, such as: /xxxxxx.xx',
'project.fileManagement.gitFilePathSub': 'No need to add file path separator before root directory: /',
'project.fileManagement.batchMoveTitle': 'Batch move',
'project.fileManagement.singleMoveTitle': 'Move',
'project.fileManagement.batchMoveTitleSub': '({count} files selected)',
'project.fileManagement.batchMoveSearchPlaceholder': 'Please enter the module name to search',
'project.fileManagement.batchMoveConfirm': 'Move to selected module',
'project.fileManagement.batchMoveSuccess': 'File moved successfully',
};

View File

@ -64,7 +64,11 @@ export default {
'project.fileManagement.cancelTipContent': '取消后,未上传成功的文件不会被保存,请谨慎操作!',
'project.fileManagement.cancelConfirm': '取消上传',
'project.fileManagement.name': '文件名称',
'project.fileManagement.type': '文件类型',
'project.fileManagement.fileNamePlaceholder': '请输入文件名称,按回车键保存',
'project.fileManagement.desc': '文件描述',
'project.fileManagement.updateDesc': '更新描述',
'project.fileManagement.descPlaceholder': '请输入内容',
'project.fileManagement.type': '文件格式',
'project.fileManagement.tag': '标签',
'project.fileManagement.creator': '创建人',
'project.fileManagement.updater': '更新人',
@ -72,10 +76,12 @@ export default {
'project.fileManagement.createTime': '创建时间',
'project.fileManagement.download': '下载',
'project.fileManagement.disabled': '禁用',
'project.fileManagement.move': '移动',
'project.fileManagement.deleteFileTipTitle': '确认删除 {name} 这个文件吗?',
'project.fileManagement.deleteFileTipContent': '删除后,导致关联该文件的用例执行失败,请谨慎操作!',
'project.fileManagement.deleteFileTipContent': '删除后,导致关联该文件的用例执行失败,请谨慎操作!',
'project.fileManagement.disabledFileTipTitle': '确认禁用 {name} 这个文件吗?',
'project.fileManagement.disabledFileTipContent': '禁用后,会导致关联该文件的自定义脚本执行失败,请谨慎操作!',
'project.fileManagement.batchDeleteFileTipTitle': '确认删除这 {count} 个文件吗?',
'project.fileManagement.detail': '文件详情',
'project.fileManagement.prev': '上一个',
'project.fileManagement.noPrev': '当前已是第一个',
@ -97,4 +103,16 @@ export default {
'project.fileManagement.record': '更新历史',
'project.fileManagement.caseList': '用例列表',
'project.fileManagement.search': '输入名称搜索',
'project.fileManagement.gitBranchNotNull': 'git 分支不能为空',
'project.fileManagement.gitBranchPlaceholder': '请输入 git 分支master',
'project.fileManagement.gitFilePath': '文件路径',
'project.fileManagement.gitFilePathNotNull': '文件路径不能为空',
'project.fileManagement.gitFilePathPlaceholder': '请输入文件路径,如:/xxxxxx.xx',
'project.fileManagement.gitFilePathSub': '根目录前无需添加文件路径分隔符:/',
'project.fileManagement.batchMoveTitle': '批量移动',
'project.fileManagement.singleMoveTitle': '移动',
'project.fileManagement.batchMoveTitleSub': '(已选 {count} 个文件)',
'project.fileManagement.batchMoveSearchPlaceholder': '请输入模块名称进行搜索',
'project.fileManagement.batchMoveConfirm': '移动至所选模块',
'project.fileManagement.batchMoveSuccess': '文件移动成功',
};

View File

@ -157,13 +157,14 @@
allow-clear
/>
</a-form-item>
<a-form-item :label="t('system.config.email.from')" field="from" asterisk-position="end" :rules="[emailRule]">
<a-form-item :label="t('system.config.email.from')" field="from" asterisk-position="end">
<a-input
v-model:model-value="emailConfigForm.from"
:max-length="250"
:placeholder="t('system.config.email.fromPlaceholder')"
allow-clear
></a-input>
<MsFormItemSub :text="t('system.config.email.fromTip')" :show-fill-icon="false" />
</a-form-item>
<a-form-item
:label="t('system.config.email.recipient')"

View File

@ -39,6 +39,8 @@ export default {
'system.config.email.passwordRequired': 'SMTP password cannot be empty',
'system.config.email.passwordPlaceholder': 'Please enter SMTP password',
'system.config.email.fromPlaceholder': 'Please enter the designated sender email',
'system.config.email.fromTip':
'Note: It must be an email address that has been verified by the mail server, otherwise it will be sent by SMTP account by default',
'system.config.email.recipientPlaceholder': 'Please enter the email address of the test recipient',
'system.config.email.sslTip': 'If the SMTP port is 465, SSL needs to be enabled',
'system.config.email.tslTip': 'If the SMTP port is 587, TSL needs to be enabled',

View File

@ -39,6 +39,7 @@ export default {
'system.config.email.passwordRequired': 'SMTP 密码不能为空',
'system.config.email.passwordPlaceholder': '请输入SMTP 密码',
'system.config.email.fromPlaceholder': '请输入指定发件人邮箱',
'system.config.email.fromTip': '注:必须是邮件服务器验证通过的邮箱,否则默认为 SMTP 账户发送',
'system.config.email.recipientPlaceholder': '请输入测试收件人邮箱',
'system.config.email.sslTip': '若 SMTP 端口是 465需要启用 SSL',
'system.config.email.tslTip': '若 SMTP 端口是 587需要启用 TSL',

View File

@ -345,7 +345,6 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
import { getYaml, YamlType, job } from './template';

View File

@ -298,9 +298,9 @@
/**
* 重置密码
*/
function resetPassword(record: any, isBatch?: boolean) {
function resetPassword(record?: UserListItem, isBatch?: boolean) {
let title = t('system.user.resetPswTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id];
let selectIds = [record?.id || ''];
if (isBatch) {
title = t('system.user.batchResetPswTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[];
@ -330,9 +330,9 @@
/**
* 禁用用户
*/
function disabledUser(record: any, isBatch?: boolean) {
function disabledUser(record?: UserListItem, isBatch?: boolean) {
let title = t('system.user.disableUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id];
let selectIds = [record?.id || ''];
if (isBatch) {
title = t('system.user.batchDisableUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[];
@ -365,9 +365,9 @@
/**
* 启用用户
*/
function enableUser(record: any, isBatch?: boolean) {
function enableUser(record?: UserListItem, isBatch?: boolean) {
let title = t('system.user.enableUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id];
let selectIds = [record?.id || ''];
if (isBatch) {
title = t('system.user.batchEnableUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[];
@ -400,9 +400,9 @@
/**
* 删除用户
*/
function deleteUser(record: any, isBatch?: boolean) {
function deleteUser(record?: UserListItem, isBatch?: boolean) {
let title = t('system.user.deleteUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id];
let selectIds = [record?.id || ''];
if (isBatch) {
title = t('system.user.batchDeleteUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[];
@ -508,16 +508,16 @@
showBatchModal.value = true;
break;
case 'resetPassword':
resetPassword(null, true);
resetPassword(undefined, true);
break;
case 'disabled':
disabledUser(null, true);
disabledUser(undefined, true);
break;
case 'enable':
enableUser(null, true);
enableUser(undefined, true);
break;
case 'delete':
deleteUser(null, true);
deleteUser(undefined, true);
break;
default:
break;
@ -528,7 +528,7 @@
* 处理表格更多按钮事件
* @param item
*/
function handleSelect(item: ActionsItem, record: any) {
function handleSelect(item: ActionsItem, record: UserListItem) {
switch (item.eventTag) {
case 'resetPassword':
resetPassword(record);