feat(功能用例): 功能用例页面搭建&搜索面板初版&项目管理基本信息调整&插件管理展开折叠调整
This commit is contained in:
parent
eb3e76b45e
commit
c6cc6e064c
|
@ -0,0 +1,76 @@
|
||||||
|
import { OPERATORS } from './operator';
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-shadow
|
||||||
|
export enum CaseKeyEnum {
|
||||||
|
NAME = 'name',
|
||||||
|
UPDATE_TIME = 'updateTime',
|
||||||
|
MODULES = 'modules',
|
||||||
|
CREATE_TIME = 'createTime',
|
||||||
|
CREATOR = 'creator',
|
||||||
|
TAGS = 'tags',
|
||||||
|
REVIEW_RESULT = 'reviewResults',
|
||||||
|
FOLLOW_PEOPLE = 'followPeople',
|
||||||
|
ASSOCIATED_REQUIREMENTS = 'associated_requirements',
|
||||||
|
CASE_LEVEL = 'caseLevel',
|
||||||
|
CASE_STATUS = 'caseStatus',
|
||||||
|
PRINCIPAL = 'principal', // 责任人
|
||||||
|
}
|
||||||
|
|
||||||
|
// 名称
|
||||||
|
export const NAME = {
|
||||||
|
key: CaseKeyEnum.NAME, // 对应字段key
|
||||||
|
type: 'a-input', // Vue控件名称
|
||||||
|
label: '显示名称', // 显示名称
|
||||||
|
operator: {
|
||||||
|
value: OPERATORS.LIKE.value, // 如果未设置value初始值,则value初始值为options[0]
|
||||||
|
options: [OPERATORS.LIKE, OPERATORS.NOT_LIKE], // 运算符选项
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 标签
|
||||||
|
export const TAGS = {
|
||||||
|
key: CaseKeyEnum.TAGS,
|
||||||
|
type: 'a-input',
|
||||||
|
label: '标签',
|
||||||
|
operator: {
|
||||||
|
value: OPERATORS.LIKE.value,
|
||||||
|
options: [OPERATORS.LIKE, OPERATORS.NOT_LIKE],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 所属模块
|
||||||
|
export const MODULE = {
|
||||||
|
key: 'module',
|
||||||
|
type: 'a-tree-select',
|
||||||
|
label: '所属模块',
|
||||||
|
operator: {
|
||||||
|
value: OPERATORS.LIKE.value,
|
||||||
|
options: [OPERATORS.LIKE, OPERATORS.NOT_LIKE],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建时间
|
||||||
|
export const CREATE_TIME = {
|
||||||
|
key: CaseKeyEnum.CREATE_TIME,
|
||||||
|
type: 'time-select', // 时间选择器
|
||||||
|
label: '创建时间',
|
||||||
|
props: {},
|
||||||
|
operator: {
|
||||||
|
options: [OPERATORS.BETWEEN, OPERATORS.GT, OPERATORS.LT],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
export const UPDATE_TIME = {
|
||||||
|
key: CaseKeyEnum.UPDATE_TIME,
|
||||||
|
type: 'time-select',
|
||||||
|
label: '更新时间',
|
||||||
|
props: {},
|
||||||
|
operator: {
|
||||||
|
options: [OPERATORS.BETWEEN, OPERATORS.GT, OPERATORS.LT],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// 功能用例所需要列表
|
||||||
|
export const TEST_PLAN_TEST_CASE = [NAME, TAGS, MODULE, CREATE_TIME, UPDATE_TIME];
|
||||||
|
|
||||||
|
export default {};
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<a-date-picker
|
||||||
|
v-if="props.operationType !== 'between'"
|
||||||
|
v-model:model-value="timeValue"
|
||||||
|
class="w-[100%]"
|
||||||
|
show-time
|
||||||
|
allow-clear
|
||||||
|
:time-picker-props="{ defaultValue: '00:00:00' }"
|
||||||
|
format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
position="br"
|
||||||
|
@change="changeHandler"
|
||||||
|
/>
|
||||||
|
<a-range-picker
|
||||||
|
v-else
|
||||||
|
v-model:model-value="timeRangeValue"
|
||||||
|
position="br"
|
||||||
|
show-time
|
||||||
|
class="w-[100%]"
|
||||||
|
format="YYYY-MM-DD HH:mm"
|
||||||
|
:time-picker-props="{
|
||||||
|
defaultValue: ['00:00:00', '00:00:00'],
|
||||||
|
}"
|
||||||
|
@change="changeHandler"
|
||||||
|
></a-range-picker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CalendarValue } from '@arco-design/web-vue/es/date-picker/interface';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
type PickerType = 'between' | 'gt' | 'lt'; // 之间 | 大于 | 小于
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue: [] | string; // 传入当前值
|
||||||
|
operationType: PickerType; // 查询运算符条件类型
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['updateTime']);
|
||||||
|
|
||||||
|
const timeValue = ref<string>('');
|
||||||
|
|
||||||
|
const timeRangeValue = ref([]);
|
||||||
|
|
||||||
|
const changeHandler = (value: Date | string | number | undefined | (CalendarValue | undefined)[] | undefined) => {
|
||||||
|
emits('updateTime', value);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
if (props.operationType === 'between') timeRangeValue.value = [];
|
||||||
|
timeValue.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,28 @@
|
||||||
|
export default {
|
||||||
|
operators: {
|
||||||
|
is_empty: 'Is empty',
|
||||||
|
is_not_empty: 'Is not empty',
|
||||||
|
like: 'Contains',
|
||||||
|
not_like: 'Not included',
|
||||||
|
in: 'Belong to',
|
||||||
|
not_in: 'Not belonging',
|
||||||
|
gt: 'Greater than',
|
||||||
|
ge: 'Greater than or equal to',
|
||||||
|
lt: 'Less than',
|
||||||
|
le: 'Less than or equal to',
|
||||||
|
equals: 'Equal to',
|
||||||
|
not_equals: 'Not Equal to',
|
||||||
|
between: 'Between',
|
||||||
|
current_user: 'Current user',
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
all: 'all',
|
||||||
|
oneOf: 'or',
|
||||||
|
},
|
||||||
|
searchPanel: {
|
||||||
|
addCondition: 'Add Conditions',
|
||||||
|
reset: 'reset',
|
||||||
|
filter: 'filter',
|
||||||
|
selectTip: 'Please select the query field',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
export default {
|
||||||
|
// 操作符号
|
||||||
|
operators: {
|
||||||
|
is_empty: '空',
|
||||||
|
is_not_empty: '非空',
|
||||||
|
like: '包含',
|
||||||
|
not_like: '不包含',
|
||||||
|
in: '属于',
|
||||||
|
not_in: '不属于',
|
||||||
|
gt: '大于',
|
||||||
|
ge: '大于等于',
|
||||||
|
lt: '小于',
|
||||||
|
le: '小于等于',
|
||||||
|
equals: '等于',
|
||||||
|
not_equals: '不等于',
|
||||||
|
between: '之间',
|
||||||
|
current_user: '是当前用户',
|
||||||
|
},
|
||||||
|
condition: {
|
||||||
|
all: '所有',
|
||||||
|
oneOf: '任一',
|
||||||
|
},
|
||||||
|
searchPanel: {
|
||||||
|
addCondition: '添加条件',
|
||||||
|
reset: '重置',
|
||||||
|
filter: '筛选',
|
||||||
|
selectTip: '请选择查询字段',
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,49 @@
|
||||||
|
// 运算符号
|
||||||
|
export const OPERATORS = {
|
||||||
|
LIKE: {
|
||||||
|
label: 'operators.like',
|
||||||
|
value: 'like',
|
||||||
|
},
|
||||||
|
NOT_LIKE: {
|
||||||
|
label: 'operators.not_like',
|
||||||
|
value: 'not like',
|
||||||
|
},
|
||||||
|
IN: {
|
||||||
|
label: 'operators.in',
|
||||||
|
value: 'in',
|
||||||
|
},
|
||||||
|
NOT_IN: {
|
||||||
|
label: 'operators.not_in',
|
||||||
|
value: 'not in',
|
||||||
|
},
|
||||||
|
GT: {
|
||||||
|
label: 'operators.gt',
|
||||||
|
value: 'gt',
|
||||||
|
},
|
||||||
|
GE: {
|
||||||
|
label: 'operators.ge',
|
||||||
|
value: 'ge',
|
||||||
|
},
|
||||||
|
LT: {
|
||||||
|
label: 'operators.lt',
|
||||||
|
value: 'lt',
|
||||||
|
},
|
||||||
|
LE: {
|
||||||
|
label: 'operators.le',
|
||||||
|
value: 'le',
|
||||||
|
},
|
||||||
|
EQ: {
|
||||||
|
label: 'operators.equals',
|
||||||
|
value: 'eq',
|
||||||
|
},
|
||||||
|
BETWEEN: {
|
||||||
|
label: 'operators.between',
|
||||||
|
value: 'between',
|
||||||
|
},
|
||||||
|
CURRENT_USER: {
|
||||||
|
label: 'operators.current_user',
|
||||||
|
value: 'current user',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {};
|
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<div class="overflow-y-auto">
|
||||||
|
<div class="flex flex-wrap items-start gap-[8px]">
|
||||||
|
<div class="flex-1">
|
||||||
|
<component
|
||||||
|
:is="form.searchKey.type"
|
||||||
|
v-bind="form.searchKey.props"
|
||||||
|
v-model="form.searchKey.value"
|
||||||
|
@change="cate1ChangeHandler"
|
||||||
|
>
|
||||||
|
<a-optgroup
|
||||||
|
v-for="(group, index) of props.selectGroupList"
|
||||||
|
:key="`${group.label as string + index}`"
|
||||||
|
:label="group.label"
|
||||||
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="groupOptions of group.options"
|
||||||
|
:key="groupOptions.value"
|
||||||
|
:value="groupOptions.value"
|
||||||
|
:disabled="isDisabledList.indexOf(groupOptions.value) > -1"
|
||||||
|
>{{ groupOptions.label }}</a-option
|
||||||
|
>
|
||||||
|
</a-optgroup>
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
<div class="w-[100px]">
|
||||||
|
<component
|
||||||
|
:is="form.operatorCondition.type"
|
||||||
|
v-bind="form.operatorCondition.props"
|
||||||
|
v-model="form.operatorCondition.value"
|
||||||
|
@change="operatorChangeHandler"
|
||||||
|
>
|
||||||
|
<a-option v-for="operator of form.operatorCondition.options" :key="operator.value" :value="operator.value">
|
||||||
|
{{ t(operator.label) }}
|
||||||
|
</a-option>
|
||||||
|
</component>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1">
|
||||||
|
<component
|
||||||
|
:is="form.queryContent.type"
|
||||||
|
v-if="form.queryContent.type !== 'time-select'"
|
||||||
|
v-bind="form.queryContent.props"
|
||||||
|
v-model="form.queryContent.value"
|
||||||
|
@change="filterKeyChange"
|
||||||
|
>
|
||||||
|
<template v-if="form.queryContent.type === 'a-select'">
|
||||||
|
<a-option v-for="opt of form.queryContent.options" :key="opt.value" :value="opt.value">{{
|
||||||
|
opt.label
|
||||||
|
}}</a-option>
|
||||||
|
</template>
|
||||||
|
<template v-if="form.queryContent.type === 'a-select-group'">
|
||||||
|
<a-select v-model="form.queryContent.value" v-bind="form.queryContent.props">
|
||||||
|
<a-optgroup v-for="group of form.searchKey.options" :key="group.id" :label="group.label">
|
||||||
|
<a-option v-for="groupOptions of group.options" :key="groupOptions.id" :value="groupOptions.id">{{
|
||||||
|
groupOptions.label
|
||||||
|
}}</a-option>
|
||||||
|
</a-optgroup>
|
||||||
|
</a-select>
|
||||||
|
</template>
|
||||||
|
</component>
|
||||||
|
<TimerSelect
|
||||||
|
v-else
|
||||||
|
:model-value="form.queryContent.value"
|
||||||
|
v-bind="form.queryContent.props"
|
||||||
|
:operation-type="form.operatorCondition.value"
|
||||||
|
@update-time="updateTimeValue"
|
||||||
|
/>
|
||||||
|
<div class="minus"> <slot></slot></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watchEffect } from 'vue';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import { TEST_PLAN_TEST_CASE } from './caseUtils';
|
||||||
|
import TimerSelect from './component/ms-date-picker.vue';
|
||||||
|
import { SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
formItem: Record<string, any>;
|
||||||
|
index: number;
|
||||||
|
formList: Record<string, any>[];
|
||||||
|
selectGroupList: SelectOptionData[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['dataUpdated']);
|
||||||
|
|
||||||
|
const form = ref({ ...cloneDeep(props.formItem) });
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
form.value.queryContent.value = props.formItem.queryContent.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 一级属性变化回调
|
||||||
|
const cate1ChangeHandler = (value: string) => {
|
||||||
|
const { operatorCondition, queryContent } = form.value;
|
||||||
|
operatorCondition.value = '';
|
||||||
|
operatorCondition.options = [];
|
||||||
|
// 获取当前选中查询Key属性的配置项
|
||||||
|
const currentKeysConfig = TEST_PLAN_TEST_CASE.find((item) => item.key === value);
|
||||||
|
if (currentKeysConfig) {
|
||||||
|
operatorCondition.options = currentKeysConfig.operator.options;
|
||||||
|
operatorCondition.value = currentKeysConfig.operator.options[0].value;
|
||||||
|
queryContent.type = currentKeysConfig.type;
|
||||||
|
}
|
||||||
|
emits('dataUpdated', form.value, props.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 禁用已选择选项
|
||||||
|
const isDisabledList = computed(() => {
|
||||||
|
return props.formList.map((item) => item.searchKey.value) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 运算符条件发生变化回调
|
||||||
|
const operatorChangeHandler = (value: string) => {
|
||||||
|
form.value.queryContent.value = value === 'between' ? [] : '';
|
||||||
|
emits('dataUpdated', form.value, props.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新数据
|
||||||
|
const filterKeyChange = () => {
|
||||||
|
emits('dataUpdated', form.value, props.index);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新时间值
|
||||||
|
const updateTimeValue = (time: string | []) => {
|
||||||
|
form.value.queryContent.value = time;
|
||||||
|
emits('dataUpdated', form.value, props.index);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,231 @@
|
||||||
|
<template>
|
||||||
|
<div class="filter-panel">
|
||||||
|
<div class="mb-4 flex items-center justify-between">
|
||||||
|
<div class="condition-text">{{ t('featureTest.featureCase.setFilterCondition') }}</div>
|
||||||
|
<div>
|
||||||
|
<span class="condition-text">{{ t('featureTest.featureCase.followingCondition') }}</span>
|
||||||
|
<a-select v-model="filterConditions.unit" class="mx-4 w-[68px]" size="small">
|
||||||
|
<a-option v-for="version of conditionOptions" :key="version.id" :value="version.value">{{
|
||||||
|
version.name
|
||||||
|
}}</a-option>
|
||||||
|
</a-select>
|
||||||
|
<span class="condition-text">{{ t('featureTest.featureCase.condition') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-for="(formItem, index) in formModels" :key="index" class="mb-[8px]">
|
||||||
|
<a-scrollbar class="overflow-y-auto" :style="{ 'max-height': props.maxHeight || '300px' }">
|
||||||
|
<QueryFromItem
|
||||||
|
:form-item="formItem"
|
||||||
|
:form-list="formModels"
|
||||||
|
:select-group-list="selectGroupList"
|
||||||
|
:index="index"
|
||||||
|
@data-updated="handleDataUpdated"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-show="formModels.length > 1"
|
||||||
|
:class="[
|
||||||
|
'flex',
|
||||||
|
'h-[32px]',
|
||||||
|
'w-[32px]',
|
||||||
|
'p-[2px]',
|
||||||
|
'cursor-pointer',
|
||||||
|
'items-center',
|
||||||
|
'justify-center',
|
||||||
|
'text-[var(--color-text-4)]',
|
||||||
|
'hover:text-[rgb(var(--primary-5))]',
|
||||||
|
'hover:bg-[rgb(var(--primary-9))]',
|
||||||
|
'rounded',
|
||||||
|
'ml-[8px]',
|
||||||
|
]"
|
||||||
|
@click="removeField(index)"
|
||||||
|
>
|
||||||
|
<icon-minus-circle />
|
||||||
|
</div>
|
||||||
|
</QueryFromItem>
|
||||||
|
</a-scrollbar>
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full items-center justify-between">
|
||||||
|
<a-button class="px-0" type="text" :disabled="isDisabledAdd" @click="addField">
|
||||||
|
<template #icon>
|
||||||
|
<icon-plus class="text-[14px]" />
|
||||||
|
</template>
|
||||||
|
{{ t('searchPanel.addCondition') }}
|
||||||
|
</a-button>
|
||||||
|
<div>
|
||||||
|
<a-button type="secondary" @click="resetField">{{ t('searchPanel.reset') }}</a-button>
|
||||||
|
<a-button class="ml-3" type="primary">{{ t('searchPanel.filter') }}</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import QueryFromItem from './query-form-item.vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import type { ConditionOptions, QueryTemplate } from './type';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const props = defineProps<{
|
||||||
|
maxHeight?: string; // 查询区域高度 默认300px
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const filterConditions = ref({
|
||||||
|
unit: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditionOptions = ref<ConditionOptions[]>([
|
||||||
|
{
|
||||||
|
id: '1001',
|
||||||
|
name: t('condition.all'),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1002',
|
||||||
|
name: t('condition.oneOf'),
|
||||||
|
value: 'oneOf',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 查询字段列表
|
||||||
|
const selectGroupList = ref([
|
||||||
|
{
|
||||||
|
label: '系统字段',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'name',
|
||||||
|
label: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模版字段',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'tags',
|
||||||
|
label: '标签',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'module',
|
||||||
|
label: '模块',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 默认模版
|
||||||
|
const deaultTemplate: QueryTemplate = {
|
||||||
|
// 查询关键字段
|
||||||
|
searchKey: {
|
||||||
|
label: '',
|
||||||
|
type: 'a-select',
|
||||||
|
value: '',
|
||||||
|
field: 'nameKey',
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择字段',
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: '系统字段',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'name',
|
||||||
|
label: '名称',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'updateTime',
|
||||||
|
label: '更新时间',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '模版字段',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: 'tags',
|
||||||
|
label: '标签',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'module',
|
||||||
|
label: '模块',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// 查询运算符条件
|
||||||
|
operatorCondition: {
|
||||||
|
label: '',
|
||||||
|
type: 'a-select',
|
||||||
|
value: '',
|
||||||
|
field: 'operator',
|
||||||
|
props: {
|
||||||
|
placeholder: '请选择条件',
|
||||||
|
},
|
||||||
|
options: [],
|
||||||
|
},
|
||||||
|
// 查询内容
|
||||||
|
queryContent: {
|
||||||
|
label: '',
|
||||||
|
type: 'a-input',
|
||||||
|
value: '',
|
||||||
|
field: 'condition',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const formModels = ref<QueryTemplate[]>([{ ...deaultTemplate }]);
|
||||||
|
|
||||||
|
// 移除查询条件项
|
||||||
|
const removeField = (index: number) => {
|
||||||
|
formModels.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加查询条件项
|
||||||
|
const addField = () => {
|
||||||
|
const ishasCondition = formModels.value.some((item) => !item.searchKey.value);
|
||||||
|
if (ishasCondition) {
|
||||||
|
Message.warning(t('searchPanel.selectTip'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
formModels.value.push(deaultTemplate);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理更新后的数据
|
||||||
|
const handleDataUpdated = (newFromData: any, index: number) => {
|
||||||
|
formModels.value.splice(index, 1, newFromData);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 计算所有条件都选中不能再添加条件
|
||||||
|
const isDisabledAdd = computed(() => {
|
||||||
|
const isSelectValueKeys = formModels.value.map((item) => item.searchKey.value).filter((item) => item);
|
||||||
|
const allOptions = selectGroupList.value.flatMap((group) => group.options).map((item) => item.value);
|
||||||
|
const isAllExistValue = allOptions.every((item) => isSelectValueKeys.indexOf(item) > -1);
|
||||||
|
return isAllExistValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置三级查询条件
|
||||||
|
const resetField = () => {
|
||||||
|
formModels.value = formModels.value.map((item) => ({
|
||||||
|
...item,
|
||||||
|
queryContent: {
|
||||||
|
...item.queryContent,
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.filter-panel {
|
||||||
|
background: var(--color-text-n9);
|
||||||
|
@apply mt-1 rounded-md p-3;
|
||||||
|
.condition-text {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { SelectOptionData } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
export interface ConditionOptions {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选项组下拉options
|
||||||
|
export interface Option {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryField {
|
||||||
|
label: string;
|
||||||
|
type: 'a-select' | 'a-input' | 'a-input-number' | 'time-select' | 'a-tree-select';
|
||||||
|
value: string;
|
||||||
|
field: string;
|
||||||
|
props?: {
|
||||||
|
placeholder: string;
|
||||||
|
[key: string]: string | number | boolean;
|
||||||
|
};
|
||||||
|
options?: SelectOptionData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryTemplate {
|
||||||
|
searchKey: QueryField;
|
||||||
|
operatorCondition: QueryField;
|
||||||
|
queryContent: QueryField;
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ export enum BugManagementRouteEnum {
|
||||||
|
|
||||||
export enum FeatureTestRouteEnum {
|
export enum FeatureTestRouteEnum {
|
||||||
FEATURE_TEST = 'featureTest',
|
FEATURE_TEST = 'featureTest',
|
||||||
|
FEATURE_TEST_CASE = 'featureTestCase',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PerformanceTestRouteEnum {
|
export enum PerformanceTestRouteEnum {
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default {
|
||||||
'menu.performanceTest': 'Performance Test',
|
'menu.performanceTest': 'Performance Test',
|
||||||
'menu.projectManagement': 'Project',
|
'menu.projectManagement': 'Project',
|
||||||
'menu.projectManagement.fileManagement': 'File Management',
|
'menu.projectManagement.fileManagement': 'File Management',
|
||||||
|
'menu.featureTest.featureCase': 'Feature Case',
|
||||||
'menu.projectManagement.projectPermission': 'Project Permission',
|
'menu.projectManagement.projectPermission': 'Project Permission',
|
||||||
'menu.projectManagement.log': 'Log',
|
'menu.projectManagement.log': 'Log',
|
||||||
'menu.settings': 'Settings',
|
'menu.settings': 'Settings',
|
||||||
|
|
|
@ -28,6 +28,7 @@ export default {
|
||||||
'menu.projectManagement': '项目管理',
|
'menu.projectManagement': '项目管理',
|
||||||
'menu.projectManagement.log': '日志',
|
'menu.projectManagement.log': '日志',
|
||||||
'menu.projectManagement.fileManagement': '文件管理',
|
'menu.projectManagement.fileManagement': '文件管理',
|
||||||
|
'menu.featureTest.featureCase': '功能用例',
|
||||||
'menu.projectManagement.projectPermission': '项目与权限',
|
'menu.projectManagement.projectPermission': '项目与权限',
|
||||||
'menu.settings': '系统设置',
|
'menu.settings': '系统设置',
|
||||||
'menu.settings.system': '系统',
|
'menu.settings.system': '系统',
|
||||||
|
|
|
@ -40,6 +40,7 @@ export interface ProjectBasicInfoModel {
|
||||||
adminList: AdminList[]; // 管理员
|
adminList: AdminList[]; // 管理员
|
||||||
projectCreateUserIsAdmin: boolean; // 创建人是否是管理员
|
projectCreateUserIsAdmin: boolean; // 创建人是否是管理员
|
||||||
moduleIds: string[];
|
moduleIds: string[];
|
||||||
|
resourcePoolList: { name: string; id: string }[]; // 资源池列表
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateProject {
|
export interface UpdateProject {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import type { AppRouteRecordRaw } from '../types';
|
||||||
const FeatureTest: AppRouteRecordRaw = {
|
const FeatureTest: AppRouteRecordRaw = {
|
||||||
path: '/feature-test',
|
path: '/feature-test',
|
||||||
name: FeatureTestRouteEnum.FEATURE_TEST,
|
name: FeatureTestRouteEnum.FEATURE_TEST,
|
||||||
redirect: '/feature-test/index',
|
redirect: '/feature-test/featureCase',
|
||||||
component: DEFAULT_LAYOUT,
|
component: DEFAULT_LAYOUT,
|
||||||
meta: {
|
meta: {
|
||||||
locale: 'menu.featureTest',
|
locale: 'menu.featureTest',
|
||||||
|
@ -15,12 +15,15 @@ const FeatureTest: AppRouteRecordRaw = {
|
||||||
hideChildrenInMenu: true,
|
hideChildrenInMenu: true,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
// 功能用例
|
||||||
{
|
{
|
||||||
path: 'index',
|
path: 'featureCase',
|
||||||
name: 'featureTestIndex',
|
name: FeatureTestRouteEnum.FEATURE_TEST_CASE,
|
||||||
component: () => import('@/views/feature-test/index.vue'),
|
component: () => import('@/views/feature-test/featureCase/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
|
locale: 'menu.featureTest.featureCase',
|
||||||
roles: ['*'],
|
roles: ['*'],
|
||||||
|
isTopMenu: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import JSEncrypt from 'jsencrypt';
|
import JSEncrypt from 'jsencrypt';
|
||||||
import { isObject } from './is';
|
import { isObject } from './is';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
type TargetContext = '_self' | '_parent' | '_blank' | '_top';
|
||||||
|
|
||||||
|
@ -275,13 +274,6 @@ export const downloadUrlFile = (url: string, fileName: string) => {
|
||||||
link.style.display = 'none';
|
link.style.display = 'none';
|
||||||
link.click();
|
link.click();
|
||||||
};
|
};
|
||||||
/**
|
|
||||||
* 时间格式转换
|
|
||||||
* @param time 时间戳
|
|
||||||
*/
|
|
||||||
export const getTime = (time: string): string => {
|
|
||||||
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 URL 哈希参数
|
* 获取 URL 哈希参数
|
||||||
|
|
|
@ -0,0 +1,125 @@
|
||||||
|
<template>
|
||||||
|
<a-popconfirm
|
||||||
|
v-model:popup-visible="isVisible"
|
||||||
|
class="ms-pop-confirm--hidden-icon"
|
||||||
|
position="bottom"
|
||||||
|
:ok-loading="loading"
|
||||||
|
:on-before-ok="beforeConfirm"
|
||||||
|
:popup-container="props.popupContainer || 'body'"
|
||||||
|
@popup-visible-change="visibleChange"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="mb-[8px] font-medium">
|
||||||
|
{{ props.title || '' }}
|
||||||
|
</div>
|
||||||
|
<a-form ref="formRef" :model="form" layout="vertical">
|
||||||
|
<a-form-item
|
||||||
|
class="hidden-item"
|
||||||
|
field="name"
|
||||||
|
:rules="[{ required: true, message: t('featureTest.featureCase.nameNotNullTip') }]"
|
||||||
|
>
|
||||||
|
<a-input
|
||||||
|
v-model:model-value="form.name"
|
||||||
|
:max-length="50"
|
||||||
|
:placeholder="props.placeholder || ''"
|
||||||
|
class="w-[245px]"
|
||||||
|
@press-enter="beforeConfirm(undefined)"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
<slot></slot>
|
||||||
|
</a-popconfirm>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import type { FormInstance } from '@arco-design/web-vue';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
export type OperationType = 'add' | 'rename';
|
||||||
|
|
||||||
|
const isVisible = ref<boolean>(false);
|
||||||
|
|
||||||
|
const loading = ref<boolean>(false);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
operationType: OperationType;
|
||||||
|
title: string;
|
||||||
|
nodeName?: string;
|
||||||
|
visible?: boolean;
|
||||||
|
popupContainer?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
(e: 'close'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: props.title || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRef = ref<FormInstance>();
|
||||||
|
|
||||||
|
const visibleChange = () => {
|
||||||
|
form.value.name = '';
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeConfirm = (done?: (closed: boolean) => void) => {
|
||||||
|
formRef.value?.validate(async (errors) => {
|
||||||
|
if (!errors) {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
Message.success(
|
||||||
|
props.operationType === 'add'
|
||||||
|
? t('featureTest.featureCase.addSubModuleSuccess')
|
||||||
|
: t('featureTest.featureCase.renameSuccess')
|
||||||
|
);
|
||||||
|
if (done) {
|
||||||
|
done(true);
|
||||||
|
} else {
|
||||||
|
isVisible.value = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
} else if (done) {
|
||||||
|
done(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.nodeName,
|
||||||
|
(val) => {
|
||||||
|
form.value.name = val || '';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.visible,
|
||||||
|
(val) => {
|
||||||
|
isVisible.value = val;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => isVisible.value,
|
||||||
|
(val) => {
|
||||||
|
if (!val) {
|
||||||
|
emits('close');
|
||||||
|
}
|
||||||
|
emits('update:visible', val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-header h-[34px]">
|
||||||
|
<div class="text-[var(--color-text-1)]"
|
||||||
|
>{{ t('featureTest.featureCase.allCase') }}
|
||||||
|
<span class="text-[var(--color-text-4)]"> ({{ allCaseCount }})</span></div
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<a-select class="w-[240px]" :placeholder="t('featureTest.featureCase.versionPlaceholder')">
|
||||||
|
<a-option v-for="version of versionOptions" :key="version.id" :value="version.id">{{ version.name }}</a-option>
|
||||||
|
</a-select>
|
||||||
|
<a-input-search
|
||||||
|
v-model:model-value="keyword"
|
||||||
|
:placeholder="t('featureTest.featureCase.versionPlaceholder')"
|
||||||
|
allow-clear
|
||||||
|
class="mx-[8px] w-[240px]"
|
||||||
|
></a-input-search>
|
||||||
|
<MsTag
|
||||||
|
:type="isExpandFilter ? 'primary' : 'default'"
|
||||||
|
:theme="isExpandFilter ? 'lightOutLine' : 'outline'"
|
||||||
|
size="large"
|
||||||
|
class="-mt-[3px] cursor-pointer"
|
||||||
|
>
|
||||||
|
<span :class="!isExpandFilter ? 'text-[var(--color-text-4)]' : ''" @click="isExpandFilterHandler"
|
||||||
|
><icon-filter class="mr-[4px]" :style="{ 'font-size': '16px' }" />{{
|
||||||
|
t('featureTest.featureCase.filter')
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</MsTag>
|
||||||
|
<a-radio-group v-model:model-value="showType" type="button" class="file-show-type ml-[4px]">
|
||||||
|
<a-radio value="list" class="show-type-icon p-[2px]"><MsIcon type="icon-icon_view-list_outlined" /></a-radio>
|
||||||
|
<a-radio value="xmind" class="show-type-icon p-[2px]"><icon-mind-mapping /></a-radio>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FilterPanel v-show="isExpandFilter"></FilterPanel>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import FilterPanel from '@/components/business/ms-filter-panel/searchForm.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const versionOptions = ref([
|
||||||
|
{
|
||||||
|
id: '1001',
|
||||||
|
name: 'v_1.0',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const keyword = ref<string>();
|
||||||
|
|
||||||
|
const showType = ref<string>('list');
|
||||||
|
|
||||||
|
const allCaseCount = ref<number>(100);
|
||||||
|
|
||||||
|
const isExpandFilter = ref<boolean>(false);
|
||||||
|
|
||||||
|
// 是否展开||折叠高级筛选
|
||||||
|
const isExpandFilterHandler = () => {
|
||||||
|
isExpandFilter.value = !isExpandFilter.value;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.page-header {
|
||||||
|
@apply flex items-center justify-between;
|
||||||
|
}
|
||||||
|
.filter-panel {
|
||||||
|
background: var(--color-text-n9);
|
||||||
|
@apply mt-1 rounded-md p-3;
|
||||||
|
.condition-text {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,200 @@
|
||||||
|
<template>
|
||||||
|
<MsTree
|
||||||
|
v-model:focus-node-key="focusNodeKey"
|
||||||
|
:selected-keys="props.selectedKeys"
|
||||||
|
:data="caseTree"
|
||||||
|
:keyword="groupKeyword"
|
||||||
|
:node-more-actions="caseMoreActions"
|
||||||
|
:expand-all="props.isExpandAll"
|
||||||
|
:empty-text="t('featureTest.featureCase.caseEmptyContent')"
|
||||||
|
draggable
|
||||||
|
block-node
|
||||||
|
@select="caseNodeSelect"
|
||||||
|
@more-action-select="handleCaseMoreSelect"
|
||||||
|
@more-actions-close="moreActionsClose"
|
||||||
|
>
|
||||||
|
<template #title="nodeData">
|
||||||
|
<span class="text-[var(--color-text-1)]">{{ nodeData.title }}</span>
|
||||||
|
<span class="ml-[4px] text-[var(--color-text-4)]">({{ nodeData.count }})</span>
|
||||||
|
</template>
|
||||||
|
<template #extra="nodeData">
|
||||||
|
<ActionPopConfirm
|
||||||
|
operation-type="add"
|
||||||
|
:all-names="[]"
|
||||||
|
:title="t('featureTest.featureCase.addSubModule')"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" size="mini" class="ms-tree-node-extra__btn !mr-0" @click="setFocusKey(nodeData)">
|
||||||
|
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
|
||||||
|
</MsButton>
|
||||||
|
</ActionPopConfirm>
|
||||||
|
<ActionPopConfirm
|
||||||
|
operation-type="rename"
|
||||||
|
:title="t('featureTest.featureCase.rename')"
|
||||||
|
:node-name="renameCaseName"
|
||||||
|
:all-names="[]"
|
||||||
|
@close="resetFocusNodeKey"
|
||||||
|
>
|
||||||
|
<span :id="`renameSpan${nodeData.key}`" class="relative"></span>
|
||||||
|
</ActionPopConfirm>
|
||||||
|
</template>
|
||||||
|
</MsTree>
|
||||||
|
<div class="recycle w-[88%]">
|
||||||
|
<a-divider class="mb-[16px]" />
|
||||||
|
<div class="recycle-bin pt-2">
|
||||||
|
<MsIcon type="icon-icon_delete-trash_outlined" size="16" class="mx-[10px] text-[var(--color-text-4)]" />
|
||||||
|
<div class="text-[var(--color-text-1)]">{{ t('featureTest.featureCase.recycle') }}</div>
|
||||||
|
<div class="recycle-count">({{ recycleCount }})</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import MsTree from '@/components/business/ms-tree/index.vue';
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';
|
||||||
|
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
|
||||||
|
import ActionPopConfirm from './actionPopConfirm.vue';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const focusNodeKey = ref<string | number>('');
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
selectedKeys?: Array<string | number>; // 选中的节点 key
|
||||||
|
isExpandAll: boolean; // 是否展开用例节点
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits(['update:selectedKeys', 'caseNodeSelect']);
|
||||||
|
|
||||||
|
const groupKeyword = ref<string>('');
|
||||||
|
|
||||||
|
const caseTree = ref([
|
||||||
|
{
|
||||||
|
title: 'Trunk',
|
||||||
|
key: 'node1',
|
||||||
|
count: 18,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Leaf',
|
||||||
|
key: 'node2',
|
||||||
|
count: 28,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Trunk',
|
||||||
|
key: 'node3',
|
||||||
|
count: 180,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Leaf',
|
||||||
|
key: 'node4',
|
||||||
|
count: 138,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Leaf',
|
||||||
|
key: 'node5',
|
||||||
|
count: 108,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Trunk',
|
||||||
|
key: 'node6',
|
||||||
|
children: [],
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const caseMoreActions: ActionsItem[] = [
|
||||||
|
{
|
||||||
|
label: 'featureTest.featureCase.rename',
|
||||||
|
eventTag: 'rename',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'featureTest.featureCase.delete',
|
||||||
|
eventTag: 'delete',
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const renameCaseName = ref('');
|
||||||
|
|
||||||
|
const selectedNodeKeys = ref(props.selectedKeys || []);
|
||||||
|
|
||||||
|
const renamePopVisible = ref(false);
|
||||||
|
|
||||||
|
// 用例树节点选中事件
|
||||||
|
const caseNodeSelect = (selectedKeys: (string | number)[]) => {
|
||||||
|
emits('caseNodeSelect', selectedKeys);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteHandler = (node: MsTreeNodeData) => {};
|
||||||
|
|
||||||
|
function resetFocusNodeKey() {
|
||||||
|
focusNodeKey.value = '';
|
||||||
|
renamePopVisible.value = false;
|
||||||
|
renameCaseName.value = '';
|
||||||
|
}
|
||||||
|
// 用例树节点更多事件
|
||||||
|
const handleCaseMoreSelect = (item: ActionsItem, node: MsTreeNodeData) => {
|
||||||
|
switch (item.eventTag) {
|
||||||
|
case 'delete':
|
||||||
|
deleteHandler(node);
|
||||||
|
resetFocusNodeKey();
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
renameCaseName.value = node.title || '';
|
||||||
|
renamePopVisible.value = true;
|
||||||
|
document.querySelector(`#renameSpan${node.key}`)?.dispatchEvent(new Event('click'));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const moreActionsClose = () => {
|
||||||
|
if (!renamePopVisible.value) {
|
||||||
|
resetFocusNodeKey();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFocusKey = (node: MsTreeNodeData) => {
|
||||||
|
focusNodeKey.value = node.key || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const recycleCount = ref<number>(100);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.selectedKeys,
|
||||||
|
(val) => {
|
||||||
|
selectedNodeKeys.value = val || [];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => selectedNodeKeys.value,
|
||||||
|
(val) => {
|
||||||
|
emits('update:selectedKeys', val);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.recycle {
|
||||||
|
@apply absolute bottom-0 bg-white pb-4;
|
||||||
|
:deep(.arco-divider-horizontal) {
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
.recycle-bin {
|
||||||
|
@apply bottom-0 flex items-center bg-white;
|
||||||
|
.recycle-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,154 @@
|
||||||
|
<template>
|
||||||
|
<div class="mb-[16px]">
|
||||||
|
<a-button type="primary" class="mr-[12px]"> {{ t('featureTest.featureCase.creatingCase') }} </a-button>
|
||||||
|
<a-button type="outline"> {{ t('featureTest.featureCase.importCase') }} </a-button>
|
||||||
|
</div>
|
||||||
|
<div class="pageWrap">
|
||||||
|
<MsSplitBox>
|
||||||
|
<template #left>
|
||||||
|
<div class="p-[24px]">
|
||||||
|
<div class="feature-case">
|
||||||
|
<div class="case h-[38px]">
|
||||||
|
<div class="flex items-center" :class="getActiveClass('public')" @click="selectActive('public')">
|
||||||
|
<MsIcon type="icon-icon_folder_outlined-1" class="folder-icon" />
|
||||||
|
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.publicCase') }}</div>
|
||||||
|
<div class="folder-count">({{ publicCaseCount }})</div></div
|
||||||
|
>
|
||||||
|
<div class="back"><icon-arrow-right /></div>
|
||||||
|
</div>
|
||||||
|
<a-divider class="my-[8px]" />
|
||||||
|
<a-input-search class="mb-4" :placeholder="t('featureTest.featureCase.searchTip')" />
|
||||||
|
<div class="case h-[38px]">
|
||||||
|
<div class="flex items-center" :class="getActiveClass('all')" @click="selectActive('all')">
|
||||||
|
<MsIcon type="icon-icon_folder_filled1" class="folder-icon" />
|
||||||
|
<div class="folder-name mx-[4px]">{{ t('featureTest.featureCase.allCase') }}</div>
|
||||||
|
<div class="folder-count">(100)</div></div
|
||||||
|
>
|
||||||
|
<div class="ml-auto flex items-center">
|
||||||
|
<a-tooltip
|
||||||
|
:content="
|
||||||
|
isExpandAll ? t('project.fileManagement.collapseAll') : t('project.fileManagement.expandAll')
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" status="secondary" class="!mr-0 p-[4px]" @click="expandHandler">
|
||||||
|
<MsIcon :type="isExpandAll ? 'icon-icon_folder_collapse1' : 'icon-icon_folder_expansion1'" />
|
||||||
|
</MsButton>
|
||||||
|
</a-tooltip>
|
||||||
|
<ActionPopConfirm
|
||||||
|
operation-type="add"
|
||||||
|
:title="t('featureTest.featureCase.addSubModule')"
|
||||||
|
:all-names="[]"
|
||||||
|
>
|
||||||
|
<MsButton type="icon" class="!mr-0 p-[2px]">
|
||||||
|
<MsIcon
|
||||||
|
type="icon-icon_create_planarity"
|
||||||
|
size="18"
|
||||||
|
class="text-[rgb(var(--primary-5))] hover:text-[rgb(var(--primary-4))]"
|
||||||
|
/>
|
||||||
|
</MsButton>
|
||||||
|
</ActionPopConfirm>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a-divider class="my-[8px]" />
|
||||||
|
<FeatureCaseTree
|
||||||
|
v-model:selected-keys="selectedKeys"
|
||||||
|
:is-expand-all="isExpandAll"
|
||||||
|
@case-node-select="caseNodeSelect"
|
||||||
|
></FeatureCaseTree>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #right>
|
||||||
|
<div class="p-[24px]">
|
||||||
|
<CaseTable></CaseTable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MsSplitBox>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
|
||||||
|
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||||
|
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||||
|
import FeatureCaseTree from './components/featureCaseTree.vue';
|
||||||
|
import ActionPopConfirm from './components/actionPopConfirm.vue';
|
||||||
|
import CaseTable from './components/caseTable.vue';
|
||||||
|
import { useI18n } from '@/hooks/useI18n';
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const isExpandAll = ref(false);
|
||||||
|
|
||||||
|
const activeCase = ref<string | number>('public'); // 激活用例
|
||||||
|
|
||||||
|
const publicCaseCount = ref<number>(100); // 公共用例数量
|
||||||
|
|
||||||
|
// 设置当前激活用例类型
|
||||||
|
const selectActive = (type: string) => {
|
||||||
|
activeCase.value = type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取激活用例样式
|
||||||
|
const getActiveClass = (type: string) => {
|
||||||
|
return activeCase.value === type ? 'folder-text case-active' : 'folder-text';
|
||||||
|
};
|
||||||
|
|
||||||
|
const expandHandler = () => {
|
||||||
|
isExpandAll.value = !isExpandAll.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选中节点
|
||||||
|
const selectedKeys = computed({
|
||||||
|
get: () => [activeCase.value],
|
||||||
|
set: (val) => val,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理用例树节点选中
|
||||||
|
function caseNodeSelect(keys: (string | number)[]) {
|
||||||
|
[activeCase.value] = keys;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.pageWrap {
|
||||||
|
min-width: 1000px;
|
||||||
|
height: calc(100vh - 136px);
|
||||||
|
border-radius: var(--border-radius-large);
|
||||||
|
@apply bg-white;
|
||||||
|
.case {
|
||||||
|
@apply flex cursor-pointer items-center justify-between;
|
||||||
|
.folder-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.folder-name {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
.folder-count {
|
||||||
|
margin-left: 4px;
|
||||||
|
color: var(--color-text-4);
|
||||||
|
}
|
||||||
|
.case-active {
|
||||||
|
.folder-icon,
|
||||||
|
.folder-name,
|
||||||
|
.folder-count {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
margin-right: 8px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 1px solid #ffffff;
|
||||||
|
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
|
||||||
|
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
|
||||||
|
.arco-icon {
|
||||||
|
color: rgb(var(--primary-5));
|
||||||
|
}
|
||||||
|
@apply flex cursor-pointer items-center rounded-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default {
|
||||||
|
'featureTest.featureCase.creatingCase': 'Create Case',
|
||||||
|
'featureTest.featureCase.importCase': 'Import Case',
|
||||||
|
'featureTest.featureCase.publicCase': 'Public of Cases',
|
||||||
|
'featureTest.featureCase.allCase': 'All of Cases',
|
||||||
|
'featureTest.featureCase.searchTip': 'Please enter a group name',
|
||||||
|
'featureTest.featureCase.caseEmptyContent': 'No use case data yet, please click the button above to create or import',
|
||||||
|
'featureTest.featureCase.addSubModule': 'Add submodules',
|
||||||
|
'featureTest.featureCase.rename': 'rename',
|
||||||
|
'featureTest.featureCase.recycle': 'Recycle',
|
||||||
|
'featureTest.featureCase.versionPlaceholder': 'The default is the latest version',
|
||||||
|
'featureTest.featureCase.searchByNameAndId': 'Search by ID or name',
|
||||||
|
'featureTest.featureCase.filter': 'filter',
|
||||||
|
'featureTest.featureCase.setFilterCondition': 'Set filters',
|
||||||
|
'featureTest.featureCase.followingCondition': 'Conform to the following',
|
||||||
|
'featureTest.featureCase.condition': 'Condition',
|
||||||
|
'featureTest.featureCase.delete': 'delete',
|
||||||
|
'featureTest.featureCase.addSubModuleSuccess': 'Add submodule successfully',
|
||||||
|
'featureTest.featureCase.renameSuccess': 'Rename successful',
|
||||||
|
'featureTest.featureCase.nameNotNullTip': 'The name can not be null',
|
||||||
|
};
|
|
@ -0,0 +1,21 @@
|
||||||
|
export default {
|
||||||
|
'featureTest.featureCase.creatingCase': '创建用例',
|
||||||
|
'featureTest.featureCase.importCase': '导入用例',
|
||||||
|
'featureTest.featureCase.publicCase': '公共用例库',
|
||||||
|
'featureTest.featureCase.allCase': '全部用例',
|
||||||
|
'featureTest.featureCase.searchTip': '请输入分组名称',
|
||||||
|
'featureTest.featureCase.caseEmptyContent': '暂无用例数据,请点击上方按钮创建或导入',
|
||||||
|
'featureTest.featureCase.addSubModule': '添加子模块',
|
||||||
|
'featureTest.featureCase.rename': '重命名',
|
||||||
|
'featureTest.featureCase.recycle': '回收站',
|
||||||
|
'featureTest.featureCase.versionPlaceholder': '默认为最新版本',
|
||||||
|
'featureTest.featureCase.searchByNameAndId': '通过 ID 或名称搜索',
|
||||||
|
'featureTest.featureCase.filter': '筛选',
|
||||||
|
'featureTest.featureCase.setFilterCondition': '设置筛选条件',
|
||||||
|
'featureTest.featureCase.followingCondition': '符合以下',
|
||||||
|
'featureTest.featureCase.condition': '条件',
|
||||||
|
'featureTest.featureCase.delete': '删除',
|
||||||
|
'featureTest.featureCase.addSubModuleSuccess': '添加子模块成功',
|
||||||
|
'featureTest.featureCase.renameSuccess': '重命名成功',
|
||||||
|
'featureTest.featureCase.nameNotNullTip': '名称不能为空',
|
||||||
|
};
|
|
@ -18,10 +18,7 @@
|
||||||
<span v-if="!projectDetail?.deleted && projectDetail?.enable" class="button enable-button mr-1">{{
|
<span v-if="!projectDetail?.deleted && projectDetail?.enable" class="button enable-button mr-1">{{
|
||||||
t('project.basicInfo.enable')
|
t('project.basicInfo.enable')
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="!projectDetail?.deleted && !projectDetail?.enable" class="button disabled-button mr-1">{{
|
<span v-else class="button delete-button">{{ t('project.basicInfo.deleted') }}</span>
|
||||||
t('project.basicInfo.disabled')
|
|
||||||
}}</span>
|
|
||||||
<span v-if="projectDetail?.deleted" class="button delete-button">{{ t('project.basicInfo.deleted') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="one-line-text text-xs text-[--color-text-4]">{{ projectDetail?.description }}</div>
|
<div class="one-line-text text-xs text-[--color-text-4]">{{ projectDetail?.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,11 +35,11 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="label-item">
|
<div class="label-item">
|
||||||
<span class="label">{{ t('project.basicInfo.resourcePool') }}</span>
|
<span class="label">{{ t('project.basicInfo.resourcePool') }}</span>
|
||||||
<MsTag>资源池</MsTag>
|
<MsTag v-for="pool of projectDetail?.resourcePoolList" :key="pool.id">{{ pool.name }}</MsTag>
|
||||||
</div>
|
</div>
|
||||||
<div class="label-item">
|
<div class="label-item">
|
||||||
<span class="label">{{ t('project.basicInfo.createTime') }}</span>
|
<span class="label">{{ t('project.basicInfo.createTime') }}</span>
|
||||||
<span>{{ getTime(projectDetail?.createTime as string) }}</span>
|
<span>{{ dayjs(projectDetail?.createTime).format('YYYY-MM-DD HH:mm:ss') }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UpdateProjectModal ref="projectDetailRef" v-model:visible="isVisible" @success="getProjectDetail()" />
|
<UpdateProjectModal ref="projectDetailRef" v-model:visible="isVisible" @success="getProjectDetail()" />
|
||||||
|
@ -56,7 +53,7 @@
|
||||||
import { useAppStore } from '@/store';
|
import { useAppStore } from '@/store';
|
||||||
import { getProjectInfo } from '@/api/modules/project-management/basicInfo';
|
import { getProjectInfo } from '@/api/modules/project-management/basicInfo';
|
||||||
import type { ProjectBasicInfoModel } from '@/models/projectManagement/basicInfo';
|
import type { ProjectBasicInfoModel } from '@/models/projectManagement/basicInfo';
|
||||||
import { getTime } from '@/utils';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
|
|
|
@ -119,12 +119,18 @@
|
||||||
</a-table-column>
|
</a-table-column>
|
||||||
</template>
|
</template>
|
||||||
<template #expand-icon="{ record, expanded }">
|
<template #expand-icon="{ record, expanded }">
|
||||||
<span v-if="(record.pluginForms || []).length && !expanded" class="collapsebtn"
|
<span
|
||||||
><icon-plus :style="{ 'font-size': '12px' }"
|
v-if="(record.pluginForms || []).length && !expanded"
|
||||||
/></span>
|
class="collapsebtn flex items-center justify-center"
|
||||||
<span v-else-if="(record.pluginForms || []).length && expanded" class="expand"
|
>
|
||||||
><icon-minus class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }"
|
<icon-right class="text-[var(--color-text-4)]" :style="{ 'font-size': '12px' }" />
|
||||||
/></span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="(record.pluginForms || []).length && expanded"
|
||||||
|
class="expand flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<icon-down class="text-[rgb(var(--primary-6))]" :style="{ 'font-size': '12px' }" />
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -403,23 +409,27 @@
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
:deep(.collapsebtn) {
|
:deep(.collapsebtn) {
|
||||||
padding: 0 1px;
|
width: 16px;
|
||||||
border: 1px solid var(--color-text-4);
|
height: 16px;
|
||||||
border-radius: 3px;
|
border-radius: 50%;
|
||||||
|
background: var(--color-text-n8) !important;
|
||||||
@apply bg-white;
|
@apply bg-white;
|
||||||
}
|
}
|
||||||
:deep(.expand) {
|
:deep(.expand) {
|
||||||
padding: 0 1px;
|
width: 16px;
|
||||||
border: 1px solid rgb(var(--primary-5));
|
height: 16px;
|
||||||
border-radius: 3px;
|
border-radius: 50%;
|
||||||
@apply bg-white;
|
background: rgb(var(--primary-1));
|
||||||
}
|
}
|
||||||
:deep(.arco-table-expand-btn) {
|
:deep(.arco-table-expand-btn) {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
border-width: 2px;
|
border: none;
|
||||||
border-radius: 3px;
|
border-radius: 50%;
|
||||||
@apply bg-white;
|
background: var(--color-text-n8) !important;
|
||||||
|
}
|
||||||
|
:deep(.arco-table .arco-table-expand-btn:hover) {
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
.ms-footerNum {
|
.ms-footerNum {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
Loading…
Reference in New Issue