feat(接口测试): 场景 csv 文件删除提示

This commit is contained in:
baiqi 2024-10-23 16:52:57 +08:00 committed by Craftsman
parent 2cdc7f802a
commit 307f58d36f
9 changed files with 310 additions and 147 deletions

View File

@ -0,0 +1,189 @@
<template>
<a-popover
v-model:popup-visible="visible"
trigger="click"
position="bl"
:disabled="inputFiles.length === 0"
content-class="ms-add-attachment-files-popover"
arrow-class="hidden"
:popup-offset="0"
>
<slot></slot>
<template #content>
<div class="flex w-[200px] flex-col gap-[8px]">
<template v-if="alreadyDeleteFiles.length > 0">
<div class="flex items-center gap-[4px]">
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
<div class="text-[var(--color-text-4)]">{{ t('ms.add.attachment.alreadyDelete') }}</div>
<MsButton type="text" :disabled="props.disabled" @click="clearDeletedFiles">
{{ t('ms.add.attachment.quickClear') }}
</MsButton>
</div>
<div class="file-list">
<div v-for="file of alreadyDeleteFiles" :key="file.value" class="file-list-item">
<MsTag size="small" max-width="100%">
{{ file.label }}
</MsTag>
<a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
</div>
</template>
<template v-if="otherFiles.length > 0">
<div v-if="alreadyDeleteFiles.length > 0" class="mt-[4px] text-[var(--color-text-4)]">
{{ t('ms.add.attachment.other') }}
</div>
<div class="file-list">
<div v-for="file of otherFiles" :key="file.value" class="file-list-item">
<MsTag size="small" max-width="100%">
{{ file.label }}
</MsTag>
<div v-if="file.local === true" class="flex items-center">
<template v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])">
<a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton
type="text"
status="secondary"
class="!mr-0"
:disabled="props.disabled"
@click="handleOpenSaveAs(file)"
>
<MsIcon
type="icon-icon_unloading"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
<a-divider direction="vertical" :margin="4"></a-divider>
</template>
<a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_delete-trash_outlined1"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
<a-tooltip v-else :content="t('ms.add.attachment.cancelAssociate')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
</div>
</template>
</div>
</template>
</a-popover>
</template>
<script setup lang="ts">
import { TagData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import { MsFileItem } from '@/components/pure/ms-upload/types';
import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission';
const props = withDefaults(
defineProps<{
disabled?: boolean;
inputClass?: string;
inputSize?: 'small' | 'medium' | 'large' | 'mini';
tagSize?: Size;
fields?: {
id: string; // id
name: string;
};
}>(),
{
fields: () => ({
id: 'uid',
name: 'name',
}),
}
);
const emit = defineEmits<{
(e: 'deleteFile', fileId?: string | number): void;
(e: 'openSaveAs', file: TagData): void;
}>();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
required: true,
});
//
const inputFiles = defineModel<TagData[]>('inputFiles', {
required: true,
});
const fileList = defineModel<MsFileItem[]>('fileList', {
// TODO:MsFileItem
required: true,
});
const alreadyDeleteFiles = computed(() => {
return inputFiles.value.filter((item) => item.delete);
});
const otherFiles = computed(() => {
return inputFiles.value.filter((item) => !item.delete);
});
function clearDeletedFiles() {
inputFiles.value = inputFiles.value.filter((item) => !item.delete);
}
function handleClose(data: TagData) {
inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value);
fileList.value = fileList.value.filter((item) => (item.uid || item[props.fields.id]) !== data.value);
if (inputFiles.value.length === 0) {
visible.value = false;
}
emit('deleteFile', data.value);
}
function handleOpenSaveAs(file: TagData) {
emit('openSaveAs', file);
}
</script>
<style lang="less">
.ms-add-attachment-files-popover {
padding: 16px;
.arco-popover-content {
margin-top: 0;
}
}
</style>
<style lang="less" scoped>
.file-list {
@apply flex flex-col overflow-y-auto overflow-x-hidden;
.ms-scroll-bar();
gap: 8px;
max-height: 200px;
.file-list-item {
@apply flex items-center justify-between;
gap: 8px;
}
}
</style>

View File

@ -70,14 +70,17 @@
:file-module-options-api="props.fileModuleOptionsApi" :file-module-options-api="props.fileModuleOptionsApi"
@finish="handleSaveFileFinish" @finish="handleSaveFileFinish"
/> />
<a-popover <filesPopover
v-model:popup-visible="inputFilesPopoverVisible" v-model:visible="inputFilesPopoverVisible"
trigger="click" v-model:file-list="fileList"
position="bl" v-model:input-files="inputFiles"
:disabled="inputFiles.length === 0" :disabled="props.disabled"
content-class="ms-add-attachment-files-popover" :input-class="props.inputClass"
arrow-class="hidden" :input-size="props.inputSize"
:popup-offset="0" :fields="props.fields"
:tag-size="props.tagSize"
@open-save-as="handleOpenSaveAs"
@delete-file="emit('deleteFile', $event)"
> >
<div class="h-full flex-1"> <div class="h-full flex-1">
<MsTagsInput <MsTagsInput
@ -106,90 +109,7 @@
</template> </template>
</MsTagsInput> </MsTagsInput>
</div> </div>
<template #content> </filesPopover>
<div class="flex w-[200px] flex-col gap-[8px]">
<template v-if="alreadyDeleteFiles.length > 0">
<div class="flex items-center gap-[4px]">
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
<div class="text-[var(--color-text-4)]">{{ t('ms.add.attachment.alreadyDelete') }}</div>
<MsButton type="text" :disabled="props.disabled" @click="clearDeletedFiles">
{{ t('ms.add.attachment.quickClear') }}
</MsButton>
</div>
<div class="file-list">
<div v-for="file of alreadyDeleteFiles" :key="file.value" class="file-list-item">
<a-tooltip :content="file.label" :mouse-enter-delay="300">
<MsTag size="small" max-width="100%">
{{ file.label }}
</MsTag>
</a-tooltip>
<a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
</div>
</template>
<template v-if="otherFiles.length > 0">
<div v-if="alreadyDeleteFiles.length > 0" class="mt-[4px] text-[var(--color-text-4)]">
{{ t('ms.add.attachment.other') }}
</div>
<div class="file-list">
<div v-for="file of otherFiles" :key="file.value" class="file-list-item">
<a-tooltip :content="file.label" :mouse-enter-delay="300">
<MsTag size="small" max-width="100%">
{{ file.label }}
</MsTag>
</a-tooltip>
<div v-if="file.local === true" class="flex items-center">
<template v-if="hasAnyPermission(['PROJECT_FILE_MANAGEMENT:READ+ADD'])">
<a-tooltip :content="t('ms.add.attachment.saveAs')">
<MsButton
type="text"
status="secondary"
class="!mr-0"
:disabled="props.disabled"
@click="handleOpenSaveAs(file)"
>
<MsIcon
type="icon-icon_unloading"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
<a-divider direction="vertical" :margin="4"></a-divider>
</template>
<a-tooltip :content="t('ms.add.attachment.remove')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_delete-trash_outlined1"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
<a-tooltip v-else :content="t('ms.add.attachment.cancelAssociate')">
<MsButton type="text" status="secondary" :disabled="props.disabled" @click="handleClose(file)">
<MsIcon
type="icon-icon_unlink"
:class="props.disabled ? '' : 'hover:text-[rgb(var(--primary-5))]'"
size="16"
/>
</MsButton>
</a-tooltip>
</div>
</div>
</template>
</div>
</template>
</a-popover>
</div> </div>
<div v-else class="flex w-full items-center gap-[4px]"> <div v-else class="flex w-full items-center gap-[4px]">
<dropdownMenu <dropdownMenu
@ -206,6 +126,11 @@
allow-clear allow-clear
readonly readonly
> >
<template v-if="fileList[0]?.delete" #prefix>
<a-tooltip :content="t('ms.add.attachment.fileDeletedTip')">
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="18" />
</a-tooltip>
</template>
<template v-if="inputFileName" #suffix> <template v-if="inputFileName" #suffix>
<div class="arco-icon-hover arco-input-icon-hover arco-input-clear-btn" @click.stop="handleFileClear"> <div class="arco-icon-hover arco-input-icon-hover arco-input-clear-btn" @click.stop="handleFileClear">
<icon-close /> <icon-close />
@ -229,7 +154,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { TagData } from '@arco-design/web-vue'; import { TagData } from '@arco-design/web-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 MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue'; import MsTag, { Size } from '@/components/pure/ms-tag/ms-tag.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
@ -237,13 +161,13 @@
import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types'; import type { MsFileItem, UploadType } from '@/components/pure/ms-upload/types';
import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue'; import LinkFileDrawer from '@/components/business/ms-link-file/associatedFileDrawer.vue';
import dropdownMenu from './dropdownMenu.vue'; import dropdownMenu from './dropdownMenu.vue';
import filesPopover from './filesPopover.vue';
import saveAsFilePopover from './saveAsFilePopover.vue'; import saveAsFilePopover from './saveAsFilePopover.vue';
import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase'; import { getAssociatedFileListUrl } from '@/api/modules/case-management/featureCase';
import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement'; import { getModules, getModulesCount } from '@/api/modules/project-management/fileManagement';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import { AssociatedList } from '@/models/caseManagement/featureCase'; import { AssociatedList } from '@/models/caseManagement/featureCase';
import { TableQueryParams, TransferFileParams } from '@/models/common'; import { TableQueryParams, TransferFileParams } from '@/models/common';
@ -399,13 +323,6 @@
const alreadyDeleteFiles = computed(() => { const alreadyDeleteFiles = computed(() => {
return inputFiles.value.filter((item) => item.delete); return inputFiles.value.filter((item) => item.delete);
}); });
const otherFiles = computed(() => {
return inputFiles.value.filter((item) => !item.delete);
});
function clearDeletedFiles() {
inputFiles.value = inputFiles.value.filter((item) => !item.delete);
}
function handleClose(data: TagData) { function handleClose(data: TagData) {
inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value); inputFiles.value = inputFiles.value.filter((item) => item.value !== data.value);
@ -455,28 +372,7 @@
} }
</script> </script>
<style lang="less">
.ms-add-attachment-files-popover {
padding: 16px;
.arco-popover-content {
margin-top: 0;
}
}
</style>
<style lang="less" scoped> <style lang="less" scoped>
.file-list {
@apply flex flex-col overflow-y-auto overflow-x-hidden;
.ms-scroll-bar();
gap: 8px;
max-height: 200px;
.file-list-item {
@apply flex items-center justify-between;
gap: 8px;
}
}
:deep(.arco-input-tag-has-prefix) { :deep(.arco-input-tag-has-prefix) {
padding-left: 4px; padding-left: 4px;
} }

View File

@ -12,4 +12,5 @@ export default {
'ms.add.attachment.saveAsNamePlaceholder': 'Please enter file name', 'ms.add.attachment.saveAsNamePlaceholder': 'Please enter file name',
'ms.add.attachment.saveAsModulePlaceholder': 'Please select the transfer directory', 'ms.add.attachment.saveAsModulePlaceholder': 'Please select the transfer directory',
'ms.add.attachment.saveAsSuccess': 'File transfer successful', 'ms.add.attachment.saveAsSuccess': 'File transfer successful',
'ms.add.attachment.fileDeletedTip': 'The file has been deleted, please replace it',
}; };

View File

@ -12,4 +12,5 @@ export default {
'ms.add.attachment.saveAsNamePlaceholder': '请输入文件名称', 'ms.add.attachment.saveAsNamePlaceholder': '请输入文件名称',
'ms.add.attachment.saveAsModulePlaceholder': '请选择转存目录', 'ms.add.attachment.saveAsModulePlaceholder': '请选择转存目录',
'ms.add.attachment.saveAsSuccess': '文件转存成功', 'ms.add.attachment.saveAsSuccess': '文件转存成功',
'ms.add.attachment.fileDeletedTip': '该文件已被删除,请替换',
}; };

View File

@ -113,7 +113,7 @@ export function getFileEnum(fileType?: string): keyof typeof UploadAcceptEnum {
* @param status * @param status
*/ */
export function getFileIcon(item: MsFileItem, status?: UploadStatus) { export function getFileIcon(item: MsFileItem, status?: UploadStatus) {
const fileType = item.file?.name.split('.').pop(); // 通过文件后缀判断文件类型 const fileType = item.file?.name?.split('.').pop(); // 通过文件后缀判断文件类型
const _status = status || item.status; const _status = status || item.status;
if (_status === UploadStatus.done) { if (_status === UploadStatus.done) {
return FileIconMap[getFileEnum(fileType)]?.[_status] ?? FileIconMap.unknown[UploadStatus.done]; return FileIconMap[getFileEnum(fileType)]?.[_status] ?? FileIconMap.unknown[UploadStatus.done];

View File

@ -11,5 +11,6 @@ export type MsFileItem = FileItem & {
enable?: boolean; // jar类型文件是否可用 enable?: boolean; // jar类型文件是否可用
uploadedTime?: string | number; // 上传完成时间 uploadedTime?: string | number; // 上传完成时间
errMsg?: string; // 上传失败的错误信息 errMsg?: string; // 上传失败的错误信息
delete?: boolean; // 是否删除
[key: string]: any; [key: string]: any;
}; };

View File

@ -1,7 +0,0 @@
<template>
<div> quote </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -11,31 +11,85 @@
{{ `CSV ${props.step.csvIds?.length}` }} {{ `CSV ${props.step.csvIds?.length}` }}
</div> </div>
<template #content> <template #content>
<div class="mb-[4px] font-medium text-[var(--color-text-4)]"> <template v-if="alreadyDeleteFiles.length > 0">
{{ `${t('apiScenario.csvQuote')}${props.step.csvIds?.length}` }} <div class="flex items-center">
<div class="flex flex-1 items-center gap-[4px] leading-[18px]">
<icon-exclamation-circle-fill class="!text-[rgb(var(--warning-6))]" :size="14" />
<div class="text-[var(--color-text-4)]">{{ t('ms.add.attachment.alreadyDelete') }}</div>
</div> </div>
<div v-for="csv of csvList" :key="csv.id" class="flex items-center justify-between px-[8px] py-[4px]"> <MsButton
type="text"
:disabled="props.disabled"
size="mini"
@click="
emit(
'removeDeleted',
alreadyDeleteFiles.map((e) => e.id)
)
"
>
{{ t('ms.add.attachment.quickClear') }}
</MsButton>
</div>
<div
v-for="csv of alreadyDeleteFiles"
:key="csv.id"
class="flex items-center justify-between py-[4px] leading-[18px]"
>
<a-tooltip :content="csv.name"> <a-tooltip :content="csv.name">
<div class="one-line-text w-[142px] text-[var(--color-text-1)]"> <div class="one-line-text w-[142px] text-[var(--color-text-1)]">
{{ csv.name }} {{ csv.name }}
</div> </div>
</a-tooltip> </a-tooltip>
<div class="flex items-center"> <div class="flex items-center gap-[8px]">
<MsButton type="text" size="mini" class="!mr-0" @click="() => emit('replace', csv.id)"> <MsIcon
{{ t('common.replace') }} type="icon-icon_update_rotatiorn"
</MsButton> class="cursor-pointer hover:text-[rgb(var(--primary-5))]"
<a-divider direction="vertical" :margin="8"></a-divider> :size="14"
<MsButton type="text" size="mini" class="!mr-0" @click="() => emit('remove', csv.id)"> @click="() => emit('replace', csv.id)"
{{ t('common.remove') }} />
</MsButton> <MsIcon
type="icon-icon_delete-trash_outlined1"
class="cursor-pointer hover:text-[rgb(var(--primary-5))]"
:size="14"
@click="() => emit('remove', csv.id)"
/>
</div> </div>
</div> </div>
</template> </template>
<template v-if="otherFiles.length > 0">
<div v-if="alreadyDeleteFiles.length > 0" class="mt-[4px] text-[var(--color-text-4)]">
{{ t('ms.add.attachment.other') }}
</div>
<div v-for="csv of otherFiles" :key="csv.id" class="flex items-center justify-between py-[4px] leading-[18px]">
<a-tooltip :content="csv.name">
<div class="one-line-text w-[142px] text-[var(--color-text-1)]">
{{ csv.name }}
</div>
</a-tooltip>
<div class="flex items-center gap-[8px]">
<MsIcon
type="icon-icon_update_rotatiorn"
class="cursor-pointer hover:text-[rgb(var(--primary-5))]"
:size="14"
@click="() => emit('replace', csv.id)"
/>
<MsIcon
type="icon-icon_delete-trash_outlined1"
class="cursor-pointer hover:text-[rgb(var(--primary-5))]"
:size="14"
@click="() => emit('remove', csv.id)"
/>
</div>
</div>
</template>
</template>
</a-popover> </a-popover>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
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 { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -45,10 +99,12 @@
const props = defineProps<{ const props = defineProps<{
step: ScenarioStepItem; step: ScenarioStepItem;
csvVariables: CsvVariable[]; csvVariables: CsvVariable[];
disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'replace', id?: string): void; (e: 'replace', id?: string): void;
(e: 'remove', id?: string): void; (e: 'remove', id?: string): void;
(e: 'removeDeleted', ids: string[]): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -63,11 +119,27 @@
} }
return []; return [];
}); });
const alreadyDeleteFiles = computed(() => {
return csvList.value.filter((item) => item.file.delete);
});
const otherFiles = computed(() => {
return csvList.value.filter((item) => !item.file.delete);
});
watch(
() => props.step.csvIds,
(arr) => {
if (!arr || arr.length === 0) {
popoverVisible.value = false;
}
}
);
</script> </script>
<style lang="less"> <style lang="less">
.csv-popover { .csv-popover {
padding: 6px; padding: 6px 12px;
.arco-popover-content { .arco-popover-content {
margin-top: 0; margin-top: 0;
} }

View File

@ -100,8 +100,10 @@
<csvTag <csvTag
:step="step" :step="step"
:csv-variables="scenario.scenarioConfig.variable.csvVariables" :csv-variables="scenario.scenarioConfig.variable.csvVariables"
:disabled="!!step.isQuoteScenarioStep"
@remove="(id) => removeCsv(step, id)" @remove="(id) => removeCsv(step, id)"
@replace="(id) => replaceCsv(step, id)" @replace="(id) => replaceCsv(step, id)"
@remove-deleted="(ids) => removeDeletedCsv(step, ids)"
/> />
<!-- 自定义请求APICASE场景步骤名称 --> <!-- 自定义请求APICASE场景步骤名称 -->
<template v-if="checkStepIsApi(step)"> <template v-if="checkStepIsApi(step)">
@ -889,6 +891,14 @@
replaceCsvId.value = id || ''; replaceCsvId.value = id || '';
} }
function removeDeletedCsv(step: ScenarioStepItem, ids: string[]) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
if (realStep) {
realStep.csvIds = realStep.csvIds.filter((item: string) => !ids.includes(item));
scenario.value.unSaved = true;
}
}
function handleQuoteCsvConfirm(keys: string[]) { function handleQuoteCsvConfirm(keys: string[]) {
if (activeStep.value) { if (activeStep.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.uniqueId, 'uniqueId'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.uniqueId, 'uniqueId');