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 { export default {
'common.pleaseSelectMember': 'Please select member', 'common.pleaseSelectMember': 'Please select member',
'common.add': 'Add', 'common.add': 'Add',
'common.saveAndContinue': 'Save & Continue',
'common.edit': 'Edit', 'common.edit': 'Edit',
'common.delete': 'Delete', 'common.delete': 'Delete',
'common.save': 'Save', 'common.save': 'Save',

View File

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

View File

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

View File

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

View File

@ -282,3 +282,23 @@ export const downloadUrlFile = (url: string, fileName: string) => {
export const getTime = (time: string): string => { export const getTime = (time: string): string => {
return dayjs(time).format('YYYY-MM-DD HH:mm:ss'); 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> </div>
</template> </template>
<div class="flex h-full"> <div class="flex h-full">
<a-spin :loading="fileLoading"> <div class="file-detail">
<div class="file-detail"> <a-skeleton v-if="fileLoading" :loading="fileLoading" :animation="true">
<div class="file-detail-icon" @click="handleFileIconClick"> <a-skeleton-shape size="large" class="mb-[16px] h-[102px] w-[102px]" />
<img <a-space direction="vertical" class="w-[28%]" size="large">
v-if="fileType === 'image'" <a-skeleton-line :rows="11" :line-height="24" />
src="https://p1-arco.byteimg.com/tos-cn-i-uwbnlip3yd/6480dbc69be1b5de95010289787d64f1.png~tplv-uwbnlip3yd-webp.webp" </a-space>
class="h-full w-full" <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> </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 }"> <template #value="{ item }">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<a-tooltip <a-tooltip
@ -99,7 +110,7 @@
:disabled="item.value === undefined || item.value === null || item.value?.toString() === ''" :disabled="item.value === undefined || item.value === null || item.value?.toString() === ''"
mini 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() === '' item.value === undefined || item.value === null || item.value?.toString() === ''
? '-' ? '-'
@ -108,18 +119,43 @@
</div> </div>
</a-tooltip> </a-tooltip>
<template v-if="item.key === 'name'"> <template v-if="item.key === 'name'">
<popConfirm mode="rename" :title="t('common.rename')" :all-names="[]"> <popConfirm
<MsButton class="!mr-[4px] ml-[8px]">{{ t('common.rename') }}</MsButton> mode="rename"
:field-config="{ placeholder: t('project.fileManagement.fileNamePlaceholder') }"
:all-names="[]"
>
<MsButton class="!mr-0 ml-[8px]">{{ t('common.rename') }}</MsButton>
</popConfirm>
<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> </popConfirm>
<MsButton v-if="fileType === 'image'" class="ml-0" @click="previewVisible = true">
{{ t('common.preview') }}
</MsButton>
</template> </template>
</div> </div>
</template> </template>
</MsDescription> </MsDescription>
</div> </template>
</a-spin> </div>
<div class="file-relation"> <div class="file-relation">
<a-tabs v-model:active-key="activeTab" :disabled="fileLoading" class="no-content"> <a-tabs v-model:active-key="activeTab" :disabled="fileLoading" class="no-content">
<a-tab-pane key="case" :title="t('project.fileManagement.cases')" /> <a-tab-pane key="case" :title="t('project.fileManagement.cases')" />
@ -170,15 +206,16 @@
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/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 MsDescription, { type Description } from '@/components/pure/ms-description/index.vue';
import popConfirm from './popConfirm.vue'; import popConfirm from './popConfirm.vue';
import { UploadStatus } from '@/enums/uploadEnum';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import { getFileDetail, getFileCases, getFileVersions } from '@/api/modules/project-management/fileManagement'; import { getFileDetail, getFileCases, getFileVersions } from '@/api/modules/project-management/fileManagement';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; 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 { TableKeyEnum } from '@/enums/tableEnum';
import { downloadUrlFile } from '@/utils'; import { downloadUrlFile } from '@/utils';
import useLocale from '@/locale/useLocale';
import type { MsTableColumn } from '@/components/pure/ms-table/type'; import type { MsTableColumn } from '@/components/pure/ms-table/type';
@ -193,6 +230,7 @@
const { file: newFile, open } = useFileSystemAccess(); const { file: newFile, open } = useFileSystemAccess();
const { t } = useI18n(); const { t } = useI18n();
const { currentLocale } = useLocale();
const innerVisible = ref(false); const innerVisible = ref(false);
const fileDetail = ref(); const fileDetail = ref();
@ -228,6 +266,11 @@
value: fileDetail.value.name, value: fileDetail.value.name,
key: 'name', key: 'name',
}, },
{
label: t('project.fileManagement.desc'),
value: fileDetail.value.desc,
key: 'desc',
},
{ {
label: t('project.fileManagement.type'), label: t('project.fileManagement.type'),
value: fileDetail.value.type, value: fileDetail.value.type,
@ -289,9 +332,6 @@
() => props.fileId, () => props.fileId,
() => { () => {
initFileDetail(); initFileDetail();
},
{
immediate: true,
} }
); );
@ -414,14 +454,17 @@
{ {
title: 'project.fileManagement.record', title: 'project.fileManagement.record',
dataIndex: 'record', dataIndex: 'record',
showTooltip: true,
}, },
{ {
title: 'project.fileManagement.creator', title: 'project.fileManagement.creator',
dataIndex: 'creator', dataIndex: 'creator',
showTooltip: true,
}, },
{ {
title: 'project.fileManagement.createTime', title: 'project.fileManagement.createTime',
dataIndex: 'createTime', dataIndex: 'createTime',
width: 180,
}, },
]; ];
const { const {
@ -436,10 +479,12 @@
}); });
watchEffect(() => { watchEffect(() => {
if (activeTab.value === 'case') { if (innerVisible.value) {
loadCaseList(); if (activeTab.value === 'case') {
} else { loadCaseList();
loadVersionList(); } else {
loadVersionList();
}
} }
}); });
</script> </script>
@ -451,30 +496,6 @@
padding: 16px; padding: 16px;
width: 300px; width: 300px;
border-right: 1px solid var(--color-text-n8); 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 { .file-relation {
width: 660px; width: 660px;

View File

@ -3,7 +3,7 @@
v-model:model-value="moduleKeyword" v-model:model-value="moduleKeyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')" :placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear allow-clear
class="mb-[8px]" class="mb-[16px]"
></a-input> ></a-input>
<MsTree <MsTree
v-model:focus-node-key="focusNodeKey" v-model:focus-node-key="focusNodeKey"
@ -13,7 +13,8 @@
:node-more-actions="folderMoreActions" :node-more-actions="folderMoreActions"
:expand-all="props.isExpandAll" :expand-all="props.isExpandAll"
:empty-text="t('project.fileManagement.noFolder')" :empty-text="t('project.fileManagement.noFolder')"
draggable :draggable="!props.isModal"
:virtual-list-props="virtualListProps"
block-node block-node
@select="folderNodeSelect" @select="folderNodeSelect"
@more-action-select="handleFolderMoreSelect" @more-action-select="handleFolderMoreSelect"
@ -21,15 +22,15 @@
> >
<template #title="nodeData"> <template #title="nodeData">
<span class="text-[var(--color-text-1)]">{{ nodeData.title }}</span> <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>
<template #extra="nodeData"> <template v-if="!props.isModal" #extra="nodeData">
<popConfirm mode="add" :all-names="[]" @close="resetFocusNodeKey"> <popConfirm mode="add" :all-names="[]" @close="resetFocusNodeKey">
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusNodeKe(nodeData)"> <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)]" /> <MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton> </MsButton>
</popConfirm> </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> <span :id="`renameSpan${nodeData.key}`" class="relative"></span>
</popConfirm> </popConfirm>
</template> </template>
@ -37,7 +38,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -52,12 +53,23 @@
const props = defineProps<{ const props = defineProps<{
isExpandAll: boolean; isExpandAll: boolean;
selectedKeys?: Array<string | number>; // key selectedKeys?: Array<string | number>; // key
isModal?: boolean; //
}>(); }>();
const emit = defineEmits(['update:selectedKeys', 'folderNodeSelect']); const emit = defineEmits(['update:selectedKeys', 'folderNodeSelect']);
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const virtualListProps = computed(() => {
if (props.isModal) {
return {
height: 'calc(60vh - 190px)',
};
}
return {
height: 'calc(100vh - 320px)',
};
});
const moduleKeyword = ref(''); const moduleKeyword = ref('');
const folderTree = ref([ const folderTree = ref([
{ {
@ -70,6 +82,36 @@
key: 'node2', key: 'node2',
count: 28, 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', key: 'node5',
count: 108, 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> <template #content>
<div class="mb-[8px] font-medium"> <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> </div>
<a-form ref="formRef" :model="form" layout="vertical"> <a-form ref="formRef" :model="form" layout="vertical">
<a-form-item <a-form-item
class="hidden-item" class="hidden-item"
field="name" field="field"
:rules="[{ required: true, message: t('project.fileManagement.nameNotNull') }, { validator: validateName }]" :rules="[{ required: true, message: t('project.fileManagement.nameNotNull') }, { validator: validateName }]"
> >
<a-input <a-textarea
v-model:model-value="form.name" v-if="props.fieldConfig?.isTextArea"
:max-length="50" v-model:model-value="form.field"
:placeholder="props.placeholder || t('project.fileManagement.namePlaceholder')" :max-length="props.fieldConfig?.maxLength"
:auto-size="{ maxRows: 4 }"
:placeholder="props.fieldConfig?.placeholder || t('project.fileManagement.namePlaceholder')"
class="w-[245px]" class="w-[245px]"
@press-enter="beforeConfirm(undefined)" @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-item>
</a-form> </a-form>
</template> </template>
@ -34,11 +48,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch } from 'vue'; import { onBeforeMount, ref, watch } from 'vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { Message } from '@arco-design/web-vue'; 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<{ const props = defineProps<{
mode: 'add' | 'rename'; mode: 'add' | 'rename';
@ -46,7 +68,7 @@
title?: string; title?: string;
allNames: string[]; allNames: string[];
popupContainer?: string; popupContainer?: string;
placeholder?: string; fieldConfig?: FieldConfig;
}>(); }>();
const emit = defineEmits(['update:visible', 'close']); const emit = defineEmits(['update:visible', 'close']);
@ -55,15 +77,15 @@
const innerVisible = ref(props.visible || false); const innerVisible = ref(props.visible || false);
const form = ref({ const form = ref({
name: props.title || '', field: props.fieldConfig?.field || '',
}); });
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const loading = ref(false); const loading = ref(false);
watch( watch(
() => props.title, () => props.fieldConfig?.field,
(val) => { (val) => {
form.value.name = val || ''; form.value.field = val || '';
} }
); );
@ -117,7 +139,7 @@
} }
function reset() { function reset() {
form.value.name = ''; form.value.field = '';
formRef.value?.resetFields(); formRef.value?.resetFields();
} }
</script> </script>

View File

@ -1,14 +1,20 @@
<template> <template>
<div class="p-[24px]"> <div class="flex h-[calc(100vh-88px)] flex-col overflow-hidden p-[24px]">
<div class="header"> <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"> <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 <a-input-search
v-model:model-value="keyword" v-model:model-value="keyword"
:placeholder="t('project.fileManagement.folderSearchPlaceholder')" :placeholder="t('project.fileManagement.folderSearchPlaceholder')"
allow-clear allow-clear
class="w-[240px]" class="w-[240px]"
></a-input-search> />
<a-radio-group <a-radio-group
v-if="props.activeFolderType === 'folder'" v-if="props.activeFolderType === 'folder'"
v-model:model-value="fileType" v-model:model-value="fileType"
@ -25,7 +31,15 @@
</a-radio-group> </a-radio-group>
</div> </div>
</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 }"> <template #name="{ record, rowIndex }">
<a-button type="text" class="px-0" @click="openFileDetail(record.id, rowIndex)">{{ record.name }}</a-button> <a-button type="text" class="px-0" @click="openFileDetail(record.id, rowIndex)">{{ record.name }}</a-button>
</template> </template>
@ -38,18 +52,37 @@
</MsButton> </MsButton>
<MsTableMoreAction <MsTableMoreAction
:list="record.type === 'JAR' ? jarFileActions : normalFileActions" :list="record.type === 'JAR' ? jarFileActions : normalFileActions"
@select="handleSelect($event, record)" @select="handleMoreActionSelect($event, record)"
></MsTableMoreAction> />
</template> </template>
<template v-if="keyword.trim() === ''" #empty> <template v-if="keyword.trim() === ''" #empty>
<div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]"> <div class="flex items-center justify-center p-[8px] text-[var(--color-text-4)]">
{{ t('project.fileManagement.tableNoFile') }} {{ t('project.fileManagement.tableNoFile') }}
<MsButton class="ml-[8px]" @click="uploadDrawerVisible = true"> <MsButton class="ml-[8px]" @click="handleAddClick">
{{ t('project.fileManagement.addFile') }} {{ t('project.fileManagement.addFile') }}
</MsButton> </MsButton>
</div> </div>
</template> </template>
</ms-base-table> </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> </div>
<MsDrawer v-model:visible="uploadDrawerVisible" :title="t('project.fileManagement.addFile')" :width="680"> <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)]"> <div class="mb-[8px] flex items-center justify-between text-[var(--color-text-1)]">
@ -123,6 +156,86 @@
</a-button> </a-button>
</template> </template>
</MsDrawer> </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 <fileDetailDrawerVue
v-model:visible="showDetailDrawer" v-model:visible="showDetailDrawer"
:file-id="activeFileId" :file-id="activeFileId"
@ -136,7 +249,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, onBeforeMount, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; 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 { debounce } from 'lodash-es';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/store/modules/ms-table'; import useTableStore from '@/store/modules/ms-table';
@ -156,9 +269,14 @@
import MsFileList from '@/components/pure/ms-upload/fileList.vue'; import MsFileList from '@/components/pure/ms-upload/fileList.vue';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue'; import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import MsTagGroup from '@/components/pure/ms-tag/ms-tag-group.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 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 { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
@ -172,9 +290,8 @@
const asyncTaskStore = useAsyncTaskStore(); const asyncTaskStore = useAsyncTaskStore();
const { openModal } = useModal(); const { openModal } = useModal();
const keyword = ref(''); const fileType = ref('module'); // /
const fileType = ref('module'); const acceptType = ref<UploadType>('none'); // -
const acceptType = ref<UploadType>('none');
const isUploading = ref(false); const isUploading = ref(false);
watch( watch(
@ -188,15 +305,11 @@
} }
); );
function changeFileType() { function changeFileType() {}
console.log(fileType.value);
}
const showType = ref('list'); const showType = ref<'list' | 'card'>('list'); //
function changeShowType() { function changeShowType() {}
console.log(showType.value);
}
function getCardClass(type: 'none' | 'jar') { function getCardClass(type: 'none' | 'jar') {
if (acceptType.value !== type && isUploading.value) { if (acceptType.value !== type && isUploading.value) {
@ -209,6 +322,13 @@
} }
const normalFileActions: ActionsItem[] = [ const normalFileActions: ActionsItem[] = [
{
label: 'project.fileManagement.move',
eventTag: 'move',
},
{
isDivider: true,
},
{ {
label: 'project.fileManagement.delete', label: 'project.fileManagement.delete',
eventTag: 'delete', eventTag: 'delete',
@ -217,6 +337,10 @@
]; ];
const jarFileActions: ActionsItem[] = [ const jarFileActions: ActionsItem[] = [
{
label: 'project.fileManagement.move',
eventTag: 'move',
},
{ {
label: 'common.disable', label: 'common.disable',
eventTag: 'disabled', eventTag: 'disabled',
@ -241,6 +365,7 @@
{ {
title: 'project.fileManagement.type', title: 'project.fileManagement.type',
dataIndex: 'type', dataIndex: 'type',
width: 90,
}, },
{ {
title: 'project.fileManagement.tag', title: 'project.fileManagement.tag',
@ -251,20 +376,22 @@
{ {
title: 'project.fileManagement.creator', title: 'project.fileManagement.creator',
dataIndex: 'creator', dataIndex: 'creator',
showTooltip: true,
}, },
{ {
title: 'project.fileManagement.updater', title: 'project.fileManagement.updater',
dataIndex: 'updater', dataIndex: 'updater',
showTooltip: true,
}, },
{ {
title: 'project.fileManagement.updateTime', title: 'project.fileManagement.updateTime',
dataIndex: 'updateTime', dataIndex: 'updateTime',
width: 170, width: 180,
}, },
{ {
title: 'project.fileManagement.createTime', title: 'project.fileManagement.createTime',
dataIndex: 'createTime', dataIndex: 'createTime',
width: 170, width: 180,
}, },
{ {
title: 'common.operation', title: 'common.operation',
@ -276,32 +403,67 @@
]; ];
const tableStore = useTableStore(); const tableStore = useTableStore();
tableStore.initColumn(TableKeyEnum.FILE_MANAGEMENT_FILE, columns, 'drawer'); 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, tableKey: TableKeyEnum.FILE_MANAGEMENT_FILE,
columns, columns,
showSetting: true, showSetting: true,
selectable: true,
showSelectAll: true,
}); });
const moduleFileBatchActions = {
baseAction: [
{
label: 'project.fileManagement.download',
eventTag: 'download',
},
{
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)[]>([]);
watch( /**
() => props.activeFolder, * 处理表格选中
() => { */
keyword.value = ''; function handleTableSelect(arr: (string | number)[]) {
debounce(loadList, 200)(); tableSelected.value = arr;
},
{ immediate: true }
);
function downloadFile(url: string, name: string) {
downloadUrlFile(url, name);
} }
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({ openModal({
type: 'error', type: 'error',
title: t('project.fileManagement.deleteFileTipTitle', { name: characterLimit(record.name) }), title,
content: t('project.fileManagement.deleteFileTipContent'), content: t('project.fileManagement.deleteFileTipContent'),
okText: t('common.confirmDelete'), okText: t('common.confirmDelete'),
cancelText: t('common.cancel'), cancelText: t('common.cancel'),
@ -311,6 +473,8 @@
maskClosable: false, maskClosable: false,
onBeforeOk: async () => { onBeforeOk: async () => {
try { try {
console.log(selectIds);
Message.success(t('common.deleteSuccess')); Message.success(t('common.deleteSuccess'));
loadList(); loadList();
} catch (error) { } 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 文件 * 禁用 jar 文件
*/ */
@ -348,8 +608,13 @@
* 处理表格更多按钮事件 * 处理表格更多按钮事件
* @param item * @param item
*/ */
function handleSelect(item: ActionsItem, record: any) { function handleMoreActionSelect(item: ActionsItem, record: any) {
switch (item.eventTag) { switch (item.eventTag) {
case 'move':
isBatchMove.value = false;
activeFile.value = record;
moveModalVisible.value = true;
break;
case 'delete': case 'delete':
delFile(record); delFile(record);
break; break;
@ -376,7 +641,7 @@
propsRes.value.msPagination!.total propsRes.value.msPagination!.total
); );
async function openFileDetail(id: string, index: number) { async function openFileDetail(id: string | number, index: number) {
showDetailDrawer.value = true; showDetailDrawer.value = true;
activeFileId.value = id; activeFileId.value = id;
activeFileIndex.value = index; activeFileIndex.value = index;
@ -414,15 +679,19 @@
} }
} }
const uploadDrawerVisible = ref(false); const uploadDrawerVisible = ref(false); // -
const fileList = ref<MsFileItem[]>(asyncTaskStore.uploadFileTask.fileList); const fileList = ref<MsFileItem[]>(asyncTaskStore.uploadFileTask.fileList);
//
const noWaitingUpload = computed( const noWaitingUpload = computed(
() => () =>
fileList.value.filter((e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading)) fileList.value.filter((e) => e.status && (e.status === UploadStatus.init || e.status === UploadStatus.uploading))
.length === 0 .length === 0
); );
/**
* 设置上传文件类型
* @param type 文件类型
*/
function setAcceptType(type: UploadType) { function setAcceptType(type: UploadType) {
if (isUploading.value) return; if (isUploading.value) return;
acceptType.value = type; acceptType.value = type;
@ -461,6 +730,9 @@
isUploading.value = true; isUploading.value = true;
} }
/**
* 后台上传
*/
function backstageUpload() { function backstageUpload() {
fileListRef.value?.backstageUpload(); fileListRef.value?.backstageUpload();
uploadDrawerVisible.value = false; 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> </script>
<style lang="less" scoped> <style lang="less" scoped>
@ -583,4 +930,13 @@
color: var(--color-text-4); 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> </style>

View File

@ -29,7 +29,12 @@
</div> </div>
</template> </template>
<template #itemAction="{ item }"> <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> <span :id="`renameSpan${item.key}`" class="relative"></span>
</popConfirm> </popConfirm>
</template> </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!', '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.cancelConfirm': 'Cancel upload',
'project.fileManagement.name': 'File name', '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.tag': 'Tag',
'project.fileManagement.creator': 'Creator', 'project.fileManagement.creator': 'Creator',
'project.fileManagement.updater': 'Updater', 'project.fileManagement.updater': 'Updater',
@ -76,10 +80,47 @@ export default {
'project.fileManagement.createTime': 'Created time', 'project.fileManagement.createTime': 'Created time',
'project.fileManagement.download': 'Download', 'project.fileManagement.download': 'Download',
'project.fileManagement.disabled': 'Disable', 'project.fileManagement.disabled': 'Disable',
'project.fileManagement.move': 'Move to',
'project.fileManagement.deleteFileTipTitle': 'Are you sure you want to delete the file {name}?', 'project.fileManagement.deleteFileTipTitle': 'Are you sure you want to delete the file {name}?',
'project.fileManagement.deleteFileTipContent': 'project.fileManagement.deleteFileTipContent':
'After deletion, the use cases associated with the file will fail to execute. Please operate with caution!', '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.disabledFileTipTitle': 'Are you sure you want to disable the file {name}?',
'project.fileManagement.disabledFileTipContent': 'project.fileManagement.disabledFileTipContent':
'After being disabled, the custom script associated with the file will fail to execute, so please operate with caution!', '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.cancelTipContent': '取消后,未上传成功的文件不会被保存,请谨慎操作!',
'project.fileManagement.cancelConfirm': '取消上传', 'project.fileManagement.cancelConfirm': '取消上传',
'project.fileManagement.name': '文件名称', '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.tag': '标签',
'project.fileManagement.creator': '创建人', 'project.fileManagement.creator': '创建人',
'project.fileManagement.updater': '更新人', 'project.fileManagement.updater': '更新人',
@ -72,10 +76,12 @@ export default {
'project.fileManagement.createTime': '创建时间', 'project.fileManagement.createTime': '创建时间',
'project.fileManagement.download': '下载', 'project.fileManagement.download': '下载',
'project.fileManagement.disabled': '禁用', 'project.fileManagement.disabled': '禁用',
'project.fileManagement.move': '移动',
'project.fileManagement.deleteFileTipTitle': '确认删除 {name} 这个文件吗?', 'project.fileManagement.deleteFileTipTitle': '确认删除 {name} 这个文件吗?',
'project.fileManagement.deleteFileTipContent': '删除后,导致关联该文件的用例执行失败,请谨慎操作!', 'project.fileManagement.deleteFileTipContent': '删除后,导致关联该文件的用例执行失败,请谨慎操作!',
'project.fileManagement.disabledFileTipTitle': '确认禁用 {name} 这个文件吗?', 'project.fileManagement.disabledFileTipTitle': '确认禁用 {name} 这个文件吗?',
'project.fileManagement.disabledFileTipContent': '禁用后,会导致关联该文件的自定义脚本执行失败,请谨慎操作!', 'project.fileManagement.disabledFileTipContent': '禁用后,会导致关联该文件的自定义脚本执行失败,请谨慎操作!',
'project.fileManagement.batchDeleteFileTipTitle': '确认删除这 {count} 个文件吗?',
'project.fileManagement.detail': '文件详情', 'project.fileManagement.detail': '文件详情',
'project.fileManagement.prev': '上一个', 'project.fileManagement.prev': '上一个',
'project.fileManagement.noPrev': '当前已是第一个', 'project.fileManagement.noPrev': '当前已是第一个',
@ -97,4 +103,16 @@ export default {
'project.fileManagement.record': '更新历史', 'project.fileManagement.record': '更新历史',
'project.fileManagement.caseList': '用例列表', 'project.fileManagement.caseList': '用例列表',
'project.fileManagement.search': '输入名称搜索', '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 allow-clear
/> />
</a-form-item> </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 <a-input
v-model:model-value="emailConfigForm.from" v-model:model-value="emailConfigForm.from"
:max-length="250" :max-length="250"
:placeholder="t('system.config.email.fromPlaceholder')" :placeholder="t('system.config.email.fromPlaceholder')"
allow-clear allow-clear
></a-input> ></a-input>
<MsFormItemSub :text="t('system.config.email.fromTip')" :show-fill-icon="false" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item
:label="t('system.config.email.recipient')" :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.passwordRequired': 'SMTP password cannot be empty',
'system.config.email.passwordPlaceholder': 'Please enter SMTP password', 'system.config.email.passwordPlaceholder': 'Please enter SMTP password',
'system.config.email.fromPlaceholder': 'Please enter the designated sender email', '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.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.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', '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.passwordRequired': 'SMTP 密码不能为空',
'system.config.email.passwordPlaceholder': '请输入SMTP 密码', 'system.config.email.passwordPlaceholder': '请输入SMTP 密码',
'system.config.email.fromPlaceholder': '请输入指定发件人邮箱', 'system.config.email.fromPlaceholder': '请输入指定发件人邮箱',
'system.config.email.fromTip': '注:必须是邮件服务器验证通过的邮箱,否则默认为 SMTP 账户发送',
'system.config.email.recipientPlaceholder': '请输入测试收件人邮箱', 'system.config.email.recipientPlaceholder': '请输入测试收件人邮箱',
'system.config.email.sslTip': '若 SMTP 端口是 465需要启用 SSL', 'system.config.email.sslTip': '若 SMTP 端口是 465需要启用 SSL',
'system.config.email.tslTip': '若 SMTP 端口是 587需要启用 TSL', 'system.config.email.tslTip': '若 SMTP 端口是 587需要启用 TSL',

View File

@ -345,7 +345,6 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsBatchForm from '@/components/business/ms-batch-form/index.vue'; import MsBatchForm from '@/components/business/ms-batch-form/index.vue';
import MsCodeEditor from '@/components/pure/ms-code-editor/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 MsFormItemSub from '@/components/business/ms-form-item-sub/index.vue';
import JobTemplateDrawer from './components/jobTemplateDrawer.vue'; import JobTemplateDrawer from './components/jobTemplateDrawer.vue';
import { getYaml, YamlType, job } from './template'; 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 title = t('system.user.resetPswTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id]; let selectIds = [record?.id || ''];
if (isBatch) { if (isBatch) {
title = t('system.user.batchResetPswTip', { count: tableSelected.value.length }); title = t('system.user.batchResetPswTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[]; 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 title = t('system.user.disableUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id]; let selectIds = [record?.id || ''];
if (isBatch) { if (isBatch) {
title = t('system.user.batchDisableUserTip', { count: tableSelected.value.length }); title = t('system.user.batchDisableUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[]; 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 title = t('system.user.enableUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id]; let selectIds = [record?.id || ''];
if (isBatch) { if (isBatch) {
title = t('system.user.batchEnableUserTip', { count: tableSelected.value.length }); title = t('system.user.batchEnableUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[]; 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 title = t('system.user.deleteUserTip', { name: characterLimit(record?.name) });
let selectIds = [record?.id]; let selectIds = [record?.id || ''];
if (isBatch) { if (isBatch) {
title = t('system.user.batchDeleteUserTip', { count: tableSelected.value.length }); title = t('system.user.batchDeleteUserTip', { count: tableSelected.value.length });
selectIds = tableSelected.value as string[]; selectIds = tableSelected.value as string[];
@ -508,16 +508,16 @@
showBatchModal.value = true; showBatchModal.value = true;
break; break;
case 'resetPassword': case 'resetPassword':
resetPassword(null, true); resetPassword(undefined, true);
break; break;
case 'disabled': case 'disabled':
disabledUser(null, true); disabledUser(undefined, true);
break; break;
case 'enable': case 'enable':
enableUser(null, true); enableUser(undefined, true);
break; break;
case 'delete': case 'delete':
deleteUser(null, true); deleteUser(undefined, true);
break; break;
default: default:
break; break;
@ -528,7 +528,7 @@
* 处理表格更多按钮事件 * 处理表格更多按钮事件
* @param item * @param item
*/ */
function handleSelect(item: ActionsItem, record: any) { function handleSelect(item: ActionsItem, record: UserListItem) {
switch (item.eventTag) { switch (item.eventTag) {
case 'resetPassword': case 'resetPassword':
resetPassword(record); resetPassword(record);